diff --git a/.changeset/five-turtles-grab.md b/.changeset/five-turtles-grab.md new file mode 100644 index 0000000000..67739756bd --- /dev/null +++ b/.changeset/five-turtles-grab.md @@ -0,0 +1,5 @@ +--- +"@neo4j/graphql": patch +--- + +Remove `connectComponentsPlanner` argument from the `CypherQueryOptions` diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 3b722507fb..137c802311 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -27,15 +27,16 @@ "scripts": { "build": "tsc --build src/tsconfig.production.json", "clean": "cd src/ && tsc --build --clean", + "test:v6": "jest tests/api-v6", "cleanup:package-tests": "cd ../package-tests/ && yarn run cleanup && rimraf ./package/ && rimraf *.tgz", "performance": "ts-node tests/performance/performance.ts", "posttest:package-tests": "yarn run cleanup:package-tests", "setup:package-tests": "yarn pack && mv *.tgz ../package-tests/ && cd ../package-tests/ && rimraf package && tar -xvzf *.tgz && cd package && cd ../ && yarn install && yarn run setup", "test:e2e": "jest tests/e2e", - "test:int": "jest tests/integration", + "test:int": "jest tests/integration tests/api-v6/integration", "test:package-tests": "yarn run setup:package-tests && cd ../package-tests/ && yarn run test:all", - "test:schema": "jest tests/schema -c jest.minimal.config.js", - "test:tck": "jest tests/tck -c jest.minimal.config.js", + "test:schema": "jest tests/schema tests/api-v6/schema -c jest.minimal.config.js", + "test:tck": "jest tests/tck tests/api-v6/tck -c jest.minimal.config.js", "test:unit": "jest src --coverage=true -c jest.minimal.config.js", "test": "jest" }, diff --git a/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts new file mode 100644 index 0000000000..f6d6d76d6a --- /dev/null +++ b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts @@ -0,0 +1,414 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import type { QueryASTContext } from "../../translate/queryAST/ast/QueryASTContext"; +import type { QueryASTNode } from "../../translate/queryAST/ast/QueryASTNode"; +import type { Field } from "../../translate/queryAST/ast/fields/Field"; +import { OperationField } from "../../translate/queryAST/ast/fields/OperationField"; +import type { Filter } from "../../translate/queryAST/ast/filters/Filter"; +import type { AuthorizationFilters } from "../../translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters"; +import { CypherScalarOperation } from "../../translate/queryAST/ast/operations/CypherScalarOperation"; +import { Operation, type OperationTranspileResult } from "../../translate/queryAST/ast/operations/operations"; +import type { Pagination } from "../../translate/queryAST/ast/pagination/Pagination"; +import type { EntitySelection } from "../../translate/queryAST/ast/selection/EntitySelection"; +import { CypherPropertySort } from "../../translate/queryAST/ast/sort/CypherPropertySort"; +import type { Sort, SortField } from "../../translate/queryAST/ast/sort/Sort"; +import { wrapSubqueriesInCypherCalls } from "../../translate/queryAST/utils/wrap-subquery-in-calls"; +import { filterTruthy } from "../../utils/utils"; + +export class V6ReadOperation extends Operation { + public readonly relationship: RelationshipAdapter | undefined; + public readonly target: ConcreteEntityAdapter; + + public nodeFields: Field[] = []; + public edgeFields: Field[] = []; // TODO: merge with attachedTo? + protected filters: Filter[] = []; + protected pagination: Pagination | undefined; + protected sortFields: Array<{ node: Sort[]; edge: Sort[] }> = []; + protected authFilters: AuthorizationFilters[] = []; + + protected selection: EntitySelection; + + constructor({ + relationship, + target, + selection, + fields, + sortFields, + pagination, + filters, + }: { + relationship?: RelationshipAdapter; + target: ConcreteEntityAdapter; + selection: EntitySelection; + fields?: { + node: Field[]; + edge: Field[]; + }; + pagination?: Pagination; + sortFields?: Array<{ node: Sort[]; edge: Sort[] }>; + filters: Filter[]; + }) { + super(); + this.relationship = relationship; + this.target = target; + this.selection = selection; + this.nodeFields = fields?.node ?? []; + this.edgeFields = fields?.edge ?? []; + this.sortFields = sortFields ?? []; + this.pagination = pagination; + this.filters = filters ?? []; + } + + public setNodeFields(fields: Field[]) { + this.nodeFields = fields; + } + + public addFilters(...filters: Filter[]) { + this.filters.push(...filters); + } + + public setEdgeFields(fields: Field[]) { + this.edgeFields = fields; + } + + public addAuthFilters(...filter: AuthorizationFilters[]) { + this.authFilters.push(...filter); + } + + public addSort(sortElement: { node: Sort[]; edge: Sort[] }): void { + this.sortFields.push(sortElement); + } + + public addPagination(pagination: Pagination): void { + this.pagination = pagination; + } + + public getChildren(): QueryASTNode[] { + const sortFields = this.sortFields.flatMap((s) => { + return [...s.edge, ...s.node]; + }); + + return filterTruthy([ + this.selection, + ...this.nodeFields, + ...this.edgeFields, + ...this.filters, + ...this.authFilters, + this.pagination, + ...sortFields, + ]); + } + + public transpile(context: QueryASTContext): OperationTranspileResult { + if (!context.target) throw new Error(); + + // eslint-disable-next-line prefer-const + let { selection: selectionClause, nestedContext } = this.selection.apply(context); + + let extraMatches: Array = this.getChildren().flatMap((f) => { + return f.getSelection(nestedContext); + }); + + if (extraMatches.length > 0) { + extraMatches = [selectionClause, ...extraMatches]; + selectionClause = new Cypher.With("*"); + } + + const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => { + return new Cypher.Call(sq).importWith(nestedContext.target); + }); + + const normalFilterSubqueries = this.getFilterSubqueries(nestedContext).map((sq) => { + return new Cypher.Call(sq).importWith(nestedContext.target); + }); + + const filtersSubqueries = [...authFilterSubqueries, ...normalFilterSubqueries]; + + const edgesVar = new Cypher.NamedVariable("edges"); + const totalCount = new Cypher.NamedVariable("totalCount"); + const edgesProjectionVar = new Cypher.Variable(); + + const unwindAndProjectionSubquery = this.createUnwindAndProjectionSubquery( + nestedContext, + edgesVar, + edgesProjectionVar + ); + + let withWhere: Cypher.With | undefined; + + if (filtersSubqueries.length > 0) { + withWhere = new Cypher.With("*"); + this.addFiltersToClause(withWhere, nestedContext); + } else { + this.addFiltersToClause(selectionClause, nestedContext); + } + + const nodeAndRelationshipMap = new Cypher.Map({ + node: nestedContext.target, + }); + + if (nestedContext.relationship) { + nodeAndRelationshipMap.set("relationship", nestedContext.relationship); + } + + const withCollectEdgesAndTotalCount = new Cypher.With([Cypher.collect(nodeAndRelationshipMap), edgesVar]).with( + edgesVar, + [Cypher.size(edgesVar), totalCount] + ); + + const returnClause = new Cypher.Return([ + new Cypher.Map({ + connection: new Cypher.Map({ + edges: edgesProjectionVar, + totalCount: totalCount, + }), + }), + context.returnVariable, + ]); + + return { + clauses: [ + Cypher.concat( + ...extraMatches, + selectionClause, + ...filtersSubqueries, + withWhere, + withCollectEdgesAndTotalCount, + unwindAndProjectionSubquery, + returnClause + ), + ], + projectionExpr: context.returnVariable, + }; + } + + protected getAuthFilterSubqueries(context: QueryASTContext): Cypher.Clause[] { + return this.authFilters.flatMap((f) => f.getSubqueries(context)); + } + + protected getFilterSubqueries(context: QueryASTContext): Cypher.Clause[] { + return this.filters.flatMap((f) => f.getSubqueries(context)); + } + + protected getAuthFilterPredicate(context: QueryASTContext): Cypher.Predicate[] { + return filterTruthy(this.authFilters.map((f) => f.getPredicate(context))); + } + + private createUnwindAndProjectionSubquery( + context: QueryASTContext, + edgesVar: Cypher.Variable, + returnVar: Cypher.Variable + ) { + const edgeVar = new Cypher.NamedVariable("edge"); + const { prePaginationSubqueries, postPaginationSubqueries } = this.getPreAndPostSubqueries(context); + + let unwindClause: Cypher.With; + if (context.relationship) { + unwindClause = new Cypher.Unwind([edgesVar, edgeVar]).with( + [edgeVar.property("node"), context.target], + [edgeVar.property("relationship"), context.relationship] + ); + } else { + unwindClause = new Cypher.Unwind([edgesVar, edgeVar]).with([edgeVar.property("node"), context.target]); + } + + const edgeProjectionMap = this.createProjectionMapForEdge(context); + + const paginationWith = this.generateSortAndPaginationClause(context); + + return new Cypher.Call( + Cypher.concat( + unwindClause, + ...prePaginationSubqueries, + paginationWith, + ...postPaginationSubqueries, + new Cypher.Return([Cypher.collect(edgeProjectionMap), returnVar]) + ) + ).importWith(edgesVar); + } + + private createProjectionMapForEdge(context: QueryASTContext): Cypher.Map { + const nodeProjectionMap = this.generateProjectionMapForFields(this.nodeFields, context.target); + if (nodeProjectionMap.size === 0) { + nodeProjectionMap.set({ + __id: Cypher.id(context.target), + }); + } + nodeProjectionMap.set({ + __resolveType: new Cypher.Literal(this.target.name), + }); + + const edgeProjectionMap = new Cypher.Map(); + + if (context.relationship) { + const propertiesProjectionMap = this.generateProjectionMapForFields(this.edgeFields, context.relationship); + if (propertiesProjectionMap.size) { + if (this.relationship?.propertiesTypeName) { + // should be true if getting here but just in case.. + propertiesProjectionMap.set( + "__resolveType", + new Cypher.Literal(this.relationship.propertiesTypeName) + ); + } + edgeProjectionMap.set("properties", propertiesProjectionMap); + } + } + + edgeProjectionMap.set("node", nodeProjectionMap); + return edgeProjectionMap; + } + + private generateProjectionMapForFields(fields: Field[], target: Cypher.Variable): Cypher.Map { + const projectionMap = new Cypher.Map(); + fields + .map((f) => f.getProjectionField(target)) + .forEach((p) => { + if (typeof p === "string") { + projectionMap.set(p, target.property(p)); + } else { + projectionMap.set(p); + } + }); + + return projectionMap; + } + + private generateSortAndPaginationClause(context: QueryASTContext): Cypher.With | undefined { + const shouldGenerateSortWith = this.pagination || this.sortFields.length > 0; + if (!shouldGenerateSortWith) { + return; + } + const paginationWith = new Cypher.With("*"); + this.addPaginationSubclauses(paginationWith); + this.addSortSubclause(paginationWith, context); + + return paginationWith; + } + + private addPaginationSubclauses(clause: Cypher.With): void { + const paginationField = this.pagination && this.pagination.getPagination(); + if (paginationField?.limit) { + clause.limit(paginationField.limit); + } + if (paginationField?.skip) { + clause.skip(paginationField.skip); + } + } + + private addSortSubclause(clause: Cypher.With, context: QueryASTContext): void { + if (this.sortFields.length > 0) { + const sortFields = this.getSortFields({ + context: context, + nodeVar: context.target, + edgeVar: context.relationship, + }); + clause.orderBy(...sortFields); + } + } + + private addFiltersToClause( + clause: Cypher.With | Cypher.Match | Cypher.Yield, + context: QueryASTContext + ): void { + const predicates = this.filters.map((f) => f.getPredicate(context)); + const authPredicate = this.getAuthFilterPredicate(context); + const predicate = Cypher.and(...predicates, ...authPredicate); + if (predicate) { + clause.where(predicate); + } + } + + private getSortFields({ + context, + nodeVar, + edgeVar, + }: { + context: QueryASTContext; + nodeVar: Cypher.Variable | Cypher.Property; + edgeVar?: Cypher.Variable | Cypher.Property; + }): SortField[] { + const aliasSort = true; + return this.sortFields.flatMap(({ node, edge }) => { + const nodeFields = node.flatMap((s) => s.getSortFields(context, nodeVar, aliasSort)); + if (edgeVar) { + const edgeFields = edge.flatMap((s) => s.getSortFields(context, edgeVar, aliasSort)); + return [...nodeFields, ...edgeFields]; + } + return nodeFields; + }); + } + /** + * This method resolves all the subqueries for each field and splits them into separate fields: `prePaginationSubqueries` and `postPaginationSubqueries`, + * in the `prePaginationSubqueries` are present all the subqueries required for the pagination purpose. + **/ + private getPreAndPostSubqueries(context: QueryASTContext): { + prePaginationSubqueries: Cypher.Clause[]; + postPaginationSubqueries: Cypher.Clause[]; + } { + if (!context.hasTarget()) { + throw new Error("No parent node found!"); + } + const sortNodeFields = this.sortFields.flatMap((sf) => sf.node); + /** + * cypherSortFieldsFlagMap is a Record that holds the name of the sort field as key + * and a boolean flag defined as true when the field is a `@cypher` field. + **/ + const cypherSortFieldsFlagMap = sortNodeFields.reduce>( + (sortFieldsFlagMap, sortField) => { + if (sortField instanceof CypherPropertySort) { + sortFieldsFlagMap[sortField.getFieldName()] = true; + } + return sortFieldsFlagMap; + }, + {} + ); + + const preAndPostFields = this.nodeFields.reduce>( + (acc, nodeField) => { + if ( + nodeField instanceof OperationField && + nodeField.isCypherField() && + nodeField.operation instanceof CypherScalarOperation + ) { + const cypherFieldName = nodeField.operation.cypherAttributeField.name; + if (cypherSortFieldsFlagMap[cypherFieldName]) { + acc.Pre.push(nodeField); + return acc; + } + } + + acc.Post.push(nodeField); + return acc; + }, + { Pre: [], Post: [] } + ); + const preNodeSubqueries = wrapSubqueriesInCypherCalls(context, preAndPostFields.Pre, [context.target]); + const postNodeSubqueries = wrapSubqueriesInCypherCalls(context, preAndPostFields.Post, [context.target]); + const sortSubqueries = wrapSubqueriesInCypherCalls(context, sortNodeFields, [context.target]); + + return { + prePaginationSubqueries: [...sortSubqueries, ...preNodeSubqueries], + postPaginationSubqueries: postNodeSubqueries, + }; + } +} diff --git a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts new file mode 100644 index 0000000000..289e305098 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts @@ -0,0 +1,101 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { QueryASTContext } from "../../translate/queryAST/ast/QueryASTContext"; +import type { QueryASTNode } from "../../translate/queryAST/ast/QueryASTNode"; +import type { InputField } from "../../translate/queryAST/ast/input-fields/InputField"; +import type { OperationTranspileResult } from "../../translate/queryAST/ast/operations/operations"; +import { MutationOperation } from "../../translate/queryAST/ast/operations/operations"; +import { getEntityLabels } from "../../translate/queryAST/utils/create-node-from-entity"; +import { filterTruthy } from "../../utils/utils"; +import type { V6ReadOperation } from "./ConnectionReadOperation"; + +export class V6CreateOperation extends MutationOperation { + public readonly target: ConcreteEntityAdapter; + private readonly inputFields: InputField[]; + private readonly createInputParam: Cypher.Param | Cypher.Property; + private readonly unwindVariable: Cypher.Variable; + private readonly projection: V6ReadOperation | undefined; + + constructor({ + target, + createInputParam, + inputFields, + projection, + }: { + target: ConcreteEntityAdapter; + createInputParam: Cypher.Param | Cypher.Property; + inputFields: InputField[]; + projection?: V6ReadOperation; + }) { + super(); + this.target = target; + this.inputFields = inputFields; + this.createInputParam = createInputParam; + this.unwindVariable = new Cypher.Variable(); + this.projection = projection; + } + + public getChildren(): QueryASTNode[] { + return filterTruthy([...this.inputFields.values(), this.projection]); + } + + public transpile(context: QueryASTContext): OperationTranspileResult { + if (!context.hasTarget()) { + throw new Error("No parent node found!"); + } + + const unwindClause = new Cypher.Unwind([this.createInputParam, this.unwindVariable]); + + const createClause = new Cypher.Create( + new Cypher.Pattern(context.target, { labels: getEntityLabels(this.target, context.neo4jGraphQLContext) }) + ); + const setSubqueries: Cypher.Clause[] = []; + for (const field of this.inputFields) { + if (field.attachedTo === "node") { + createClause.set(...field.getSetParams(context, this.unwindVariable)); + setSubqueries.push(...field.getSubqueries(context)); + } + } + + const nestedSubqueries = setSubqueries.flatMap((clause) => [ + new Cypher.With(context.target, this.unwindVariable), + new Cypher.Call(clause).importWith(context.target, this.unwindVariable), + ]); + + const unwindCreateClauses = Cypher.concat(createClause, ...nestedSubqueries); + + const subQueryClause: Cypher.Clause = new Cypher.Call( + Cypher.concat(unwindCreateClauses, new Cypher.Return(context.target)) + ).importWith(this.unwindVariable); + + const projectionContext = context.setReturn(new Cypher.NamedVariable("data")); + const clauses = Cypher.concat(unwindClause, subQueryClause, ...this.getProjectionClause(projectionContext)); + return { projectionExpr: context.returnVariable, clauses: [clauses] }; + } + + private getProjectionClause(context: QueryASTContext): Cypher.Clause[] { + if (!this.projection) { + return []; + } + return this.projection.transpile(context).clauses; + } +} diff --git a/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts b/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts new file mode 100644 index 0000000000..5530d0eda5 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts @@ -0,0 +1,188 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { InterfaceEntityAdapter } from "../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; +import type { Filter } from "../../translate/queryAST/ast/filters/Filter"; +import type { OperationTranspileResult } from "../../translate/queryAST/ast/operations/operations"; +import { MutationOperation } from "../../translate/queryAST/ast/operations/operations"; +import type { QueryASTContext } from "../../translate/queryAST/ast/QueryASTContext"; +import type { QueryASTNode } from "../../translate/queryAST/ast/QueryASTNode"; +import type { EntitySelection, SelectionClause } from "../../translate/queryAST/ast/selection/EntitySelection"; +import { wrapSubqueriesInCypherCalls } from "../../translate/queryAST/utils/wrap-subquery-in-calls"; + +export class V6DeleteOperation extends MutationOperation { + public readonly target: ConcreteEntityAdapter | InterfaceEntityAdapter; + private selection: EntitySelection; + private filters: Filter[]; + private nestedOperations: V6DeleteOperation[]; + + constructor({ + target, + selection, + nestedOperations = [], + filters = [], + }: { + target: ConcreteEntityAdapter | InterfaceEntityAdapter; + selection: EntitySelection; + filters?: Filter[]; + nestedOperations?: V6DeleteOperation[]; + }) { + super(); + this.target = target; + this.selection = selection; + this.filters = filters; + this.nestedOperations = nestedOperations; + } + + public getChildren(): QueryASTNode[] { + return [this.selection, ...this.filters, ...this.nestedOperations]; + } + + public transpile(context: QueryASTContext): OperationTranspileResult { + if (!context.hasTarget()) { + throw new Error("No parent node found!"); + } + const { selection, nestedContext } = this.selection.apply(context); + if (nestedContext.relationship) { + return this.transpileNested(selection, nestedContext); + } + return this.transpileTopLevel(selection, nestedContext); + } + + private transpileTopLevel( + selection: SelectionClause, + context: QueryASTContext + ): OperationTranspileResult { + this.validateSelection(selection); + const filterSubqueries = wrapSubqueriesInCypherCalls(context, this.filters, [context.target]); + const predicate = this.getPredicate(context); + const extraSelections = this.getExtraSelections(context); + + const nestedOperations: (Cypher.Call | Cypher.With)[] = this.getNestedDeleteSubQueries(context); + + let statements = [selection, ...extraSelections, ...filterSubqueries]; + + statements = this.appendFilters(statements, predicate); + if (nestedOperations.length) { + statements.push(new Cypher.With("*"), ...nestedOperations); + } + statements = this.appendDeleteClause(statements, context); + const ret = Cypher.concat(...statements); + + return { clauses: [ret], projectionExpr: context.target }; + } + + private transpileNested( + selection: SelectionClause, + context: QueryASTContext + ): OperationTranspileResult { + this.validateSelection(selection); + if (!context.relationship) { + throw new Error("Transpile error: No relationship found!"); + } + const filterSubqueries = wrapSubqueriesInCypherCalls(context, this.filters, [context.target]); + const predicate = this.getPredicate(context); + const extraSelections = this.getExtraSelections(context); + const collect = Cypher.collect(context.target).distinct(); + const deleteVar = new Cypher.Variable(); + const withBeforeDeleteBlock = new Cypher.With(context.relationship, [collect, deleteVar]); + + const unwindDeleteVar = new Cypher.Variable(); + const deleteClause = new Cypher.Unwind([deleteVar, unwindDeleteVar]).detachDelete(unwindDeleteVar); + + const deleteBlock = new Cypher.Call(deleteClause).importWith(deleteVar); + const nestedOperations: (Cypher.Call | Cypher.With)[] = this.getNestedDeleteSubQueries(context); + const statements = this.appendFilters([selection, ...extraSelections, ...filterSubqueries], predicate); + + if (nestedOperations.length) { + statements.push(new Cypher.With("*"), ...nestedOperations); + } + statements.push(withBeforeDeleteBlock, deleteBlock); + const ret = Cypher.concat(...statements); + return { clauses: [ret], projectionExpr: Cypher.Null }; + } + + private appendDeleteClause(clauses: Cypher.Clause[], context: QueryASTContext): Cypher.Clause[] { + const lastClause = this.getLastClause(clauses); + if ( + lastClause instanceof Cypher.Match || + lastClause instanceof Cypher.OptionalMatch || + lastClause instanceof Cypher.With + ) { + lastClause.detachDelete(context.target); + return clauses; + } + const extraWith = new Cypher.With("*"); + extraWith.detachDelete(context.target); + clauses.push(extraWith); + return clauses; + } + + private getLastClause(clauses: Cypher.Clause[]): Cypher.Clause { + const lastClause = clauses[clauses.length - 1]; + if (!lastClause) { + throw new Error("Transpile error"); + } + return lastClause; + } + + private appendFilters(clauses: Cypher.Clause[], predicate: Cypher.Predicate | undefined): Cypher.Clause[] { + if (!predicate) { + return clauses; + } + const lastClause = this.getLastClause(clauses); + if ( + lastClause instanceof Cypher.Match || + lastClause instanceof Cypher.OptionalMatch || + lastClause instanceof Cypher.With + ) { + lastClause.where(predicate); + return clauses; + } + const withClause = new Cypher.With("*"); + withClause.where(predicate); + clauses.push(withClause); + return clauses; + } + + private getNestedDeleteSubQueries(context: QueryASTContext): Cypher.Call[] { + const nestedOperations: Cypher.Call[] = []; + for (const nestedDeleteOperation of this.nestedOperations) { + const { clauses } = nestedDeleteOperation.transpile(context); + nestedOperations.push(...clauses.map((c) => new Cypher.Call(c).importWith("*"))); + } + return nestedOperations; + } + + private validateSelection(selection: SelectionClause): asserts selection is Cypher.Match | Cypher.With { + if (!(selection instanceof Cypher.Match || selection instanceof Cypher.With)) { + throw new Error("Cypher Yield statement is not a valid selection for Delete Operation"); + } + } + + private getPredicate(queryASTContext: QueryASTContext): Cypher.Predicate | undefined { + return Cypher.and(...this.filters.map((f) => f.getPredicate(queryASTContext))); + } + + private getExtraSelections(context: QueryASTContext): (Cypher.Match | Cypher.With)[] { + return this.getChildren().flatMap((f) => f.getSelection(context)); + } +} diff --git a/packages/graphql/src/api-v6/queryIR/MutationInput/UpdateProperty.ts b/packages/graphql/src/api-v6/queryIR/MutationInput/UpdateProperty.ts new file mode 100644 index 0000000000..ecd693875e --- /dev/null +++ b/packages/graphql/src/api-v6/queryIR/MutationInput/UpdateProperty.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import type { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; +import type { QueryASTContext } from "../../../translate/queryAST/ast/QueryASTContext"; +import type { QueryASTNode } from "../../../translate/queryAST/ast/QueryASTNode"; +import { InputField } from "../../../translate/queryAST/ast/input-fields/InputField"; + +export class UpdateProperty extends InputField { + private value: unknown; + private attribute: AttributeAdapter; + + constructor({ + value, + attribute, + attachedTo = "node", + }: { + value: unknown; + attribute: AttributeAdapter; + attachedTo: "node" | "relationship"; + }) { + super(attribute.name, attachedTo); + this.value = value; + this.attribute = attribute; + } + + public getChildren(): QueryASTNode[] { + return []; + } + + public getSetParams(queryASTContext: QueryASTContext): Cypher.SetParam[] { + const target = this.getTarget(queryASTContext); + + const targetProperty = target.property(this.attribute.databaseName); + const rightExpr = new Cypher.Param(this.value); + const setField: Cypher.SetParam = [targetProperty, rightExpr]; + + return [setField]; + } +} diff --git a/packages/graphql/src/api-v6/queryIR/UpdateOperation.ts b/packages/graphql/src/api-v6/queryIR/UpdateOperation.ts new file mode 100644 index 0000000000..7e3e8416be --- /dev/null +++ b/packages/graphql/src/api-v6/queryIR/UpdateOperation.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { QueryASTContext } from "../../translate/queryAST/ast/QueryASTContext"; +import type { QueryASTNode } from "../../translate/queryAST/ast/QueryASTNode"; +import type { Filter } from "../../translate/queryAST/ast/filters/Filter"; +import type { OperationTranspileResult } from "../../translate/queryAST/ast/operations/operations"; +import { MutationOperation } from "../../translate/queryAST/ast/operations/operations"; +import type { EntitySelection } from "../../translate/queryAST/ast/selection/EntitySelection"; +import { filterTruthy } from "../../utils/utils"; +import type { V6ReadOperation } from "./ConnectionReadOperation"; +import type { UpdateProperty } from "./MutationInput/UpdateProperty"; + +export class V6UpdateOperation extends MutationOperation { + public readonly target: ConcreteEntityAdapter; + private readonly inputFields: UpdateProperty[]; + private readonly projection: V6ReadOperation | undefined; + + protected filters: Filter[]; + protected selection: EntitySelection; + + constructor({ + target, + inputFields, + projection, + selection, + filters = [], + }: { + selection: EntitySelection; + target: ConcreteEntityAdapter; + inputFields: UpdateProperty[]; + projection?: V6ReadOperation; + filters?: Filter[]; + }) { + super(); + this.target = target; + this.inputFields = inputFields; + this.projection = projection; + this.selection = selection; + this.filters = filters; + } + + public getChildren(): QueryASTNode[] { + return filterTruthy([...this.inputFields.values(), ...this.filters, this.selection, this.projection]); + } + + public transpile(context: QueryASTContext): OperationTranspileResult { + if (!context.hasTarget()) { + throw new Error("No parent node found!"); + } + + const { selection: selectionClause, nestedContext } = this.selection.apply(context); + + const setParams = this.inputFields.flatMap((p) => p.getSetParams(nestedContext)); + + selectionClause.set(...setParams); + + // TODO: filter subqueries + const filterPredicates = this.filters.map((f) => { + return f.getPredicate(nestedContext); + }); + selectionClause.where(Cypher.and(...filterPredicates)); + + const clauses = Cypher.concat(selectionClause, ...this.getProjectionClause(nestedContext)); + return { projectionExpr: context.returnVariable, clauses: [clauses] }; + } + + private getProjectionClause(context: QueryASTContext): Cypher.Clause[] { + if (!this.projection) { + return []; + } + return this.projection.transpile(context).clauses; + } +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts new file mode 100644 index 0000000000..f054fea029 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import { QueryAST } from "../../translate/queryAST/ast/QueryAST"; +import { IdField } from "../../translate/queryAST/ast/input-fields/IdField"; +import type { InputField } from "../../translate/queryAST/ast/input-fields/InputField"; +import { PropertyInputField } from "../../translate/queryAST/ast/input-fields/PropertyInputField"; +import { getAutogeneratedFields } from "../../translate/queryAST/factory/parsers/get-autogenerated-fields"; +import type { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; +import { V6CreateOperation } from "../queryIR/CreateOperation"; +import { ReadOperationFactory } from "./ReadOperationFactory"; +import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "./resolve-tree-parser/graphql-tree/graphql-tree"; +import { raiseOnConflictingInput } from "./utils/raise-on-conflicting-input"; +import { getAttribute } from "./utils/get-attribute"; + +export class CreateOperationFactory { + public schemaModel: Neo4jGraphQLSchemaModel; + private readFactory: ReadOperationFactory; + + constructor(schemaModel: Neo4jGraphQLSchemaModel) { + this.schemaModel = schemaModel; + this.readFactory = new ReadOperationFactory(schemaModel); + } + + public createAST({ + graphQLTreeCreate, + entity, + }: { + graphQLTreeCreate: GraphQLTreeCreate; + entity: ConcreteEntity; + }): QueryAST { + const operation = this.generateCreateOperation({ + graphQLTreeCreate, + entity, + }); + return new QueryAST(operation); + } + + private generateCreateOperation({ + graphQLTreeCreate, + entity, + }: { + graphQLTreeCreate: GraphQLTreeCreate; + entity: ConcreteEntity; + }): V6CreateOperation { + const topLevelCreateInput = graphQLTreeCreate.args.input; + const targetAdapter = new ConcreteEntityAdapter(entity); + let projection: V6ReadOperation | undefined; + if (graphQLTreeCreate.fields) { + projection = this.readFactory.generateMutationProjection({ + graphQLTreeNode: graphQLTreeCreate, + entity, + }); + } + + const inputFields = this.getInputFields({ + target: targetAdapter, + createInput: topLevelCreateInput, + }); + const createOP = new V6CreateOperation({ + target: targetAdapter, + createInputParam: new Cypher.Param(topLevelCreateInput), + projection, + inputFields, + }); + + return createOP; + } + + private getInputFields({ + target, + createInput, + }: { + target: ConcreteEntityAdapter; + createInput: GraphQLTreeCreateInput[]; + }): InputField[] { + // inputFieldsExistence is used to keep track of the fields that have been added to the inputFields array + // as with the unwind clause we define a single tree for multiple inputs + // this is to avoid adding the same field multiple times + const inputFieldsExistence = new Set(); + const inputFields: InputField[] = this.addAutogeneratedFields(target, inputFieldsExistence); + + for (const inputItem of createInput) { + raiseOnConflictingInput(inputItem, target.entity); + for (const key of Object.keys(inputItem)) { + const attribute = getAttribute(target, key); + + const attachedTo = "node"; + if (inputFieldsExistence.has(attribute.name)) { + continue; + } + inputFieldsExistence.add(attribute.name); + const propertyInputField = new PropertyInputField({ + attribute, + attachedTo, + }); + inputFields.push(propertyInputField); + } + } + return inputFields; + } + + private addAutogeneratedFields(target: ConcreteEntityAdapter, inputFieldsExistence: Set): InputField[] { + // TODO: remove generated field filter when we support other autogenerated fields + const autoGeneratedFields = getAutogeneratedFields(target).filter((field) => field instanceof IdField); + const inputFields: InputField[] = []; + for (const field of autoGeneratedFields) { + if (inputFieldsExistence.has(field.name)) { + continue; + } + inputFieldsExistence.add(field.name); + inputFields.push(field); + } + return inputFields; + } +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts new file mode 100644 index 0000000000..3ffa96e930 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts @@ -0,0 +1,189 @@ +/* + * 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 { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { QueryAST } from "../../translate/queryAST/ast/QueryAST"; +import { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection"; +import { RelationshipSelection } from "../../translate/queryAST/ast/selection/RelationshipSelection"; +import { isInterfaceEntity } from "../../translate/queryAST/utils/is-interface-entity"; +import { isUnionEntity } from "../../translate/queryAST/utils/is-union-entity"; +import { asArray, filterTruthy, isRecord } from "../../utils/utils"; +import { V6DeleteOperation } from "../queryIR/DeleteOperation"; +import { FilterFactory } from "./FilterFactory"; +import type { GraphQLTreeDelete, GraphQLTreeDeleteInput } from "./resolve-tree-parser/graphql-tree/graphql-tree"; + +export class DeleteOperationFactory { + public schemaModel: Neo4jGraphQLSchemaModel; + private filterFactory: FilterFactory; + + constructor(schemaModel: Neo4jGraphQLSchemaModel) { + this.schemaModel = schemaModel; + this.filterFactory = new FilterFactory(schemaModel); + } + + public deleteAST({ + graphQLTreeDelete, + entity, + }: { + graphQLTreeDelete: GraphQLTreeDelete; + entity: ConcreteEntity; + }): QueryAST { + const operation = this.generateDeleteOperation({ + graphQLTreeDelete, + entity, + }); + return new QueryAST(operation); + } + + private generateDeleteOperation({ + graphQLTreeDelete, + entity, + }: { + graphQLTreeDelete: GraphQLTreeDelete; + entity: ConcreteEntity; + }): V6DeleteOperation { + const targetAdapter = new ConcreteEntityAdapter(entity); + + const selection = new NodeSelection({ + target: targetAdapter, + }); + + const filters = filterTruthy([ + this.filterFactory.createFilters({ + entity, + where: graphQLTreeDelete.args.where, + }), + ]); + + const nestedDeleteOperations: V6DeleteOperation[] = []; + if (graphQLTreeDelete.args.input) { + nestedDeleteOperations.push( + ...this.createNestedDeleteOperations(graphQLTreeDelete.args.input, targetAdapter) + ); + } + + const deleteOP = new V6DeleteOperation({ + target: targetAdapter, + selection, + filters, + nestedOperations: nestedDeleteOperations, + }); + + return deleteOP; + } + + private createNestedDeleteOperations( + deleteArg: GraphQLTreeDeleteInput, + source: ConcreteEntityAdapter + ): V6DeleteOperation[] { + return filterTruthy( + Object.entries(deleteArg).flatMap(([key, valueArr]) => { + return asArray(valueArr).flatMap((value) => { + const relationship = source.findRelationship(key); + if (!relationship) { + throw new Error(`Failed to find relationship ${key}`); + } + const target = relationship.target; + + if (isInterfaceEntity(target)) { + // TODO: Implement createNestedDeleteOperationsForInterface + // return this.createNestedDeleteOperationsForInterface({ + // deleteArg: value, + // relationship, + // target, + // }); + return; + } + if (isUnionEntity(target)) { + // TODO: Implement createNestedDeleteOperationsForUnion + // return this.createNestedDeleteOperationsForUnion({ + // deleteArg: value, + // relationship, + // target, + // }); + return; + } + + return this.createNestedDeleteOperation({ + relationship, + target, + args: value as Record, + }); + }); + }) + ); + } + + private parseDeleteArgs( + args: Record, + isTopLevel: boolean + ): { + whereArg: { node: Record; edge: Record }; + deleteArg: Record; + } { + let whereArg; + const rawWhere = isRecord(args.where) ? args.where : {}; + if (isTopLevel) { + whereArg = { node: rawWhere.node ?? {}, edge: rawWhere.edge ?? {} }; + } else { + whereArg = { node: rawWhere, edge: {} }; + } + const deleteArg = isRecord(args.delete) ? args.delete : {}; + return { whereArg, deleteArg }; + } + + private createNestedDeleteOperation({ + relationship, + target, + args, + }: { + relationship: RelationshipAdapter; + target: ConcreteEntityAdapter; + args: Record; + }): V6DeleteOperation[] { + const { whereArg, deleteArg } = this.parseDeleteArgs(args, true); + + const selection = new RelationshipSelection({ + relationship, + directed: true, + optional: true, + targetOverride: target, + }); + + const filters = filterTruthy([ + this.filterFactory.createFilters({ + entity: target.entity, + where: whereArg, + }), + ]); + + const nestedDeleteOperations = this.createNestedDeleteOperations(deleteArg, target); + return [ + new V6DeleteOperation({ + target, + selection, + filters, + nestedOperations: nestedDeleteOperations, + }), + ]; + } +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts new file mode 100644 index 0000000000..2d6314b9d9 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -0,0 +1,286 @@ +/* + * 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 { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import type { Attribute } from "../../schema-model/attribute/Attribute"; +import { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { Relationship } from "../../schema-model/relationship/Relationship"; +import { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { ConnectionFilter } from "../../translate/queryAST/ast/filters/ConnectionFilter"; +import type { Filter, LogicalOperators } from "../../translate/queryAST/ast/filters/Filter"; +import { LogicalFilter } from "../../translate/queryAST/ast/filters/LogicalFilter"; +import { DurationFilter } from "../../translate/queryAST/ast/filters/property-filters/DurationFilter"; +import { PropertyFilter } from "../../translate/queryAST/ast/filters/property-filters/PropertyFilter"; +import { SpatialFilter } from "../../translate/queryAST/ast/filters/property-filters/SpatialFilter"; +import { fromGlobalId } from "../../utils/global-ids"; +import { asArray, filterTruthy } from "../../utils/utils"; +import { getFilterOperator, getRelationshipOperator } from "./FilterOperators"; +import type { + GraphQLAttributeFilters, + GraphQLEdgeWhere, + GraphQLNodeFilters, + GraphQLNodeWhere, + GraphQLWhere, + GraphQLWhereTopLevel, + RelationshipFilters, +} from "./resolve-tree-parser/graphql-tree/where"; + +export class FilterFactory { + public schemaModel: Neo4jGraphQLSchemaModel; + + constructor(schemaModel: Neo4jGraphQLSchemaModel) { + this.schemaModel = schemaModel; + } + + public createFilters({ + where = {}, + relationship, + entity, + }: { + entity: ConcreteEntity; + relationship?: Relationship; + where?: GraphQLWhere | GraphQLWhereTopLevel | GraphQLEdgeWhere; + }): Filter | undefined { + const filters = filterTruthy( + Object.entries(where).map(([key, value]) => { + if (key === "AND" || key === "OR" || key === "NOT") { + const whereInLogicalOperation = asArray(value) as Array< + GraphQLWhere | GraphQLWhereTopLevel | GraphQLEdgeWhere + >; + const nestedFilters = whereInLogicalOperation.map((nestedWhere) => { + return this.createFilters({ + where: nestedWhere, + relationship, + entity, + }); + }); + + return new LogicalFilter({ + operation: key, + filters: filterTruthy(nestedFilters), + }); + } + + if (key === "edges") { + return this.createFilters({ entity, relationship, where: value }); + } + + if (key === "node") { + return this.createNodeFilter({ entity, where: value }); + } + + if (key === "properties" && relationship) { + return this.createEdgePropertiesFilters({ + where: value, + relationship, + }); + } + }) + ); + + return this.mergeFilters(filters); + } + + private createNodeFilter({ + where = {}, + entity, + }: { + entity: ConcreteEntity; + where?: GraphQLNodeWhere; + }): Filter | undefined { + const nodePropertiesFilters = Object.entries(where).flatMap(([fieldName, nestedWhere]) => { + if (fieldName === "AND" || fieldName === "OR" || fieldName === "NOT") { + const whereInLogicalOperator = asArray(nestedWhere) as Array; + const nestedFilters = whereInLogicalOperator.map((nestedWhere) => { + return this.createNodeFilter({ + where: nestedWhere, + entity, + }); + }); + + return new LogicalFilter({ + operation: fieldName, + filters: filterTruthy(nestedFilters), + }); + } + + let filters = nestedWhere as GraphQLNodeFilters; + + let attribute: Attribute | undefined; + if (fieldName === "id" && entity.globalIdField) { + attribute = entity.globalIdField; + filters = this.parseGlobalIdFilters(entity, filters); + } else { + attribute = entity.findAttribute(fieldName); + } + + if (attribute) { + const attributeAdapter = new AttributeAdapter(attribute); + // We need to cast for now because filters can be plain attribute or relationships, but the check is done by checking the findAttribute + // TODO: Use a helper method that handles this casting with `is GraphQLAttributeFilters` + return this.createPropertyFilters(attributeAdapter, filters as GraphQLAttributeFilters); + } + + const relationship = entity.findRelationship(fieldName); + if (relationship) { + return this.createRelationshipFilters(relationship, filters as RelationshipFilters); + } + return []; + }); + + return new LogicalFilter({ + operation: "AND", + filters: filterTruthy(nodePropertiesFilters), + }); + } + + /** Transforms globalId filters into normal property filters */ + private parseGlobalIdFilters(entity: ConcreteEntity, filters: GraphQLNodeFilters): GraphQLNodeFilters { + return Object.entries(filters).reduce((acc, [key, value]) => { + const relayIdData = fromGlobalId(value); + const { typeName, field, id } = relayIdData; + + if (typeName !== entity.name || !field || !id) { + throw new Error(`Cannot query Relay Id on "${entity.name}"`); + } + acc[key] = id; + return acc; + }, {}); + } + + private createRelationshipFilters(relationship: Relationship, filters: RelationshipFilters): Filter[] { + const relationshipAdapter = new RelationshipAdapter(relationship); + + const target = relationshipAdapter.target as ConcreteEntityAdapter; + + return Object.entries(filters).map(([rawOperator, edgeFilter]) => { + const filter = edgeFilter.edges; + const relatedNodeFilters = this.createFilters({ + where: filter, + relationship: relationship, + entity: relationship.target as ConcreteEntity, + }); + const operator = getRelationshipOperator(rawOperator); + const relationshipFilter = new ConnectionFilter({ + relationship: relationshipAdapter, + target, + operator, + }); + if (relatedNodeFilters) { + relationshipFilter.addFilters([relatedNodeFilters]); + } + return relationshipFilter; + }); + } + + private createEdgePropertiesFilters({ + where = {}, + relationship, + }: { + where: GraphQLEdgeWhere["properties"]; + relationship: Relationship; + }): Filter | undefined { + const filters = Object.entries(where).flatMap(([fieldName, filters]) => { + if (fieldName === "AND" || fieldName === "OR" || fieldName === "NOT") { + const whereInLogicalOperator = asArray(filters) as Array; + const nestedFilters = whereInLogicalOperator.map((nestedWhere) => { + return this.createEdgePropertiesFilters({ + where: nestedWhere, + relationship, + }); + }); + + return new LogicalFilter({ + operation: fieldName, + filters: filterTruthy(nestedFilters), + }); + } + + const attribute = relationship.findAttribute(fieldName); + if (!attribute) return []; + const attributeAdapter = new AttributeAdapter(attribute); + return this.createPropertyFilters(attributeAdapter, filters, "relationship"); + }); + + return this.mergeFilters(filters); + } + + // TODO: remove adapter from here + private createPropertyFilters( + attribute: AttributeAdapter, + filters: GraphQLAttributeFilters | null, + attachedTo: "node" | "relationship" = "node" + ): Filter[] { + if (!filters) { + return []; + } + return Object.entries(filters).map(([key, value]) => { + if (key === "AND" || key === "OR" || key === "NOT") { + return new LogicalFilter({ + operation: key as LogicalOperators, + filters: this.createPropertyFilters(attribute, value), + }); + } + const operator = getFilterOperator(attribute, key); + if (!operator) { + throw new Error(`Invalid operator: ${key}`); + } + if (attribute.typeHelper.isDuration()) { + return new DurationFilter({ + attribute, + comparisonValue: value, + operator, + attachedTo, + }); + } + if (attribute.typeHelper.isSpatial()) { + return new SpatialFilter({ + attribute, + relationship: undefined, + comparisonValue: value, + operator, + attachedTo, + }); + } + return new PropertyFilter({ + attribute, + relationship: undefined, + comparisonValue: value, + operator, + attachedTo, + }); + }); + } + + private mergeFilters(filters: Filter[]): Filter | undefined { + if (filters.length == 0) { + return undefined; + } + if (filters.length === 1) { + return filters[0]; + } + + return new LogicalFilter({ + operation: "AND", + filters: filters, + }); + } +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts new file mode 100644 index 0000000000..679db95f39 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts @@ -0,0 +1,103 @@ +/* + * 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 { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; +import type { FilterOperator, RelationshipWhereOperator } from "../../translate/queryAST/ast/filters/Filter"; + +// TODO: remove Adapter dependency in v6 +export function getFilterOperator(attribute: AttributeAdapter, operator: string): FilterOperator | undefined { + if (attribute.typeHelper.isString() || attribute.typeHelper.isID()) { + return getStringOperator(operator); + } + + if (attribute.typeHelper.isBoolean()) { + return getBooleanOperator(operator); + } + + if (attribute.typeHelper.isNumeric() || attribute.typeHelper.isTemporal()) { + return getNumberOperator(operator); + } + + if (attribute.typeHelper.isSpatial()) { + return getSpatialOperator(operator); + } +} + +function getSpatialOperator(operator: string): FilterOperator | undefined { + // TODO: avoid this mapping + const spatialOperator = { + equals: "EQ", + in: "IN", + lt: "LT", + lte: "LTE", + gt: "GT", + gte: "GTE", + distance: "DISTANCE", + }; + + return spatialOperator[operator]; +} + +function getStringOperator(operator: string): FilterOperator | undefined { + // TODO: avoid this mapping + const stringOperatorMap = { + equals: "EQ", + in: "IN", + matches: "MATCHES", + contains: "CONTAINS", + startsWith: "STARTS_WITH", + endsWith: "ENDS_WITH", + } as const; + + return stringOperatorMap[operator]; +} + +function getNumberOperator(operator: string): FilterOperator | undefined { + // TODO: avoid this mapping + const numberOperatorMap = { + equals: "EQ", + in: "IN", + lt: "LT", + lte: "LTE", + gt: "GT", + gte: "GTE", + } as const; + + return numberOperatorMap[operator]; +} + +function getBooleanOperator(operator: string): FilterOperator | undefined { + // TODO: avoid this mapping + const numberOperatorMap = { + equals: "EQ", + } as const; + + return numberOperatorMap[operator]; +} + +export function getRelationshipOperator(operator: string): RelationshipWhereOperator { + const relationshipOperatorMap = { + all: "ALL", + none: "NONE", + single: "SINGLE", + some: "SOME", + } as const; + + return relationshipOperatorMap[operator]; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts new file mode 100644 index 0000000000..9613c729e1 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -0,0 +1,392 @@ +/* + * 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 { cursorToOffset } from "graphql-relay"; +import type { Integer } from "neo4j-driver"; +import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import type { LimitAnnotation } from "../../schema-model/annotation/LimitAnnotation"; +import type { Attribute } from "../../schema-model/attribute/Attribute"; +import { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; +import { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { Relationship } from "../../schema-model/relationship/Relationship"; +import { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { QueryAST } from "../../translate/queryAST/ast/QueryAST"; +import type { Field } from "../../translate/queryAST/ast/fields/Field"; +import { OperationField } from "../../translate/queryAST/ast/fields/OperationField"; +import { AttributeField } from "../../translate/queryAST/ast/fields/attribute-fields/AttributeField"; +import { DateTimeField } from "../../translate/queryAST/ast/fields/attribute-fields/DateTimeField"; +import { SpatialAttributeField } from "../../translate/queryAST/ast/fields/attribute-fields/SpatialAttributeField"; +import { Pagination } from "../../translate/queryAST/ast/pagination/Pagination"; +import { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection"; +import { RelationshipSelection } from "../../translate/queryAST/ast/selection/RelationshipSelection"; +import { WithWildCardsSelection } from "../../translate/queryAST/ast/selection/WithWildCardsSelection"; +import { PropertySort } from "../../translate/queryAST/ast/sort/PropertySort"; +import { filterTruthy } from "../../utils/utils"; +import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; +import { FilterFactory } from "./FilterFactory"; +import { FactoryParseError } from "./factory-parse-error"; +import type { GraphQLTreePoint } from "./resolve-tree-parser/graphql-tree/attributes"; +import type { + GraphQLTreeConnection, + GraphQLTreeConnectionTopLevel, + GraphQLTreeEdgeProperties, + GraphQLTreeNode, + GraphQLTreeReadOperation, + GraphQLTreeReadOperationTopLevel, +} from "./resolve-tree-parser/graphql-tree/graphql-tree"; +import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "./resolve-tree-parser/graphql-tree/sort"; + +export class ReadOperationFactory { + public schemaModel: Neo4jGraphQLSchemaModel; + private filterFactory: FilterFactory; + + constructor(schemaModel: Neo4jGraphQLSchemaModel) { + this.schemaModel = schemaModel; + this.filterFactory = new FilterFactory(schemaModel); + } + + public createAST({ + graphQLTree, + entity, + }: { + graphQLTree: GraphQLTreeReadOperationTopLevel; + entity: ConcreteEntity; + }): QueryAST { + const operation = this.generateOperation({ + graphQLTree, + entity, + }); + return new QueryAST(operation); + } + + public generateMutationProjection({ + graphQLTreeNode, + entity, + }: { + graphQLTreeNode: GraphQLTreeNode; + entity: ConcreteEntity; + }): V6ReadOperation { + const target = new ConcreteEntityAdapter(entity); + + const nodeResolveTree = graphQLTreeNode; + const nodeFields = this.getNodeFields(entity, nodeResolveTree); + + return new V6ReadOperation({ + target, + fields: { + edge: [], + node: nodeFields, + }, + filters: [], + selection: new WithWildCardsSelection(), + }); + } + + private generateOperation({ + graphQLTree, + entity, + }: { + graphQLTree: GraphQLTreeReadOperationTopLevel; + entity: ConcreteEntity; + }): V6ReadOperation { + const connectionTree = graphQLTree.fields.connection; + + if (!connectionTree) { + throw new Error("No Connection"); + } + + const target = new ConcreteEntityAdapter(entity); + const selection = new NodeSelection({ + target, + }); + + const nodeResolveTree = connectionTree.fields.edges?.fields.node; + const sortArgument = connectionTree.args.sort; + const pagination = this.getPagination(connectionTree.args, entity); + const nodeFields = this.getNodeFields(entity, nodeResolveTree); + const sortInputFields = this.getTopLevelSortInputFields({ + entity, + sortArgument, + }); + + const filters = filterTruthy([this.filterFactory.createFilters({ entity, where: graphQLTree.args.where })]); + return new V6ReadOperation({ + target, + selection, + fields: { + edge: [], + node: nodeFields, + }, + pagination, + sortFields: sortInputFields, + filters, + }); + } + + private generateRelationshipOperation({ + parsedTree, + relationship, + }: { + parsedTree: GraphQLTreeReadOperation; + relationship: Relationship; + }): V6ReadOperation { + const connectionTree = parsedTree.fields.connection; + if (!connectionTree) { + throw new Error("No Connection"); + } + + const relationshipAdapter = new RelationshipAdapter(relationship); + if (!(relationshipAdapter.target instanceof ConcreteEntityAdapter)) { + throw new FactoryParseError("Interfaces not supported"); + } + + // Selection + const selection = new RelationshipSelection({ + relationship: relationshipAdapter, + }); + + // Fields + const nodeResolveTree = connectionTree.fields.edges?.fields.node; + const propertiesResolveTree = connectionTree.fields.edges?.fields.properties; + + const edgeFields = this.getAttributeFields(relationship, propertiesResolveTree); + + const sortArgument = connectionTree.args.sort; + const relTarget = relationshipAdapter.target.entity; + const pagination = this.getPagination(connectionTree.args, relTarget); + + const nodeFields = this.getNodeFields(relTarget, nodeResolveTree); + const sortInputFields = this.getSortInputFields({ entity: relTarget, relationship, sortArgument }); + + const filters = filterTruthy([ + this.filterFactory.createFilters({ + entity: relationshipAdapter.target.entity, + relationship, + where: parsedTree.args.where, + }), + ]); + + return new V6ReadOperation({ + target: relationshipAdapter.target, + selection, + fields: { + edge: edgeFields, + node: nodeFields, + }, + sortFields: sortInputFields, + filters: filters, + pagination, + }); + } + + private getPagination( + connectionTreeArgs: GraphQLTreeConnection["args"] | GraphQLTreeConnectionTopLevel["args"], + entity: ConcreteEntity + ): Pagination | undefined { + const firstArgument = connectionTreeArgs.first; + const afterArgument = connectionTreeArgs.after ? cursorToOffset(connectionTreeArgs.after) : undefined; + const hasPagination = firstArgument ?? afterArgument; + const limitAnnotation = entity.annotations.limit; + if (hasPagination || limitAnnotation) { + const limit = this.calculatePaginationLimitArgument(firstArgument, limitAnnotation); + return new Pagination({ limit, skip: afterArgument }); + } + } + + /** + * Computes the value passed to the LIMIT clause in the Cypher Query by using the first argument and the limit annotation. + * - Branch 1: if the first argument is defined, use the first argument value, only if it is less than the limit annotation max value. + * - Branch 2: if not reached Branch 1, then use the following precedence: first argument, `@limit.default`, `@limit.max`. + * In case the first argument and `@limit` annotation are not defined, return undefined. + **/ + private calculatePaginationLimitArgument( + firstArgument: Integer | undefined, + limitAnnotation: LimitAnnotation | undefined + ): number | undefined { + if (firstArgument && limitAnnotation?.max) { + return Math.min(firstArgument.toNumber(), limitAnnotation.max); + } + return firstArgument?.toNumber() ?? limitAnnotation?.default ?? limitAnnotation?.max; + } + + private getAttributeFields(target: ConcreteEntity, propertiesTree: GraphQLTreeNode | undefined): Field[]; + private getAttributeFields(target: Relationship, propertiesTree: GraphQLTreeEdgeProperties | undefined): Field[]; + private getAttributeFields( + target: Relationship | ConcreteEntity, + propertiesTree: GraphQLTreeEdgeProperties | GraphQLTreeNode | undefined + ): Field[] { + if (!propertiesTree) { + return []; + } + + return filterTruthy( + Object.values(propertiesTree.fields).map((rawField) => { + let attribute: Attribute | undefined; + + const isGlobalId = rawField.name === "id" && target instanceof ConcreteEntity && target.globalIdField; + if (isGlobalId) { + attribute = target.globalIdField; + } else { + attribute = target.findAttribute(rawField.name); + } + + if (attribute) { + const field = rawField; + const attributeAdapter = new AttributeAdapter(attribute); + if (attributeAdapter.typeHelper.isDateTime()) { + return new DateTimeField({ + alias: rawField.alias, + attribute: attributeAdapter, + }); + } + if (attributeAdapter.typeHelper.isSpatial()) { + return new SpatialAttributeField({ + alias: rawField.alias, + attribute: attributeAdapter, + crs: Boolean((field as GraphQLTreePoint)?.fields?.crs), + }); + } + return new AttributeField({ + alias: rawField.alias, + attribute: attributeAdapter, + }); + } + return; + }) + ); + } + + private getRelationshipFields(entity: ConcreteEntity, nodeResolveTree: GraphQLTreeNode | undefined): Field[] { + if (!nodeResolveTree) { + return []; + } + + return filterTruthy( + Object.values(nodeResolveTree.fields).map((rawField) => { + const relationship = entity.findRelationship(rawField.name); + if (relationship) { + // FIX casting here + return this.generateRelationshipField(rawField as GraphQLTreeReadOperation, relationship); + } + }) + ); + } + + private getNodeFields(entity: ConcreteEntity, nodeResolveTree: GraphQLTreeNode | undefined): Field[] { + const attributeFields = this.getAttributeFields(entity, nodeResolveTree); + const relationshipFields = this.getRelationshipFields(entity, nodeResolveTree); + return [...attributeFields, ...relationshipFields]; + } + + private getSortInputFields({ + entity, + relationship, + sortArgument, + }: { + entity: ConcreteEntity; + relationship?: Relationship; + sortArgument: GraphQLSort[] | undefined; + }): Array<{ edge: PropertySort[]; node: PropertySort[] }> { + if (!sortArgument) { + return []; + } + return sortArgument.map(({ edges }): { edge: PropertySort[]; node: PropertySort[] } => { + return this.getEdgeSortInput({ + entity, + relationship, + edges, + }); + }); + } + private getTopLevelSortInputFields({ + entity, + relationship, + sortArgument, + }: { + entity: ConcreteEntity; + relationship?: Relationship; + sortArgument: GraphQLSortEdge[] | undefined; + }): Array<{ edge: PropertySort[]; node: PropertySort[] }> { + if (!sortArgument) { + return []; + } + return sortArgument.map((edges): { edge: PropertySort[]; node: PropertySort[] } => { + return this.getEdgeSortInput({ + entity, + relationship, + edges, + }); + }); + } + + private getEdgeSortInput({ + entity, + relationship, + edges, + }: { + entity: ConcreteEntity; + relationship?: Relationship; + edges: GraphQLSortEdge; + }): { edge: PropertySort[]; node: PropertySort[] } { + const nodeSortFields = edges.node ? this.getPropertiesSort({ target: entity, sortArgument: edges.node }) : []; + const edgeSortFields = + edges.properties && relationship + ? this.getPropertiesSort({ target: relationship, sortArgument: edges.properties }) + : []; + return { + edge: edgeSortFields, + node: nodeSortFields, + }; + } + + private getPropertiesSort({ + target, + sortArgument, + }: { + target: ConcreteEntity | Relationship; + sortArgument: GraphQLTreeSortElement; + }): PropertySort[] { + return Object.entries(sortArgument).map(([fieldName, direction]) => { + const attribute = target.findAttribute(fieldName); + if (!attribute) { + throw new Error(`Attribute not found: ${fieldName}`); + } + return new PropertySort({ + direction, + attribute: new AttributeAdapter(attribute), + }); + }); + } + + private generateRelationshipField( + resolveTree: GraphQLTreeReadOperation, + relationship: Relationship + ): OperationField { + const relationshipOperation = this.generateRelationshipOperation({ + relationship: relationship, + parsedTree: resolveTree, + }); + + return new OperationField({ + alias: resolveTree.alias, + operation: relationshipOperation, + }); + } +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts new file mode 100644 index 0000000000..85366a7dc7 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts @@ -0,0 +1,126 @@ +/* + * 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 { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import type { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import { QueryAST } from "../../translate/queryAST/ast/QueryAST"; +import { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection"; +import type { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; +import { UpdateProperty } from "../queryIR/MutationInput/UpdateProperty"; +import { V6UpdateOperation } from "../queryIR/UpdateOperation"; +import { FilterFactory } from "./FilterFactory"; +import { ReadOperationFactory } from "./ReadOperationFactory"; +import type { + GraphQLTreeUpdate, + GraphQLTreeUpdateField, + GraphQLTreeUpdateInput, +} from "./resolve-tree-parser/graphql-tree/graphql-tree"; +import { getAttribute } from "./utils/get-attribute"; + +export class UpdateOperationFactory { + public schemaModel: Neo4jGraphQLSchemaModel; + private readFactory: ReadOperationFactory; + private filterFactory: FilterFactory; + + constructor(schemaModel: Neo4jGraphQLSchemaModel) { + this.schemaModel = schemaModel; + this.readFactory = new ReadOperationFactory(schemaModel); + this.filterFactory = new FilterFactory(schemaModel); + } + + public createAST({ + graphQLTreeUpdate, + entity, + }: { + graphQLTreeUpdate: GraphQLTreeUpdate; + entity: ConcreteEntity; + }): QueryAST { + const operation = this.generateUpdateOperation({ + graphQLTreeUpdate, + entity, + }); + return new QueryAST(operation); + } + + private generateUpdateOperation({ + graphQLTreeUpdate, + entity, + }: { + graphQLTreeUpdate: GraphQLTreeUpdate; + entity: ConcreteEntity; + }): V6UpdateOperation { + const topLevelUpdateInput = graphQLTreeUpdate.args.input; + const targetAdapter = new ConcreteEntityAdapter(entity); + let projection: V6ReadOperation | undefined; + + if (graphQLTreeUpdate.fields) { + projection = this.readFactory.generateMutationProjection({ + graphQLTreeNode: graphQLTreeUpdate, + entity, + }); + } + const inputFields = this.getInputFields({ + target: targetAdapter, + updateInput: topLevelUpdateInput, + }); + + const filters = this.filterFactory.createFilters({ + entity, + where: graphQLTreeUpdate.args.where, + }); + + return new V6UpdateOperation({ + target: targetAdapter, + projection, + inputFields, + selection: new NodeSelection({ + target: targetAdapter, + }), + filters: filters ? [filters] : [], + }); + } + + private getInputFields({ + target, + updateInput, + }: { + target: ConcreteEntityAdapter; + updateInput: GraphQLTreeUpdateInput; + }): UpdateProperty[] { + return Object.entries(updateInput).flatMap(([attributeName, setOperations]) => { + const attribute = getAttribute(target, attributeName); + return this.getPropertyInputOperations(attribute, setOperations); + }); + } + private getPropertyInputOperations( + attribute: AttributeAdapter, + operations: GraphQLTreeUpdateField + ): UpdateProperty[] { + return Object.entries(operations).map(([_operation, value]) => { + // TODO: other operations + return new UpdateProperty({ + value, + attribute: attribute, + attachedTo: "node", + }); + }); + } +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/factory-parse-error.ts b/packages/graphql/src/api-v6/queryIRFactory/factory-parse-error.ts new file mode 100644 index 0000000000..d7dbc644d3 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/factory-parse-error.ts @@ -0,0 +1,19 @@ +/* + * 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 class FactoryParseError extends Error {} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-args.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-args.ts new file mode 100644 index 0000000000..2ed4f95131 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-args.ts @@ -0,0 +1,127 @@ +/* + * 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 { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../../schema-model/relationship/Relationship"; +import type { + GraphQLTreeConnection, + GraphQLTreeConnectionTopLevel, + GraphQLTreeReadOperation, + GraphQLTreeReadOperationTopLevel, +} from "../graphql-tree/graphql-tree"; +import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "../graphql-tree/sort"; +import { ResolveTreeParserError } from "../resolve-tree-parser-error"; + +export function parseOperationArgs(resolveTreeArgs: Record): GraphQLTreeReadOperation["args"] { + // Not properly parsed, assuming the type is the same + return { + where: resolveTreeArgs.where, + }; +} + +export function parseOperationArgsTopLevel( + resolveTreeArgs: Record +): GraphQLTreeReadOperationTopLevel["args"] { + // Not properly parsed, assuming the type is the same + return { + where: resolveTreeArgs.where, + }; +} + +export function parseConnectionArgs( + resolveTreeArgs: { [str: string]: any }, + entity: ConcreteEntity, + relationship: Relationship +): GraphQLTreeConnection["args"] { + let sortArg: GraphQLSort[] | undefined; + if (resolveTreeArgs.sort) { + sortArg = resolveTreeArgs.sort.map((sortArg): GraphQLSort => { + return { edges: parseSortEdges(sortArg.edges, entity, relationship) }; + }); + } + return { + sort: sortArg, + first: resolveTreeArgs.first, + after: resolveTreeArgs.after, + }; +} + +export function parseConnectionArgsTopLevel( + resolveTreeArgs: Record, + entity: ConcreteEntity +): GraphQLTreeConnectionTopLevel["args"] { + let sortArg: GraphQLSortEdge[] | undefined; + if (resolveTreeArgs.sort) { + sortArg = resolveTreeArgs.sort.map((sortArg) => { + return parseSortEdges(sortArg, entity); + }); + } + + return { + sort: sortArg, + first: resolveTreeArgs.first, + after: resolveTreeArgs.after, + }; +} + +function parseSortEdges( + sortEdges: { + node: Record | undefined; + properties: Record | undefined; + }, + targetNode: ConcreteEntity, + targetRelationship?: Relationship +): GraphQLSortEdge { + const sortFields: GraphQLSortEdge = {}; + const nodeFields = sortEdges.node; + + if (nodeFields) { + const fields = parseSortFields(targetNode, nodeFields); + sortFields.node = fields; + } + const edgeProperties = sortEdges.properties; + + if (edgeProperties && targetRelationship) { + const fields = parseSortFields(targetRelationship, edgeProperties); + sortFields.properties = fields; + } + return sortFields; +} + +function parseSortFields( + target: Relationship | ConcreteEntity, + sortObject: Record +): GraphQLTreeSortElement { + return Object.fromEntries( + Object.entries(sortObject).map(([fieldName, resolveTreeDirection]) => { + if (target.hasAttribute(fieldName)) { + const direction = parseDirection(resolveTreeDirection); + return [fieldName, direction]; + } + throw new ResolveTreeParserError(`Invalid sort field: ${fieldName}`); + }) + ); +} + +function parseDirection(direction: string): "ASC" | "DESC" { + if (direction === "ASC" || direction === "DESC") { + return direction; + } + throw new ResolveTreeParserError(`Invalid sort direction: ${direction}`); +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-create-args.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-create-args.ts new file mode 100644 index 0000000000..e4d86c5c86 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-create-args.ts @@ -0,0 +1,35 @@ +/* + * 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 { GraphQLTreeCreate, GraphQLTreeCreateInput } from "../graphql-tree/graphql-tree"; +import { ResolveTreeParserError } from "../resolve-tree-parser-error"; + +export function parseCreateOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLTreeCreate["args"] { + return { + input: parseCreateOperationInput(resolveTreeArgs.input), + }; +} +function parseCreateOperationInput(resolveTreeCreateInput: any): GraphQLTreeCreateInput[] { + if (!resolveTreeCreateInput || !Array.isArray(resolveTreeCreateInput)) { + throw new ResolveTreeParserError(`Invalid create input field: ${resolveTreeCreateInput}`); + } + return resolveTreeCreateInput.map((input) => { + return input.node; + }); +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-update-args.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-update-args.ts new file mode 100644 index 0000000000..be16f3315d --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-update-args.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 { GraphQLTreeUpdate, GraphQLTreeUpdateInput } from "../graphql-tree/graphql-tree"; +import { ResolveTreeParserError } from "../resolve-tree-parser-error"; + +export function parseUpdateOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLTreeUpdate["args"] { + return { + input: parseUpdateOperationInput(resolveTreeArgs.input), + where: resolveTreeArgs.where, + }; +} + +function parseUpdateOperationInput(resolveTreeUpdateInput: any): GraphQLTreeUpdateInput { + if (!resolveTreeUpdateInput) { + throw new ResolveTreeParserError(`Invalid update input field: ${resolveTreeUpdateInput}`); + } + + return resolveTreeUpdateInput.node; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/attributes.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/attributes.ts new file mode 100644 index 0000000000..01be3be588 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/attributes.ts @@ -0,0 +1,48 @@ +/* + * 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 { GraphQLTreeElement } from "./tree-element"; + +export type GraphQLTreeLeafField = GraphQLTreeScalarField | GraphQLTreePoint | GraphQLTreeCartesianPoint; + +export interface GraphQLTreeScalarField extends GraphQLTreeElement { + name: string; +} + +export interface GraphQLTreePoint extends GraphQLTreeElement { + fields: { + longitude: GraphQLTreeScalarField | undefined; + latitude: GraphQLTreeScalarField | undefined; + height: GraphQLTreeScalarField | undefined; + crs: GraphQLTreeScalarField | undefined; + srid: GraphQLTreeScalarField | undefined; + }; + name: string; +} + +export interface GraphQLTreeCartesianPoint extends GraphQLTreeElement { + fields: { + x: GraphQLTreeScalarField | undefined; + y: GraphQLTreeScalarField | undefined; + z: GraphQLTreeScalarField | undefined; + crs: GraphQLTreeScalarField | undefined; + srid: GraphQLTreeScalarField | undefined; + }; + name: string; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree.ts new file mode 100644 index 0000000000..bcfa8366b0 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree.ts @@ -0,0 +1,112 @@ +/* + * 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 { Integer } from "neo4j-driver"; +import type { GraphQLTreeLeafField } from "./attributes"; +import type { GraphQLSort, GraphQLSortEdge } from "./sort"; +import type { GraphQLTreeElement } from "./tree-element"; +import type { GraphQLWhere, GraphQLWhereTopLevel } from "./where"; + +// TODO GraphQLTreeCreateInput should be a union of PrimitiveTypes and relationship fields +export type GraphQLTreeCreateInput = Record; +export type GraphQLTreeUpdateInput = Record; +export type GraphQLTreeDeleteInput = Record; + +export type UpdateOperation = "set"; +export type GraphQLTreeUpdateField = Record; + +export interface GraphQLTreeDelete extends GraphQLTreeNode { + name: string; + args: { + where?: GraphQLWhereTopLevel; + input?: GraphQLTreeDeleteInput; + }; +} + +export interface GraphQLTreeCreate extends GraphQLTreeNode { + name: string; + args: { + input: GraphQLTreeCreateInput[]; + }; +} + +export interface GraphQLTreeUpdate extends GraphQLTreeNode { + name: string; + args: { + where: GraphQLWhereTopLevel; + input: GraphQLTreeUpdateInput; + }; +} + +export interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { + name: string; + fields: { + connection?: GraphQLTreeConnectionTopLevel; + }; + args: { + where?: GraphQLWhereTopLevel; + }; +} + +export interface GraphQLTreeReadOperation extends GraphQLTreeElement { + name: string; + fields: { + connection?: GraphQLTreeConnection; + }; + args: { + where?: GraphQLWhere; + }; +} + +export interface GraphQLTreeConnection extends GraphQLTreeElement { + fields: { + edges?: GraphQLTreeEdge; + }; + args: { + sort?: GraphQLSort[]; + first?: Integer; + after?: string; + }; +} + +export interface GraphQLTreeConnectionTopLevel extends GraphQLTreeElement { + fields: { + edges?: GraphQLTreeEdge; + }; + args: { + sort?: GraphQLSortEdge[]; + first?: Integer; + after?: string; + }; +} + +export interface GraphQLTreeEdge extends GraphQLTreeElement { + fields: { + node?: GraphQLTreeNode; + properties?: GraphQLTreeEdgeProperties; + }; +} + +export interface GraphQLTreeNode extends GraphQLTreeElement { + fields: Record; +} + +export interface GraphQLTreeEdgeProperties extends GraphQLTreeElement { + fields: Record; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/sort.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/sort.ts new file mode 100644 index 0000000000..d309ca8bd5 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/sort.ts @@ -0,0 +1,31 @@ +/* + * 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 interface GraphQLSort { + edges: GraphQLSortEdge; +} + +export interface GraphQLSortEdge { + node?: GraphQLTreeSortElement; + properties?: GraphQLTreeSortElement; +} + +export interface GraphQLTreeSortElement { + [key: string]: "ASC" | "DESC"; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/tree-element.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/tree-element.ts new file mode 100644 index 0000000000..4ab4fe0d95 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/tree-element.ts @@ -0,0 +1,23 @@ +/* + * 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 interface GraphQLTreeElement { + alias: string; + args: Record; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/where.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/where.ts new file mode 100644 index 0000000000..766c5def17 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/where.ts @@ -0,0 +1,73 @@ +/* + * 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. + */ + +/** Args for `where` in nested connections (with edge -> node) */ +export type GraphQLWhere = WithLogicalOperations<{ + edges?: GraphQLEdgeWhere; +}>; + +/** Args for `where` in top level connections only (i.e. no edge available) */ +export type GraphQLWhereTopLevel = WithLogicalOperations<{ + node?: GraphQLNodeWhere; +}>; + +export type GraphQLEdgeWhere = WithLogicalOperations<{ + properties?: Record; + node?: GraphQLNodeWhere; +}>; + +export type GraphQLNodeWhere = WithLogicalOperations>; +export type GraphQLNodeFilters = GraphQLAttributeFilters | RelationshipFilters; + +export type GraphQLAttributeFilters = StringFilters | NumberFilters; + +export type StringFilters = WithLogicalOperations<{ + equals?: string; + in?: string[]; + matches?: string; + contains?: string; + startsWith?: string; + endsWith?: string; +}>; + +export type NumberFilters = WithLogicalOperations<{ + equals?: string; + in?: string[]; + lt?: string; + lte?: string; + gt?: string; + gte?: string; +}>; + +export type RelationshipFilters = { + some?: RelationshipEdgeWhere; + single?: RelationshipEdgeWhere; + all?: RelationshipEdgeWhere; + none?: RelationshipEdgeWhere; +}; + +type RelationshipEdgeWhere = { + edges: GraphQLEdgeWhere; +}; + +type WithLogicalOperations = { + AND?: Array>; + OR?: Array>; + NOT?: WithLogicalOperations; +} & T; diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-attribute-fields.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-attribute-fields.ts new file mode 100644 index 0000000000..6578cda46a --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-attribute-fields.ts @@ -0,0 +1,129 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; +import { Point } from "neo4j-driver"; +import { CartesianPoint } from "../../../graphql/objects/CartesianPoint"; +import type { Attribute } from "../../../schema-model/attribute/Attribute"; +import { ListType } from "../../../schema-model/attribute/AttributeType"; +import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../schema-model/relationship/Relationship"; +import type { + GraphQLTreeCartesianPoint, + GraphQLTreeLeafField, + GraphQLTreePoint, + GraphQLTreeScalarField, +} from "./graphql-tree/attributes"; +import { findFieldByName } from "./utils/find-field-by-name"; + +export function parseAttributeField( + resolveTree: ResolveTree, + entity: ConcreteEntity | Relationship +): GraphQLTreeLeafField | GraphQLTreePoint | undefined { + const globalIdField = parseGlobalIdField(resolveTree, entity); + if (globalIdField) { + return globalIdField; + } + + if (entity.hasAttribute(resolveTree.name)) { + const attribute = entity.findAttribute(resolveTree.name) as Attribute; + const wrappedTypeName = attribute.type instanceof ListType ? attribute.type.ofType.name : attribute.type.name; + if (wrappedTypeName === "Point") { + return parsePointField(resolveTree); + } + if (wrappedTypeName === "CartesianPoint") { + return parseCartesianPointField(resolveTree); + } + return { + alias: resolveTree.alias, + args: resolveTree.args, + name: resolveTree.name, + fields: undefined, + }; + } +} + +function parseGlobalIdField( + resolveTree: ResolveTree, + entity: ConcreteEntity | Relationship +): GraphQLTreeLeafField | undefined { + if (resolveTree.name === "id") { + if (entity instanceof ConcreteEntity && entity.globalIdField) { + return { + alias: entity.globalIdField.name, + args: resolveTree.args, + name: resolveTree.name, + fields: undefined, + }; + } + } +} + +function parsePointField(resolveTree: ResolveTree): GraphQLTreePoint { + const longitude = findFieldByName(resolveTree, Point.name, "longitude"); + const latitude = findFieldByName(resolveTree, Point.name, "latitude"); + const height = findFieldByName(resolveTree, Point.name, "height"); + const crs = findFieldByName(resolveTree, Point.name, "crs"); + const srid = findFieldByName(resolveTree, Point.name, "srid"); + + return { + alias: resolveTree.alias, + args: resolveTree.args, + name: resolveTree.name, + fields: { + longitude: resolveTreeToLeafField(longitude), + latitude: resolveTreeToLeafField(latitude), + height: resolveTreeToLeafField(height), + crs: resolveTreeToLeafField(crs), + srid: resolveTreeToLeafField(srid), + }, + }; +} + +function parseCartesianPointField(resolveTree: ResolveTree): GraphQLTreeCartesianPoint { + const x = findFieldByName(resolveTree, CartesianPoint.name, "x"); + const y = findFieldByName(resolveTree, CartesianPoint.name, "y"); + const z = findFieldByName(resolveTree, CartesianPoint.name, "z"); + const crs = findFieldByName(resolveTree, CartesianPoint.name, "crs"); + const srid = findFieldByName(resolveTree, CartesianPoint.name, "srid"); + + return { + alias: resolveTree.alias, + args: resolveTree.args, + name: resolveTree.name, + fields: { + x: resolveTreeToLeafField(x), + y: resolveTreeToLeafField(y), + z: resolveTreeToLeafField(z), + crs: resolveTreeToLeafField(crs), + srid: resolveTreeToLeafField(srid), + }, + }; +} + +function resolveTreeToLeafField(resolveTree: ResolveTree | undefined): GraphQLTreeScalarField | undefined { + if (!resolveTree) { + return undefined; + } + return { + alias: resolveTree.alias, + args: resolveTree.args, + name: resolveTree.name, + }; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-edges.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-edges.ts new file mode 100644 index 0000000000..afe70c662d --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-edges.ts @@ -0,0 +1,87 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; +import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import { Relationship } from "../../../schema-model/relationship/Relationship"; +import type { GraphQLTreeLeafField } from "./graphql-tree/attributes"; +import type { GraphQLTreeEdge, GraphQLTreeEdgeProperties } from "./graphql-tree/graphql-tree"; +import { parseAttributeField } from "./parse-attribute-fields"; +import { parseNode } from "./parse-node"; +import { ResolveTreeParserError } from "./resolve-tree-parser-error"; +import { findFieldByName } from "./utils/find-field-by-name"; + +export function parseEdges(resolveTree: ResolveTree, entity: Relationship | ConcreteEntity): GraphQLTreeEdge { + const nodeTarget = entity instanceof Relationship ? (entity.target as ConcreteEntity) : entity; + + const edgeType = entity.typeNames.edge; + + const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); + const resolveTreeProperties = findFieldByName(resolveTree, edgeType, "properties"); + + const nodeFields = nodeResolveTree ? parseNode(nodeResolveTree, nodeTarget) : undefined; + + let edgeProperties: GraphQLTreeEdgeProperties | undefined; + if (entity instanceof Relationship) { + edgeProperties = resolveTreeProperties ? parseEdgeProperties(resolveTreeProperties, entity) : undefined; + } + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: { + node: nodeFields, + properties: edgeProperties, + }, + }; +} + +function parseEdgeProperties( + resolveTree: ResolveTree, + relationship: Relationship +): GraphQLTreeEdgeProperties | undefined { + if (!relationship.typeNames.properties) { + return; + } + const fieldsResolveTree = resolveTree.fieldsByTypeName[relationship.typeNames.properties] ?? {}; + + const fields = getEdgePropertyFields(fieldsResolveTree, relationship); + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: fields, + }; +} + +function getEdgePropertyFields( + fields: Record, + relationship: Relationship +): Record { + const propertyFields: Record = {}; + for (const [key, fieldResolveTree] of Object.entries(fields)) { + const fieldName = fieldResolveTree.name; + const field = parseAttributeField(fieldResolveTree, relationship); + if (!field) { + throw new ResolveTreeParserError(`${fieldName} is not an attribute of edge`); + } + propertyFields[key] = field; + } + return propertyFields; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-global-resolve-info-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-global-resolve-info-tree.ts new file mode 100644 index 0000000000..014d64cd1f --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-global-resolve-info-tree.ts @@ -0,0 +1,66 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; +import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { GraphQLTreeReadOperationTopLevel } from "./graphql-tree/graphql-tree"; +import { parseNode } from "./parse-node"; + +/** Parses the resolve info tree for a global node query */ +export function parseGlobalNodeResolveInfoTree({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTreeReadOperationTopLevel { + const entityTypes = entity.typeNames; + resolveTree.fieldsByTypeName[entityTypes.node] = { + ...resolveTree.fieldsByTypeName["Node"], + ...resolveTree.fieldsByTypeName[entityTypes.node], + }; + const node = resolveTree ? parseNode(resolveTree, entity) : undefined; + + return { + alias: resolveTree.alias, + args: { + where: { + node: { + id: { equals: resolveTree.args.id as any }, + }, + }, + }, + name: resolveTree.name, + fields: { + connection: { + alias: "connection", + args: {}, + fields: { + edges: { + alias: "edges", + args: {}, + fields: { + node, + }, + }, + }, + }, + }, + }; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-node.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-node.ts new file mode 100644 index 0000000000..d1b0c61e9e --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-node.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 { ResolveTree } from "graphql-parse-resolve-info"; +import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { GraphQLTreeLeafField } from "./graphql-tree/attributes"; +import type { GraphQLTreeNode, GraphQLTreeReadOperation } from "./graphql-tree/graphql-tree"; +import { parseAttributeField } from "./parse-attribute-fields"; +import { parseRelationshipField } from "./parse-resolve-info-tree"; +import { ResolveTreeParserError } from "./resolve-tree-parser-error"; + +export function parseNode(resolveTree: ResolveTree, targetNode: ConcreteEntity): GraphQLTreeNode { + const entityTypes = targetNode.typeNames; + const fieldsResolveTree = resolveTree.fieldsByTypeName[entityTypes.node] ?? {}; + + const fields = getNodeFields(fieldsResolveTree, targetNode); + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: fields, + }; +} + +export function getNodeFields( + fields: Record, + targetNode: ConcreteEntity +): Record { + const propertyFields: Record = {}; + for (const [key, fieldResolveTree] of Object.entries(fields)) { + const fieldName = fieldResolveTree.name; + const field = + parseRelationshipField(fieldResolveTree, targetNode) ?? parseAttributeField(fieldResolveTree, targetNode); + if (!field) { + throw new ResolveTreeParserError(`${fieldName} is not a field of node`); + } + propertyFields[key] = field; + } + return propertyFields; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-resolve-info-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-resolve-info-tree.ts new file mode 100644 index 0000000000..da73beb3f1 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-resolve-info-tree.ts @@ -0,0 +1,200 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; +import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../schema-model/relationship/Relationship"; +import { + parseConnectionArgs, + parseConnectionArgsTopLevel, + parseOperationArgs, + parseOperationArgsTopLevel, +} from "./argument-parser/parse-args"; +import { parseCreateOperationArgsTopLevel } from "./argument-parser/parse-create-args"; +import { parseUpdateOperationArgsTopLevel } from "./argument-parser/parse-update-args"; +import type { + GraphQLTreeConnection, + GraphQLTreeConnectionTopLevel, + GraphQLTreeCreate, + GraphQLTreeDelete, + GraphQLTreeReadOperation, + GraphQLTreeReadOperationTopLevel, + GraphQLTreeUpdate, +} from "./graphql-tree/graphql-tree"; +import { parseEdges } from "./parse-edges"; +import { getNodeFields } from "./parse-node"; +import { findFieldByName } from "./utils/find-field-by-name"; + +export function parseResolveInfoTree({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTreeReadOperationTopLevel { + const connectionResolveTree = findFieldByName(resolveTree, entity.typeNames.connectionOperation, "connection"); + const connection = connectionResolveTree ? parseTopLevelConnection(connectionResolveTree, entity) : undefined; + const connectionOperationArgs = parseOperationArgsTopLevel(resolveTree.args); + + return { + alias: resolveTree.alias, + args: connectionOperationArgs, + name: resolveTree.name, + fields: { + connection, + }, + }; +} + +export function parseResolveInfoTreeCreate({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTreeCreate { + const entityTypes = entity.typeNames; + const createResponse = findFieldByName(resolveTree, entityTypes.createResponse, entityTypes.queryField); + const createArgs = parseCreateOperationArgsTopLevel(resolveTree.args); + if (!createResponse) { + return { + alias: resolveTree.alias, + name: resolveTree.name, + args: createArgs, + fields: {}, + }; + } + const fieldsResolveTree = createResponse.fieldsByTypeName[entityTypes.node] ?? {}; + const fields = getNodeFields(fieldsResolveTree, entity); + return { + alias: resolveTree.alias, + name: resolveTree.name, + args: createArgs, + fields, + }; +} +export function parseResolveInfoTreeUpdate({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTreeUpdate { + const entityTypes = entity.typeNames; + const createResponse = findFieldByName(resolveTree, entityTypes.updateResponse, entityTypes.queryField); + const createArgs = parseUpdateOperationArgsTopLevel(resolveTree.args); + if (!createResponse) { + return { + alias: resolveTree.alias, + name: resolveTree.name, + args: createArgs, + fields: {}, + }; + } + const fieldsResolveTree = createResponse.fieldsByTypeName[entityTypes.node] ?? {}; + const fields = getNodeFields(fieldsResolveTree, entity); + return { + alias: resolveTree.alias, + name: resolveTree.name, + args: createArgs, + fields, + }; +} + +export function parseResolveInfoTreeDelete({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTreeDelete { + const entityTypes = entity.typeNames; + const deleteResponse = findFieldByName(resolveTree, "DeleteResponse", entityTypes.queryField); + const deleteArgs = parseOperationArgsTopLevel(resolveTree.args); + if (!deleteResponse) { + return { + alias: resolveTree.alias, + name: resolveTree.name, + args: deleteArgs, + fields: {}, + }; + } + const fieldsResolveTree = deleteResponse.fieldsByTypeName[entityTypes.node] ?? {}; + const fields = getNodeFields(fieldsResolveTree, entity); + return { + alias: resolveTree.alias, + name: resolveTree.name, + args: deleteArgs, + fields, + }; +} + +export function parseConnection(resolveTree: ResolveTree, entity: Relationship): GraphQLTreeConnection { + const entityTypes = entity.typeNames; + const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges"); + const edgeResolveTree = edgesResolveTree ? parseEdges(edgesResolveTree, entity) : undefined; + const connectionArgs = parseConnectionArgs(resolveTree.args, entity.target as ConcreteEntity, entity); + return { + alias: resolveTree.alias, + args: connectionArgs, + fields: { + edges: edgeResolveTree, + }, + }; +} + +export function parseRelationshipField( + resolveTree: ResolveTree, + entity: ConcreteEntity +): GraphQLTreeReadOperation | undefined { + const relationship = entity.findRelationship(resolveTree.name); + if (!relationship) { + return; + } + const connectionResolveTree = findFieldByName( + resolveTree, + relationship.typeNames.connectionOperation, + "connection" + ); + const connection = connectionResolveTree ? parseConnection(connectionResolveTree, relationship) : undefined; + const connectionOperationArgs = parseOperationArgs(resolveTree.args); + return { + alias: resolveTree.alias, + args: connectionOperationArgs, + name: resolveTree.name, + fields: { + connection, + }, + }; +} + +function parseTopLevelConnection(resolveTree: ResolveTree, entity: ConcreteEntity): GraphQLTreeConnectionTopLevel { + const entityTypes = entity.typeNames; + const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges"); + const edgeResolveTree = edgesResolveTree ? parseEdges(edgesResolveTree, entity) : undefined; + const connectionArgs = parseConnectionArgsTopLevel(resolveTree.args, entity); + + return { + alias: resolveTree.alias, + args: connectionArgs, + fields: { + edges: edgeResolveTree, + }, + }; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/resolve-tree-parser-error.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/resolve-tree-parser-error.ts new file mode 100644 index 0000000000..130911fef7 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/resolve-tree-parser-error.ts @@ -0,0 +1,20 @@ +/* + * 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 class ResolveTreeParserError extends Error {} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/utils/find-field-by-name.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/utils/find-field-by-name.ts new file mode 100644 index 0000000000..4cda5630e6 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/utils/find-field-by-name.ts @@ -0,0 +1,28 @@ +/* + * 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 { ResolveTree } from "graphql-parse-resolve-info"; + +/** Returns the field of the resolve tree by passing the typename and name */ +export function findFieldByName(resolveTree: ResolveTree, typeName: string, name: string): ResolveTree | undefined { + const fieldsByTypeName = resolveTree.fieldsByTypeName[typeName] ?? {}; + return Object.values(fieldsByTypeName).find((field) => { + return field.name === name; + }); +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/utils/get-attribute.ts b/packages/graphql/src/api-v6/queryIRFactory/utils/get-attribute.ts new file mode 100644 index 0000000000..5b680955c6 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/utils/get-attribute.ts @@ -0,0 +1,33 @@ +/* + * 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 { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; +import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import { FactoryParseError } from "../factory-parse-error"; + +/** + * Get the attribute from the entity, in case it doesn't exist throw an error + **/ +export function getAttribute(entity: ConcreteEntityAdapter, key: string): AttributeAdapter { + const attribute = entity.attributes.get(key); + if (!attribute) { + throw new FactoryParseError(`Transpile Error: Input field ${key} not found in entity ${entity.name}`); + } + return attribute; +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/utils/raise-on-conflicting-input.ts b/packages/graphql/src/api-v6/queryIRFactory/utils/raise-on-conflicting-input.ts new file mode 100644 index 0000000000..59e3f96071 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/utils/raise-on-conflicting-input.ts @@ -0,0 +1,45 @@ +/* + * 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 { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../schema-model/relationship/Relationship"; +import { FactoryParseError } from "../factory-parse-error"; +import type { GraphQLTreeCreateInput } from "../resolve-tree-parser/graphql-tree/graphql-tree"; + +export function raiseOnConflictingInput( + input: GraphQLTreeCreateInput, // TODO: add Update types as well + entityOrRel: ConcreteEntity | Relationship +): void { + const hash = {}; + const properties = Object.keys(input); + properties.forEach((property) => { + const dbName = entityOrRel.findAttribute(property)?.databaseName; + if (dbName === undefined) { + throw new FactoryParseError(`Impossible to translate property ${property} on entity ${entityOrRel.name}`); + } + if (hash[dbName]) { + throw new FactoryParseError( + `Conflicting modification of ${[hash[dbName], property].map((n) => `[[${n}]]`).join(", ")} on type ${ + entityOrRel.name + }` + ); + } + hash[dbName] = property; + }); +} diff --git a/packages/graphql/src/api-v6/resolvers/connection-operation-resolver.ts b/packages/graphql/src/api-v6/resolvers/connection-operation-resolver.ts new file mode 100644 index 0000000000..19fa210da6 --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/connection-operation-resolver.ts @@ -0,0 +1,45 @@ +/* + * 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 { GraphQLResolveInfo } from "graphql"; +import type { PageInfo } from "graphql-relay"; +import { createConnectionWithEdgeProperties } from "../../schema/pagination"; +import type { ConnectionQueryArgs } from "../../types"; +import { isNeoInt } from "../../utils/utils"; + +/** Maps the connection results adding pageInfo and cursors */ +export function connectionOperationResolver(source, args: ConnectionQueryArgs, _ctx, _info: GraphQLResolveInfo) { + const totalCount = isNeoInt(source.connection.totalCount) + ? source.connection.totalCount.toNumber() + : source.connection.totalCount; + + const connection = createConnectionWithEdgeProperties({ + selectionSet: undefined, + source: source.connection, + args: { first: args.first, after: args.after }, + totalCount, + }); + const edges = connection.edges as any[]; + const pageInfo = connection.pageInfo as PageInfo; + + return { + edges, + pageInfo, + }; +} diff --git a/packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts b/packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts new file mode 100644 index 0000000000..692e964917 --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts @@ -0,0 +1,43 @@ +/* + * 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 { GraphQLResolveInfo } from "graphql"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { ConnectionQueryArgs } from "../../types"; +import { toGlobalId } from "../../utils/global-ids"; + +/** Maps the database id field to globalId */ +export function generateGlobalIdFieldResolver({ entity }: { entity: ConcreteEntity }) { + return function resolve(source, _args: ConnectionQueryArgs, _ctx, _info: GraphQLResolveInfo) { + const globalAttribute = entity.globalIdField; + if (!globalAttribute) { + throw new Error("Global Id Field not found"); + } + + const field = globalAttribute.name; + const value = source[field] as string | number; + + const globalId = toGlobalId({ + typeName: entity.name, + field, + id: value, + }); + return globalId; + }; +} diff --git a/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts b/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts new file mode 100644 index 0000000000..02e9c9b20b --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts @@ -0,0 +1,70 @@ +/* + * 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 { GraphQLResolveInfo } from "graphql"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { GlobalNodeArgs } from "../../types"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { execute } from "../../utils"; +import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree"; +import { fromGlobalId } from "../../utils/global-ids"; +import { parseGlobalNodeResolveInfoTree } from "../queryIRFactory/resolve-tree-parser/parse-global-resolve-info-tree"; +import { translateReadOperation } from "../translators/translate-read-operation"; + +/** Maps the database id field to globalId */ +export function generateGlobalNodeResolver({ globalEntities }: { globalEntities: ConcreteEntity[] }) { + return async function resolve( + _source, + args: GlobalNodeArgs, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) { + const resolveTree = getNeo4jResolveTree(info, { args }); + context.resolveTree = resolveTree; + + const { typeName, field, id } = fromGlobalId(args.id); + if (!typeName || !field || !id) return null; + + const entity = globalEntities.find((n) => n.name === typeName); + if (!entity) return null; + + const graphQLTree = parseGlobalNodeResolveInfoTree({ resolveTree: context.resolveTree, entity }); + const { cypher, params } = translateReadOperation({ + context: context, + graphQLTree, + entity, + }); + const executeResult = await execute({ + cypher, + params, + defaultAccessMode: "READ", + context, + info, + }); + + let obj = null; + + const thisValue = executeResult.records[0]?.this.connection.edges[0].node; + + if (executeResult.records.length && thisValue) { + obj = { ...thisValue, id: args.id, __resolveType: entity.name }; + } + return obj; + }; +} diff --git a/packages/graphql/src/api-v6/resolvers/read-resolver.ts b/packages/graphql/src/api-v6/resolvers/read-resolver.ts new file mode 100644 index 0000000000..0ccae39a14 --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/read-resolver.ts @@ -0,0 +1,53 @@ +/* + * 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 { GraphQLResolveInfo } from "graphql"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { execute } from "../../utils"; +import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree"; +import { parseResolveInfoTree } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; +import { translateReadOperation } from "../translators/translate-read-operation"; + +export function generateReadResolver({ entity }: { entity: ConcreteEntity }) { + return async function resolve( + _root: any, + args: any, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) { + const resolveTree = getNeo4jResolveTree(info, { args }); + context.resolveTree = resolveTree; + const graphQLTree = parseResolveInfoTree({ resolveTree: context.resolveTree, entity }); + const { cypher, params } = translateReadOperation({ + context: context, + graphQLTree, + entity, + }); + const executeResult = await execute({ + cypher, + params, + defaultAccessMode: "READ", + context, + info, + }); + + return executeResult.records[0]?.this; + }; +} diff --git a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts new file mode 100644 index 0000000000..9491af47e6 --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts @@ -0,0 +1,60 @@ +/* + * 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 { GraphQLResolveInfo } from "graphql"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { execute } from "../../utils"; +import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree"; +import { parseResolveInfoTreeCreate } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; +import { translateCreateOperation } from "../translators/translate-create-operation"; + +export function generateCreateResolver({ entity }: { entity: ConcreteEntity }) { + return async function resolve( + _root: any, + args: any, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) { + const resolveTree = getNeo4jResolveTree(info, { args }); + context.resolveTree = resolveTree; + const graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity }); + const { cypher, params } = translateCreateOperation({ + context: context, + graphQLTreeCreate, + entity, + }); + const executeResult = await execute({ + cypher, + params, + defaultAccessMode: "WRITE", + context, + info, + }); + + return { + [entity.typeNames.queryField]: executeResult.records[0]?.data.connection.edges.map( + (edge: any) => edge.node + ), + info: { + ...executeResult.statistics, + }, + }; + }; +} diff --git a/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts new file mode 100644 index 0000000000..0df06f1250 --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts @@ -0,0 +1,57 @@ +/* + * 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 { GraphQLResolveInfo } from "graphql"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { execute } from "../../utils"; +import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree"; +import { parseResolveInfoTreeDelete } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; +import { translateDeleteResolver } from "../translators/translate-delete-operation"; + +export function generateDeleteResolver({ entity }: { entity: ConcreteEntity }) { + return async function resolve( + _root: any, + args: any, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) { + const resolveTree = getNeo4jResolveTree(info, { args }); + context.resolveTree = resolveTree; + // TODO: Implement delete resolver + const graphQLTreeDelete = parseResolveInfoTreeDelete({ resolveTree: context.resolveTree, entity }); + const { cypher, params } = translateDeleteResolver({ + context: context, + graphQLTreeDelete, + entity, + }); + const executeResult = await execute({ + cypher, + params, + defaultAccessMode: "WRITE", + context, + info, + }); + return { + info: { + ...executeResult.statistics, + }, + }; + }; +} diff --git a/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts new file mode 100644 index 0000000000..e3d63686c1 --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts @@ -0,0 +1,59 @@ +/* + * 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 { GraphQLResolveInfo } from "graphql"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { execute } from "../../utils"; +import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree"; +import { parseResolveInfoTreeUpdate } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; +import { translateUpdateOperation } from "../translators/translate-update-operation"; + +export function generateUpdateResolver({ entity }: { entity: ConcreteEntity }) { + return async function resolve( + _root: any, + args: any, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) { + const resolveTree = getNeo4jResolveTree(info, { args }); + context.resolveTree = resolveTree; + const graphQLTreeUpdate = parseResolveInfoTreeUpdate({ resolveTree: context.resolveTree, entity }); + const { cypher, params } = translateUpdateOperation({ + context: context, + graphQLTreeUpdate, + entity, + }); + const executeResult = await execute({ + cypher, + params, + defaultAccessMode: "WRITE", + context, + info, + }); + return { + [entity.typeNames.queryField]: executeResult.records[0]?.this.connection.edges.map( + (edge: any) => edge.node + ), + info: { + ...executeResult.statistics, + }, + }; + }; +} diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts new file mode 100644 index 0000000000..257b118f7d --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -0,0 +1,232 @@ +/* + * 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 { GraphQLInputType, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema } from "graphql"; +import type { + EnumTypeComposer, + InputTypeComposer, + InterfaceTypeComposer, + ListComposer, + NonNullComposer, + ObjectTypeComposer, + ScalarTypeComposer, +} from "graphql-compose"; +import { SchemaComposer } from "graphql-compose"; +import { SchemaBuilderTypes } from "./SchemaBuilderTypes"; + +export type TypeDefinition = string | WrappedComposer; +export type InputTypeDefinition = string | WrappedComposer; + +type ObjectOrInputTypeComposer = ObjectTypeComposer | InputTypeComposer; + +type ListOrNullComposer = + | ListComposer + | ListComposer> + | NonNullComposer + | NonNullComposer> + | NonNullComposer>>; + +export type WrappedComposer = T | ListOrNullComposer; + +export type GraphQLResolver = (...args) => any; + +export type FieldDefinition = { + resolve?: GraphQLResolver; + type: TypeDefinition; + args?: Record; + deprecationReason?: string | null; + description?: string | null; +}; + +export type InputFieldDefinition = { + type: InputTypeDefinition; + args?: Record; + deprecationReason?: string | null; + description?: string | null; + defaultValue?: any; +}; + +export class SchemaBuilder { + public readonly types: SchemaBuilderTypes; + private composer: SchemaComposer; + + constructor() { + this.composer = new SchemaComposer(); + this.types = new SchemaBuilderTypes(this.composer); + } + + public createScalar(scalar: GraphQLScalarType): void { + this.composer.createScalarTC(scalar); + } + + public createObject(object: GraphQLObjectType): void { + this.composer.createObjectTC(object); + } + + public getOrCreateObjectType( + name: string, + onCreate: () => { + fields: Record>; + description?: string; + iface?: InterfaceTypeComposer; + } + ): ObjectTypeComposer { + return this.composer.getOrCreateOTC(name, (tc) => { + const { fields, description, iface } = onCreate(); + if (fields) { + tc.addFields(fields); + } + if (description) { + tc.setDescription(description); + } + if (iface) { + tc.addInterface(iface); + } + }); + } + + public getOrCreateInterfaceType( + name: string, + onCreate: () => { + fields: Record>; + description?: string; + } + ): InterfaceTypeComposer { + return this.composer.getOrCreateIFTC(name, (tc) => { + const { fields, description } = onCreate(); + + if (fields) { + tc.addFields(fields); + } + if (description) { + tc.setDescription(description); + } + // This is used for global node, not sure if needed for other interfaces + tc.setResolveType((obj) => { + return obj.__resolveType; + }); + }); + } + + public getOrCreateInputType( + name: string, + onCreate: (itc: InputTypeComposer) => { + fields: Record< + string, + | EnumTypeComposer + | GraphQLInputType + | GraphQLNonNull + | WrappedComposer + | InputFieldDefinition + >; + description?: string; + } + ): InputTypeComposer { + return this.composer.getOrCreateITC(name, (itc) => { + const { fields, description } = onCreate(itc); + if (fields) { + itc.addFields(fields); + } + if (description) { + itc.setDescription(description); + } + }); + } + + public createInputObjectType( + name: string, + fields: Record< + string, + | EnumTypeComposer + | GraphQLInputType + | GraphQLNonNull + | WrappedComposer + | InputFieldDefinition + >, + description?: string + ): InputTypeComposer { + return this.composer.createInputTC({ + name, + description, + fields, + }); + } + + public createEnumType(name: string, values: string[], description?: string): EnumTypeComposer { + const enumValuesFormatted: Record = values.reduce((acc, value) => { + acc[value] = { value }; + return acc; + }, {}); + return this.composer.createEnumTC({ + name, + description, + values: enumValuesFormatted, + }); + } + + public addQueryField({ + name, + type, + args, + resolver, + description, + }: { + name: string; + type: ObjectTypeComposer | InterfaceTypeComposer; + args: Record>; + resolver: (...args: any[]) => any; + description?: string; + }): void { + this.composer.Query.addFields({ + [name]: { + type: type, + args, + resolve: resolver, + description, + }, + }); + } + + public addMutationField({ + name, + type, + args, + resolver, + description, + }: { + name: string; + type: ObjectTypeComposer | InterfaceTypeComposer; + args: Record>; + resolver: (...args: any[]) => any; + description?: string; + }): void { + this.composer.Mutation.addFields({ + [name]: { + type: type, + args, + resolve: resolver, + description, + }, + }); + } + + public build(): GraphQLSchema { + return this.composer.buildSchema(); + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts new file mode 100644 index 0000000000..c1eb6ff8a7 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; +import type { SchemaComposer } from "graphql-compose"; +import { InputTypeComposer, ObjectTypeComposer, ScalarTypeComposer } from "graphql-compose"; +import { Memoize } from "typescript-memoize"; +import { CartesianPointInput } from "../../graphql/input-objects/CartesianPointInput"; +import { PointInput } from "../../graphql/input-objects/PointInput"; +import { CartesianPoint } from "../../graphql/objects/CartesianPoint"; +import { Point } from "../../graphql/objects/Point"; +import { + GraphQLBigInt, + GraphQLDate, + GraphQLDateTime, + GraphQLDuration, + GraphQLLocalDateTime, + GraphQLLocalTime, + GraphQLTime, +} from "../../graphql/scalars"; + +export class SchemaBuilderTypes { + private composer: SchemaComposer; + + constructor(composer: SchemaComposer) { + this.composer = composer; + } + + @Memoize() + public get id(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLID, this.composer); + } + @Memoize() + public get int(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLInt, this.composer); + } + @Memoize() + public get float(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLFloat, this.composer); + } + @Memoize() + public get bigInt(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLBigInt, this.composer); + } + @Memoize() + public get string(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLString, this.composer); + } + @Memoize() + public get boolean(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLBoolean, this.composer); + } + @Memoize() + public get date(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLDate, this.composer); + } + @Memoize() + public get dateTime(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLDateTime, this.composer); + } + @Memoize() + public get localDateTime(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLLocalDateTime, this.composer); + } + @Memoize() + public get time(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLTime, this.composer); + } + @Memoize() + public get localTime(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLLocalTime, this.composer); + } + @Memoize() + public get duration(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLDuration, this.composer); + } + @Memoize() + public get point(): ObjectTypeComposer { + return new ObjectTypeComposer(Point, this.composer); + } + @Memoize() + public get pointInput(): InputTypeComposer { + return new InputTypeComposer(PointInput, this.composer); + } + @Memoize() + public get cartesianPoint(): ObjectTypeComposer { + return new ObjectTypeComposer(CartesianPoint, this.composer); + } + @Memoize() + public get cartesianPointInput(): InputTypeComposer { + return new InputTypeComposer(CartesianPointInput, this.composer); + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts new file mode 100644 index 0000000000..ac9fe5ab5d --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -0,0 +1,118 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type GraphQLSchema } from "graphql"; +import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import { generateGlobalNodeResolver } from "../resolvers/global-node-resolver"; +import { generateReadResolver } from "../resolvers/read-resolver"; +import { generateCreateResolver } from "../resolvers/translate-create-resolver"; +import { generateDeleteResolver } from "../resolvers/translate-delete-resolver"; +import { generateUpdateResolver } from "../resolvers/translate-update-resolver"; +import { SchemaBuilder } from "./SchemaBuilder"; +import { SchemaTypes } from "./schema-types/SchemaTypes"; +import { StaticSchemaTypes } from "./schema-types/StaticSchemaTypes"; +import { TopLevelEntitySchemaTypes } from "./schema-types/TopLevelEntitySchemaTypes"; + +export class SchemaGenerator { + private schemaBuilder: SchemaBuilder; + private staticTypes: StaticSchemaTypes; + + constructor() { + this.schemaBuilder = new SchemaBuilder(); + this.staticTypes = new StaticSchemaTypes({ schemaBuilder: this.schemaBuilder }); + } + + public generate(schemaModel: Neo4jGraphQLSchemaModel): GraphQLSchema { + const entityTypesMap = this.generateEntityTypes(schemaModel); + this.generateTopLevelQueryFields(entityTypesMap); + this.generateTopLevelMutationFields(entityTypesMap); + + this.generateGlobalNodeQueryField(schemaModel); + + return this.schemaBuilder.build(); + } + + private generateEntityTypes(schemaModel: Neo4jGraphQLSchemaModel): Map { + const entityTypesMap = new Map(); + const schemaTypes = new SchemaTypes({ + staticTypes: this.staticTypes, + entitySchemas: entityTypesMap, + }); + + for (const entity of schemaModel.entities.values()) { + if (entity.isConcreteEntity()) { + const entitySchemaTypes = new TopLevelEntitySchemaTypes({ + entity, + schemaBuilder: this.schemaBuilder, + schemaTypes, + }); + entityTypesMap.set(entity, entitySchemaTypes); + } + } + + return entityTypesMap; + } + + private generateTopLevelQueryFields(entityTypesMap: Map): void { + for (const [entity, entitySchemaTypes] of entityTypesMap.entries()) { + const resolver = generateReadResolver({ + entity, + }); + entitySchemaTypes.addTopLevelQueryField(resolver); + } + } + + private generateTopLevelMutationFields(entityTypesMap: Map): void { + for (const [entity, entitySchemaTypes] of entityTypesMap.entries()) { + entitySchemaTypes.addTopLevelCreateField( + generateCreateResolver({ + entity, + }) + ); + entitySchemaTypes.addTopLevelUpdateField( + generateUpdateResolver({ + entity, + }) + ); + + entitySchemaTypes.addTopLevelDeleteField( + generateDeleteResolver({ + entity, + }) + ); + } + } + + private generateGlobalNodeQueryField(schemaModel: Neo4jGraphQLSchemaModel): void { + const globalEntities = schemaModel.concreteEntities.filter((e) => e.globalIdField); + + if (globalEntities.length > 0) { + this.schemaBuilder.addQueryField({ + name: "node", + type: this.staticTypes.globalNodeInterface, + args: { + id: this.schemaBuilder.types.id.NonNull, + }, + description: "Fetches an object given its ID", + resolver: generateGlobalNodeResolver({ globalEntities }), + }); + } + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts new file mode 100644 index 0000000000..c52940d803 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -0,0 +1,224 @@ +/* + * 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 { + EnumTypeComposer, + InputTypeComposer, + ListComposer, + NonNullComposer, + ObjectTypeComposer, + ScalarTypeComposer, +} from "graphql-compose"; +import { Memoize } from "typescript-memoize"; +import type { Attribute } from "../../../schema-model/attribute/Attribute"; +import { + GraphQLBuiltInScalarType, + Neo4jGraphQLNumberType, + Neo4jGraphQLTemporalType, +} from "../../../schema-model/attribute/AttributeType"; +import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; +import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../schema-model/relationship/Relationship"; +import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; +import { connectionOperationResolver } from "../../resolvers/connection-operation-resolver"; +import type { RelatedEntityTypeNames } from "../../schema-model/graphql-type-names/RelatedEntityTypeNames"; +import type { SchemaBuilder } from "../SchemaBuilder"; +import type { SchemaTypes } from "./SchemaTypes"; +import type { TopLevelEntitySchemaTypes } from "./TopLevelEntitySchemaTypes"; +import { RelatedEntityFilterSchemaTypes } from "./filter-schema-types/RelatedEntityFilterSchemaTypes"; + +export class RelatedEntitySchemaTypes { + public filterSchemaTypes: RelatedEntityFilterSchemaTypes; + private relationship: Relationship; + private schemaBuilder: SchemaBuilder; + private entityTypeNames: RelatedEntityTypeNames; + private schemaTypes: SchemaTypes; + + constructor({ + relationship, + schemaBuilder, + entityTypeNames, + schemaTypes, + }: { + schemaBuilder: SchemaBuilder; + relationship: Relationship; + schemaTypes: SchemaTypes; + entityTypeNames: RelatedEntityTypeNames; + }) { + this.relationship = relationship; + this.filterSchemaTypes = new RelatedEntityFilterSchemaTypes({ + schemaBuilder, + relationship: relationship, + schemaTypes, + }); + this.schemaBuilder = schemaBuilder; + this.entityTypeNames = entityTypeNames; + this.schemaTypes = schemaTypes; + } + + public get connectionOperation(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connectionOperation, () => { + const args: { + first: ScalarTypeComposer; + after: ScalarTypeComposer; + sort?: ListComposer>; + } = { + first: this.schemaBuilder.types.int, + after: this.schemaBuilder.types.string, + }; + if (this.isSortable()) { + args.sort = this.connectionSort.NonNull.List; + } + return { + fields: { + connection: { + type: this.connection, + args: args, + resolve: connectionOperationResolver, + }, + }, + }; + }); + } + + private get connection(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connection, () => { + return { + fields: { + pageInfo: this.schemaTypes.staticTypes.pageInfo, + edges: this.edge.List, + }, + }; + }); + } + + private get connectionSort(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { + return { + fields: { + edges: this.edgeSort, + }, + }; + }); + } + + private get edge(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.edge, () => { + const properties = this.getEdgeProperties(); + const fields = { + node: this.nodeType, + cursor: this.schemaBuilder.types.string, + }; + + if (properties) { + fields["properties"] = properties; + } + return { + fields, + }; + }); + } + + private get edgeSort(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.edgeSort, () => { + const edgeSortFields = {}; + const properties = this.getEdgeSortProperties(); + if (properties) { + edgeSortFields["properties"] = properties; + } + if (this.getTargetEntitySchemaTypes().isSortable()) { + edgeSortFields["node"] = this.nodeSort; + } + + return { fields: edgeSortFields }; + }); + } + + public get nodeType(): ObjectTypeComposer { + return this.getTargetEntitySchemaTypes().nodeType; + } + + public get nodeSort(): InputTypeComposer { + return this.getTargetEntitySchemaTypes().nodeSort; + } + + @Memoize() + private getTargetEntitySchemaTypes(): TopLevelEntitySchemaTypes { + const target = this.relationship.target; + if (!(target instanceof ConcreteEntity)) { + throw new Error("Interfaces not supported yet"); + } + return this.schemaTypes.getEntitySchemaTypes(target); + } + + public isSortable(): boolean { + const isTargetSortable = this.getTargetEntitySchemaTypes().isSortable(); + return this.getRelationshipSortableFields().length > 0 || isTargetSortable; + } + + @Memoize() + private getRelationshipFields(): Attribute[] { + return [...this.relationship.attributes.values()]; + } + + private getEdgeSortProperties(): InputTypeComposer | undefined { + if (this.entityTypeNames.propertiesSort && this.getRelationshipSortableFields().length > 0) { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.propertiesSort, () => { + return { + fields: this.getRelationshipSortFields(), + }; + }); + } + } + + private getRelationshipFieldsDefinition(): Record { + const entityAttributes = this.getRelationshipFields().map((attribute) => new AttributeAdapter(attribute)); + return attributeAdapterToComposeFields(entityAttributes, new Map()); + } + + private getRelationshipSortFields(): Record { + return Object.fromEntries( + this.getRelationshipSortableFields().map((attribute) => [ + attribute.name, + this.schemaTypes.staticTypes.sortDirection, + ]) + ); + } + + @Memoize() + private getRelationshipSortableFields(): Attribute[] { + return this.getRelationshipFields().filter( + (field) => + field.type.name === GraphQLBuiltInScalarType[field.type.name] || + field.type.name === Neo4jGraphQLNumberType[field.type.name] || + field.type.name === Neo4jGraphQLTemporalType[field.type.name] + ); + } + + private getEdgeProperties(): ObjectTypeComposer | undefined { + if (this.entityTypeNames.properties) { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.properties, () => { + const fields = this.getRelationshipFieldsDefinition(); + return { + fields, + }; + }); + } + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/SchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/SchemaTypes.ts new file mode 100644 index 0000000000..1f6ff01f3d --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/SchemaTypes.ts @@ -0,0 +1,47 @@ +/* + * 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 { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { StaticSchemaTypes } from "./StaticSchemaTypes"; +import type { TopLevelEntitySchemaTypes } from "./TopLevelEntitySchemaTypes"; + +export class SchemaTypes { + public readonly staticTypes: StaticSchemaTypes; + private entitySchemas: Map; + + constructor({ + staticTypes, + entitySchemas, + }: { + staticTypes: StaticSchemaTypes; + entitySchemas: Map; + }) { + this.staticTypes = staticTypes; + this.entitySchemas = entitySchemas; + } + + public getEntitySchemaTypes(entity: ConcreteEntity): TopLevelEntitySchemaTypes { + const entitySchema = this.entitySchemas.get(entity); + if (!entitySchema) { + throw new Error("EntitySchema not found"); + } + + return entitySchema; + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts new file mode 100644 index 0000000000..91392175e3 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -0,0 +1,473 @@ +/* + * 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 { GraphQLInputType } from "graphql"; +import { GraphQLBoolean } from "graphql"; +import type { + EnumTypeComposer, + InputTypeComposer, + InterfaceTypeComposer, + ListComposer, + ObjectTypeComposer, + ScalarTypeComposer, +} from "graphql-compose"; +import { Memoize } from "typescript-memoize"; +import { CartesianPoint } from "../../../graphql/objects/CartesianPoint"; +import { Point } from "../../../graphql/objects/Point"; +import * as Scalars from "../../../graphql/scalars"; +import type { SchemaBuilder } from "../SchemaBuilder"; + +export class StaticSchemaTypes { + private schemaBuilder: SchemaBuilder; + public readonly filters: StaticFilterTypes; + + constructor({ schemaBuilder }: { schemaBuilder: SchemaBuilder }) { + this.schemaBuilder = schemaBuilder; + this.filters = new StaticFilterTypes({ schemaBuilder }); + this.addBuiltInTypes(); + } + + private addBuiltInTypes(): void { + Object.values(Scalars).forEach((scalar) => { + this.schemaBuilder.createScalar(scalar); + }); + this.schemaBuilder.createObject(CartesianPoint); + this.schemaBuilder.createObject(Point); + } + + public get pageInfo(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType("PageInfo", () => { + return { + fields: { + hasNextPage: this.schemaBuilder.types.boolean.NonNull, + hasPreviousPage: this.schemaBuilder.types.boolean.NonNull, + startCursor: this.schemaBuilder.types.string, + endCursor: this.schemaBuilder.types.string, + }, + }; + }); + } + + public get createInfo(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType("CreateInfo", () => { + return { + fields: { + nodesCreated: this.schemaBuilder.types.int.NonNull, + relationshipsCreated: this.schemaBuilder.types.int.NonNull, + }, + }; + }); + } + + public get deleteInfo(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType("DeleteInfo", () => { + return { + fields: { + nodesDeleted: this.schemaBuilder.types.int.NonNull, + relationshipsDeleted: this.schemaBuilder.types.int.NonNull, + }, + }; + }); + } + + public get deleteResponse(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType("DeleteResponse", () => { + return { + fields: { + info: this.deleteInfo, + }, + }; + }); + } + + @Memoize() + public get sortDirection(): EnumTypeComposer { + return this.schemaBuilder.createEnumType("SortDirection", ["ASC", "DESC"]); + } + + public get globalNodeInterface(): InterfaceTypeComposer { + return this.schemaBuilder.getOrCreateInterfaceType("Node", () => { + return { + fields: { + id: this.schemaBuilder.types.id.NonNull, + }, + }; + }); + } +} + +class StaticFilterTypes { + private schemaBuilder: SchemaBuilder; + + constructor({ schemaBuilder }: { schemaBuilder: SchemaBuilder }) { + this.schemaBuilder = schemaBuilder; + } + + public getStringListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.string.NonNull.List, + }, + }; + }); + } + + public get stringWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("StringWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createStringOperators(this.schemaBuilder.types.string), + in: this.schemaBuilder.types.string.NonNull.List, + }, + }; + }); + } + + public get globalIdWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("GlobalIdWhere", (_itc) => { + return { + fields: { + equals: this.schemaBuilder.types.string, + // TODO: Boolean fields and IN operator: + // ...this.createBooleanOperators(itc), + // in: toGraphQLList(toGraphQLNonNull(GraphQLString)), + }, + }; + }); + } + + public get dateWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DateWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + in: this.schemaBuilder.types.date.NonNull.List, + ...this.createNumericOperators(this.schemaBuilder.types.date), + }, + }; + }); + } + public getDateListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DateListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.date.NonNull.List, + }, + }; + }); + } + + public get dateTimeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DateTimeWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + in: this.schemaBuilder.types.dateTime.NonNull.List, + ...this.createNumericOperators(this.schemaBuilder.types.dateTime), + }, + }; + }); + } + + public getDateTimeListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DateTimeListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.dateTime.NonNull.List, + }, + }; + }); + } + + public get localDateTimeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("LocalDateTimeWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createNumericOperators(this.schemaBuilder.types.localDateTime), + in: this.schemaBuilder.types.localDateTime.NonNull.List, + }, + }; + }); + } + + public getLocalDateTimeListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("LocalDateTimeListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.localDateTime.NonNull.List, + }, + }; + }); + } + + public get durationWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DurationWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createNumericOperators(this.schemaBuilder.types.duration), + in: this.schemaBuilder.types.duration.NonNull.List, + }, + }; + }); + } + + public getDurationListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DurationListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.duration.NonNull.List, + }, + }; + }); + } + + public get timeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("TimeWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createNumericOperators(this.schemaBuilder.types.time), + in: this.schemaBuilder.types.time.NonNull.List, + }, + }; + }); + } + + public getTimeListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("TimeListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.time.NonNull.List, + }, + }; + }); + } + + public get localTimeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("LocalTimeWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createNumericOperators(this.schemaBuilder.types.localTime), + in: this.schemaBuilder.types.localTime.NonNull.List, + }, + }; + }); + } + + public getLocalTimeListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("LocalTimeListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.localTime.NonNull.List, + }, + }; + }); + } + + public get booleanWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("BooleanWhere", (itc) => { + return { + fields: { + OR: itc.NonNull.List, + AND: itc.NonNull.List, + NOT: itc, + equals: GraphQLBoolean, + }, + }; + }); + } + + public getIdListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("IDListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.id.NonNull.List, + }, + }; + }); + } + + public get idWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("IDWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createStringOperators(this.schemaBuilder.types.id), + in: this.schemaBuilder.types.id.NonNull.List, + }, + }; + }); + } + + public getIntListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("IntListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.int.NonNull.List, + }, + }; + }); + } + public get intWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("IntWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createNumericOperators(this.schemaBuilder.types.int), + in: this.schemaBuilder.types.int.NonNull.List, + }, + }; + }); + } + + public getBigIntListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("BigIntListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.bigInt.NonNull.List, + }, + }; + }); + } + + public get bigIntWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("BigIntWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createNumericOperators(this.schemaBuilder.types.bigInt), + in: this.schemaBuilder.types.bigInt.NonNull.List, + }, + }; + }); + } + + public getFloatListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("FloatListWhere", () => { + return { + fields: { + equals: this.schemaBuilder.types.float.NonNull.List, + }, + }; + }); + } + + public get floatWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("FloatWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createNumericOperators(this.schemaBuilder.types.float), + in: this.schemaBuilder.types.float.NonNull.List, + }, + }; + }); + } + + // public getCartesianListWhere(): InputTypeComposer { + + // return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhere", () => { + // return { + // fields: { + // equals: toGraphQLList(toGraphQLNonNull(CartesianPointInput)), + // }, + // }; + // }); + // } + + // public getPointListWhere(): InputTypeComposer { + + // return this.schemaBuilder.getOrCreateInputType("PointListPointWhere", () => { + // return { + // fields: { + // equals: toGraphQLList(toGraphQLNonNull(PointInput)), + // }, + // }; + // }); + // } + + // public get cartesianPointWhere(): InputTypeComposer { + // return this.schemaBuilder.getOrCreateInputType("CartesianPointWhere", (itc) => { + // return { + // fields: { + // ...this.createBooleanOperators(itc), + // equals: CartesianPointInput, + // in: toGraphQLList(toGraphQLNonNull(CartesianPointInput)), + // lt: CartesianPointDistance, + // lte: CartesianPointDistance, + // gt: CartesianPointDistance, + // gte: CartesianPointDistance, + // distance: CartesianPointDistance, + // }, + // }; + // }); + // } + + // public get pointWhere(): InputTypeComposer { + // return this.schemaBuilder.getOrCreateInputType("PointWhere", (itc) => { + // return { + // fields: { + // ...this.createBooleanOperators(itc), + // equals: PointInput, + // in: toGraphQLList(toGraphQLNonNull(PointInput)), + // lt: PointDistance, + // lte: PointDistance, + // gt: PointDistance, + // gte: PointDistance, + // distance: PointDistance, + // }, + // }; + // }); + // } + + private createStringOperators(type: ScalarTypeComposer): Record { + return { + equals: type, + // matches: type, + contains: type, + startsWith: type, + endsWith: type, + }; + } + + private createNumericOperators( + type: GraphQLInputType | ScalarTypeComposer + ): Record { + return { + equals: type, + lt: type, + lte: type, + gt: type, + gte: type, + }; + } + + private createBooleanOperators(itc: InputTypeComposer): Record { + return { + OR: itc.NonNull.List, + AND: itc.NonNull.List, + NOT: itc, + }; + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts new file mode 100644 index 0000000000..76a471e569 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -0,0 +1,422 @@ +/* + * 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 GraphQLResolveInfo } from "graphql"; +import type { + InputTypeComposer, + InterfaceTypeComposer, + ListComposer, + NonNullComposer, + ObjectTypeComposer, + ScalarTypeComposer, +} from "graphql-compose"; +import { Memoize } from "typescript-memoize"; +import type { Attribute } from "../../../schema-model/attribute/Attribute"; +import type { AttributeType, Neo4jGraphQLScalarType } from "../../../schema-model/attribute/AttributeType"; +import { + GraphQLBuiltInScalarType, + ListType, + Neo4jGraphQLNumberType, + Neo4jGraphQLTemporalType, + ScalarType, +} from "../../../schema-model/attribute/AttributeType"; +import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import { idResolver } from "../../../schema/resolvers/field/id"; +import { numericalResolver } from "../../../schema/resolvers/field/numerical"; +import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; +import { connectionOperationResolver } from "../../resolvers/connection-operation-resolver"; +import { generateGlobalIdFieldResolver } from "../../resolvers/global-id-field-resolver"; +import type { TopLevelEntityTypeNames } from "../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; +import type { FieldDefinition, GraphQLResolver, SchemaBuilder } from "../SchemaBuilder"; +import { RelatedEntitySchemaTypes } from "./RelatedEntitySchemaTypes"; +import type { SchemaTypes } from "./SchemaTypes"; +import { TopLevelFilterSchemaTypes } from "./filter-schema-types/TopLevelFilterSchemaTypes"; +import { TopLevelCreateSchemaTypes } from "./mutation-schema-types/TopLevelCreateSchemaTypes"; +import { TopLevelDeleteSchemaTypes } from "./mutation-schema-types/TopLevelDeleteSchemaTypes"; +import { TopLevelUpdateSchemaTypes } from "./mutation-schema-types/TopLevelUpdateSchemaTypes"; + +export class TopLevelEntitySchemaTypes { + private entity: ConcreteEntity; + private filterSchemaTypes: TopLevelFilterSchemaTypes; + private schemaBuilder: SchemaBuilder; + private entityTypeNames: TopLevelEntityTypeNames; + private schemaTypes: SchemaTypes; + private createSchemaTypes: TopLevelCreateSchemaTypes; + private updateSchemaTypes: TopLevelUpdateSchemaTypes; + private deleteSchemaTypes: TopLevelDeleteSchemaTypes; + + constructor({ + entity, + schemaBuilder, + schemaTypes, + }: { + schemaBuilder: SchemaBuilder; + entity: ConcreteEntity; + schemaTypes: SchemaTypes; + }) { + this.entity = entity; + this.filterSchemaTypes = new TopLevelFilterSchemaTypes({ schemaBuilder, entity, schemaTypes }); + this.schemaBuilder = schemaBuilder; + this.entityTypeNames = entity.typeNames; + this.schemaTypes = schemaTypes; + this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity }); + this.updateSchemaTypes = new TopLevelUpdateSchemaTypes({ schemaBuilder, entity, schemaTypes }); + this.deleteSchemaTypes = new TopLevelDeleteSchemaTypes({ schemaBuilder, entity, schemaTypes }); + } + + public addTopLevelQueryField( + resolver: ( + _root: any, + args: any, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) => Promise + ): void { + this.schemaBuilder.addQueryField({ + name: this.entity.typeNames.queryField, + type: this.connectionOperation, + args: { + where: this.filterSchemaTypes.operationWhereTopLevel, + }, + resolver, + }); + } + + public get connectionOperation(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connectionOperation, () => { + const args: { + first: ScalarTypeComposer; + after: ScalarTypeComposer; + sort?: ListComposer>; + } = { + first: this.schemaBuilder.types.int, + after: this.schemaBuilder.types.string, + }; + if (this.isSortable()) { + args.sort = this.connectionSort.NonNull.List; + } + return { + fields: { + connection: { + type: this.connection, + args: args, + resolve: connectionOperationResolver, + }, + }, + }; + }); + } + + private get connection(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connection, () => { + return { + fields: { + pageInfo: this.schemaTypes.staticTypes.pageInfo, + edges: this.edge.List, + }, + }; + }); + } + + public addTopLevelCreateField( + resolver: ( + _root: any, + args: any, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) => Promise + ) { + this.schemaBuilder.addMutationField({ + name: this.entity.typeNames.createField, + type: this.createType, + args: { + input: this.createSchemaTypes.createInput.NonNull.List.NonNull, + }, + resolver, + }); + } + public addTopLevelUpdateField( + resolver: ( + _root: any, + args: any, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) => Promise + ) { + this.schemaBuilder.addMutationField({ + name: this.entity.typeNames.updateField, + type: this.updateType, + args: { + input: this.updateSchemaTypes.updateInput.NonNull, + where: this.filterSchemaTypes.operationWhereTopLevel, + }, + resolver, + }); + } + + public addTopLevelDeleteField( + resolver: ( + _root: any, + args: any, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) => Promise + ) { + this.schemaBuilder.addMutationField({ + name: this.entity.typeNames.deleteField, + type: this.schemaTypes.staticTypes.deleteResponse, + args: { + input: this.deleteSchemaTypes.deleteInput, + where: this.filterSchemaTypes.operationWhereTopLevel, + }, + resolver, + }); + } + + protected get connectionSort(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { + return { + fields: { + node: this.nodeSort, + }, + }; + }); + } + + private get edge(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.edge, () => { + return { + fields: { + node: this.nodeType, + cursor: this.schemaBuilder.types.string, + }, + }; + }); + } + + private get edgeSort(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.edgeSort, () => { + return { + fields: { + node: this.nodeSort, + }, + }; + }); + } + + public get nodeType(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.node, () => { + const fields = this.getNodeFieldsDefinitions(); + const relationships = this.getRelationshipFields(); + + let iface: InterfaceTypeComposer | undefined; + if (this.entity.isConcreteEntity() && this.entity.globalIdField) { + iface = this.schemaTypes.staticTypes.globalNodeInterface; + } + + return { + fields: { ...fields, ...relationships }, + iface, + }; + }); + } + + public get nodeSort(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.nodeSort, () => { + const sortFields = Object.fromEntries( + this.getSortableFields().map((field) => { + return [field.name, this.schemaTypes.staticTypes.sortDirection]; + }) + ); + + return { + fields: sortFields, + }; + }); + } + + public get nodeWhere(): InputTypeComposer { + return this.filterSchemaTypes.nodeWhere; + } + /** + * Used to avoid creating empty sort input which make the generated schema invalid + **/ + public isSortable(): boolean { + return this.getSortableFields().length > 0; + } + + @Memoize() + private getFields(): Attribute[] { + return [...this.entity.attributes.values()]; + } + + @Memoize() + private getSortableFields(): Attribute[] { + return this.getFields().filter( + (field) => + field.type.name in GraphQLBuiltInScalarType || + field.type.name in Neo4jGraphQLNumberType || + field.type.name in Neo4jGraphQLTemporalType + ); + } + + private getNodeFieldsDefinitions(): Record { + const entries: [string, FieldDefinition][] = this.getFields().map((attribute) => { + if (attribute.type instanceof ScalarType) { + return [ + attribute.name, + { + type: attributeTypeToString(attribute.type), + args: {}, + description: attribute.description, + resolve: typeToResolver(attribute.type.name), + }, + ]; + } + if (attribute.type instanceof ListType && attribute.type.ofType instanceof ScalarType) { + return [ + attribute.name, + { + type: attributeTypeToString(attribute.type), + args: {}, + description: attribute.description, + resolve: typeToResolver(attribute.type.ofType.name), + }, + ]; + } + return [ + attribute.name, + { + type: attributeTypeToString(attribute.type), + args: {}, + description: attribute.description, + }, + ]; + }); + + const fields = Object.fromEntries(entries); + this.addGlobalIdField(fields); + return fields; + } + + private addGlobalIdField(fields: Record): void { + const globalIdField = this.entity.globalIdField; + if (globalIdField) { + fields["id"] = { + type: this.schemaBuilder.types.id.NonNull, + args: {}, + description: "", + resolve: generateGlobalIdFieldResolver({ entity: this.entity }), + }; + } + } + + private getRelationshipFields(): Record }> { + return Object.fromEntries( + [...this.entity.relationships.values()].map((relationship) => { + const relationshipTypes = new RelatedEntitySchemaTypes({ + schemaBuilder: this.schemaBuilder, + relationship, + entityTypeNames: relationship.typeNames, + schemaTypes: this.schemaTypes, + }); + const relationshipType = relationshipTypes.connectionOperation; + const operationWhere = relationshipTypes.filterSchemaTypes.operationWhereNested; + return [relationship.name, { type: relationshipType, args: { where: operationWhere } }]; + }) + ); + } + + public get createType(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.createResponse, () => { + const nodeType = this.nodeType; + + return { + fields: { + [this.entityTypeNames.queryField]: nodeType.NonNull.List.NonNull, + info: this.schemaTypes.staticTypes.createInfo, + }, + }; + }); + } + + public get updateType(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.updateResponse, () => { + const nodeType = this.nodeType; + const info = this.updateInfo; + + return { + fields: { + [this.entityTypeNames.queryField]: nodeType.NonNull.List.NonNull, + info, + }, + }; + }); + } + + public get createInfo(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.createInfo, () => { + return { + fields: { + nodesCreated: this.schemaBuilder.types.int.NonNull, + relationshipsCreated: this.schemaBuilder.types.int.NonNull, + }, + }; + }); + } + + public get updateInfo(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.createInfo, () => { + return { + fields: { + nodesCreated: this.schemaBuilder.types.int.NonNull, + nodesDelete: this.schemaBuilder.types.int.NonNull, + relationshipsCreated: this.schemaBuilder.types.int.NonNull, + relationshipsDeleted: this.schemaBuilder.types.int.NonNull, + }, + }; + }); + } +} + +function typeToResolver(type: Neo4jGraphQLScalarType): GraphQLResolver | undefined { + switch (type) { + case GraphQLBuiltInScalarType.Int: + case GraphQLBuiltInScalarType.Float: + case Neo4jGraphQLNumberType.BigInt: { + return numericalResolver; + } + case GraphQLBuiltInScalarType.ID: { + return idResolver; + } + } +} + +function attributeTypeToString(type: AttributeType): string { + if (type instanceof ListType) { + if (type.isRequired) { + return `[${attributeTypeToString(type.ofType)}]!`; + } + return `[${attributeTypeToString(type.ofType)}]`; + } + if (type.isRequired) { + return `${type.name}!`; + } + return type.name; +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/FilterSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/FilterSchemaTypes.ts new file mode 100644 index 0000000000..4cbd24d20e --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/FilterSchemaTypes.ts @@ -0,0 +1,235 @@ +/* + * 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 { GraphQLScalarType } from "graphql"; +import type { InputTypeComposer } from "graphql-compose"; +import type { Attribute } from "../../../../schema-model/attribute/Attribute"; +import { + GraphQLBuiltInScalarType, + ListType, + Neo4jGraphQLNumberType, + Neo4jGraphQLTemporalType, + Neo4jTemporalType, + ScalarType, +} from "../../../../schema-model/attribute/AttributeType"; +import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; +import { filterTruthy } from "../../../../utils/utils"; +import type { RelatedEntityTypeNames } from "../../../schema-model/graphql-type-names/RelatedEntityTypeNames"; +import type { TopLevelEntityTypeNames } from "../../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; +import type { SchemaBuilder } from "../../SchemaBuilder"; +import type { SchemaTypes } from "../SchemaTypes"; + +export abstract class FilterSchemaTypes { + protected entityTypeNames: T; + protected schemaTypes: SchemaTypes; + protected schemaBuilder: SchemaBuilder; + + constructor({ + entityTypeNames, + schemaBuilder, + schemaTypes, + }: { + entityTypeNames: T; + schemaBuilder: SchemaBuilder; + schemaTypes: SchemaTypes; + }) { + this.entityTypeNames = entityTypeNames; + this.schemaBuilder = schemaBuilder; + this.schemaTypes = schemaTypes; + } + + public get operationWhereTopLevel(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType( + this.entityTypeNames.operationWhere, + (itc: InputTypeComposer) => { + return { + fields: { + AND: itc.NonNull.List, + OR: itc.NonNull.List, + NOT: itc, + node: this.nodeWhere, + }, + }; + } + ); + } + + public get operationWhereNested(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType( + this.entityTypeNames.operationWhere, + (itc: InputTypeComposer) => { + return { + fields: { + AND: itc.NonNull.List, + OR: itc.NonNull.List, + NOT: itc, + edges: this.edgeWhere, + }, + }; + } + ); + } + + protected createPropertyFilters(attributes: Attribute[]): Record { + const fields: Array<[string, InputTypeComposer | GraphQLScalarType] | []> = filterTruthy( + attributes.map((attribute) => { + const propertyFilter = this.attributeToPropertyFilter(attribute); + if (propertyFilter) { + return [attribute.name, propertyFilter]; + } + }) + ); + return Object.fromEntries(fields); + } + + protected createGlobalIdFilters(entity: ConcreteEntity): Record { + const globalIdField = entity.globalIdField; + if (globalIdField) { + return { + id: this.schemaTypes.staticTypes.filters.globalIdWhere, + }; + } + return {}; + } + + private attributeToPropertyFilter(attribute: Attribute): InputTypeComposer | undefined { + const isList = attribute.type instanceof ListType; + const wrappedType = isList ? attribute.type.ofType : attribute.type; + if (wrappedType instanceof ScalarType) { + return this.createScalarType(wrappedType, isList); + } + if (wrappedType instanceof Neo4jTemporalType) { + return this.createTemporalType(wrappedType, isList); + } + // if (wrappedType instanceof Neo4jSpatialType) { + // return this.createSpatialType(wrappedType, isList); + // } + } + + private createScalarType(type: ScalarType, isList: boolean): InputTypeComposer { + switch (type.name) { + case GraphQLBuiltInScalarType.Boolean: { + return this.schemaTypes.staticTypes.filters.booleanWhere; + } + + case GraphQLBuiltInScalarType.String: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getStringListWhere(); + } + return this.schemaTypes.staticTypes.filters.stringWhere; + } + + case GraphQLBuiltInScalarType.ID: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getIdListWhere(); + } + return this.schemaTypes.staticTypes.filters.idWhere; + } + + case GraphQLBuiltInScalarType.Int: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getIntListWhere(); + } + return this.schemaTypes.staticTypes.filters.intWhere; + } + + case GraphQLBuiltInScalarType.Float: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getFloatListWhere(); + } + return this.schemaTypes.staticTypes.filters.floatWhere; + } + + case Neo4jGraphQLNumberType.BigInt: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getBigIntListWhere(); + } + return this.schemaTypes.staticTypes.filters.bigIntWhere; + } + } + } + + private createTemporalType(type: Neo4jTemporalType, isList: boolean): InputTypeComposer { + switch (type.name) { + case Neo4jGraphQLTemporalType.Date: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getDateListWhere(); + } + return this.schemaTypes.staticTypes.filters.dateWhere; + } + + case Neo4jGraphQLTemporalType.DateTime: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getDateTimeListWhere(); + } + return this.schemaTypes.staticTypes.filters.dateTimeWhere; + } + + case Neo4jGraphQLTemporalType.LocalDateTime: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getLocalDateTimeListWhere(); + } + return this.schemaTypes.staticTypes.filters.localDateTimeWhere; + } + + case Neo4jGraphQLTemporalType.Duration: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getDurationListWhere(); + } + return this.schemaTypes.staticTypes.filters.durationWhere; + } + + case Neo4jGraphQLTemporalType.Time: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getTimeListWhere(); + } + return this.schemaTypes.staticTypes.filters.timeWhere; + } + + case Neo4jGraphQLTemporalType.LocalTime: { + if (isList) { + return this.schemaTypes.staticTypes.filters.getLocalTimeListWhere(); + } + return this.schemaTypes.staticTypes.filters.localTimeWhere; + } + } + } + + // private createSpatialType(type: Neo4jSpatialType, isList: boolean): InputTypeComposer { + // switch (type.name) { + // case Neo4jGraphQLSpatialType.CartesianPoint: { + // if (isList) { + // + // return this.schemaTypes.staticTypes.filters.getCartesianListWhere(); + // } + // return this.schemaTypes.staticTypes.filters.cartesianPointWhere; + // } + // case Neo4jGraphQLSpatialType.Point: { + // if (isList) { + // + // return this.schemaTypes.staticTypes.filters.getPointListWhere(); + // } + // return this.schemaTypes.staticTypes.filters.pointWhere; + // } + // } + // } + + protected abstract get edgeWhere(): InputTypeComposer; + protected abstract get nodeWhere(): InputTypeComposer; +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/RelatedEntityFilterSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/RelatedEntityFilterSchemaTypes.ts new file mode 100644 index 0000000000..819cafb41d --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/RelatedEntityFilterSchemaTypes.ts @@ -0,0 +1,116 @@ +/* + * 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 { InputTypeComposer } from "graphql-compose"; +import { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../../schema-model/relationship/Relationship"; +import type { RelatedEntityTypeNames } from "../../../schema-model/graphql-type-names/RelatedEntityTypeNames"; +import type { SchemaBuilder } from "../../SchemaBuilder"; +import type { SchemaTypes } from "../SchemaTypes"; +import { FilterSchemaTypes } from "./FilterSchemaTypes"; + +export class RelatedEntityFilterSchemaTypes extends FilterSchemaTypes { + private relationship: Relationship; + constructor({ + relationship, + schemaBuilder, + schemaTypes, + }: { + schemaBuilder: SchemaBuilder; + relationship: Relationship; + schemaTypes: SchemaTypes; + }) { + super({ entityTypeNames: relationship.typeNames, schemaBuilder, schemaTypes }); + this.relationship = relationship; + } + protected get edgeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.edgeWhere, (itc: InputTypeComposer) => { + const fields = { + AND: itc.NonNull.List, + OR: itc.NonNull.List, + NOT: itc, + node: this.nodeWhere, + }; + const properties = this.propertiesWhere; + if (properties) { + fields["properties"] = properties; + } + return { fields }; + }); + } + + public get nestedOperationWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType( + this.relationship.typeNames.nestedOperationWhere, + (itc: InputTypeComposer) => { + return { + fields: { + AND: itc.NonNull.List, + OR: itc.NonNull.List, + NOT: itc, + all: this.edgeListWhere, + none: this.edgeListWhere, + single: this.edgeListWhere, + some: this.edgeListWhere, + }, + }; + } + ); + } + + private get edgeListWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.edgeListWhere, (itc: InputTypeComposer) => { + return { + fields: { + AND: itc.NonNull.List, + OR: itc.NonNull.List, + NOT: itc, + edges: this.edgeWhere, + }, + }; + }); + } + + protected get nodeWhere(): InputTypeComposer { + const target = this.relationship.target; + if (!(target instanceof ConcreteEntity)) { + throw new Error("Interfaces not supported yet"); + } + const targetSchemaTypes = this.schemaTypes.getEntitySchemaTypes(target); + return targetSchemaTypes.nodeWhere; + } + + private get propertiesWhere(): InputTypeComposer | undefined { + if (this.entityTypeNames.properties) { + return this.schemaBuilder.getOrCreateInputType( + this.entityTypeNames.propertiesWhere, + (itc: InputTypeComposer) => { + return { + fields: { + AND: itc.NonNull.List, + OR: itc.NonNull.List, + NOT: itc, + ...this.createPropertyFilters([...this.relationship.attributes.values()]), + }, + }; + } + ); + } + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/TopLevelFilterSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/TopLevelFilterSchemaTypes.ts new file mode 100644 index 0000000000..faa8fd8896 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/TopLevelFilterSchemaTypes.ts @@ -0,0 +1,87 @@ +/* + * 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 { GraphQLScalarType } from "graphql"; +import type { InputTypeComposer } from "graphql-compose"; +import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../../schema-model/relationship/Relationship"; +import type { TopLevelEntityTypeNames } from "../../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; +import type { SchemaBuilder } from "../../SchemaBuilder"; +import type { SchemaTypes } from "../SchemaTypes"; +import { FilterSchemaTypes } from "./FilterSchemaTypes"; +import { RelatedEntityFilterSchemaTypes } from "./RelatedEntityFilterSchemaTypes"; + +export class TopLevelFilterSchemaTypes extends FilterSchemaTypes { + private entity: ConcreteEntity; + constructor({ + entity, + schemaBuilder, + schemaTypes, + }: { + schemaBuilder: SchemaBuilder; + entity: ConcreteEntity; + schemaTypes: SchemaTypes; + }) { + super({ entityTypeNames: entity.typeNames, schemaBuilder, schemaTypes }); + this.entity = entity; + } + + protected get edgeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.edgeWhere, (itc: InputTypeComposer) => { + return { + fields: { + AND: itc.NonNull.List, + OR: itc.NonNull.List, + NOT: itc, + node: this.nodeWhere, + }, + }; + }); + } + + public get nodeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.nodeWhere, (itc: InputTypeComposer) => { + return { + fields: { + AND: itc.NonNull.List, + OR: itc.NonNull.List, + NOT: itc, + ...this.createPropertyFilters([...this.entity.attributes.values()]), + ...this.createRelationshipFilters([...this.entity.relationships.values()]), + ...this.createGlobalIdFilters(this.entity), + }, + }; + }); + } + + private createRelationshipFilters(relationships: Relationship[]): Record { + const fields: Array<[string, InputTypeComposer | GraphQLScalarType] | []> = relationships.map( + (relationship) => { + const relatedFilterSchemaTypes = new RelatedEntityFilterSchemaTypes({ + schemaBuilder: this.schemaBuilder, + relationship, + schemaTypes: this.schemaTypes, + }); + + return [relationship.name, relatedFilterSchemaTypes.nestedOperationWhere]; + } + ); + return Object.fromEntries(fields); + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/RelatedEntityDeleteSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/RelatedEntityDeleteSchemaTypes.ts new file mode 100644 index 0000000000..25ef1f9837 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/RelatedEntityDeleteSchemaTypes.ts @@ -0,0 +1,97 @@ +/* + * 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 { InputTypeComposer } from "graphql-compose"; +import { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../../schema-model/relationship/Relationship"; +import type { SchemaBuilder } from "../../SchemaBuilder"; +import { RelatedEntityFilterSchemaTypes } from "../filter-schema-types/RelatedEntityFilterSchemaTypes"; +import type { SchemaTypes } from "../SchemaTypes"; +import { TopLevelDeleteSchemaTypes } from "./TopLevelDeleteSchemaTypes"; + +export class RelatedEntityDeleteSchemaTypes { + private relationship: Relationship; + protected schemaTypes: SchemaTypes; + private schemaBuilder: SchemaBuilder; + constructor({ + relationship, + schemaBuilder, + schemaTypes, + }: { + schemaBuilder: SchemaBuilder; + relationship: Relationship; + schemaTypes: SchemaTypes; + }) { + this.relationship = relationship; + this.schemaBuilder = schemaBuilder; + this.schemaTypes = schemaTypes; + } + + public get deleteOperation(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType( + this.relationship.typeNames.deleteOperation, + (_itc: InputTypeComposer) => { + return { + fields: { + delete: this.deleteInput, + }, + }; + } + ); + } + + public get deleteInput(): InputTypeComposer { + const relatedFilterSchemaTypes = new RelatedEntityFilterSchemaTypes({ + schemaBuilder: this.schemaBuilder, + relationship: this.relationship, + schemaTypes: this.schemaTypes, + }); + + if (this.relationship.target instanceof ConcreteEntity) { + const topLevelDeleteSchemaTypes = new TopLevelDeleteSchemaTypes({ + schemaBuilder: this.schemaBuilder, + entity: this.relationship.target, + schemaTypes: this.schemaTypes, + }); + + return this.schemaBuilder.getOrCreateInputType( + this.relationship.typeNames.deleteInput, + (_itc: InputTypeComposer) => { + return { + fields: { + input: topLevelDeleteSchemaTypes.deleteInput, + where: relatedFilterSchemaTypes.operationWhereTopLevel, + }, + }; + } + ); + } else { + return this.schemaBuilder.getOrCreateInputType( + this.relationship.typeNames.deleteInput, + (_itc: InputTypeComposer) => { + return { + fields: { + where: relatedFilterSchemaTypes.operationWhereTopLevel, + }, + }; + } + ); + } + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts new file mode 100644 index 0000000000..4986ca6063 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts @@ -0,0 +1,201 @@ +/* + * 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 { InputTypeComposer, ScalarTypeComposer } from "graphql-compose"; +import type { Attribute } from "../../../../schema-model/attribute/Attribute"; +import type { AttributeType } from "../../../../schema-model/attribute/AttributeType"; +import { + GraphQLBuiltInScalarType, + ListType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, + Neo4jSpatialType, + Neo4jTemporalType, + ScalarType, +} from "../../../../schema-model/attribute/AttributeType"; +import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; +import { filterTruthy } from "../../../../utils/utils"; +import type { TopLevelEntityTypeNames } from "../../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; +import type { InputFieldDefinition, SchemaBuilder, WrappedComposer } from "../../SchemaBuilder"; + +export class TopLevelCreateSchemaTypes { + private entityTypeNames: TopLevelEntityTypeNames; + private schemaBuilder: SchemaBuilder; + private entity: ConcreteEntity; + + constructor({ entity, schemaBuilder }: { entity: ConcreteEntity; schemaBuilder: SchemaBuilder }) { + this.entity = entity; + this.entityTypeNames = entity.typeNames; + this.schemaBuilder = schemaBuilder; + } + + public get createInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.createInput, (_itc: InputTypeComposer) => { + return { + fields: { + node: this.createNode.NonNull, + }, + }; + }); + } + + public get createNode(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.createNode, (_itc: InputTypeComposer) => { + const relevantFields = [...this.entity.attributes.values()].filter( + (attribute) => !attribute.annotations.id + ); + const inputFields = this.getInputFields(relevantFields); + const isEmpty = Object.keys(inputFields).length === 0; + const fields = isEmpty ? { _emptyInput: this.schemaBuilder.types.boolean } : inputFields; + return { + fields, + }; + }); + } + + private getInputFields(attributes: Attribute[]): Record { + const inputFields: Array<[string, InputFieldDefinition] | []> = filterTruthy( + attributes.map((attribute) => { + const inputField = this.attributeToInputField(attribute.type); + const fieldDefinition: InputFieldDefinition = { + type: inputField, + defaultValue: attribute.annotations.default?.value, + }; + if (inputField) { + return [attribute.name, fieldDefinition]; + } + }) + ); + return Object.fromEntries(inputFields); + } + + private attributeToInputField(type: AttributeType): any { + if (type instanceof ListType) { + if (type.isRequired) { + return this.attributeToInputField(type.ofType).List.NonNull; + } + return this.attributeToInputField(type.ofType).List; + } + if (type instanceof ScalarType) { + return this.createBuiltInFieldInput(type); + } + if (type instanceof Neo4jTemporalType) { + return this.createTemporalFieldInput(type); + } + if (type instanceof Neo4jSpatialType) { + return this.createSpatialFieldInput(type); + } + } + + private createBuiltInFieldInput(type: ScalarType): WrappedComposer { + let builtInType: ScalarTypeComposer; + switch (type.name) { + case GraphQLBuiltInScalarType.Boolean: { + builtInType = this.schemaBuilder.types.boolean; + break; + } + case GraphQLBuiltInScalarType.String: { + builtInType = this.schemaBuilder.types.string; + break; + } + case GraphQLBuiltInScalarType.ID: { + builtInType = this.schemaBuilder.types.id; + break; + } + case GraphQLBuiltInScalarType.Int: { + builtInType = this.schemaBuilder.types.int; + break; + } + case GraphQLBuiltInScalarType.Float: { + builtInType = this.schemaBuilder.types.float; + break; + } + case Neo4jGraphQLNumberType.BigInt: { + builtInType = this.schemaBuilder.types.bigInt; + break; + } + default: { + throw new Error(`Unsupported type: ${type.name}`); + } + } + if (type.isRequired) { + return builtInType.NonNull; + } + return builtInType; + } + + private createTemporalFieldInput(type: Neo4jTemporalType): WrappedComposer { + let builtInType: ScalarTypeComposer; + switch (type.name) { + case Neo4jGraphQLTemporalType.Date: { + builtInType = this.schemaBuilder.types.date; + break; + } + case Neo4jGraphQLTemporalType.DateTime: { + builtInType = this.schemaBuilder.types.dateTime; + break; + } + case Neo4jGraphQLTemporalType.LocalDateTime: { + builtInType = this.schemaBuilder.types.localDateTime; + break; + } + case Neo4jGraphQLTemporalType.Time: { + builtInType = this.schemaBuilder.types.time; + break; + } + case Neo4jGraphQLTemporalType.LocalTime: { + builtInType = this.schemaBuilder.types.localTime; + break; + } + case Neo4jGraphQLTemporalType.Duration: { + builtInType = this.schemaBuilder.types.duration; + break; + } + default: { + throw new Error(`Unsupported type: ${type.name}`); + } + } + if (type.isRequired) { + return builtInType.NonNull; + } + return builtInType; + } + + private createSpatialFieldInput(type: Neo4jSpatialType): WrappedComposer { + let builtInType: InputTypeComposer; + switch (type.name) { + case Neo4jGraphQLSpatialType.CartesianPoint: { + builtInType = this.schemaBuilder.types.cartesianPointInput; + break; + } + case Neo4jGraphQLSpatialType.Point: { + builtInType = this.schemaBuilder.types.pointInput; + break; + } + default: { + throw new Error(`Unsupported type: ${type.name}`); + } + } + if (type.isRequired) { + return builtInType.NonNull; + } + return builtInType; + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelDeleteSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelDeleteSchemaTypes.ts new file mode 100644 index 0000000000..6517355490 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelDeleteSchemaTypes.ts @@ -0,0 +1,85 @@ +/* + * 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 { InputTypeComposer } from "graphql-compose"; +import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; +import type { TopLevelEntityTypeNames } from "../../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; +import type { InputFieldDefinition, SchemaBuilder } from "../../SchemaBuilder"; +import type { SchemaTypes } from "../SchemaTypes"; +import { RelatedEntityDeleteSchemaTypes } from "./RelatedEntityDeleteSchemaTypes"; + +export class TopLevelDeleteSchemaTypes { + private entityTypeNames: TopLevelEntityTypeNames; + private schemaBuilder: SchemaBuilder; + private entity: ConcreteEntity; + private schemaTypes: SchemaTypes; + + constructor({ + entity, + schemaBuilder, + schemaTypes, + }: { + entity: ConcreteEntity; + schemaBuilder: SchemaBuilder; + schemaTypes: SchemaTypes; + }) { + this.entity = entity; + this.entityTypeNames = entity.typeNames; + this.schemaBuilder = schemaBuilder; + this.schemaTypes = schemaTypes; + } + + public get deleteInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.deleteInput, (_itc: InputTypeComposer) => { + return { + fields: { + node: this.deleteNode, + }, + }; + }); + } + + public get deleteNode(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.deleteNode, (_itc: InputTypeComposer) => { + const relationshipFields = Array.from(this.entity.relationships.values()).reduce< + Record + >((acc, relationship) => { + const relatedEntityDeleteSchemaTypes = new RelatedEntityDeleteSchemaTypes({ + relationship, + schemaBuilder: this.schemaBuilder, + schemaTypes: this.schemaTypes, + }); + acc[relationship.name] = { + type: relatedEntityDeleteSchemaTypes.deleteOperation, + }; + return acc; + }, {}); + + const inputFields: Record = { + ...relationshipFields, + }; + + const isEmpty = Object.keys(inputFields).length === 0; + const fields = isEmpty ? { _emptyInput: this.schemaBuilder.types.boolean } : inputFields; + return { + fields, + }; + }); + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelUpdateSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelUpdateSchemaTypes.ts new file mode 100644 index 0000000000..25ff9e7786 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelUpdateSchemaTypes.ts @@ -0,0 +1,325 @@ +/* + * 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 { GraphQLScalarType } from "graphql"; +import type { InputTypeComposer, NonNullComposer, ScalarTypeComposer } from "graphql-compose"; +import type { Attribute } from "../../../../schema-model/attribute/Attribute"; +import type { AttributeType } from "../../../../schema-model/attribute/AttributeType"; +import { + GraphQLBuiltInScalarType, + ListType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, + Neo4jSpatialType, + Neo4jTemporalType, + ScalarType, +} from "../../../../schema-model/attribute/AttributeType"; +import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; +import { filterTruthy } from "../../../../utils/utils"; +import type { TopLevelEntityTypeNames } from "../../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; +import type { SchemaBuilder } from "../../SchemaBuilder"; +import type { SchemaTypes } from "../SchemaTypes"; + +export class TopLevelUpdateSchemaTypes { + private entityTypeNames: TopLevelEntityTypeNames; + private schemaTypes: SchemaTypes; + private schemaBuilder: SchemaBuilder; + private entity: ConcreteEntity; + + constructor({ + entity, + schemaBuilder, + schemaTypes, + }: { + entity: ConcreteEntity; + schemaBuilder: SchemaBuilder; + schemaTypes: SchemaTypes; + }) { + this.entity = entity; + this.entityTypeNames = entity.typeNames; + this.schemaBuilder = schemaBuilder; + this.schemaTypes = schemaTypes; + } + + public get updateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.updateInput, (_itc: InputTypeComposer) => { + return { + fields: { + node: this.updateNode.NonNull, + }, + }; + }); + } + + public get updateNode(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.updateNode, (_itc: InputTypeComposer) => { + const inputFields = this.getInputFields([...this.entity.attributes.values()]); + const isEmpty = Object.keys(inputFields).length === 0; + const fields = isEmpty ? { _emptyInput: this.schemaBuilder.types.boolean } : inputFields; + return { + fields, + }; + }); + } + + private getInputFields(attributes: Attribute[]): Record { + const inputFields: Array<[string, InputTypeComposer | GraphQLScalarType] | []> = filterTruthy( + attributes.map((attribute) => { + const inputField = this.attributeToInputField(attribute.type); + if (inputField) { + return [attribute.name, inputField]; + } + }) + ); + return Object.fromEntries(inputFields); + } + + private attributeToInputField(type: AttributeType): any { + const updateType: AttributeType = type instanceof ListType ? type.ofType : type; + + if (updateType instanceof ScalarType) { + return this.createBuiltInFieldInput(updateType); + } + if (updateType instanceof Neo4jTemporalType) { + return this.createTemporalFieldInput(updateType); + } + if (updateType instanceof Neo4jSpatialType) { + return this.createSpatialFieldInput(updateType); + } + } + + private createBuiltInFieldInput( + type: ScalarType + ): InputTypeComposer | ScalarTypeComposer | NonNullComposer { + let builtInType: ScalarTypeComposer | InputTypeComposer; + switch (type.name) { + case GraphQLBuiltInScalarType.Boolean: { + builtInType = this.booleanUpdateInput; + break; + } + case GraphQLBuiltInScalarType.String: { + builtInType = this.stringUpdateInput; + break; + } + case GraphQLBuiltInScalarType.ID: { + builtInType = this.idUpdateInput; + break; + } + case GraphQLBuiltInScalarType.Int: { + builtInType = this.intUpdateInput; + break; + } + case GraphQLBuiltInScalarType.Float: { + builtInType = this.floatUpdateInput; + break; + } + case Neo4jGraphQLNumberType.BigInt: { + builtInType = this.bigIntUpdateInput; + break; + } + default: { + throw new Error(`Unsupported type: ${type.name}`); + } + } + + return builtInType; + } + + private createTemporalFieldInput(type: Neo4jTemporalType): InputTypeComposer { + let builtInType: InputTypeComposer; + switch (type.name) { + case Neo4jGraphQLTemporalType.Date: { + builtInType = this.dateUpdateInput; + break; + } + case Neo4jGraphQLTemporalType.DateTime: { + builtInType = this.dateTimeUpdateInput; + break; + } + case Neo4jGraphQLTemporalType.LocalDateTime: { + builtInType = this.localDateTimeUpdateInput; + break; + } + case Neo4jGraphQLTemporalType.Time: { + builtInType = this.timeUpdateInput; + break; + } + case Neo4jGraphQLTemporalType.LocalTime: { + builtInType = this.localTimeUpdateInput; + break; + } + case Neo4jGraphQLTemporalType.Duration: { + builtInType = this.durationUpdateInput; + break; + } + default: { + throw new Error(`Unsupported type: ${type.name}`); + } + } + return builtInType; + } + + private createSpatialFieldInput(type: Neo4jSpatialType): InputTypeComposer { + let builtInType: InputTypeComposer; + switch (type.name) { + case Neo4jGraphQLSpatialType.CartesianPoint: { + builtInType = this.cartesianPointUpdateInput; + break; + } + case Neo4jGraphQLSpatialType.Point: { + builtInType = this.pointUpdateInput; + break; + } + default: { + throw new Error(`Unsupported type: ${type.name}`); + } + } + return builtInType; + } + + private get intUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("IntUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.int, + }, + }; + }); + } + + private get booleanUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("IntUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.boolean, + }, + }; + }); + } + private get stringUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("StringUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.string, + }, + }; + }); + } + private get idUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("IDUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.id, + }, + }; + }); + } + private get floatUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("FloatUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.float, + }, + }; + }); + } + private get bigIntUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("BigIntUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.bigInt, + }, + }; + }); + } + + private get dateUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DateUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.date, + }, + }; + }); + } + private get dateTimeUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DateTimeUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.dateTime, + }, + }; + }); + } + private get localDateTimeUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("LocalDateTimeUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.localDateTime, + }, + }; + }); + } + private get timeUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("TimeUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.time, + }, + }; + }); + } + private get localTimeUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("LocalTimeUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.localTime, + }, + }; + }); + } + private get durationUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DurationUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.duration, + }, + }; + }); + } + private get cartesianPointUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("CartesianPointInputUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.cartesianPointInput, + }, + }; + }); + } + private get pointUpdateInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("PointInputUpdate", () => { + return { + fields: { + set: this.schemaBuilder.types.pointInput, + }, + }; + }); + } +} diff --git a/packages/graphql/src/api-v6/schema-model/graphql-type-names/EntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/EntityTypeNames.ts new file mode 100644 index 0000000000..1549e4ab25 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-model/graphql-type-names/EntityTypeNames.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 { Entity } from "../../../schema-model/entity/Entity"; + +/** Abstract class to hold the typenames of a given entity */ +export abstract class EntityTypeNames { + protected readonly entityName: string; + + constructor(entity: Entity) { + this.entityName = entity.name; + } + + public abstract get connectionOperation(): string; + public abstract get operationWhere(): string; + public abstract get connection(): string; + public abstract get connectionSort(): string; + public abstract get edge(): string; + public abstract get edgeSort(): string; +} diff --git a/packages/graphql/src/api-v6/schema-model/graphql-type-names/RelatedEntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/RelatedEntityTypeNames.ts new file mode 100644 index 0000000000..bb9d46bff8 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-model/graphql-type-names/RelatedEntityTypeNames.ts @@ -0,0 +1,94 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Relationship } from "../../../schema-model/relationship/Relationship"; +import { upperFirst } from "../../../utils/upper-first"; +import { EntityTypeNames } from "./EntityTypeNames"; + +/** Typenames for a related entity, including edge properties */ +export class RelatedEntityTypeNames extends EntityTypeNames { + private relationship: Relationship; + private relatedEntityTypeName: string; + + constructor(relationship: Relationship) { + super(relationship.target); + + this.relatedEntityTypeName = `${relationship.source.name}${upperFirst(relationship.name)}`; + this.relationship = relationship; + } + + public get connectionOperation(): string { + return `${this.relatedEntityTypeName}Operation`; + } + + public get operationWhere(): string { + return `${this.relatedEntityTypeName}OperationWhere`; + } + + public get nestedOperationWhere(): string { + return `${this.relatedEntityTypeName}NestedOperationWhere`; + } + + public get connection(): string { + return `${this.relatedEntityTypeName}Connection`; + } + + public get connectionSort(): string { + return `${this.relatedEntityTypeName}ConnectionSort`; + } + + public get edge(): string { + return `${this.relatedEntityTypeName}Edge`; + } + + public get edgeSort(): string { + return `${this.relatedEntityTypeName}EdgeSort`; + } + + public get edgeWhere(): string { + return `${this.relatedEntityTypeName}EdgeWhere`; + } + + public get edgeListWhere(): string { + return `${this.relatedEntityTypeName}EdgeListWhere`; + } + + public get properties(): string | undefined { + return this.relationship.propertiesTypeName; + } + + public get propertiesWhere(): string { + return `${this.relationship.propertiesTypeName}Where`; + } + + public get propertiesSort(): string | undefined { + if (!this.relationship.propertiesTypeName) { + return; + } + return `${this.relationship.propertiesTypeName}Sort`; + } + + public get deleteOperation(): string { + return `${this.relatedEntityTypeName}DeleteOperation`; + } + + public get deleteInput(): string { + return `${this.relatedEntityTypeName}DeleteInput`; + } +} diff --git a/packages/graphql/src/api-v6/schema-model/graphql-type-names/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/TopLevelEntityTypeNames.ts new file mode 100644 index 0000000000..6847970f9f --- /dev/null +++ b/packages/graphql/src/api-v6/schema-model/graphql-type-names/TopLevelEntityTypeNames.ts @@ -0,0 +1,125 @@ +/* + * 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 { plural } from "../../../schema-model/utils/string-manipulation"; +import { upperFirst } from "../../../utils/upper-first"; +import { EntityTypeNames } from "./EntityTypeNames"; + +/** Top level node typenames */ +export class TopLevelEntityTypeNames extends EntityTypeNames { + /** Top Level Query field */ + public get queryField(): string { + return plural(this.entityName); + } + + public get connectionOperation(): string { + return `${this.entityName}Operation`; + } + + public get operationWhere(): string { + return `${this.entityName}OperationWhere`; + } + + public get connection(): string { + return `${this.entityName}Connection`; + } + + public get connectionSort(): string { + return `${this.entityName}ConnectionSort`; + } + + public get edge(): string { + return `${this.entityName}Edge`; + } + + public get edgeSort(): string { + return `${this.entityName}EdgeSort`; + } + + public get edgeWhere(): string { + return `${this.entityName}EdgeWhere`; + } + + public get nodeWhere(): string { + return `${this.entityName}Where`; + } + + public get nodeSort(): string { + return `${this.entityName}Sort`; + } + + public get node(): string { + return this.entityName; + } + + /** Top Level Create field */ + public get createField(): string { + return `create${upperFirst(plural(this.entityName))}`; + } + + public get createNode(): string { + return `${upperFirst(this.entityName)}CreateNode`; + } + + public get createInput(): string { + return `${upperFirst(this.entityName)}CreateInput`; + } + + public get createResponse(): string { + return `${upperFirst(this.entityName)}CreateResponse`; + } + + public get createInfo(): string { + return `${upperFirst(this.entityName)}CreateInfo`; + } + + /** Top Level Update field */ + public get updateField(): string { + return `update${upperFirst(plural(this.entityName))}`; + } + + public get updateNode(): string { + return `${upperFirst(this.entityName)}UpdateNode`; + } + + public get updateInput(): string { + return `${upperFirst(this.entityName)}UpdateInput`; + } + + public get updateResponse(): string { + return `${upperFirst(this.entityName)}UpdateResponse`; + } + + public get updateInfo(): string { + return `${upperFirst(this.entityName)}UpdateInfo`; + } + + /** Top Level Delete field */ + public get deleteField(): string { + return `delete${upperFirst(plural(this.entityName))}`; + } + + public get deleteNode(): string { + return `${upperFirst(this.entityName)}DeleteNode`; + } + + public get deleteInput(): string { + return `${upperFirst(this.entityName)}DeleteInput`; + } +} diff --git a/packages/graphql/src/api-v6/translators/translate-create-operation.ts b/packages/graphql/src/api-v6/translators/translate-create-operation.ts new file mode 100644 index 0000000000..5477f199fa --- /dev/null +++ b/packages/graphql/src/api-v6/translators/translate-create-operation.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 Cypher from "@neo4j/cypher-builder"; +import Debug from "debug"; +import { DEBUG_TRANSLATE } from "../../constants"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { CreateOperationFactory } from "../queryIRFactory/CreateOperationFactory"; +import type { GraphQLTreeCreate } from "../queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree"; + +const debug = Debug(DEBUG_TRANSLATE); + +export function translateCreateOperation({ + context, + entity, + graphQLTreeCreate, +}: { + context: Neo4jGraphQLTranslationContext; + graphQLTreeCreate: GraphQLTreeCreate; + entity: ConcreteEntity; +}): Cypher.CypherResult { + const createFactory = new CreateOperationFactory(context.schemaModel); + const createAST = createFactory.createAST({ graphQLTreeCreate, entity }); + debug(createAST.print()); + const results = createAST.build(context); + return results.build(); +} diff --git a/packages/graphql/src/api-v6/translators/translate-delete-operation.ts b/packages/graphql/src/api-v6/translators/translate-delete-operation.ts new file mode 100644 index 0000000000..fff0cbe358 --- /dev/null +++ b/packages/graphql/src/api-v6/translators/translate-delete-operation.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 Cypher from "@neo4j/cypher-builder"; +import Debug from "debug"; +import { DEBUG_TRANSLATE } from "../../constants"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { DeleteOperationFactory } from "../queryIRFactory/DeleteOperationFactory"; +import type { GraphQLTreeDelete } from "../queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree"; + +const debug = Debug(DEBUG_TRANSLATE); + +export function translateDeleteResolver({ + context, + entity, + graphQLTreeDelete, +}: { + context: Neo4jGraphQLTranslationContext; + graphQLTreeDelete: GraphQLTreeDelete; + entity: ConcreteEntity; +}): Cypher.CypherResult { + const deleteFactory = new DeleteOperationFactory(context.schemaModel); + const deleteAST = deleteFactory.deleteAST({ graphQLTreeDelete, entity }); + debug(deleteAST.print()); + const results = deleteAST.build(context); + return results.build(); +} diff --git a/packages/graphql/src/api-v6/translators/translate-read-operation.ts b/packages/graphql/src/api-v6/translators/translate-read-operation.ts new file mode 100644 index 0000000000..a60c403c31 --- /dev/null +++ b/packages/graphql/src/api-v6/translators/translate-read-operation.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type Cypher from "@neo4j/cypher-builder"; +import Debug from "debug"; +import { DEBUG_TRANSLATE } from "../../constants"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { ReadOperationFactory } from "../queryIRFactory/ReadOperationFactory"; +import type { GraphQLTreeReadOperationTopLevel } from "../queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree"; + +const debug = Debug(DEBUG_TRANSLATE); + +export function translateReadOperation({ + context, + entity, + graphQLTree, +}: { + context: Neo4jGraphQLTranslationContext; + graphQLTree: GraphQLTreeReadOperationTopLevel; + entity: ConcreteEntity; +}): Cypher.CypherResult { + const readFactory = new ReadOperationFactory(context.schemaModel); + + const readOperation = readFactory.createAST({ graphQLTree, entity }); + debug(readOperation.print()); + const results = readOperation.build(context); + return results.build(); +} diff --git a/packages/graphql/src/api-v6/translators/translate-update-operation.ts b/packages/graphql/src/api-v6/translators/translate-update-operation.ts new file mode 100644 index 0000000000..1680c7025d --- /dev/null +++ b/packages/graphql/src/api-v6/translators/translate-update-operation.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 Cypher from "@neo4j/cypher-builder"; +import Debug from "debug"; +import { DEBUG_TRANSLATE } from "../../constants"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { UpdateOperationFactory } from "../queryIRFactory/UpdateOperationFactory"; +import type { GraphQLTreeUpdate } from "../queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree"; + +const debug = Debug(DEBUG_TRANSLATE); + +export function translateUpdateOperation({ + context, + entity, + graphQLTreeUpdate, +}: { + context: Neo4jGraphQLTranslationContext; + graphQLTreeUpdate: GraphQLTreeUpdate; + entity: ConcreteEntity; +}): Cypher.CypherResult { + const updateFactory = new UpdateOperationFactory(context.schemaModel); + const updateAST = updateFactory.createAST({ graphQLTreeUpdate, entity }); + debug(updateAST.print()); + const results = updateAST.build(context); + return results.build(); +} diff --git a/packages/graphql/src/api-v6/validation/rules/types.ts b/packages/graphql/src/api-v6/validation/rules/types.ts new file mode 100644 index 0000000000..966be6b683 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { Kind } from "graphql"; + +export type TypePath = (string | Kind.LIST_TYPE | Kind.NON_NULL_TYPE)[]; diff --git a/packages/graphql/src/api-v6/validation/rules/utils/find-type-path-in-type-paths.ts b/packages/graphql/src/api-v6/validation/rules/utils/find-type-path-in-type-paths.ts new file mode 100644 index 0000000000..088efe75e9 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/utils/find-type-path-in-type-paths.ts @@ -0,0 +1,24 @@ +/* + * 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 { TypePath } from "../types"; + +export function findTypePathInTypePaths(typePathToFind: TypePath, typePaths: TypePath[]): boolean { + const typePathString = typePathToFind.join(); + return typePaths.some((typePath) => typePathString === typePath.join()); +} diff --git a/packages/graphql/src/api-v6/validation/rules/utils/get-type-path.ts b/packages/graphql/src/api-v6/validation/rules/utils/get-type-path.ts new file mode 100644 index 0000000000..062af0bf94 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/utils/get-type-path.ts @@ -0,0 +1,28 @@ +/* + * 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 { TypeNode } from "graphql"; +import { Kind } from "graphql"; +import type { TypePath } from "../types"; + +export function getTypePath(typeNode: TypeNode, currentPath: TypePath = []): TypePath { + if (typeNode.kind === Kind.NON_NULL_TYPE || typeNode.kind === Kind.LIST_TYPE) { + return getTypePath(typeNode.type, [...currentPath, typeNode.kind]); + } + return [...currentPath, typeNode.name.value]; +} diff --git a/packages/graphql/src/api-v6/validation/rules/utils/is-type-a-built-in-type.ts b/packages/graphql/src/api-v6/validation/rules/utils/is-type-a-built-in-type.ts new file mode 100644 index 0000000000..8ea71ab117 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/utils/is-type-a-built-in-type.ts @@ -0,0 +1,29 @@ +/* + * 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 { + GraphQLBuiltInScalarType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, +} from "../../../../schema-model/attribute/AttributeType"; +export function isTypeABuiltInType(expectedType: string): boolean { + return [GraphQLBuiltInScalarType, Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType].some( + (enumValue) => enumValue[expectedType] === expectedType + ); +} diff --git a/packages/graphql/src/api-v6/validation/rules/utils/type-node-to-string.ts b/packages/graphql/src/api-v6/validation/rules/utils/type-node-to-string.ts new file mode 100644 index 0000000000..6444b6e786 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/utils/type-node-to-string.ts @@ -0,0 +1,29 @@ +/* + * 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 { TypeNode } from "graphql"; +import { Kind } from "graphql"; + +export function typeNodeToString(typeNode: TypeNode): string { + if (typeNode.kind === Kind.NON_NULL_TYPE) { + return `${typeNodeToString(typeNode.type)}!`; + } else if (typeNode.kind === Kind.LIST_TYPE) { + return `[${typeNodeToString(typeNode.type)}]`; + } + return typeNode.name.value; +} diff --git a/packages/graphql/src/api-v6/validation/rules/valid-default.ts b/packages/graphql/src/api-v6/validation/rules/valid-default.ts new file mode 100644 index 0000000000..fd4e615430 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/valid-default.ts @@ -0,0 +1,89 @@ +/* + * 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 { ASTVisitor, FieldDefinitionNode, StringValueNode } from "graphql"; +import type { SDLValidationContext } from "graphql/validation/ValidationContext"; +import { isSpatial, isTemporal } from "../../../constants"; +import { defaultDirective } from "../../../graphql/directives"; +import { + assertValid, + createGraphQLError, + DocumentValidationError, +} from "../../../schema/validation/custom-rules/utils/document-validation-error"; +import { getPathToNode } from "../../../schema/validation/custom-rules/utils/path-parser"; +import { assertArgumentHasSameTypeAsField } from "../../../schema/validation/custom-rules/utils/same-type-argument-as-field"; +import { getInnerTypeName, isArrayType } from "../../../schema/validation/custom-rules/utils/utils"; +import { isTypeABuiltInType } from "./utils/is-type-a-built-in-type"; + +export function ValidDefault(context: SDLValidationContext): ASTVisitor { + return { + FieldDefinition(fieldDefinitionNode: FieldDefinitionNode, _key, _parent, path, ancestors) { + const { directives } = fieldDefinitionNode; + if (!directives) { + return; + } + const defaultDirectiveNode = directives.find((directive) => directive.name.value === defaultDirective.name); + + if (!defaultDirectiveNode || !defaultDirectiveNode.arguments) { + return; + } + const defaultValue = defaultDirectiveNode.arguments.find((a) => a.name.value === "value"); + if (!defaultValue) { + return; + } + const expectedType = getInnerTypeName(fieldDefinitionNode.type); + const { isValid, errorMsg, errorPath } = assertValid(() => { + if (!isArrayType(fieldDefinitionNode)) { + if (isSpatial(expectedType)) { + throw new DocumentValidationError(`@default is not supported by Spatial types.`, ["value"]); + } else if (isTemporal(expectedType)) { + if (Number.isNaN(Date.parse((defaultValue?.value as StringValueNode).value))) { + throw new DocumentValidationError( + `@default.${defaultValue.name.value} is not a valid ${expectedType}`, + ["value"] + ); + } + } else if (!isTypeABuiltInType(expectedType)) { + //TODO: Add check for user defined enums that are currently not implemented + throw new DocumentValidationError( + `@default directive can only be used on Temporal types and types: Int | Float | String | Boolean | ID | Enum`, + [] + ); + } + } + assertArgumentHasSameTypeAsField({ + directiveName: "@default", + traversedDef: fieldDefinitionNode, + argument: defaultValue, + enums: [], + }); + }); + const [pathToNode] = getPathToNode(path, ancestors); + if (!isValid) { + context.reportError( + createGraphQLError({ + nodes: [fieldDefinitionNode], + path: [...pathToNode, fieldDefinitionNode.name.value, ...errorPath], + errorMsg, + }) + ); + } + }, + }; +} diff --git a/packages/graphql/src/api-v6/validation/rules/valid-id.ts b/packages/graphql/src/api-v6/validation/rules/valid-id.ts new file mode 100644 index 0000000000..fe6829d670 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/valid-id.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ASTVisitor, FieldDefinitionNode } from "graphql"; +import { GraphQLID, Kind } from "graphql"; +import type { SDLValidationContext } from "graphql/validation/ValidationContext"; +import { idDirective } from "../../../graphql/directives"; +import { + assertValid, + createGraphQLError, + DocumentValidationError, +} from "../../../schema/validation/custom-rules/utils/document-validation-error"; +import { getPathToNode } from "../../../schema/validation/custom-rules/utils/path-parser"; +import type { TypePath } from "./types"; +import { findTypePathInTypePaths } from "./utils/find-type-path-in-type-paths"; +import { getTypePath } from "./utils/get-type-path"; + +export function ValidID(context: SDLValidationContext): ASTVisitor { + return { + FieldDefinition(fieldDefinitionNode: FieldDefinitionNode, _key, _parent, path, ancestors) { + const { directives, type } = fieldDefinitionNode; + if (!directives) { + return; + } + const idAnnotation = directives.find((directive) => directive.name.value === idDirective.name); + + if (!idAnnotation) { + return; + } + + const { isValid, errorMsg, errorPath } = assertValid(() => { + const validTypePaths: TypePath[] = [[GraphQLID.name], [Kind.NON_NULL_TYPE, GraphQLID.name]]; + const typePath = getTypePath(type); + if (!findTypePathInTypePaths(typePath, validTypePaths)) { + if (typePath.includes(Kind.LIST_TYPE)) { + throw new DocumentValidationError("Cannot autogenerate an array.", ["@id"]); + } + throw new DocumentValidationError("Cannot autogenerate a non ID field.", ["@id"]); + } + }); + const [pathToNode] = getPathToNode(path, ancestors); + if (!isValid) { + context.reportError( + createGraphQLError({ + nodes: [fieldDefinitionNode], + path: [...pathToNode, fieldDefinitionNode.name.value, ...errorPath], + errorMsg, + }) + ); + } + }, + }; +} diff --git a/packages/graphql/src/api-v6/validation/rules/valid-limit.ts b/packages/graphql/src/api-v6/validation/rules/valid-limit.ts new file mode 100644 index 0000000000..32ab832587 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/valid-limit.ts @@ -0,0 +1,57 @@ +/* + * 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 { ASTVisitor, ObjectTypeDefinitionNode } from "graphql"; +import type { SDLValidationContext } from "graphql/validation/ValidationContext"; +import { limitDirective } from "../../../graphql/directives"; +import { verifyLimit } from "../../../schema/validation/custom-rules/directives/limit"; +import { + assertValid, + createGraphQLError, +} from "../../../schema/validation/custom-rules/utils/document-validation-error"; +import { getPathToNode } from "../../../schema/validation/custom-rules/utils/path-parser"; + +export function ValidLimit(context: SDLValidationContext): ASTVisitor { + return { + ObjectTypeDefinition(objectTypeDefinitionNode: ObjectTypeDefinitionNode, _key, _parent, path, ancestors) { + const { directives } = objectTypeDefinitionNode; + if (!directives) { + return; + } + const limitDirectiveNode = directives.find((directive) => directive.name.value === limitDirective.name); + if (!limitDirectiveNode) { + return; + } + + const { isValid, errorMsg, errorPath } = assertValid(() => + verifyLimit({ directiveNode: limitDirectiveNode }) + ); + const [pathToNode] = getPathToNode(path, ancestors); + if (!isValid) { + context.reportError( + createGraphQLError({ + nodes: [objectTypeDefinitionNode], + path: [...pathToNode, objectTypeDefinitionNode.name.value, ...errorPath], + errorMsg, + }) + ); + } + }, + }; +} diff --git a/packages/graphql/src/api-v6/validation/rules/valid-list-element.ts b/packages/graphql/src/api-v6/validation/rules/valid-list-element.ts new file mode 100644 index 0000000000..354431104d --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/valid-list-element.ts @@ -0,0 +1,72 @@ +/* + * 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 { ASTVisitor, FieldDefinitionNode } from "graphql"; +import { Kind } from "graphql"; +import type { SDLValidationContext } from "graphql/validation/ValidationContext"; +import { relationshipDirective } from "../../../graphql/directives"; +import { + assertValid, + createGraphQLError, + DocumentValidationError, +} from "../../../schema/validation/custom-rules/utils/document-validation-error"; +import { getPathToNode } from "../../../schema/validation/custom-rules/utils/path-parser"; +import { getInnerTypeName } from "../../../schema/validation/custom-rules/utils/utils"; +import type { TypePath } from "./types"; +import { findTypePathInTypePaths } from "./utils/find-type-path-in-type-paths"; +import { getTypePath } from "./utils/get-type-path"; +import { typeNodeToString } from "./utils/type-node-to-string"; + +export function ValidListField(context: SDLValidationContext): ASTVisitor { + return { + FieldDefinition(fieldDefinitionNode: FieldDefinitionNode, _key, _parent, path, ancestors) { + const { type, directives } = fieldDefinitionNode; + if (directives && directives.some((directive) => directive.name.value === relationshipDirective.name)) { + return; // Skip relationship fields as they are validated separately with a more specific message + } + const { isValid, errorMsg, errorPath } = assertValid(() => { + const typePath = getTypePath(type); + if (typePath.includes(Kind.LIST_TYPE)) { + const wrappedType = getInnerTypeName(type); + const validTypePaths: TypePath[] = [ + [Kind.LIST_TYPE, Kind.NON_NULL_TYPE, wrappedType], + [Kind.NON_NULL_TYPE, Kind.LIST_TYPE, Kind.NON_NULL_TYPE, wrappedType], + ]; + if (!findTypePathInTypePaths(typePath, validTypePaths)) { + const typeStr = typeNodeToString(type); + throw new DocumentValidationError( + `List of non-null elements are not supported. Found: ${typeStr}`, + [] + ); + } + } + }); + const [pathToNode] = getPathToNode(path, ancestors); + if (!isValid) { + context.reportError( + createGraphQLError({ + nodes: [fieldDefinitionNode], + path: [...pathToNode, ...errorPath], + errorMsg, + }) + ); + } + }, + }; +} diff --git a/packages/graphql/src/api-v6/validation/rules/valid-relationship.ts b/packages/graphql/src/api-v6/validation/rules/valid-relationship.ts new file mode 100644 index 0000000000..d160eba0bd --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/valid-relationship.ts @@ -0,0 +1,107 @@ +/* + * 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 { ASTVisitor, FieldDefinitionNode, NamedTypeNode, TypeNode } from "graphql"; +import { Kind } from "graphql"; +import type { SDLValidationContext } from "graphql/validation/ValidationContext"; +import { relationshipDirective } from "../../../graphql/directives"; +import { + GraphQLBuiltInScalarType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, +} from "../../../schema-model/attribute/AttributeType"; +import { + DocumentValidationError, + assertValid, + createGraphQLError, +} from "../../../schema/validation/custom-rules/utils/document-validation-error"; +import { getPathToNode } from "../../../schema/validation/custom-rules/utils/path-parser"; + +export function ValidRelationship(context: SDLValidationContext): ASTVisitor { + return { + FieldDefinition(fieldDefinitionNode: FieldDefinitionNode, _key, _parent, path, ancestors) { + const { type, directives } = fieldDefinitionNode; + if (!directives) { + return; + } + const relationshipDirectiveNode = directives.find( + (directive) => directive.name.value === relationshipDirective.name + ); + if (!relationshipDirectiveNode) { + return; + } + + const { isValid, errorMsg, errorPath } = assertValid(() => assertIsValidTargetForRelationship(type)); + const [pathToNode] = getPathToNode(path, ancestors); + if (!isValid) { + context.reportError( + createGraphQLError({ + nodes: [fieldDefinitionNode], + path: [...pathToNode, fieldDefinitionNode.name.value, ...errorPath], + errorMsg, + }) + ); + } + }, + }; +} +/** + * Check the following condition: + * Target is of type List and elements are non-nullable. + * Wrapped Type is of type ObjectType. + **/ +function assertIsValidTargetForRelationship(type: TypeNode): void { + if (type.kind === Kind.NON_NULL_TYPE) { + return assertIsValidTargetForRelationship(type.type); + } + if (type.kind !== Kind.LIST_TYPE) { + throw new DocumentValidationError(`@relationship can only be used on List target`, []); + } + const innerType = type.type; + const wrappedType = getWrappedKind(innerType); + const wrappedTypeName = wrappedType.name.value; + assertIsNotInBuiltInTypes(wrappedType); + if (innerType.kind !== Kind.NON_NULL_TYPE) { + const nullableListErrorMessage = `Invalid field type: List type relationship fields must be non-nullable and have non-nullable entries, please change type to [${wrappedTypeName}!]!`; + throw new DocumentValidationError(nullableListErrorMessage, []); + } +} + +function assertIsNotInBuiltInTypes(type: NamedTypeNode): void { + if (typeInBuiltInTypes(type)) { + throw new DocumentValidationError(`@relationship cannot be used with type: ${type.name.value}`, []); + } +} + +function typeInBuiltInTypes(type: NamedTypeNode): boolean { + return [GraphQLBuiltInScalarType, Neo4jGraphQLSpatialType, Neo4jGraphQLNumberType, Neo4jGraphQLTemporalType].some( + (typeEnum) => typeEnum[typeEnum[type.name.value]] + ); +} + +function getWrappedKind(typeNode: TypeNode): NamedTypeNode { + if (typeNode.kind === Kind.LIST_TYPE) { + return getWrappedKind(typeNode.type); + } + if (typeNode.kind === Kind.NON_NULL_TYPE) { + return getWrappedKind(typeNode.type); + } + return typeNode; +} diff --git a/packages/graphql/src/api-v6/validation/validate-v6-document.ts b/packages/graphql/src/api-v6/validation/validate-v6-document.ts new file mode 100644 index 0000000000..0f09d50925 --- /dev/null +++ b/packages/graphql/src/api-v6/validation/validate-v6-document.ts @@ -0,0 +1,137 @@ +/* + * 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, + EnumTypeDefinitionNode, + GraphQLDirective, + GraphQLNamedType, + InterfaceTypeDefinitionNode, + ObjectTypeDefinitionNode, + UnionTypeDefinitionNode, +} from "graphql"; +import { GraphQLSchema, extendSchema, specifiedDirectives, validateSchema } from "graphql"; +import { specifiedSDLRules } from "graphql/validation/specifiedRules"; +import * as directives from "../../graphql/directives"; +import { typeDependantDirectivesScaffolds } from "../../graphql/directives/type-dependant-directives/scaffolds"; +import { SortDirection } from "../../graphql/enums/SortDirection"; +import { CartesianPointDistance } from "../../graphql/input-objects/CartesianPointDistance"; +import { CartesianPointInput } from "../../graphql/input-objects/CartesianPointInput"; +import { PointDistance } from "../../graphql/input-objects/PointDistance"; +import { PointInput } from "../../graphql/input-objects/PointInput"; +import { CartesianPoint } from "../../graphql/objects/CartesianPoint"; +import { Point } from "../../graphql/objects/Point"; +import * as scalars from "../../graphql/scalars"; +import { ValidRelationshipProperties } from "../../schema/validation/custom-rules/features/valid-relationship-properties"; +import { ReservedTypeNames } from "../../schema/validation/custom-rules/valid-types/reserved-type-names"; +import { DirectiveCombinationValid } from "../../schema/validation/custom-rules/valid-types/valid-directive-combination"; +import { WarnIfListOfListsFieldDefinition } from "../../schema/validation/custom-rules/warnings/list-of-lists"; +import { validateSDL } from "../../schema/validation/validate-sdl"; +import type { Neo4jFeaturesSettings } from "../../types"; +import { ValidDefault } from "./rules/valid-default"; +import { ValidID } from "./rules/valid-id"; +import { ValidLimit } from "./rules/valid-limit"; +import { ValidListField } from "./rules/valid-list-element"; +import { ValidRelationship } from "./rules/valid-relationship"; + +function runNeo4jGraphQLValidationRules({ + schema, + document, +}: { + schema: GraphQLSchema; + document: DocumentNode; + extra: { + enums?: EnumTypeDefinitionNode[]; + interfaces?: InterfaceTypeDefinitionNode[]; + unions?: UnionTypeDefinitionNode[]; + objects?: ObjectTypeDefinitionNode[]; + }; +}) { + const errors = validateSDL( + document, + [ + ...specifiedSDLRules, + ValidRelationship, + ValidListField, + ValidLimit, + ValidDefault, + ValidID, + DirectiveCombinationValid, + ValidRelationshipProperties, + ReservedTypeNames, + WarnIfListOfListsFieldDefinition, + ], + schema + ); + if (errors.length) { + throw errors; + } +} + +export function validateV6Document({ + document, + additionalDefinitions, +}: { + document: DocumentNode; + features: Neo4jFeaturesSettings | undefined; + additionalDefinitions: { + additionalDirectives?: GraphQLDirective[]; + additionalTypes?: GraphQLNamedType[]; + enums?: EnumTypeDefinitionNode[]; + interfaces?: InterfaceTypeDefinitionNode[]; + unions?: UnionTypeDefinitionNode[]; + objects?: ObjectTypeDefinitionNode[]; + }; +}): void { + const filteredDocument = document; + const { additionalDirectives, additionalTypes, ...extra } = additionalDefinitions; + const schemaToExtend = new GraphQLSchema({ + directives: [ + ...Object.values(directives), + ...typeDependantDirectivesScaffolds, + ...specifiedDirectives, + ...(additionalDirectives ?? []), + ], + types: [ + ...Object.values(scalars), + Point, + CartesianPoint, + PointInput, + PointDistance, + CartesianPointInput, + CartesianPointDistance, + SortDirection, + ...(additionalTypes || []), + ], + }); + + runNeo4jGraphQLValidationRules({ + schema: schemaToExtend, + document: filteredDocument, + extra, + }); + + const schema = extendSchema(schemaToExtend, filteredDocument); + + const errors = validateSchema(schema); + const filteredErrors = errors.filter((e) => e.message !== "Query root type must be provided."); + if (filteredErrors.length) { + throw filteredErrors; + } +} diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index c08c0f0f93..e625ce963b 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -25,6 +25,9 @@ import { forEachField, getResolversFromSchema } from "@graphql-tools/utils"; import Debug from "debug"; import type { DocumentNode, GraphQLSchema } from "graphql"; import type { Driver, SessionConfig } from "neo4j-driver"; +import { Memoize } from "typescript-memoize"; +import { SchemaGenerator } from "../api-v6/schema-generation/SchemaGenerator"; +import { validateV6Document } from "../api-v6/validation/validate-v6-document"; import { DEBUG_ALL } from "../constants"; import { makeAugmentedSchema } from "../schema"; import type { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; @@ -35,8 +38,8 @@ import type { WrapResolverArguments } from "../schema/resolvers/composition/wrap import { wrapQueryAndMutation } from "../schema/resolvers/composition/wrap-query-and-mutation"; import { wrapSubscription, type WrapSubscriptionArgs } from "../schema/resolvers/composition/wrap-subscription"; import { defaultFieldResolver } from "../schema/resolvers/field/defaultField"; -import { validateDocument } from "../schema/validation"; import { validateUserDefinition } from "../schema/validation/schema-validation"; +import validateDocument from "../schema/validation/validate-document"; import type { ContextFeatures, Neo4jFeaturesSettings, Neo4jGraphQLSubscriptionsEngine } from "../types"; import { asArray } from "../utils/utils"; import type { ExecutorConstructorParam, Neo4jGraphQLSessionConfig } from "./Executor"; @@ -115,6 +118,34 @@ class Neo4jGraphQL { return this.getExecutableSchema(); } + @Memoize() + public getAuraSchema(): Promise { + const document = this.normalizeTypeDefinitions(this.typeDefs); + + if (this.validate) { + const { + enumTypes: enums, + interfaceTypes: interfaces, + unionTypes: unions, + objectTypes: objects, + } = getDefinitionNodes(document); + + validateV6Document({ + document: document, + features: this.features, + additionalDefinitions: { enums, interfaces, unions, objects }, + }); + } + + this.schemaModel = this.generateSchemaModel(document, true); + const schemaGenerator = new SchemaGenerator(); + + this._nodes = []; + this._relationships = []; + + return Promise.resolve(this.composeSchema(schemaGenerator.generate(this.schemaModel))); + } + public async getExecutableSchema(): Promise { if (!this.executableSchema) { this.executableSchema = this.generateExecutableSchema(); @@ -346,9 +377,9 @@ class Neo4jGraphQL { }; } - private generateSchemaModel(document: DocumentNode): Neo4jGraphQLSchemaModel { + private generateSchemaModel(document: DocumentNode, isAura = false): Neo4jGraphQLSchemaModel { if (!this.schemaModel) { - return generateModel(document); + return generateModel(document, isAura); } return this.schemaModel; } diff --git a/packages/graphql/src/classes/Subgraph.ts b/packages/graphql/src/classes/Subgraph.ts index e7f15b8abe..6819baa197 100644 --- a/packages/graphql/src/classes/Subgraph.ts +++ b/packages/graphql/src/classes/Subgraph.ts @@ -21,7 +21,7 @@ import { buildSubgraphSchema } from "@apollo/subgraph"; import { mergeTypeDefs } from "@graphql-tools/merge"; import type { IResolvers, TypeSource } from "@graphql-tools/utils"; import type { - ConstDirectiveNode, + DirectiveNode, DocumentNode, GraphQLDirective, GraphQLNamedType, @@ -30,12 +30,12 @@ import type { } from "graphql"; import { Kind, parse, print } from "graphql"; import type { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; +import type { Neo4jGraphQLComposedContext } from "../schema/resolvers/composition/wrap-query-and-mutation"; import { translateResolveReference } from "../translate/translate-resolve-reference"; +import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; import { execute } from "../utils"; import getNeo4jResolveTree from "../utils/get-neo4j-resolve-tree"; import { isInArray } from "../utils/is-in-array"; -import type { Neo4jGraphQLComposedContext } from "../schema/resolvers/composition/wrap-query-and-mutation"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; import type { ValueOf } from "../utils/value-of"; // TODO fetch the directive names from the spec @@ -189,7 +189,7 @@ export class Subgraph { private findFederationLinkMeta( typeDefs: TypeSource - ): { extension: SchemaExtensionNode; directive: ConstDirectiveNode } | undefined { + ): { extension: SchemaExtensionNode; directive: DirectiveNode } | undefined { const document = mergeTypeDefs(typeDefs); for (const definition of document.definitions) { @@ -221,7 +221,7 @@ export class Subgraph { return name.replace("@", ""); } - private parseLinkImportArgument(directive: ConstDirectiveNode): void { + private parseLinkImportArgument(directive: DirectiveNode): void { const argument = directive.arguments?.find((arg) => arg.name.value === "import"); if (argument) { diff --git a/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts b/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts index 482e47981e..55aa8fc72a 100644 --- a/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts +++ b/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts @@ -18,7 +18,7 @@ */ import Debug from "debug"; -import type { Driver, Session } from "neo4j-driver"; +import { type Driver, type Session } from "neo4j-driver"; import { DEBUG_EXECUTE } from "../../constants"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; @@ -312,55 +312,34 @@ async function getMissingConstraints({ schemaModel: Neo4jGraphQLSchemaModel; session: Session; }): Promise { - const existingConstraints: Record = {}; - - const constraintsCypher = "SHOW UNIQUE CONSTRAINTS"; - debug(`About to execute Cypher: ${constraintsCypher}`); - const constraintsResult = await session.run<{ labelsOrTypes: [string]; properties: [string] }>(constraintsCypher); - - constraintsResult.records - .map((record) => { - return record.toObject(); - }) - .forEach((constraint) => { - const label = constraint.labelsOrTypes[0]; - const property = constraint.properties[0]; - - const existingConstraint = existingConstraints[label]; - - if (existingConstraint) { - existingConstraint.push(property); - } else { - existingConstraints[label] = [property]; - } - }); - + const existingUniqueConstraints = await getExistingUniqueConstraints(session); const missingConstraints: MissingConstraint[] = []; for (const entity of schemaModel.concreteEntities) { const entityAdapter = new ConcreteEntityAdapter(entity); + if (!entityAdapter.uniqueFields.length) { + continue; + } for (const uniqueField of entityAdapter.uniqueFields) { if (!uniqueField.annotations.unique) { continue; } - - let anyLabelHasConstraint = false; - for (const label of entity.labels) { - // If any of the constraints for the label already exist, skip to the next unique field - if (existingConstraints[label]?.includes(uniqueField.databaseName)) { - anyLabelHasConstraint = true; - break; + // TODO: remove default value once the constraintName argument is required + const constraintName = + uniqueField.annotations.unique.constraintName ?? `${entity.name}_${uniqueField.databaseName}`; + + const hasUniqueConstraint = [...entity.labels].some((label) => { + const constraintsForLabel = existingUniqueConstraints.get(label); + if (!constraintsForLabel) { + return false; } - } - if (anyLabelHasConstraint === false) { - // TODO: The fallback value of `${entity.name}_${uniqueField.databaseName}` should be changed to use the main label of the entity - // But this can only be done once the translation layer has been updated to use the schema model instead of the Node class - const constraintName = - uniqueField.annotations.unique.constraintName || `${entity.name}_${uniqueField.databaseName}`; + return constraintsForLabel.get(uniqueField.databaseName) === constraintName; + }); + if (!hasUniqueConstraint) { missingConstraints.push({ - constraintName, + constraintName: constraintName, label: entityAdapter.getMainLabel(), property: uniqueField.databaseName, }); @@ -370,3 +349,30 @@ async function getMissingConstraints({ return missingConstraints; } + +// Map that holds as key the label name and as value a Map with properties -> constraint name +type UniqueConstraints = Map>; + +async function getExistingUniqueConstraints(session: Session): Promise { + const CYPHER_SHOW_UNIQUE_CONSTRAINTS = "SHOW UNIQUE CONSTRAINTS"; + debug(`About to execute Cypher: ${CYPHER_SHOW_UNIQUE_CONSTRAINTS}`); + const constraintsQueryResult = await session.run<{ labelsOrTypes: [string]; properties: [string]; name: string }>( + CYPHER_SHOW_UNIQUE_CONSTRAINTS + ); + + const existingConstraints: UniqueConstraints = constraintsQueryResult.records + .map((record) => { + return record.toObject(); + }) + .reduce((acc, current) => { + const label = current.labelsOrTypes[0]; + const property = current.properties[0]; + if (acc.has(label)) { + acc.get(label)?.set(property, current.name); + return acc; + } + acc.set(label, new Map([[property, current.name]])); + return acc; + }, new Map>()); + return existingConstraints; +} diff --git a/packages/graphql/src/graphql/directives/unique.ts b/packages/graphql/src/graphql/directives/unique.ts index 0a0e84b9c1..de263158b9 100644 --- a/packages/graphql/src/graphql/directives/unique.ts +++ b/packages/graphql/src/graphql/directives/unique.ts @@ -26,9 +26,8 @@ export const uniqueDirective = new GraphQLDirective({ locations: [DirectiveLocation.FIELD_DEFINITION], args: { constraintName: { - description: - "The name which should be used for this constraint. By default; type name, followed by an underscore, followed by the field name.", - type: GraphQLString, + description: "The name which should be used for this constraint", + type: GraphQLString, // TODO: make the constraintName required in v6 }, }, }); diff --git a/packages/graphql/src/schema-model/argument/model-adapters/ArgumentAdapter.ts b/packages/graphql/src/schema-model/argument/model-adapters/ArgumentAdapter.ts index e4f6e247dc..11252010bf 100644 --- a/packages/graphql/src/schema-model/argument/model-adapters/ArgumentAdapter.ts +++ b/packages/graphql/src/schema-model/argument/model-adapters/ArgumentAdapter.ts @@ -18,21 +18,8 @@ */ import type { AttributeType } from "../../attribute/AttributeType"; -import { - EnumType, - GraphQLBuiltInScalarType, - InterfaceType, - ListType, - Neo4jCartesianPointType, - Neo4jGraphQLNumberType, - Neo4jGraphQLSpatialType, - Neo4jGraphQLTemporalType, - Neo4jPointType, - ObjectType, - ScalarType, - UnionType, - UserScalarType, -} from "../../attribute/AttributeType"; +import { ListType } from "../../attribute/AttributeType"; +import { AttributeTypeHelper } from "../../attribute/AttributeTypeHelper"; import type { Argument } from "../Argument"; // TODO: this file has a lot in common with AttributeAdapter @@ -43,6 +30,7 @@ export class ArgumentAdapter { public type: AttributeType; public description?: string; public defaultValue?: string; + public typeHelper: AttributeTypeHelper; private assertionOptions: { includeLists: boolean; }; @@ -54,193 +42,29 @@ export class ArgumentAdapter { this.assertionOptions = { includeLists: true, }; - } - - /** - * Just a helper to get the wrapped type in case of a list, useful for the assertions - */ - private getTypeForAssertion(includeLists: boolean) { - if (includeLists) { - return this.isList() ? this.type.ofType : this.type; - } - return this.type; - } - - isBoolean(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.Boolean; - } - - isID(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.ID; - } - - isInt(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.Int; - } - - isFloat(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.Float; - } - - isString(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.String; - } - - isCartesianPoint(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof Neo4jCartesianPointType; - } - isPoint(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof Neo4jPointType; + this.typeHelper = new AttributeTypeHelper(argument.type); } - isBigInt(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLNumberType.BigInt; - } - - isDate(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.Date; - } - - isDateTime(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.DateTime; - } - - isLocalDateTime(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.LocalDateTime; - } - - isTime(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.Time; - } - - isLocalTime(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return (type.name as Neo4jGraphQLTemporalType) === Neo4jGraphQLTemporalType.LocalTime; - } - - isDuration(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return (type.name as Neo4jGraphQLTemporalType) === Neo4jGraphQLTemporalType.Duration; - } - - isObject(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ObjectType; - } - - isEnum(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof EnumType; - } - - isInterface(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof InterfaceType; - } - - isUnion(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof UnionType; - } - - isList(): this is this & { type: ListType } { - return this.type instanceof ListType; - } - - isUserScalar(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type instanceof UserScalarType; - } - - isTemporal(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type.name in Neo4jGraphQLTemporalType; - } - - isListElementRequired(): boolean { - if (!(this.type instanceof ListType)) { - return false; - } - return this.type.ofType.isRequired; - } - - isRequired(): boolean { - return this.type.isRequired; - } - - /** - * - * Schema Generator Stuff - * - */ - isGraphQLBuiltInScalar(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type.name in GraphQLBuiltInScalarType; - } - - isSpatial(options = this.assertionOptions): boolean { - const type = this.getTypeForAssertion(options.includeLists); - return type.name in Neo4jGraphQLSpatialType; - } - - isAbstract(options = this.assertionOptions): boolean { - return this.isInterface(options) || this.isUnion(options); - } - /** - * Returns true for both built-in and user-defined scalars - **/ - isScalar(options = this.assertionOptions): boolean { - return ( - this.isGraphQLBuiltInScalar(options) || - this.isTemporal(options) || - this.isBigInt(options) || - this.isUserScalar(options) - ); - } - - isNumeric(options = this.assertionOptions): boolean { - return this.isBigInt(options) || this.isFloat(options) || this.isInt(options); - } - - /** - * END of category assertions - */ - - /** - * - * Schema Generator Stuff - * - */ - // Duplicate from AttributeAdapter getTypePrettyName(): string { - if (!this.isList()) { - return `${this.getTypeName()}${this.isRequired() ? "!" : ""}`; + if (!this.typeHelper.isList()) { + return `${this.getTypeName()}${this.typeHelper.isRequired() ? "!" : ""}`; } const listType = this.type as ListType; if (listType.ofType instanceof ListType) { // matrix case - return `[[${this.getTypeName()}${this.isListElementRequired() ? "!" : ""}]]${this.isRequired() ? "!" : ""}`; + return `[[${this.getTypeName()}${this.typeHelper.isListElementRequired() ? "!" : ""}]]${ + this.typeHelper.isRequired() ? "!" : "" + }`; } - return `[${this.getTypeName()}${this.isListElementRequired() ? "!" : ""}]${this.isRequired() ? "!" : ""}`; + return `[${this.getTypeName()}${this.typeHelper.isListElementRequired() ? "!" : ""}]${ + this.typeHelper.isRequired() ? "!" : "" + }`; } - // Duplicate from AttributeAdapter getTypeName(): string { - if (!this.isList()) { + if (!this.typeHelper.isList()) { return this.type.name; } const listType = this.type as ListType; diff --git a/packages/graphql/src/schema-model/attribute/AttributeType.ts b/packages/graphql/src/schema-model/attribute/AttributeType.ts index 2f15091b60..02b82bd650 100644 --- a/packages/graphql/src/schema-model/attribute/AttributeType.ts +++ b/packages/graphql/src/schema-model/attribute/AttributeType.ts @@ -25,15 +25,17 @@ export enum GraphQLBuiltInScalarType { ID = "ID", } +export enum Neo4jGraphQLNumberType { + BigInt = "BigInt", +} + +export type Neo4jGraphQLScalarType = GraphQLBuiltInScalarType | Neo4jGraphQLNumberType; + export enum Neo4jGraphQLSpatialType { CartesianPoint = "CartesianPoint", Point = "Point", } -export enum Neo4jGraphQLNumberType { - BigInt = "BigInt", -} - export enum Neo4jGraphQLTemporalType { DateTime = "DateTime", LocalDateTime = "LocalDateTime", @@ -43,32 +45,30 @@ export enum Neo4jGraphQLTemporalType { Duration = "Duration", } -export type Neo4jGraphQLScalarType = Neo4jGraphQLTemporalType | Neo4jGraphQLNumberType; - // The ScalarType class is not used to represent user defined scalar types, see UserScalarType for that. export class ScalarType { - public readonly name: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType; + public readonly name: Neo4jGraphQLScalarType; public readonly isRequired: boolean; - constructor(name: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType, isRequired: boolean) { + constructor(name: Neo4jGraphQLScalarType, isRequired: boolean) { this.name = name; this.isRequired = isRequired; } } -export class Neo4jCartesianPointType { - public readonly name: string; +export class Neo4jTemporalType { + public readonly name: Neo4jGraphQLTemporalType; public readonly isRequired: boolean; - constructor(isRequired: boolean) { - this.name = Neo4jGraphQLSpatialType.CartesianPoint; + constructor(name: Neo4jGraphQLTemporalType, isRequired: boolean) { + this.name = name; this.isRequired = isRequired; } } -export class Neo4jPointType { - public readonly name: string; +export class Neo4jSpatialType { + public readonly name: Neo4jGraphQLSpatialType; public readonly isRequired: boolean; - constructor(isRequired: boolean) { - this.name = Neo4jGraphQLSpatialType.Point; + constructor(name: Neo4jGraphQLSpatialType, isRequired: boolean) { + this.name = name; this.isRequired = isRequired; } } @@ -159,6 +159,8 @@ export class UnknownType { export type AttributeType = | ScalarType + | Neo4jSpatialType + | Neo4jTemporalType | UserScalarType | ObjectType | ListType diff --git a/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts b/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts index 8b56681082..f0c42efa6b 100644 --- a/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts +++ b/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts @@ -23,13 +23,10 @@ import { InputType, InterfaceType, ListType, - Neo4jCartesianPointType, Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - Neo4jPointType, ObjectType, - ScalarType, UnionType, UserScalarType, } from "./AttributeType"; @@ -59,72 +56,72 @@ export class AttributeTypeHelper { public isBoolean(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.Boolean; + return GraphQLBuiltInScalarType[type.name] === GraphQLBuiltInScalarType.Boolean; } public isID(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.ID; + return GraphQLBuiltInScalarType[type.name] === GraphQLBuiltInScalarType.ID; } public isInt(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.Int; + return GraphQLBuiltInScalarType[type.name] === GraphQLBuiltInScalarType.Int; } public isFloat(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.Float; + return GraphQLBuiltInScalarType[type.name] === GraphQLBuiltInScalarType.Float; } public isString(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === GraphQLBuiltInScalarType.String; + return GraphQLBuiltInScalarType[type.name] === GraphQLBuiltInScalarType.String; } public isCartesianPoint(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof Neo4jCartesianPointType; + return Neo4jGraphQLSpatialType[type.name] === Neo4jGraphQLSpatialType.CartesianPoint; } public isPoint(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof Neo4jPointType; + return Neo4jGraphQLSpatialType[type.name] === Neo4jGraphQLSpatialType.Point; } public isBigInt(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLNumberType.BigInt; + return Neo4jGraphQLNumberType[type.name] === Neo4jGraphQLNumberType.BigInt; } public isDate(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.Date; + return Neo4jGraphQLTemporalType[type.name] === Neo4jGraphQLTemporalType.Date; } public isDateTime(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.DateTime; + return Neo4jGraphQLTemporalType[type.name] === Neo4jGraphQLTemporalType.DateTime; } public isLocalDateTime(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.LocalDateTime; + return Neo4jGraphQLTemporalType[type.name] === Neo4jGraphQLTemporalType.LocalDateTime; } public isTime(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof ScalarType && type.name === Neo4jGraphQLTemporalType.Time; + return Neo4jGraphQLTemporalType[type.name] === Neo4jGraphQLTemporalType.Time; } public isLocalTime(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return (type.name as Neo4jGraphQLTemporalType) === Neo4jGraphQLTemporalType.LocalTime; + return Neo4jGraphQLTemporalType[type.name] === Neo4jGraphQLTemporalType.LocalTime; } public isDuration(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return (type.name as Neo4jGraphQLTemporalType) === Neo4jGraphQLTemporalType.Duration; + return Neo4jGraphQLTemporalType[type.name] === Neo4jGraphQLTemporalType.Duration; } public isObject(options = this.assertionOptions): boolean { diff --git a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts index 1002f10912..1cd8eab6b2 100644 --- a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.test.ts @@ -25,10 +25,11 @@ import { GraphQLBuiltInScalarType, InterfaceType, ListType, - Neo4jCartesianPointType, Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - Neo4jPointType, + Neo4jSpatialType, + Neo4jTemporalType, ObjectType, ScalarType, UnionType, @@ -154,7 +155,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new Neo4jCartesianPointType(true), + type: new Neo4jSpatialType(Neo4jGraphQLSpatialType.CartesianPoint, true), args: [], }) ); @@ -167,7 +168,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new Neo4jPointType(true), + type: new Neo4jSpatialType(Neo4jGraphQLSpatialType.Point, true), args: [], }) ); @@ -193,7 +194,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), + type: new Neo4jTemporalType(Neo4jGraphQLTemporalType.Date, true), args: [], }) ); @@ -206,7 +207,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new ScalarType(Neo4jGraphQLTemporalType.DateTime, true), + type: new Neo4jTemporalType(Neo4jGraphQLTemporalType.DateTime, true), args: [], }) ); @@ -219,7 +220,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new ScalarType(Neo4jGraphQLTemporalType.LocalDateTime, true), + type: new Neo4jTemporalType(Neo4jGraphQLTemporalType.LocalDateTime, true), args: [], }) ); @@ -232,7 +233,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new ScalarType(Neo4jGraphQLTemporalType.Time, true), + type: new Neo4jTemporalType(Neo4jGraphQLTemporalType.Time, true), args: [], }) ); @@ -245,7 +246,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new ScalarType(Neo4jGraphQLTemporalType.LocalTime, true), + type: new Neo4jTemporalType(Neo4jGraphQLTemporalType.LocalTime, true), args: [], }) ); @@ -258,7 +259,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new ScalarType(Neo4jGraphQLTemporalType.Duration, true), + type: new Neo4jTemporalType(Neo4jGraphQLTemporalType.Duration, true), args: [], }) ); @@ -421,7 +422,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new Neo4jCartesianPointType(true), + type: new Neo4jSpatialType(Neo4jGraphQLSpatialType.CartesianPoint, true), args: [], }) ); @@ -443,7 +444,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), + type: new Neo4jTemporalType(Neo4jGraphQLTemporalType.Date, true), args: [], }) ); diff --git a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts index 55aeff3485..5508df0665 100644 --- a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts @@ -371,7 +371,6 @@ export class AttributeAdapter { return listType.ofType.ofType.name; } return listType.ofType.name; - // return this.isList() ? this.type.ofType.name : this.type.name; } getFieldTypeName(): string { diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index 18d67d5ab8..2e516dd634 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -17,6 +17,8 @@ * limitations under the License. */ +import { Memoize } from "typescript-memoize"; +import { TopLevelEntityTypeNames } from "../../api-v6/schema-model/graphql-type-names/TopLevelEntityTypeNames"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import { setsAreEqual } from "../../utils/sets-are-equal"; import type { Annotations } from "../annotation/Annotation"; @@ -68,6 +70,21 @@ export class ConcreteEntity implements Entity { } } + /** Note: Types of the new API */ + @Memoize() + public get typeNames(): TopLevelEntityTypeNames { + return new TopLevelEntityTypeNames(this); + } + + @Memoize() + get globalIdField(): Attribute | undefined { + for (const attr of this.attributes.values()) { + if (attr.annotations.relayId) { + return attr; + } + } + } + public isConcreteEntity(): this is ConcreteEntity { return true; } @@ -107,4 +124,12 @@ export class ConcreteEntity implements Entity { public findRelationship(name: string): Relationship | undefined { return this.relationships.get(name); } + + public hasAttribute(name: string): boolean { + return this.attributes.has(name); + } + + public hasRelationship(name: string): boolean { + return this.relationships.has(name); + } } diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index b930863127..3628623ec2 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -25,7 +25,6 @@ import type { UnionTypeDefinitionNode, } from "graphql"; import { Neo4jGraphQLSchemaValidationError } from "../classes"; -import { SCHEMA_CONFIGURATION_OBJECT_DIRECTIVES } from "./library-directives"; import { declareRelationshipDirective, nodeDirective, @@ -33,6 +32,8 @@ import { relationshipDirective, } from "../graphql/directives"; import getFieldTypeMeta from "../schema/get-field-type-meta"; +import { getInnerTypeName } from "../schema/validation/custom-rules/utils/utils"; +import { isInArray } from "../utils/is-in-array"; import { filterTruthy } from "../utils/utils"; import type { Operations } from "./Neo4jGraphQLSchemaModel"; import { Neo4jGraphQLSchemaModel } from "./Neo4jGraphQLSchemaModel"; @@ -40,8 +41,10 @@ import { Operation } from "./Operation"; import type { Attribute } from "./attribute/Attribute"; import type { CompositeEntity } from "./entity/CompositeEntity"; import { ConcreteEntity } from "./entity/ConcreteEntity"; +import type { Entity } from "./entity/Entity"; import { InterfaceEntity } from "./entity/InterfaceEntity"; import { UnionEntity } from "./entity/UnionEntity"; +import { SCHEMA_CONFIGURATION_OBJECT_DIRECTIVES } from "./library-directives"; import type { DefinitionCollection } from "./parser/definition-collection"; import { getDefinitionCollection } from "./parser/definition-collection"; import { parseAnnotations } from "./parser/parse-annotation"; @@ -50,12 +53,9 @@ import { parseAttribute, parseAttributeArguments } from "./parser/parse-attribut import { findDirective } from "./parser/utils"; import type { NestedOperation, QueryDirection, RelationshipDirection } from "./relationship/Relationship"; import { Relationship } from "./relationship/Relationship"; -import { isInArray } from "../utils/is-in-array"; import { RelationshipDeclaration } from "./relationship/RelationshipDeclaration"; -import type { Entity } from "./entity/Entity"; -import { getInnerTypeName } from "../schema/validation/custom-rules/utils/utils"; -export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { +export function generateModel(document: DocumentNode, isAura = false): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); const operations: Operations = definitionCollection.operations.reduce((acc, definition): Operations => { @@ -65,11 +65,35 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { // hydrate interface to typeNames map hydrateInterfacesToTypeNamesMap(definitionCollection); + const definitionNodesAsArray = Array.from(definitionCollection.nodes.values()); + const definitionNodes = isAura ? getAuraNodeDefinitions(definitionNodesAsArray) : definitionNodesAsArray; - const concreteEntities = Array.from(definitionCollection.nodes.values()).map((node) => - generateConcreteEntity(node, definitionCollection) + const concreteEntities = definitionNodes.map((node) => generateConcreteEntity(node, definitionCollection)); + const compositeEntities = isAura ? [] : getCompositeEntities(concreteEntities, definitionCollection); + const annotations = parseAnnotations(definitionCollection.schemaDirectives); + + const schema = new Neo4jGraphQLSchemaModel({ + compositeEntities, + concreteEntities, + operations, + annotations, + }); + definitionNodes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); + definitionCollection.interfaceTypes.forEach((def) => + hydrateRelationshipDeclarations(def, schema, definitionCollection) ); + addCompositeEntitiesToConcreteEntity(compositeEntities); + return schema; +} +function getAuraNodeDefinitions(definitionNodes: ObjectTypeDefinitionNode[]): ObjectTypeDefinitionNode[] { + return definitionNodes.filter((nodeDef) => findDirective(nodeDef.directives, nodeDirective.name)); +} + +function getCompositeEntities( + concreteEntities: ConcreteEntity[], + definitionCollection: DefinitionCollection +): (InterfaceEntity | UnionEntity)[] { const concreteEntitiesMap = concreteEntities.reduce((acc, entity) => { if (acc.has(entity.name)) { throw new Neo4jGraphQLSchemaValidationError(`Duplicate node ${entity.name}`); @@ -77,7 +101,6 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { acc.set(entity.name, entity); return acc; }, new Map()); - const interfaceEntities = Array.from(definitionCollection.interfaceToImplementingTypeNamesMap.entries()).map( ([name, concreteEntities]) => { const interfaceNode = definitionCollection.interfaceTypes.get(name); @@ -102,21 +125,7 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { ); }); - const annotations = parseAnnotations(definitionCollection.schemaDirectives); - - const schema = new Neo4jGraphQLSchemaModel({ - compositeEntities: [...unionEntities, ...interfaceEntities], - concreteEntities, - operations, - annotations, - }); - definitionCollection.nodes.forEach((def) => hydrateRelationships(def, schema, definitionCollection)); - definitionCollection.interfaceTypes.forEach((def) => - hydrateRelationshipDeclarations(def, schema, definitionCollection) - ); - addCompositeEntitiesToConcreteEntity(interfaceEntities); - addCompositeEntitiesToConcreteEntity(unionEntities); - return schema; + return [...interfaceEntities, ...unionEntities]; } function addCompositeEntitiesToConcreteEntity(compositeEntities: CompositeEntity[]): void { @@ -498,6 +507,13 @@ function generateConcreteEntity( definition: ObjectTypeDefinitionNode, definitionCollection: DefinitionCollection ): ConcreteEntity { + // schema configuration directives are propagated onto concrete entities + const schemaDirectives = definitionCollection.schemaExtension?.directives?.filter((x) => + isInArray(SCHEMA_CONFIGURATION_OBJECT_DIRECTIVES, x.name.value) + ); + const directives = [...(definition.directives ?? []), ...(schemaDirectives ?? [])]; + const annotations = parseAnnotations(directives); + const fields = (definition.fields || []).map((fieldDefinition) => { // If the attribute is the private directive then const isPrivateAttribute = findDirective(fieldDefinition.directives, privateDirective.name); @@ -513,12 +529,6 @@ function generateConcreteEntity( return parseAttribute(fieldDefinition, definitionCollection, definition.fields); }); - // schema configuration directives are propagated onto concrete entities - const schemaDirectives = definitionCollection.schemaExtension?.directives?.filter((x) => - isInArray(SCHEMA_CONFIGURATION_OBJECT_DIRECTIVES, x.name.value) - ); - const annotations = parseAnnotations((definition.directives || []).concat(schemaDirectives || [])); - return new ConcreteEntity({ name: definition.name.value, description: definition.description?.value, diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 930f993c92..4a355cfe2f 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -29,11 +29,11 @@ import { InputType, InterfaceType, ListType, - Neo4jCartesianPointType, Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - Neo4jPointType, + Neo4jSpatialType, + Neo4jTemporalType, ObjectType, ScalarType, UnionType, @@ -99,10 +99,12 @@ function parseTypeNode( case Kind.NAMED_TYPE: { if (isScalarType(typeNode.name.value)) { return new ScalarType(typeNode.name.value, isRequired); + } else if (isNeo4jGraphQLTemporalType(typeNode.name.value)) { + return new Neo4jTemporalType(typeNode.name.value, isRequired); } else if (isPoint(typeNode.name.value)) { - return new Neo4jPointType(isRequired); + return new Neo4jSpatialType(typeNode.name.value, isRequired); } else if (isCartesianPoint(typeNode.name.value)) { - return new Neo4jCartesianPointType(isRequired); + return new Neo4jSpatialType(typeNode.name.value, isRequired); } else if (isEnum(definitionCollection, typeNode.name.value)) { return new EnumType(typeNode.name.value, isRequired); } else if (isUserScalar(definitionCollection, typeNode.name.value)) { @@ -153,11 +155,11 @@ function isInput(definitionCollection: DefinitionCollection, name: string) { return definitionCollection.inputTypes.has(name); } -function isPoint(value: string): boolean { +function isPoint(value: string): value is Neo4jGraphQLSpatialType.Point { return isNeo4jGraphQLSpatialType(value) && value === Neo4jGraphQLSpatialType.Point; } -function isCartesianPoint(value): boolean { +function isCartesianPoint(value): value is Neo4jGraphQLSpatialType.CartesianPoint { return isNeo4jGraphQLSpatialType(value) && value === Neo4jGraphQLSpatialType.CartesianPoint; } @@ -165,8 +167,8 @@ export function isNeo4jGraphQLSpatialType(value: string): value is Neo4jGraphQLS return Object.values(Neo4jGraphQLSpatialType).includes(value); } -export function isScalarType(value: string): value is GraphQLBuiltInScalarType | Neo4jGraphQLScalarType { - return isGraphQLBuiltInScalar(value) || isNeo4jGraphQLNumberType(value) || isNeo4jGraphQLTemporalType(value); +export function isScalarType(value: string): value is Neo4jGraphQLScalarType { + return isGraphQLBuiltInScalar(value) || isNeo4jGraphQLNumberType(value); } function isGraphQLBuiltInScalar(value: string): value is GraphQLBuiltInScalarType { diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index e07f518040..f93ce2f9e1 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -17,12 +17,15 @@ * limitations under the License. */ +import { Memoize } from "typescript-memoize"; +import { RelatedEntityTypeNames } from "../../api-v6/schema-model/graphql-type-names/RelatedEntityTypeNames"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import type { RelationshipNestedOperationsOption, RelationshipQueryDirectionOption } from "../../constants"; import { upperFirst } from "../../utils/upper-first"; import type { Annotations } from "../annotation/Annotation"; import type { Argument } from "../argument/Argument"; import type { Attribute } from "../attribute/Attribute"; +import { ConcreteEntity } from "../entity/ConcreteEntity"; import type { Entity } from "../entity/Entity"; export type RelationshipDirection = "IN" | "OUT"; @@ -139,6 +142,16 @@ export class Relationship { }); } + /** Note: Types of the new API */ + @Memoize() + public get typeNames(): RelatedEntityTypeNames { + if (!(this.source instanceof ConcreteEntity)) { + throw new Error("Interfaces not supported"); + } + + return new RelatedEntityTypeNames(this); + } + private addAttribute(attribute: Attribute): void { if (this.attributes.has(attribute.name)) { throw new Neo4jGraphQLSchemaValidationError(`Attribute ${attribute.name} already exists in ${this.name}.`); @@ -150,6 +163,10 @@ export class Relationship { return this.attributes.get(name); } + public hasAttribute(name: string): boolean { + return this.attributes.has(name); + } + public setSiblings(siblingPropertiesTypeNames: string[]) { this.siblings = siblingPropertiesTypeNames; } diff --git a/packages/graphql/src/schema/resolvers/field/numerical.ts b/packages/graphql/src/schema/resolvers/field/numerical.ts index dd10fc1b97..9443e55262 100644 --- a/packages/graphql/src/schema/resolvers/field/numerical.ts +++ b/packages/graphql/src/schema/resolvers/field/numerical.ts @@ -61,7 +61,7 @@ function serializeValue(value) { export function numericalResolver(source, args, context, info: GraphQLResolveInfo) { const value = defaultFieldResolver(source, args, context, info); - + if (Array.isArray(value)) { return value.map((v) => { return serializeValue(v); diff --git a/packages/graphql/src/schema/validation/custom-rules/directives/id.ts b/packages/graphql/src/schema/validation/custom-rules/directives/id.ts index cb77b9074d..9f35b2d930 100644 --- a/packages/graphql/src/schema/validation/custom-rules/directives/id.ts +++ b/packages/graphql/src/schema/validation/custom-rules/directives/id.ts @@ -18,13 +18,11 @@ */ import type { DirectiveNode, FieldDefinitionNode } from "graphql"; import { Kind } from "graphql"; -import { parseValueNode } from "../../../../schema-model/parser/parse-value-node"; -import { getInnerTypeName } from "../utils/utils"; import { DocumentValidationError } from "../utils/document-validation-error"; import type { ObjectOrInterfaceWithExtensions } from "../utils/path-parser"; +import { getInnerTypeName } from "../utils/utils"; export function verifyId({ - directiveNode, traversedDef, }: { directiveNode: DirectiveNode; @@ -34,14 +32,6 @@ export function verifyId({ // delegate return; } - // TODO: remove the following as the argument "autogenerate" does not exists anymore - const autogenerateArg = directiveNode.arguments?.find((x) => x.name.value === "autogenerate"); - if (autogenerateArg) { - const autogenerate = parseValueNode(autogenerateArg.value); - if (!autogenerate) { - return; - } - } if (traversedDef.type.kind === Kind.LIST_TYPE) { throw new DocumentValidationError("Cannot autogenerate an array.", ["@id"]); diff --git a/packages/graphql/src/schema/validation/custom-rules/utils/same-type-argument-as-field.ts b/packages/graphql/src/schema/validation/custom-rules/utils/same-type-argument-as-field.ts index 97d547da09..b83f3a6932 100644 --- a/packages/graphql/src/schema/validation/custom-rules/utils/same-type-argument-as-field.ts +++ b/packages/graphql/src/schema/validation/custom-rules/utils/same-type-argument-as-field.ts @@ -16,11 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { EnumTypeDefinitionNode, ArgumentNode, FieldDefinitionNode, ValueNode } from "graphql"; +import type { ArgumentNode, EnumTypeDefinitionNode, FieldDefinitionNode, ValueNode } from "graphql"; import { Kind } from "graphql"; -import { fromValueKind, getInnerTypeName, isArrayType } from "./utils"; import { isSpatial, isTemporal } from "../../../../constants"; import { DocumentValidationError } from "./document-validation-error"; +import { fromValueKind, getInnerTypeName, isArrayType } from "./utils"; export function assertArgumentHasSameTypeAsField({ directiveName, @@ -71,7 +71,7 @@ function doTypesMatch(expectedType: string, argumentValueType: ValueNode, enums: return true; } if (expectedType.toLowerCase() === "id") { - return !!(fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === "string"); + return Boolean(fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === "string"); } return fromValueKind(argumentValueType, enums, expectedType)?.toLowerCase() === expectedType.toLowerCase(); } diff --git a/packages/graphql/src/schema/validation/validate-document.test.ts b/packages/graphql/src/schema/validation/validate-document.test.ts index 27b5f67f0f..58227301be 100644 --- a/packages/graphql/src/schema/validation/validate-document.test.ts +++ b/packages/graphql/src/schema/validation/validate-document.test.ts @@ -407,41 +407,7 @@ describe("validation 2.0", () => { expect(executeValidate).not.toThrow(); }); }); - describe("@default", () => { - test("@default property required", () => { - const doc = gql` - type User { - name: String @default - } - `; - // TODO: is "ScalarOrEnum" type exposed to the user? - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - expect(executeValidate).toThrow( - 'Directive "@default" argument "value" of type "ScalarOrEnum!" is required, but it was not provided.' - ); - }); - test("@default ok", () => { - const doc = gql` - type User { - name: String @default(value: "dummy") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - expect(executeValidate).not.toThrow(); - }); - }); describe("@fulltext", () => { test("@fulltext property required", () => { const doc = gql` @@ -998,70 +964,6 @@ describe("validation 2.0", () => { describe("Directive Argument Value", () => { describe("@default", () => { - test("@default on datetime must be valid datetime", () => { - const doc = gql` - type User { - updatedAt: DateTime @default(value: "dummy") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value is not a valid DateTime"); - expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default", "value"]); - }); - - test("@default on datetime must be valid datetime extension", () => { - const doc = gql` - type User { - id: ID - } - extend type User { - updatedAt: DateTime @default(value: "dummy") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value is not a valid DateTime"); - expect(errors[0]).toHaveProperty("path", ["User", "updatedAt", "@default", "value"]); - }); - - test("@default on datetime must be valid datetime correct", () => { - const doc = gql` - type User { - updatedAt: DateTime @default(value: "2023-07-06T09:45:11.336Z") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - test("@default on enum must be enum", () => { const enumTypes = gql` enum Status { @@ -1214,401 +1116,6 @@ describe("validation 2.0", () => { expect(executeValidate).not.toThrow(); }); - test("@default on int must be int", () => { - const doc = gql` - type User { - age: Int @default(value: "dummy") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on Int fields must be of type Int"); - expect(errors[0]).toHaveProperty("path", ["User", "age", "@default", "value"]); - }); - - test("@default on int must be int correct", () => { - const doc = gql` - type User { - age: Int @default(value: 23) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on int list must be list of int values", () => { - const doc = gql` - type User { - ages: [Int] @default(value: ["dummy"]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on Int list fields must be a list of Int values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "ages", "@default", "value"]); - }); - - test("@default on int list must be list of int values correct", () => { - const doc = gql` - type User { - ages: [Int] @default(value: [12]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on float must be float", () => { - const doc = gql` - type User { - avg: Float @default(value: 2) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on Float fields must be of type Float"); - expect(errors[0]).toHaveProperty("path", ["User", "avg", "@default", "value"]); - }); - - test("@default on float must be float correct", () => { - const doc = gql` - type User { - avg: Float @default(value: 2.3) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on float list must be list of float values", () => { - const doc = gql` - type User { - avgs: [Float] @default(value: [1]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on Float list fields must be a list of Float values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "avgs", "@default", "value"]); - }); - - test("@default on float list must be list of float values correct", () => { - const doc = gql` - type User { - avgs: [Float] @default(value: [1.2]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on boolean must be boolean", () => { - const doc = gql` - type User { - registered: Boolean @default(value: 2) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on Boolean fields must be of type Boolean"); - expect(errors[0]).toHaveProperty("path", ["User", "registered", "@default", "value"]); - }); - - test("@default on boolean must be boolean correct", () => { - const doc = gql` - type User { - registered: Boolean @default(value: false) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on boolean list must be list of boolean values", () => { - const doc = gql` - type User { - statuses: [Boolean] @default(value: [2]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on Boolean list fields must be a list of Boolean values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "statuses", "@default", "value"]); - }); - - test("@default on boolean list must be list of boolean values correct", () => { - const doc = gql` - type User { - statuses: [Boolean] @default(value: [true]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on string must be string", () => { - const doc = gql` - type User { - name: String @default(value: 2) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on String fields must be of type String"); - expect(errors[0]).toHaveProperty("path", ["User", "name", "@default", "value"]); - }); - - test("@default on string must be string correct", () => { - const doc = gql` - type User { - registered: String @default(value: "Bob") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on string list must be list of string values", () => { - const doc = gql` - type User { - names: [String] @default(value: [2]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on String list fields must be a list of String values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "names", "@default", "value"]); - }); - - test("@default on string list must be list of string values correct", () => { - const doc = gql` - type User { - names: [String] @default(value: ["Bob"]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on ID must be ID", () => { - const doc = gql` - type User { - uid: ID @default(value: 2) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "@default.value on ID fields must be of type ID"); - expect(errors[0]).toHaveProperty("path", ["User", "uid", "@default", "value"]); - }); - - test("@default on ID list must be list of ID values", () => { - const doc = gql` - type User { - ids: [ID] @default(value: [2]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - const errors = getError(executeValidate); - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty( - "message", - "@default.value on ID list fields must be a list of ID values" - ); - expect(errors[0]).toHaveProperty("path", ["User", "ids", "@default", "value"]); - }); - - test("@default on ID list must be list of ID values correct", () => { - const doc = gql` - type User { - ids: [ID] @default(value: ["123-223"]) - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - - test("@default on ID must be ID correct", () => { - const doc = gql` - type User { - uid: ID @default(value: "234-432") - } - `; - - const executeValidate = () => - validateDocument({ - document: doc, - additionalDefinitions, - features: {}, - }); - - expect(executeValidate).not.toThrow(); - }); - test("@default not supported on Spatial types", () => { const doc = gql` type User { @@ -3018,51 +2525,6 @@ describe("validation 2.0", () => { }); }); - describe("@id", () => { - test("@id autogenerate valid", () => { - const doc = gql` - type User { - uid: ID @id - } - `; - - const executeValidate = () => validateDocument({ document: doc, features: {}, additionalDefinitions }); - expect(executeValidate).not.toThrow(); - }); - - test("@id autogenerate cannot autogenerate array", () => { - const doc = gql` - type User { - uid: [ID] @id - } - `; - - const executeValidate = () => validateDocument({ document: doc, features: {}, additionalDefinitions }); - const errors = getError(executeValidate); - - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "Cannot autogenerate an array."); - expect(errors[0]).toHaveProperty("path", ["User", "uid", "@id"]); - }); - - test("@id autogenerate cannot autogenerate a non ID field", () => { - const doc = gql` - type User { - uid: String @id - } - `; - - const executeValidate = () => validateDocument({ document: doc, features: {}, additionalDefinitions }); - const errors = getError(executeValidate); - - expect(errors).toHaveLength(1); - expect(errors[0]).not.toBeInstanceOf(NoErrorThrownError); - expect(errors[0]).toHaveProperty("message", "Cannot autogenerate a non ID field."); - expect(errors[0]).toHaveProperty("path", ["User", "uid", "@id"]); - }); - }); - // TODO: validate custom resolver // needs a schema for graphql validation but then not running validators anymore for the logical validation // validate-custom-resolver-requires -> graphql validation diff --git a/packages/graphql/src/schema/validation/validate-document.ts b/packages/graphql/src/schema/validation/validate-document.ts index 79260ba5de..25bcc914a4 100644 --- a/packages/graphql/src/schema/validation/validate-document.ts +++ b/packages/graphql/src/schema/validation/validate-document.ts @@ -225,7 +225,7 @@ function runValidationRulesOnFilteredDocument({ if (filteredErrors.length) { throw filteredErrors; } -} +} function validateDocument({ document, diff --git a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts index 97b9ab4d3f..a5c8dd3f59 100644 --- a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts +++ b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts @@ -22,11 +22,9 @@ import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphq import { createNode } from "../utils/create-node-from-entity"; import { QueryASTContext, QueryASTEnv } from "./QueryASTContext"; import type { QueryASTNode } from "./QueryASTNode"; -import { AggregationOperation } from "./operations/AggregationOperation"; -import { ConnectionReadOperation } from "./operations/ConnectionReadOperation"; -import { DeleteOperation } from "./operations/DeleteOperation"; -import { ReadOperation } from "./operations/ReadOperation"; -import { UnwindCreateOperation } from "./operations/UnwindCreateOperation"; +import { CompositeAggregationOperation } from "./operations/composite/CompositeAggregationOperation"; +import { CompositeConnectionReadOperation } from "./operations/composite/CompositeConnectionReadOperation"; +import { CompositeReadOperation } from "./operations/composite/CompositeReadOperation"; import type { Operation, OperationTranspileResult } from "./operations/operations"; export class QueryAST { @@ -81,14 +79,14 @@ export class QueryAST { private getTargetFromOperation(varName?: string): Cypher.Node | undefined { if ( - this.operation instanceof ReadOperation || - this.operation instanceof ConnectionReadOperation || - this.operation instanceof DeleteOperation || - this.operation instanceof AggregationOperation || - this.operation instanceof UnwindCreateOperation + this.operation instanceof CompositeReadOperation || + this.operation instanceof CompositeConnectionReadOperation || + this.operation instanceof CompositeAggregationOperation ) { - return createNode(varName); + return; } + return createNode(varName); + } public print(): string { diff --git a/packages/graphql/src/translate/queryAST/ast/QueryASTContext.ts b/packages/graphql/src/translate/queryAST/ast/QueryASTContext.ts index 8fb6297add..5cea1b670d 100644 --- a/packages/graphql/src/translate/queryAST/ast/QueryASTContext.ts +++ b/packages/graphql/src/translate/queryAST/ast/QueryASTContext.ts @@ -132,7 +132,7 @@ export class QueryASTContext { return new QueryASTContext({ source: this.source, diff --git a/packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/PointAttributeField.ts b/packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/SpatialAttributeField.ts similarity index 94% rename from packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/PointAttributeField.ts rename to packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/SpatialAttributeField.ts index 3838d6924d..286c5cd027 100644 --- a/packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/PointAttributeField.ts +++ b/packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/SpatialAttributeField.ts @@ -21,8 +21,8 @@ import Cypher from "@neo4j/cypher-builder"; import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter"; import { AttributeField } from "./AttributeField"; -export class PointAttributeField extends AttributeField { - private crs: boolean; +export class SpatialAttributeField extends AttributeField { + private crs: boolean; // crs flag is used to determine if the crs field should be included in the projection constructor({ attribute, alias, crs }: { attribute: AttributeAdapter; alias: string; crs: boolean }) { super({ alias, attribute }); diff --git a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts index 1a32e3e8cd..387ad40b6d 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts @@ -50,12 +50,15 @@ export class ConnectionFilter extends Filter { relationship: RelationshipAdapter; target: ConcreteEntityAdapter | InterfaceEntityAdapter; operator: RelationshipWhereOperator | undefined; - isNot: boolean; + isNot?: boolean; }) { super(); this.relationship = relationship; - this.isNot = isNot; + this.isNot = Boolean(isNot); this.operator = operator || "SOME"; + if (operator === "NONE") { + this.isNot = true; + } this.target = target; } diff --git a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PropertyFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PropertyFilter.ts index fbc82bf0de..ad21d26bc9 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PropertyFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PropertyFilter.ts @@ -19,14 +19,14 @@ import Cypher from "@neo4j/cypher-builder"; import type { AttributeAdapter } from "../../../../../schema-model/attribute/model-adapters/AttributeAdapter"; +import { InterfaceEntityAdapter } from "../../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; +import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { hasTarget } from "../../../utils/context-has-target"; import { createComparisonOperation } from "../../../utils/create-comparison-operator"; import type { QueryASTContext } from "../../QueryASTContext"; import type { QueryASTNode } from "../../QueryASTNode"; import type { FilterOperator } from "../Filter"; import { Filter } from "../Filter"; -import { hasTarget } from "../../../utils/context-has-target"; -import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; -import { InterfaceEntityAdapter } from "../../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; export class PropertyFilter extends Filter { protected attribute: AttributeAdapter; @@ -48,7 +48,7 @@ export class PropertyFilter extends Filter { relationship?: RelationshipAdapter; comparisonValue: unknown; operator: FilterOperator; - isNot: boolean; + isNot?: boolean; attachedTo?: "node" | "relationship"; }) { super(); @@ -56,7 +56,7 @@ export class PropertyFilter extends Filter { this.relationship = relationship; this.comparisonValue = comparisonValue; this.operator = operator; - this.isNot = isNot; + this.isNot = Boolean(isNot); this.attachedTo = attachedTo ?? "node"; } diff --git a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PointFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/SpatialFilter.ts similarity index 98% rename from packages/graphql/src/translate/queryAST/ast/filters/property-filters/PointFilter.ts rename to packages/graphql/src/translate/queryAST/ast/filters/property-filters/SpatialFilter.ts index 529e267b40..6baffb31bd 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/PointFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/SpatialFilter.ts @@ -22,7 +22,7 @@ import type { AttributeAdapter } from "../../../../../schema-model/attribute/mod import type { WhereOperator } from "../Filter"; import { PropertyFilter } from "./PropertyFilter"; -export class PointFilter extends PropertyFilter { +export class SpatialFilter extends PropertyFilter { protected getOperation(prop: Cypher.Property): Cypher.ComparisonOp { return this.createPointOperation({ operator: this.operator || "EQ", diff --git a/packages/graphql/src/translate/queryAST/ast/input-fields/InputField.ts b/packages/graphql/src/translate/queryAST/ast/input-fields/InputField.ts index 3e468ea5fb..67bb9c53f4 100644 --- a/packages/graphql/src/translate/queryAST/ast/input-fields/InputField.ts +++ b/packages/graphql/src/translate/queryAST/ast/input-fields/InputField.ts @@ -43,5 +43,7 @@ export abstract class InputField extends QueryASTNode { return target; } - abstract getSetParams(_queryASTContext: QueryASTContext, inputVariable?: Cypher.Variable): Cypher.SetParam[]; + public getSetParams(_queryASTContext: QueryASTContext, _inputVariable?: Cypher.Variable): Cypher.SetParam[] { + return []; + } } diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts index 5198284b5f..cb7d366a45 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts @@ -53,15 +53,22 @@ export class ConnectionReadOperation extends Operation { relationship, target, selection, + fields, }: { - relationship: RelationshipAdapter | undefined; + relationship?: RelationshipAdapter; target: ConcreteEntityAdapter; selection: EntitySelection; + fields?: { + node: Field[]; + edge: Field[]; + }; }) { super(); this.relationship = relationship; this.target = target; this.selection = selection; + this.nodeFields = fields?.node ?? []; + this.edgeFields = fields?.edge ?? []; } public setNodeFields(fields: Field[]) { diff --git a/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts index 14b04610c9..0599c8661f 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts @@ -34,6 +34,7 @@ export type FulltextOptions = { score: Cypher.Variable; }; + export class FulltextOperation extends ReadOperation { private scoreField: ScoreField | undefined; diff --git a/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts index 7afbf0f8a9..eea10fc5d0 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts @@ -23,7 +23,7 @@ import { QueryASTNode } from "../QueryASTNode"; export type SelectionClause = Cypher.Match | Cypher.With | Cypher.Yield; -export abstract class EntitySelection extends QueryASTNode { +export abstract class EntitySelection extends QueryASTNode { public getChildren(): QueryASTNode[] { return []; } @@ -32,6 +32,6 @@ export abstract class EntitySelection extends QueryASTNode { * TODO: Improve naming */ public abstract apply(context: QueryASTContext): { nestedContext: QueryASTContext; - selection: SelectionClause; + selection: T; }; } diff --git a/packages/graphql/src/translate/queryAST/ast/selection/NodeSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/NodeSelection.ts index ba19bf75e2..23f2f1c4ea 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/NodeSelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/NodeSelection.ts @@ -21,9 +21,9 @@ import Cypher from "@neo4j/cypher-builder"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { createNode, getEntityLabels } from "../../utils/create-node-from-entity"; import { QueryASTContext } from "../QueryASTContext"; -import { EntitySelection, type SelectionClause } from "./EntitySelection"; +import { EntitySelection } from "./EntitySelection"; -export class NodeSelection extends EntitySelection { +export class NodeSelection extends EntitySelection { private target: ConcreteEntityAdapter; private alias: string | undefined; private optional: boolean; @@ -49,7 +49,7 @@ export class NodeSelection extends EntitySelection { public apply(context: QueryASTContext): { nestedContext: QueryASTContext; - selection: SelectionClause; + selection: Cypher.Match; } { let node: Cypher.Node; let matchPattern: Cypher.Pattern | undefined; diff --git a/packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.ts new file mode 100644 index 0000000000..0a2e0cf804 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import type { QueryASTContext } from "../QueryASTContext"; +import { EntitySelection, type SelectionClause } from "./EntitySelection"; + +export class WithWildCardsSelection extends EntitySelection { + constructor() { + super(); + } + + public apply(context: QueryASTContext): { + nestedContext: QueryASTContext; + selection: SelectionClause; + } { + return { + selection: new Cypher.With("*"), + nestedContext: context, + }; + } +} diff --git a/packages/graphql/src/translate/queryAST/factory/FieldFactory.ts b/packages/graphql/src/translate/queryAST/factory/FieldFactory.ts index b0eb4f55fb..59c964bf25 100644 --- a/packages/graphql/src/translate/queryAST/factory/FieldFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/FieldFactory.ts @@ -37,7 +37,7 @@ import type { AggregationField } from "../ast/fields/aggregation-fields/Aggregat import { CountField } from "../ast/fields/aggregation-fields/CountField"; import { AttributeField } from "../ast/fields/attribute-fields/AttributeField"; import { DateTimeField } from "../ast/fields/attribute-fields/DateTimeField"; -import { PointAttributeField } from "../ast/fields/attribute-fields/PointAttributeField"; +import { SpatialAttributeField } from "../ast/fields/attribute-fields/SpatialAttributeField"; import type { ConnectionReadOperation } from "../ast/operations/ConnectionReadOperation"; import type { CompositeConnectionReadOperation } from "../ast/operations/composite/CompositeConnectionReadOperation"; import { isConcreteEntity } from "../utils/is-concrete-entity"; @@ -213,12 +213,13 @@ export class FieldFactory { }); } - if (attribute.typeHelper.isPoint() || attribute.typeHelper.isCartesianPoint()) { + if (attribute.typeHelper.isSpatial()) { const typeName = attribute.typeHelper.isList() ? (attribute.type as ListType).ofType.name : attribute.type.name; const { crs } = field.fieldsByTypeName[typeName] as any; - return new PointAttributeField({ + + return new SpatialAttributeField({ attribute, alias: field.alias, crs: Boolean(crs), diff --git a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts index cc31845f00..740c70803f 100644 --- a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts @@ -38,8 +38,8 @@ import { AggregationFilter } from "../ast/filters/aggregation/AggregationFilter" import { AggregationPropertyFilter } from "../ast/filters/aggregation/AggregationPropertyFilter"; import { CountFilter } from "../ast/filters/aggregation/CountFilter"; import { DurationFilter } from "../ast/filters/property-filters/DurationFilter"; -import { PointFilter } from "../ast/filters/property-filters/PointFilter"; import { PropertyFilter } from "../ast/filters/property-filters/PropertyFilter"; +import { SpatialFilter } from "../ast/filters/property-filters/SpatialFilter"; import { TypenameFilter } from "../ast/filters/property-filters/TypenameFilter"; import { getConcreteEntities } from "../utils/get-concrete-entities"; import { isConcreteEntity } from "../utils/is-concrete-entity"; @@ -191,8 +191,8 @@ export class FilterFactory { attachedTo, }); } - if (attribute.typeHelper.isPoint() || attribute.typeHelper.isCartesianPoint()) { - return new PointFilter({ + if (attribute.typeHelper.isSpatial()) { + return new SpatialFilter({ attribute, comparisonValue, isNot, diff --git a/packages/graphql/src/translate/utils/logical-operators.ts b/packages/graphql/src/translate/utils/logical-operators.ts index 7b9594e819..9b250c7adf 100644 --- a/packages/graphql/src/translate/utils/logical-operators.ts +++ b/packages/graphql/src/translate/utils/logical-operators.ts @@ -19,8 +19,8 @@ import Cypher from "@neo4j/cypher-builder"; import { LOGICAL_OPERATORS } from "../../constants"; -import type { ValueOf } from "../../utils/value-of"; import { isInArray } from "../../utils/is-in-array"; +import type { ValueOf } from "../../utils/value-of"; type LogicalOperator = ValueOf; diff --git a/packages/graphql/src/types/index.ts b/packages/graphql/src/types/index.ts index 16d2301ca2..56c8b56c5f 100644 --- a/packages/graphql/src/types/index.ts +++ b/packages/graphql/src/types/index.ts @@ -226,6 +226,10 @@ export interface ConnectionQueryArgs { sort?: ConnectionSortArg[]; } +export interface GlobalNodeArgs { + id: string; +} + /** * Representation of the options arg * passed to resolvers. @@ -265,12 +269,12 @@ export type CallbackOperations = "CREATE" | "UPDATE"; Object keys and enum values map to values at https://neo4j.com/docs/cypher-manual/current/query-tuning/query-options/#cypher-query-options */ export interface CypherQueryOptions { - runtime?: "interpreted" | "slotted" | "pipelined" | "parallel"; - planner?: "cost" | "idp" | "dp"; /** - * @deprecated The Cypher query option `connectComponentsPlanner` is deprecated and will be removed without a replacement. https://neo4j.com/docs/cypher-manual/current/planning-and-tuning/query-tuning/#cypher-connect-components-planner + * Configure the runtime used: {@link https://neo4j.com/docs/cypher-manual/current/planning-and-tuning/runtimes/concepts | Cypher Runtimes} + * "interpreted" runtime option is deprecated. */ - connectComponentsPlanner?: "greedy" | "idp"; + runtime?: "interpreted" | "slotted" | "pipelined" | "parallel"; + planner?: "cost" | "idp" | "dp"; updateStrategy?: "default" | "eager"; expressionEngine?: "default" | "interpreted" | "compiled"; operatorEngine?: "default" | "interpreted" | "compiled"; diff --git a/packages/graphql/src/utils/execute.test.ts b/packages/graphql/src/utils/execute.test.ts index 682fbd406b..1496f3166b 100644 --- a/packages/graphql/src/utils/execute.test.ts +++ b/packages/graphql/src/utils/execute.test.ts @@ -18,10 +18,10 @@ */ import type { Driver } from "neo4j-driver"; -import execute from "./execute"; import { trimmer } from "."; import { ContextBuilder } from "../../tests/utils/builders/context-builder"; import { Executor } from "../classes/Executor"; +import execute from "./execute"; describe("execute", () => { test("should execute return records.toObject", async () => { @@ -189,7 +189,7 @@ describe("execute", () => { `); const expectedCypher = trimmer(` - CYPHER runtime=interpreted planner=cost connectComponentsPlanner=greedy updateStrategy=default expressionEngine=compiled operatorEngine=compiled interpretedPipesFallback=all replan=default + CYPHER runtime=interpreted planner=cost updateStrategy=default expressionEngine=compiled operatorEngine=compiled interpretedPipesFallback=all replan=default CREATE (u:User {title: $title}) RETURN u { .title } as u `); @@ -257,7 +257,6 @@ describe("execute", () => { cypherQueryOptions: { runtime: "interpreted", planner: "cost", - connectComponentsPlanner: "greedy", updateStrategy: "default", expressionEngine: "compiled", operatorEngine: "compiled", diff --git a/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/alias-unique.int.test.ts b/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/alias-unique.int.test.ts new file mode 100644 index 0000000000..7096c4e48b --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/alias-unique.int.test.ts @@ -0,0 +1,166 @@ +/* + * 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 { generate } from "randomstring"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { isMultiDbUnsupportedError } from "../../../utils/is-multi-db-unsupported-error"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("assertIndexesAndConstraints with @alias and @unique", () => { + const testHelper = new TestHelper({ v6Api: true }); + let databaseName: string; + let IS_MULTI_DB_SUPPORTED = true; + const skipMessage = "Database does not support multiple databases, skip tests that require multi-database support"; + + let Book: UniqueType; + let Comic: UniqueType; + + beforeEach(async () => { + Book = testHelper.createUniqueType("Book"); + Comic = testHelper.createUniqueType("Comic"); + databaseName = generate({ readable: true, charset: "alphabetic" }); + + try { + await testHelper.createDatabase(databaseName); + } catch (e) { + if (e instanceof Error) { + if (isMultiDbUnsupportedError(e)) { + // No multi-db support, so we skip tests + IS_MULTI_DB_SUPPORTED = false; + await testHelper.close(); + } else { + throw e; + } + } + } + }); + + afterEach(async () => { + if (IS_MULTI_DB_SUPPORTED) { + await testHelper.dropDatabase(); + } + await testHelper.close(); + }); + + test("should throw an error when the constraint does not exist for the field", async () => { + if (!IS_MULTI_DB_SUPPORTED) { + console.log(skipMessage); + return; + } + + const typeDefs = /* GraphQL */ ` + type ${Book.name} @node { + isbn: String! @unique(constraintName: "MISSING_CONSTRAINT") @alias(property: "internationalStandardBookNumber") + title: String! + } + `; + + const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); + await neoSchema.getSchema(); + + await expect( + neoSchema.assertIndexesAndConstraints({ + driver: await testHelper.getDriver(), + sessionConfig: { database: databaseName }, + }) + ).rejects.toThrow(`Missing constraint for ${Book.name}.internationalStandardBookNumber`); + }); + + test("should throw an error when the constraint exists for the field but under a different name", async () => { + if (!IS_MULTI_DB_SUPPORTED) { + console.log(skipMessage); + return; + } + + const typeDefs = /* GraphQL */ ` + type ${Book.name} @node { + isbn: String! @unique(constraintName: "CONSTRAINT_NAME") @alias(property: "internationalStandardBookNumber") + title: String! + } + `; + + const cypher = `CREATE CONSTRAINT WRONG_NAME FOR (n:${Book.name}) REQUIRE n.internationalStandardBookNumber IS UNIQUE`; + await testHelper.executeCypher(cypher); + + const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); + await neoSchema.getSchema(); + + await expect( + neoSchema.assertIndexesAndConstraints({ + driver: await testHelper.getDriver(), + sessionConfig: { database: databaseName }, + }) + ).rejects.toThrow(`Missing constraint for ${Book.name}.internationalStandardBookNumber`); + }); + + test("should not throw an error when all necessary constraints exist", async () => { + if (!IS_MULTI_DB_SUPPORTED) { + console.log(skipMessage); + return; + } + + const typeDefs = /* GraphQL */ ` + type ${Book.name} @node { + isbn: String! @unique(constraintName: "CONSTRAINT_NAME") @alias(property: "internationalStandardBookNumber") + title: String! + } + `; + + const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); + await neoSchema.getSchema(); + + const cypher = `CREATE CONSTRAINT CONSTRAINT_NAME FOR (n:${Book.name}) REQUIRE n.internationalStandardBookNumber IS UNIQUE`; + await testHelper.executeCypher(cypher); + + await expect( + neoSchema.assertIndexesAndConstraints({ + driver: await testHelper.getDriver(), + sessionConfig: { database: databaseName }, + }) + ).resolves.not.toThrow(); + }); + + test("should not throw if constraint exists on an additional label", async () => { + if (!IS_MULTI_DB_SUPPORTED) { + console.log(skipMessage); + return; + } + + const typeDefs = /* GraphQL */ ` + type ${Book.name} @node(labels: ["${Book.name}", "${Comic.name}"]) { + isbn: String! @unique(constraintName: "CONSTRAINT_NAME") @alias(property: "internationalStandardBookNumber") + title: String! + } + `; + + const cypher = `CREATE CONSTRAINT CONSTRAINT_NAME FOR (n:${Comic.name}) REQUIRE n.internationalStandardBookNumber IS UNIQUE`; + + await testHelper.executeCypher(cypher); + + const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); + await neoSchema.getSchema(); + + await expect( + neoSchema.assertIndexesAndConstraints({ + driver: await testHelper.getDriver(), + sessionConfig: { database: databaseName }, + }) + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/unique.int.test.ts b/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/unique.int.test.ts new file mode 100644 index 0000000000..07e4e8dbc6 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/unique.int.test.ts @@ -0,0 +1,166 @@ +/* + * 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 { generate } from "randomstring"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { isMultiDbUnsupportedError } from "../../../utils/is-multi-db-unsupported-error"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("assertIndexesAndConstraints with @unique", () => { + const testHelper = new TestHelper({ v6Api: true }); + let databaseName: string; + let IS_MULTI_DB_SUPPORTED = true; + const skipMessage = "Database does not support multiple databases, skip tests that require multi-database support"; + + let Book: UniqueType; + let Comic: UniqueType; + + beforeEach(async () => { + Book = testHelper.createUniqueType("Book"); + Comic = testHelper.createUniqueType("Comic"); + databaseName = generate({ readable: true, charset: "alphabetic" }); + + try { + await testHelper.createDatabase(databaseName); + } catch (e) { + if (e instanceof Error) { + if (isMultiDbUnsupportedError(e)) { + // No multi-db support, so we skip tests + IS_MULTI_DB_SUPPORTED = false; + await testHelper.close(); + } else { + throw e; + } + } + } + }); + + afterEach(async () => { + if (IS_MULTI_DB_SUPPORTED) { + await testHelper.dropDatabase(); + } + await testHelper.close(); + }); + + test("should throw an error when the constraint does not exist for the field", async () => { + if (!IS_MULTI_DB_SUPPORTED) { + console.log(skipMessage); + return; + } + + const typeDefs = /* GraphQL */ ` + type ${Book.name} @node { + isbn: String! @unique(constraintName: "MISSING_CONSTRAINT") + title: String! + } + `; + + const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); + await neoSchema.getSchema(); + + await expect( + neoSchema.assertIndexesAndConstraints({ + driver: await testHelper.getDriver(), + sessionConfig: { database: databaseName }, + }) + ).rejects.toThrow(`Missing constraint for ${Book.name}.isbn`); + }); + + test("should throw an error when the constraint exists for the field but under a different name", async () => { + if (!IS_MULTI_DB_SUPPORTED) { + console.log(skipMessage); + return; + } + + const typeDefs = /* GraphQL */ ` + type ${Book.name} @node { + isbn: String! @unique(constraintName: "CONSTRAINT_NAME") + title: String! + } + `; + + const cypher = `CREATE CONSTRAINT WRONG_NAME FOR (n:${Book.name}) REQUIRE n.isbn IS UNIQUE`; + await testHelper.executeCypher(cypher); + + const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); + await neoSchema.getSchema(); + + await expect( + neoSchema.assertIndexesAndConstraints({ + driver: await testHelper.getDriver(), + sessionConfig: { database: databaseName }, + }) + ).rejects.toThrow(`Missing constraint for ${Book.name}.isbn`); + }); + + test("should not throw an error when all necessary constraints exist", async () => { + if (!IS_MULTI_DB_SUPPORTED) { + console.log(skipMessage); + return; + } + + const typeDefs = /* GraphQL */ ` + type ${Book.name} @node { + isbn: String! @unique(constraintName: "CONSTRAINT_NAME") + title: String! + } + `; + + const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); + await neoSchema.getSchema(); + + const cypher = `CREATE CONSTRAINT CONSTRAINT_NAME FOR (n:${Book.name}) REQUIRE n.isbn IS UNIQUE`; + await testHelper.executeCypher(cypher); + + await expect( + neoSchema.assertIndexesAndConstraints({ + driver: await testHelper.getDriver(), + sessionConfig: { database: databaseName }, + }) + ).resolves.not.toThrow(); + }); + + test("should not throw if constraint exists on an additional label", async () => { + if (!IS_MULTI_DB_SUPPORTED) { + console.log(skipMessage); + return; + } + + const typeDefs = /* GraphQL */ ` + type ${Book.name} @node(labels: ["${Book.name}", "${Comic.name}"]) { + isbn: String! @unique(constraintName: "CONSTRAINT_NAME") + title: String! + } + `; + + const cypher = `CREATE CONSTRAINT CONSTRAINT_NAME FOR (n:${Comic.name}) REQUIRE n.isbn IS UNIQUE`; + + await testHelper.executeCypher(cypher); + + const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); + await neoSchema.getSchema(); + + await expect( + neoSchema.assertIndexesAndConstraints({ + driver: await testHelper.getDriver(), + sessionConfig: { database: databaseName }, + }) + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/combinations/alias-relayId/alias-relayId.int.test.ts b/packages/graphql/tests/api-v6/integration/combinations/alias-relayId/alias-relayId.int.test.ts new file mode 100644 index 0000000000..ef6f004dac --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/combinations/alias-relayId/alias-relayId.int.test.ts @@ -0,0 +1,163 @@ +/* + * 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 { toGlobalId } from "../../../../../src/utils/global-ids"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("RelayId projection with alias directive", () => { + const testHelper = new TestHelper({ v6Api: true }); + let movieDatabaseID: string; + let genreDatabaseID: string; + let actorDatabaseID: string; + + const Movie = testHelper.createUniqueType("Movie"); + const Genre = testHelper.createUniqueType("Genre"); + const Actor = testHelper.createUniqueType("Actor"); + + beforeAll(async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + dbId: ID! @id @unique(constraintName: "MOVIE_ID_UNIQUE") @relayId @alias(property: "serverId") + title: String! + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type ${Genre} @node { + dbId: ID! @id @unique(constraintName: "GENRE_ID_UNIQUE") @relayId + name: String! + } + + type ${Actor} @node { + dbId: ID! @id @unique(constraintName: "ACTOR_ID_UNIQUE") @relayId @alias(property: "serverId") + name: String! + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const randomID = "1234"; + const randomID2 = "abcd"; + const randomID3 = "ArthurId"; + await testHelper.executeCypher(` + CREATE (m:${Movie.name} { title: "Movie1", serverId: "${randomID}" }) + CREATE (g:${Genre.name} { name: "Action", dbId: "${randomID2}" }) + CREATE (o:${Actor.name} { name: "Keanu", serverId: "${randomID3}" }) + CREATE (m)-[:HAS_GENRE]->(g) + CREATE (m)-[:ACTED_IN]->(o) + `); + movieDatabaseID = randomID; + genreDatabaseID = randomID2; + actorDatabaseID = randomID3; + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should return the correct relayId ids using the connection API", async () => { + const connectionQuery = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + id + dbId + title + genre { + connection { + edges { + node { + id + dbId + name + } + } + } + } + actors { + connection { + edges { + node { + id + dbId + name + } + } + } + } + } + } + } + } + } + `; + + const connectionQueryResult = await testHelper.executeGraphQL(connectionQuery); + + expect(connectionQueryResult.errors).toBeUndefined(); + expect(connectionQueryResult.data).toBeDefined(); + + const movieGlobalId = toGlobalId({ typeName: Movie.name, field: "dbId", id: movieDatabaseID }); + const genreGlobalId = toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDatabaseID }); + const actorGlobalId = toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDatabaseID }); + + expect(connectionQueryResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + id: movieGlobalId, + dbId: movieDatabaseID, + title: "Movie1", + genre: { + connection: { + edges: [ + { + node: { + id: genreGlobalId, + dbId: genreDatabaseID, + name: "Action", + }, + }, + ], + }, + }, + actors: { + connection: { + edges: [ + { + node: { + id: actorGlobalId, + dbId: actorDatabaseID, + name: "Keanu", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/combinations/sort-pagination/sort-pagination-relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/combinations/sort-pagination/sort-pagination-relationship.int.test.ts new file mode 100644 index 0000000000..cb5ecf3370 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/combinations/sort-pagination/sort-pagination-relationship.int.test.ts @@ -0,0 +1,197 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Sort relationship", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + ratings: Int! + description: String + } + type ${Actor} @node { + name: String + age: Int + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + role: String + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a:${Movie} {title: "The Matrix", description: "DVD edition", ratings: 5}) + CREATE (b:${Movie} {title: "The Matrix", description: "Cinema edition", ratings: 4}) + CREATE (c:${Movie} {title: "The Matrix 2", ratings: 2}) + CREATE (d:${Movie} {title: "The Matrix 3", ratings: 4}) + CREATE (e:${Movie} {title: "The Matrix 4", ratings: 3}) + CREATE (keanu:${Actor} {name: "Keanu", age: 55}) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(a) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(b) + CREATE (keanu)-[:ACTED_IN {year: 2001, role: "Mr. Anderson"}]->(c) + CREATE (keanu)-[:ACTED_IN {year: 2003, role: "Neo"}]->(d) + CREATE (keanu)-[:ACTED_IN {year: 2021, role: "Neo"}]->(e) + + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to sort by ASC order", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: { edges: { node: { title: ASC } } }, first: 3) { + edges { + node { + title + } + } + + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by DESC order", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: { edges: { node: { title: DESC } } }, first: 3) { + edges { + node { + title + } + } + + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix 4", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/combinations/sort-pagination/sort-pagination.int.test.ts b/packages/graphql/tests/api-v6/integration/combinations/sort-pagination/sort-pagination.int.test.ts new file mode 100644 index 0000000000..501c6c8fac --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/combinations/sort-pagination/sort-pagination.int.test.ts @@ -0,0 +1,138 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Sort", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + ratings: Int! + description: String + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix", description: "DVD edition", ratings: 5}) + CREATE (:${Movie} {title: "The Matrix", description: "Cinema edition", ratings: 4}) + CREATE (:${Movie} {title: "The Matrix 2", ratings: 2}) + CREATE (:${Movie} {title: "The Matrix 3", ratings: 4}) + CREATE (:${Movie} {title: "The Matrix 4", ratings: 3}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to sort by ASC order and limit", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(sort: { node: { title: ASC } }, first: 3) { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by DESC order and limit", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(sort: { node: { title: DESC } }, first: 3) { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 4", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/integration/multi-database.int.test.ts b/packages/graphql/tests/api-v6/integration/database/multi-database.int.test.ts similarity index 65% rename from packages/graphql/tests/integration/multi-database.int.test.ts rename to packages/graphql/tests/api-v6/integration/database/multi-database.int.test.ts index 3305fadab0..e9ccb0bddc 100644 --- a/packages/graphql/tests/integration/multi-database.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/database/multi-database.int.test.ts @@ -18,21 +18,21 @@ */ import type { Driver } from "neo4j-driver"; -import { generate } from "randomstring"; -import type { UniqueType } from "../utils/graphql-types"; -import { isMultiDbUnsupportedError } from "../utils/is-multi-db-unsupported-error"; -import { TestHelper } from "../utils/tests-helper"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { isMultiDbUnsupportedError } from "../../../utils/is-multi-db-unsupported-error"; +import { TestHelper } from "../../../utils/tests-helper"; describe("multi-database", () => { let driver: Driver; - const testHelper = new TestHelper(); - const id = generate({ - charset: "alphabetic", - }); + const testHelper = new TestHelper({ v6Api: true }); + const id = "movie-id"; + let MULTIDB_SUPPORT = true; const dbName = "non-default-db-name"; let Movie: UniqueType; + let typeDefs: string; + beforeAll(async () => { try { await testHelper.createDatabase(dbName); @@ -54,6 +54,12 @@ describe("multi-database", () => { driver = await testHelper.getDriver(); Movie = testHelper.createUniqueType("Movie"); + typeDefs = ` + type ${Movie} @node { + id: ID! + } + `; + await testHelper.executeCypher(`CREATE (:${Movie} {id: $id})`, { id }); } }); @@ -78,18 +84,18 @@ describe("multi-database", () => { return; } - const typeDefs = ` - type ${Movie} { - id: ID! - } - `; - await testHelper.initNeo4jGraphQL({ typeDefs }); const query = ` query { - ${Movie.plural}(where: { id: "${id}" }) { - id + ${Movie.plural}(where: { node: { id: { equals: "${id}"}}}) { + connection { + edges { + node { + id + } + } + } } } `; @@ -100,6 +106,7 @@ describe("multi-database", () => { }); expect((result.errors as any)[0].message).toBeTruthy(); }); + test("should specify the database via context", async () => { // Skip if multi-db not supported if (!MULTIDB_SUPPORT) { @@ -107,27 +114,39 @@ describe("multi-database", () => { return; } - const typeDefs = ` - type ${Movie} { - id: ID! - } - `; - await testHelper.initNeo4jGraphQL({ typeDefs }); const query = ` query { - ${Movie.plural}(where: { id: "${id}" }) { - id + ${Movie.plural}(where: { node: { id: { equals: "${id}"}}}) { + connection { + edges { + node { + id + } + } + } } } `; const result = await testHelper.executeGraphQL(query, { - variableValues: { id }, contextValue: { executionContext: driver, sessionConfig: { database: dbName } }, }); - expect((result.data as any)[Movie.plural][0].id).toBe(id); + expect(result.errors).toBeFalsy(); + expect(result.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + id: id, + }, + }, + ], + }, + }, + }); }); test("should fail for non-existing database specified via neo4j construction", async () => { @@ -137,18 +156,18 @@ describe("multi-database", () => { return; } - const typeDefs = ` - type ${Movie} { - id: ID! - } - `; - await testHelper.initNeo4jGraphQL({ typeDefs }); - const query = ` + const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { id: "${id}" }) { - id + ${Movie.plural}(where: { node: { id: { equals: "${id}"}}}) { + connection { + edges { + node { + id + } + } + } } } `; @@ -163,6 +182,7 @@ describe("multi-database", () => { "Database does not exist. Database name: 'non-existing-db'.", ]).toContain((result.errors as any)[0].message); }); + test("should specify the database via neo4j construction", async () => { // Skip if multi-db not supported if (!MULTIDB_SUPPORT) { @@ -170,17 +190,18 @@ describe("multi-database", () => { return; } - const typeDefs = ` - type ${Movie} { - id: ID! - } - `; await testHelper.initNeo4jGraphQL({ typeDefs }); - const query = ` + const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { id: "${id}" }) { - id + ${Movie.plural}(where: { node: { id: { equals: "${id}"}}}) { + connection { + edges { + node { + id + } + } + } } } `; @@ -189,6 +210,18 @@ describe("multi-database", () => { variableValues: { id }, contextValue: { sessionConfig: { database: dbName } }, // This is needed, otherwise the context in resolvers will be undefined }); - expect((result.data as any)[Movie.plural][0].id).toBe(id); + expect(result.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + id: id, + }, + }, + ], + }, + }, + }); }); }); diff --git a/packages/graphql/tests/api-v6/integration/delete/delete.int.test.ts b/packages/graphql/tests/api-v6/integration/delete/delete.int.test.ts new file mode 100644 index 0000000000..9eb3ed478d --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/delete/delete.int.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Top-Level Delete", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + released: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should delete one movie", async () => { + await testHelper.executeCypher(` + CREATE(n:${Movie} {title: "The Matrix"}) + CREATE(:${Movie} {title: "The Matrix 2"}) + `); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.delete}( + where: { + node: { + title: { + equals: "The Matrix" + } + } + }) { + info { + nodesDeleted + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.delete]: { + info: { + nodesDeleted: 1, + }, + }, + }); + + const cypherMatch = await testHelper.executeCypher( + ` + MATCH (m:${Movie}) + RETURN {title: m.title} as m + `, + {} + ); + const records = cypherMatch.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.toIncludeSameMembers([ + { + m: { + title: "The Matrix 2", + }, + }, + ]) + ); + }); + + test("should delete one movie and a relationship", async () => { + await testHelper.executeCypher(` + CREATE(n:${Movie} {title: "The Matrix"})-[:ACTED_IN]->(:Person {name: "Keanu Reeves"}) + CREATE(:${Movie} {title: "The Matrix 2"}) + `); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.delete}( + where: { + node: { + title: { + equals: "The Matrix" + } + } + }) { + info { + nodesDeleted + relationshipsDeleted + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.delete]: { + info: { + nodesDeleted: 1, + relationshipsDeleted: 1, + }, + }, + }); + + const cypherMatch = await testHelper.executeCypher( + ` + MATCH (m:${Movie}) + RETURN {title: m.title} as m + `, + {} + ); + const records = cypherMatch.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.toIncludeSameMembers([ + { + m: { + title: "The Matrix 2", + }, + }, + ]) + ); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/alias/conflicting-properties.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/conflicting-properties.int.test.ts new file mode 100644 index 0000000000..e8d30c08d9 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/alias/conflicting-properties.int.test.ts @@ -0,0 +1,163 @@ +/* + * 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 { GraphQLError } from "graphql"; +import type { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("conflicting properties", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let typeMovie: UniqueType; + let typeDirector: UniqueType; + + beforeEach(async () => { + typeMovie = testHelper.createUniqueType("Movie"); + typeDirector = testHelper.createUniqueType("Director"); + + const typeDefs = /* GraphQL */ ` + type ${typeDirector} @node { + name: String + nameAgain: String @alias(property: "name") + movies: [${typeMovie}!]! @relationship(direction: OUT, type: "DIRECTED", properties: "Directed") + } + + type Directed @relationshipProperties { + year: Int! + movieYear: Int @alias(property: "year") + } + + type ${typeMovie} @node { + title: String + titleAgain: String @alias(property: "title") + directors: [${typeDirector}!]! @relationship(direction: IN, type: "DIRECTED", properties: "Directed") + } + `; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("Create mutation with alias referring to existing field, include both fields as inputs", async () => { + const userMutation = /* GraphQL */ ` + mutation { + ${typeDirector.operations.create}(input: [{ node: { name: "Tim Burton", nameAgain: "Timmy Burton" }}]) { + ${typeDirector.plural} { + name + nameAgain + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(userMutation); + + expect(gqlResult.errors).toBeDefined(); + expect(gqlResult.errors).toHaveLength(1); + expect(gqlResult.errors).toEqual([ + new GraphQLError(`Conflicting modification of [[name]], [[nameAgain]] on type ${typeDirector.name}`), + ]); + expect(gqlResult?.data).toEqual({ + [typeDirector.operations.create]: null, + }); + }); + + test("Create mutation with alias referring to existing field, include only field as inputs", async () => { + const userMutation = /* GraphQL */ ` + mutation { + ${typeDirector.operations.create}(input: [{ node: {name: "Tim Burton"} }]) { + ${typeDirector.plural} { + name + nameAgain + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(userMutation); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult?.data).toEqual({ + [typeDirector.operations.create]: { + [typeDirector.plural]: [ + { + name: "Tim Burton", + nameAgain: "Tim Burton", + }, + ], + }, + }); + }); + + test("Create mutation with alias referring to existing field, include only alias field as inputs", async () => { + const userMutation = /* GraphQL */ ` + mutation { + ${typeDirector.operations.create}(input: [{ node: { nameAgain: "Tim Burton" } }]) { + ${typeDirector.plural} { + name + nameAgain + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(userMutation); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult?.data).toEqual({ + [typeDirector.operations.create]: { + [typeDirector.plural]: [ + { + name: "Tim Burton", + nameAgain: "Tim Burton", + }, + ], + }, + }); + }); + + test("Create mutation with alias referring to existing field, include both bad and good inputs", async () => { + const userMutation = /* GraphQL */ ` + mutation { + ${typeDirector.operations.create}(input: [{ node: {name: "Tim Burton", nameAgain: "Timmy Burton"} }, { node: { name: "Someone" }}]) { + ${typeDirector.plural} { + name + nameAgain + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(userMutation); + + expect(gqlResult.errors).toBeDefined(); + expect(gqlResult.errors).toHaveLength(1); + expect(gqlResult.errors).toEqual([ + new GraphQLError(`Conflicting modification of [[name]], [[nameAgain]] on type ${typeDirector.name}`), + ]); + expect(gqlResult?.data).toEqual({ + [typeDirector.operations.create]: null, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/alias/create-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/create-alias.int.test.ts new file mode 100644 index 0000000000..ddd77f8f8e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/alias/create-alias.int.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Create with @alias", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + id: ID! @id @alias(property: "serverId") + title: String! @alias(property: "name") + released: Int @alias(property: "year") + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should create two movies and project them", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { title: "The Matrix" } }, + { node: { title: "The Matrix 2", released: 2001 } } + ]) { + ${Movie.plural} { + id + title + released + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { id: expect.any(String), title: "The Matrix", released: null }, + { id: expect.any(String), title: "The Matrix 2", released: 2001 }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/alias/query-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/query-alias.int.test.ts new file mode 100644 index 0000000000..3491afc7e5 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/alias/query-alias.int.test.ts @@ -0,0 +1,165 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("@alias directive", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Director: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + Director = testHelper.createUniqueType("Director"); + + const typeDefs = /* GraphQL */ ` + type ${Director} @node { + name: String + nameAgain: String @alias(property: "name") + movies: [${Movie}!]! @relationship(direction: OUT, type: "DIRECTED", properties: "Directed") + } + + type Directed @relationshipProperties { + year: Int! + movieYear: Int! @alias(property: "year") + } + + type ${Movie} @node { + title: String + titleAgain: String @alias(property: "title") + directors: [${Director}!]! @relationship(direction: IN, type: "DIRECTED", properties: "Directed") + } + `; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + }); + + await testHelper.executeCypher( + ` + CREATE(m:${Movie} { title: "The Matrix" })<-[:DIRECTED {year: 1999}]-(d:${Director} {name: "Watchowsky"}) + ` + ); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("Query node with alias", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + title + titleAgain + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + titleAgain: "The Matrix", + }, + }, + ], + }, + }, + }); + }); + + test("Query node and relationship with alias", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + title + titleAgain + directors { + connection { + edges { + node { + name + nameAgain + } + properties { + year + movieYear + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + titleAgain: "The Matrix", + directors: { + connection: { + edges: [ + { + node: { + name: "Watchowsky", + nameAgain: "Watchowsky", + }, + properties: { + year: 1999, + movieYear: 1999, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/alias/sort-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/sort-alias.int.test.ts new file mode 100644 index 0000000000..5e200ea891 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/alias/sort-alias.int.test.ts @@ -0,0 +1,223 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Sort with alias", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String @alias(property: "movieTitle") + ratings: Int! @alias(property: "movieRatings") + description: String + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {movieTitle: "The Matrix", description: "DVD edition", movieRatings: 5}) + CREATE (:${Movie} {movieTitle: "The Matrix", description: "Cinema edition", movieRatings: 4}) + CREATE (:${Movie} {movieTitle: "The Matrix 2", movieRatings: 2}) + CREATE (:${Movie} {movieTitle: "The Matrix 3", movieRatings: 4}) + CREATE (:${Movie} {movieTitle: "The Matrix 4", movieRatings: 3}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to sort by ASC order", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(sort: { node: { title: ASC } }) { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 4", + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by DESC order", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(sort: { node: { title: DESC } }) { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 4", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by multiple criteria", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(sort: [{ node: { title: ASC } }, { node: { ratings: DESC } }]) { + edges { + node { + title + description + ratings + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + description: "DVD edition", + ratings: 5, + }, + }, + { + node: { + title: "The Matrix", + description: "Cinema edition", + ratings: 4, + }, + }, + + { + node: { + title: "The Matrix 2", + description: null, + ratings: 2, + }, + }, + { + node: { + title: "The Matrix 3", + description: null, + ratings: 4, + }, + }, + { + node: { + title: "The Matrix 4", + description: null, + ratings: 3, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/alias/sort-relationship-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/sort-relationship-alias.int.test.ts new file mode 100644 index 0000000000..69fac93fb6 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/alias/sort-relationship-alias.int.test.ts @@ -0,0 +1,478 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Sort relationship with alias", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String @alias(property: "movieTitle") + ratings: Int! @alias(property: "movieRatings") + description: String + } + type ${Actor} @node { + name: String + age: Int + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int @alias(property: "movieYear") + role: String @alias(property: "movieRole") + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a:${Movie} {movieTitle: "The Matrix", description: "DVD edition", movieRatings: 5}) + CREATE (b:${Movie} {movieTitle: "The Matrix", description: "Cinema edition", movieRatings: 4}) + CREATE (c:${Movie} {movieTitle: "The Matrix 2", movieRatings: 2}) + CREATE (d:${Movie} {movieTitle: "The Matrix 3", movieRatings: 4}) + CREATE (e:${Movie} {movieTitle: "The Matrix 4", movieRatings: 3}) + CREATE (keanu:${Actor} {name: "Keanu", age: 55}) + CREATE (keanu)-[:ACTED_IN {movieYear: 1999, movieRole: "Neo"}]->(a) + CREATE (keanu)-[:ACTED_IN {movieYear: 1999, movieRole: "Neo"}]->(b) + CREATE (keanu)-[:ACTED_IN {movieYear: 2001, movieRole: "Mr. Anderson"}]->(c) + CREATE (keanu)-[:ACTED_IN {movieYear: 2003, movieRole: "Neo"}]->(d) + CREATE (keanu)-[:ACTED_IN {movieYear: 2021, movieRole: "Neo"}]->(e) + + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to sort by ASC order", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: { edges: { node: { title: ASC } } }) { + edges { + node { + title + } + } + + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 4", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by DESC order", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: { edges: { node: { title: DESC } } }) { + edges { + node { + title + } + } + + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix 4", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by multiple criteria", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: [{edges: { node: { title: ASC } }}, {edges: { node: { ratings: DESC } }}]) { + edges { + node { + title + description + ratings + } + } + + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + description: "DVD edition", + ratings: 5, + }, + }, + { + node: { + title: "The Matrix", + description: "Cinema edition", + ratings: 4, + }, + }, + + { + node: { + title: "The Matrix 2", + description: null, + ratings: 2, + }, + }, + { + node: { + title: "The Matrix 3", + description: null, + ratings: 4, + }, + }, + { + node: { + title: "The Matrix 4", + description: null, + ratings: 3, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by relationship properties", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: [{edges:{ properties: { role: DESC } }}, {edges:{ properties: { year: ASC } }} ]) { + edges { + properties { + year + role + } + node { + title + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + properties: { year: 1999, role: "Neo" }, + node: { + title: "The Matrix", + }, + }, + { + properties: { year: 1999, role: "Neo" }, + node: { + title: "The Matrix", + }, + }, + { + properties: { year: 2003, role: "Neo" }, + node: { + title: "The Matrix 3", + }, + }, + { + properties: { year: 2021, role: "Neo" }, + node: { + title: "The Matrix 4", + }, + }, + { + properties: { year: 2001, role: "Mr. Anderson" }, + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by relationship properties and node properties", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: [ + {edges: { properties: { role: DESC } } }, + {edges: { node: { title: ASC } } }, + {edges: { properties: { year: DESC } } }, + {edges: { node: { description: ASC } } } + + ]) { + edges { + properties { + year + role + } + node { + title + description + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + properties: { year: 1999, role: "Neo" }, + node: { + title: "The Matrix", + description: "Cinema edition", + }, + }, + { + properties: { year: 1999, role: "Neo" }, + node: { + title: "The Matrix", + description: "DVD edition", + }, + }, + + { + properties: { year: 2003, role: "Neo" }, + node: { + title: "The Matrix 3", + description: null, + }, + }, + { + properties: { year: 2021, role: "Neo" }, + node: { + title: "The Matrix 4", + description: null, + }, + }, + { + properties: { year: 2001, role: "Mr. Anderson" }, + node: { + title: "The Matrix 2", + description: null, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/default/create-default.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/default/create-default.int.test.ts new file mode 100644 index 0000000000..5ff485814e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/default/create-default.int.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Create with @default", () => { + const testHelper = new TestHelper({ v6Api: true }); + + afterEach(async () => { + await testHelper.close(); + }); + + test.each([ + { dataType: "Int", value: 1 }, + { dataType: "Float", value: 1.2 }, + { dataType: "DateTime", value: "2024-05-28T13:56:22.368Z" }, + ] as const)("should create two movies with a $dataType default value", async ({ dataType, value }) => { + const Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + testField: ${dataType} @default(value: ${typeof value === "string" ? `"${value}"` : value}) + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { title: "The Matrix" } }, + { node: { title: "The Matrix 2"} } + ]) { + ${Movie.plural} { + title + testField + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { title: "The Matrix", testField: value }, + { title: "The Matrix 2", testField: value }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/id/create-id.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/id/create-id.int.test.ts new file mode 100644 index 0000000000..ae81878dc2 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/id/create-id.int.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { Integer } from "neo4j-driver"; +import type { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Create with @id", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + id: ID! @id + title: String! + released: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should create two movies", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { title: "The Matrix" } }, + { node: { title: "The Matrix 2", released: 2001 } } + ]) { + info { + nodesCreated + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + + const cypherMatch = await testHelper.executeCypher( + ` + MATCH (m:${Movie}) + RETURN m + `, + {} + ); + const records = cypherMatch.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.toIncludeSameMembers([ + { m: expect.objectContaining({ properties: { id: expect.any(String), title: "The Matrix" } }) }, + { + m: expect.objectContaining({ + properties: { id: expect.any(String), title: "The Matrix 2", released: new Integer(2001) }, + }), + }, + ]) + ); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/limit/query-limit.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/limit/query-limit.int.test.ts new file mode 100644 index 0000000000..4ec2a485e6 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/limit/query-limit.int.test.ts @@ -0,0 +1,333 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("@limit directive", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + let Production: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + Production = testHelper.createUniqueType("Production"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node @limit(default: 2, max: 4){ + title: String + ratings: Int! + description: String + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + type ${Actor} @node @limit(default: 3, max: 5) { + name: String + } + + type ${Production} @node @limit(max: 2) { + name: String + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (m1:${Movie} {title: "The Matrix", description: "DVD edition", movieRatings: 5}) + CREATE (m1b:${Movie} {title: "The Matrix", description: "Cinema edition", movieRatings: 4}) + CREATE (m2:${Movie} {title: "The Matrix 2", movieRatings: 2}) + CREATE (m3:${Movie} {title: "The Matrix 3", movieRatings: 4}) + CREATE (m4:${Movie} {title: "The Matrix 4", movieRatings: 3}) + + CREATE (a1:${Actor} {name: "Keanu Reeves"}) + CREATE (a2:${Actor} {name: "Joe Pantoliano"}) + CREATE (a3:${Actor} {name: "Laurence Fishburne"}) + CREATE (a4:${Actor} {name: "Carrie-Anne Moss"}) + CREATE (a5:${Actor} {name: "Hugo Weaving"}) + CREATE (a6:${Actor} {name: "Lana Wachowski"}) + + CREATE (p1:${Production} {name: "Warner Bros"}) + CREATE (p2:${Production} {name: "Disney"}) + CREATE (p3:${Production} {name: "Universal"}) + + WITH m1, m1b, m2, m3, m4, a1, a2, a3, a4, a5, a6 + UNWIND [a1, a2, a3, a4, a5, a6] as ai + CREATE (ai)-[:ACTED_IN]->(m1) + CREATE (ai)-[:ACTED_IN]->(m1b) + CREATE (ai)-[:ACTED_IN]->(m2) + CREATE (ai)-[:ACTED_IN]->(m3) + CREATE (ai)-[:ACTED_IN]->(m4) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should limit the top level query with default value", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toBeArrayOfSize(2), + }, + }, + }); + }); + + test("should override the default limit if 'first' provided", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(first: 3) { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }); + }); + + test("should limit the top level query with max value if not default is available", async () => { + const query = /* GraphQL */ ` + query { + ${Production.plural} { + connection { + edges { + node { + name + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult.data).toEqual({ + [Production.plural]: { + connection: { + edges: expect.toBeArrayOfSize(2), + }, + }, + }); + }); + + test("should limit the top level query with max value the option given is higher", async () => { + const query = /* GraphQL */ ` + query { + ${Production.plural} { + connection(first: 10){ + edges { + node { + name + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult.data).toEqual({ + [Production.plural]: { + connection: { + edges: expect.toBeArrayOfSize(2), + }, + }, + }); + }); + + test("should limit the nested field with default value", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(first: 1) { + edges { + node { + title + actors { + connection { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + expect.objectContaining({ + node: { + title: expect.any(String), + actors: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }, + }), + ], + }, + }, + }); + }); + + test("should override the default limit to the nested field if `first` provided", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(first: 1) { + edges { + node { + title + actors { + connection(first: 4) { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + expect.objectContaining({ + node: { + title: expect.any(String), + actors: { + connection: { + edges: expect.toBeArrayOfSize(4), + }, + }, + }, + }), + ], + }, + }, + }); + }); + + test("should limit the nested field with max value if `first` option given is higher", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(first: 1) { + edges { + node { + title + actors { + connection(first: 10) { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + expect.objectContaining({ + node: { + title: expect.any(String), + actors: { + connection: { + edges: expect.toBeArrayOfSize(5), + }, + }, + }, + }), + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/node/create-node.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/node/create-node.int.test.ts new file mode 100644 index 0000000000..4c20a43641 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/node/create-node.int.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { Integer } from "neo4j-driver"; +import type { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("@node with Top-Level Create", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Film: UniqueType; + let ShortMovie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Film = testHelper.createUniqueType("Film"); + ShortMovie = testHelper.createUniqueType("ShortMovie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node(labels: ["${Film}", "${ShortMovie}"]) { + title: String! + released: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should create two movies", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { title: "The Matrix" } }, + { node: { title: "The Matrix 2", released: 2001 } } + ]) { + info { + nodesCreated + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + + const cypherMatch = await testHelper.executeCypher( + ` + MATCH (m:${Film}:${ShortMovie}) + RETURN m + `, + {} + ); + const records = cypherMatch.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.toIncludeSameMembers([ + { m: expect.objectContaining({ properties: { title: "The Matrix" } }) }, + { m: expect.objectContaining({ properties: { title: "The Matrix 2", released: new Integer(2001) } }) }, + ]) + ); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/relayId/global-node-query.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/relayId/global-node-query.int.test.ts new file mode 100644 index 0000000000..961fd3233d --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/relayId/global-node-query.int.test.ts @@ -0,0 +1,143 @@ +/* + * 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 { toGlobalId } from "../../../../../src/utils/global-ids"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Global node query", () => { + const testHelper = new TestHelper({ v6Api: true }); + let movieDatabaseID: string; + let genreDatabaseID: string; + let actorDatabaseID: string; + + const Movie = testHelper.createUniqueType("Movie"); + const Genre = testHelper.createUniqueType("Genre"); + const Actor = testHelper.createUniqueType("Actor"); + + beforeAll(async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + dbId: ID! @id @unique(constraintName: "MOVIE_ID_UNIQUE") @relayId + title: String! + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type ${Genre} @node { + dbId: ID! @id @unique(constraintName: "GENRE_ID_UNIQUE") @relayId + name: String! + } + + type ${Actor} @node { + dbId: ID! @id @unique(constraintName: "ACTOR_ID_UNIQUE") @relayId + name: String! + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + movieDatabaseID = "1234"; + genreDatabaseID = "abcd"; + actorDatabaseID = "ArthurId"; + await testHelper.executeCypher(` + CREATE (m:${Movie.name} { title: "Movie1", dbId: "${movieDatabaseID}" }) + CREATE (g:${Genre.name} { name: "Action", dbId: "${genreDatabaseID}" }) + CREATE (o:${Actor.name} { name: "Keanu", dbId: "${actorDatabaseID}" }) + CREATE (m)-[:HAS_GENRE]->(g) + CREATE (m)-[:ACTED_IN]->(o) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should return the correct relayId nodes using the global node API", async () => { + const movieGlobalId = toGlobalId({ typeName: Movie.name, field: "dbId", id: movieDatabaseID }); + + const connectionQuery = /* GraphQL */ ` + query { + node(id: "${movieGlobalId}") { + ... on ${Movie} { + title + dbId + } + id + } + } + `; + + const connectionQueryResult = await testHelper.executeGraphQL(connectionQuery); + + expect(connectionQueryResult.errors).toBeUndefined(); + expect(connectionQueryResult.data).toEqual({ + node: { + title: "Movie1", + id: movieGlobalId, + dbId: movieDatabaseID, + }, + }); + }); + + test("should return the correct relayId nodes using the global node API with relationships", async () => { + const movieGlobalId = toGlobalId({ typeName: Movie.name, field: "dbId", id: movieDatabaseID }); + + const connectionQuery = /* GraphQL */ ` + query { + node(id: "${movieGlobalId}") { + ... on ${Movie} { + title + dbId + actors { + connection { + edges { + node { + name + } + } + } + } + } + id + } + } + `; + + const connectionQueryResult = await testHelper.executeGraphQL(connectionQuery); + + expect(connectionQueryResult.errors).toBeUndefined(); + expect(connectionQueryResult.data).toEqual({ + node: { + title: "Movie1", + id: movieGlobalId, + dbId: movieDatabaseID, + actors: { + connection: { + edges: [ + { + node: { + name: "Keanu", + }, + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-filters.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-filters.int.test.ts new file mode 100644 index 0000000000..cbd57ebc82 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-filters.int.test.ts @@ -0,0 +1,169 @@ +/* + * 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 { toGlobalId } from "../../../../../src/utils/global-ids"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("RelayId projection with filters", () => { + const testHelper = new TestHelper({ v6Api: true }); + let movieDatabaseID: string; + let genreDatabaseID: string; + let actorDatabaseID: string; + + const Movie = testHelper.createUniqueType("Movie"); + const Genre = testHelper.createUniqueType("Genre"); + const Actor = testHelper.createUniqueType("Actor"); + + beforeAll(async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + dbId: ID! @id @unique(constraintName: "MOVIE_ID_UNIQUE") @relayId + title: String! + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type ${Genre} @node { + dbId: ID! @id @unique(constraintName: "GENRE_ID_UNIQUE") @relayId + name: String! + } + + type ${Actor} @node { + dbId: ID! @id @unique(constraintName: "ACTOR_ID_UNIQUE") @relayId + name: String! + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const randomID = "1234"; + const randomID2 = "abcd"; + const randomID3 = "ArthurId"; + const randomID4 = "4321"; + await testHelper.executeCypher(` + CREATE (m:${Movie.name} { title: "Movie1", dbId: "${randomID}" }) + CREATE (g:${Genre.name} { name: "Action", dbId: "${randomID2}" }) + CREATE (o:${Actor.name} { name: "Keanu", dbId: "${randomID3}" }) + CREATE (:${Movie.name} { title: "Movie2", dbId: "${randomID4}" }) + CREATE (m)-[:HAS_GENRE]->(g) + CREATE (m)-[:ACTED_IN]->(o) + `); + movieDatabaseID = randomID; + genreDatabaseID = randomID2; + actorDatabaseID = randomID3; + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should return the correct relayId ids using the connection API", async () => { + const movieGlobalId = toGlobalId({ typeName: Movie.name, field: "dbId", id: movieDatabaseID }); + const genreGlobalId = toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDatabaseID }); + const actorGlobalId = toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDatabaseID }); + + const connectionQuery = /* GraphQL */ ` + query { + ${Movie.plural}(where: { + node: { + id: { equals: "${movieGlobalId}"} + } + }) { + connection { + edges { + node { + id + dbId + title + genre { + connection { + edges { + node { + id + dbId + name + } + } + } + } + actors { + connection { + edges { + node { + id + dbId + name + } + } + } + } + } + } + } + } + } + `; + + const connectionQueryResult = await testHelper.executeGraphQL(connectionQuery); + + expect(connectionQueryResult.errors).toBeUndefined(); + expect(connectionQueryResult.data).toBeDefined(); + + expect(connectionQueryResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + id: movieGlobalId, + dbId: movieDatabaseID, + title: "Movie1", + genre: { + connection: { + edges: [ + { + node: { + id: genreGlobalId, + dbId: genreDatabaseID, + name: "Action", + }, + }, + ], + }, + }, + actors: { + connection: { + edges: [ + { + node: { + id: actorGlobalId, + dbId: actorDatabaseID, + name: "Keanu", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection.int.test.ts new file mode 100644 index 0000000000..6697e9687c --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection.int.test.ts @@ -0,0 +1,250 @@ +/* + * 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 { toGlobalId } from "../../../../../src/utils/global-ids"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("RelayId projection", () => { + const testHelper = new TestHelper({ v6Api: true }); + let movieDatabaseID: string; + let genreDatabaseID: string; + let actorDatabaseID: string; + + const Movie = testHelper.createUniqueType("Movie"); + const Genre = testHelper.createUniqueType("Genre"); + const Actor = testHelper.createUniqueType("Actor"); + + beforeAll(async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + dbId: ID! @id @unique(constraintName: "MOVIE_ID_UNIQUE") @relayId + title: String! + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type ${Genre} @node { + dbId: ID! @id @unique(constraintName: "GENRE_ID_UNIQUE") @relayId + name: String! + } + + type ${Actor} @node { + dbId: ID! @id @unique(constraintName: "ACTOR_ID_UNIQUE") @relayId + name: String! + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const randomID = "1234"; + const randomID2 = "abcd"; + const randomID3 = "ArthurId"; + await testHelper.executeCypher(` + CREATE (m:${Movie.name} { title: "Movie1", dbId: "${randomID}" }) + CREATE (g:${Genre.name} { name: "Action", dbId: "${randomID2}" }) + CREATE (o:${Actor.name} { name: "Keanu", dbId: "${randomID3}" }) + CREATE (m)-[:HAS_GENRE]->(g) + CREATE (m)-[:ACTED_IN]->(o) + `); + movieDatabaseID = randomID; + genreDatabaseID = randomID2; + actorDatabaseID = randomID3; + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should return the correct relayId ids using the connection API", async () => { + const connectionQuery = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + id + dbId + title + genre { + connection { + edges { + node { + id + dbId + name + } + } + } + } + actors { + connection { + edges { + node { + id + dbId + name + } + } + } + } + } + } + } + } + } + `; + + const connectionQueryResult = await testHelper.executeGraphQL(connectionQuery); + + expect(connectionQueryResult.errors).toBeUndefined(); + expect(connectionQueryResult.data).toBeDefined(); + + const movieGlobalId = toGlobalId({ typeName: Movie.name, field: "dbId", id: movieDatabaseID }); + const genreGlobalId = toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDatabaseID }); + const actorGlobalId = toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDatabaseID }); + + expect(connectionQueryResult.data?.[Movie.plural]).toEqual({ + connection: { + edges: [ + { + node: { + id: movieGlobalId, + dbId: movieDatabaseID, + title: "Movie1", + genre: { + connection: { + edges: [ + { + node: { + id: genreGlobalId, + dbId: genreDatabaseID, + name: "Action", + }, + }, + ], + }, + }, + actors: { + connection: { + edges: [ + { + node: { + id: actorGlobalId, + dbId: actorDatabaseID, + name: "Keanu", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }); + }); + + test("should return the correct relayId ids using the connection API with aliased fields", async () => { + const connectionQuery = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + testAliasId: id + testAliasDbId: dbId + title + genre { + connection { + edges { + node { + testAliasId: id + testAliasDbId: dbId + name + } + } + } + } + actors { + connection { + edges { + node { + testAliasId: id + testAliasDbId: dbId + name + } + } + } + } + } + } + } + } + } + `; + + const connectionQueryResult = await testHelper.executeGraphQL(connectionQuery); + + expect(connectionQueryResult.errors).toBeUndefined(); + expect(connectionQueryResult.data).toBeDefined(); + + const movieGlobalId = toGlobalId({ typeName: Movie.name, field: "dbId", id: movieDatabaseID }); + const genreGlobalId = toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDatabaseID }); + const actorGlobalId = toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDatabaseID }); + + expect(connectionQueryResult.data?.[Movie.plural]).toEqual({ + connection: { + edges: [ + { + node: { + testAliasId: movieGlobalId, + testAliasDbId: movieDatabaseID, + title: "Movie1", + genre: { + connection: { + edges: [ + { + node: { + testAliasId: genreGlobalId, + testAliasDbId: genreDatabaseID, + name: "Action", + }, + }, + ], + }, + }, + actors: { + connection: { + edges: [ + { + node: { + testAliasId: actorGlobalId, + testAliasDbId: actorDatabaseID, + name: "Keanu", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }); + }); +}); diff --git a/packages/graphql/tests/integration/errors.int.test.ts b/packages/graphql/tests/api-v6/integration/errors.int.test.ts similarity index 79% rename from packages/graphql/tests/integration/errors.int.test.ts rename to packages/graphql/tests/api-v6/integration/errors.int.test.ts index c59ce182a3..61040312f6 100644 --- a/packages/graphql/tests/integration/errors.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/errors.int.test.ts @@ -19,15 +19,15 @@ import type { GraphQLError } from "graphql"; import { graphql } from "graphql"; -import { Neo4jGraphQL } from "../../src/classes"; -import { UniqueType } from "../utils/graphql-types"; +import { Neo4jGraphQL } from "../../../src"; +import { UniqueType } from "../../utils/graphql-types"; describe("Errors", () => { test("An error should be thrown if no driver is supplied", async () => { const Movie = new UniqueType("Movie"); const typeDefs = ` - type ${Movie} { + type ${Movie} @node { id: ID } `; @@ -37,13 +37,19 @@ describe("Errors", () => { const query = ` query { ${Movie.plural} { - id + connection { + edges { + node { + id + } + } + } } } `; const gqlResult = await graphql({ - schema: await neoSchema.getSchema(), + schema: await neoSchema.getAuraSchema(), source: query, }); diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.int.test.ts new file mode 100644 index 0000000000..2f92941f14 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.int.test.ts @@ -0,0 +1,151 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Filters AND", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + year: Int + runtime: Float + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix", year: 1999, runtime: 90.5})<-[:ACTED_IN {year: 1999}]-(a:${Actor} {name: "Keanu"}) + CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001, runtime: 90.5})<-[:ACTED_IN {year: 2001}]-(a) + CREATE (:${Movie} {title: "The Matrix Thingy", year: 1999, runtime: 90.5})<-[:ACTED_IN {year: 2002}]-(a) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("top level AND filter by node", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + AND: [ + { node: { runtime: { equals: 90.5 } } } + { node: { year: { equals: 1999 } } } + ] + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix Thingy", + }, + }, + ]), + }, + }, + }); + }); + + test("top level AND with nested AND filter by node", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + AND: { + AND: [ + { node: { runtime: { equals: 90.5 } } } + { node: { year: { equals: 1999 } } } + ] + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix Thingy", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.int.test.ts new file mode 100644 index 0000000000..ce1f76aa21 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.int.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Filters NOT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + year: Int + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix", year: 1999, runtime: 90.5})<-[:ACTED_IN {year: 1999}]-(a:${Actor} {name: "Keanu"}) + CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001, runtime: 90.5})<-[:ACTED_IN {year: 2001}]-(a) + CREATE (:${Movie} {title: "The Matrix Revelations", year: 2002, runtime: 70})<-[:ACTED_IN {year: 2002}]-(a) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("top level NOT filter by node", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { title: { equals: "The Matrix" } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix Revelations", + }, + }, + { + node: { + title: "The Matrix Reloaded", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.int.test.ts new file mode 100644 index 0000000000..8a784adf47 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.int.test.ts @@ -0,0 +1,224 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Filters OR", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + year: Int + runtime: Float + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix", year: 1999, runtime: 90.5})<-[:ACTED_IN {year: 1999}]-(a:${Actor} {name: "Keanu"}) + CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001, runtime: 90.5})<-[:ACTED_IN {year: 2001}]-(a) + CREATE (:${Movie} {title: "The Matrix Revelations", year: 2002, runtime: 70})<-[:ACTED_IN {year: 2002}]-(a) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("top level OR filter by node", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { OR: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 2001 } } }] } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix Reloaded", + }, + }, + ]), + }, + }, + }); + }); + + test("top level OR with nested OR filter by node", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + OR: { + OR: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 2001 } } }] + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix Reloaded", + }, + }, + ]), + }, + }, + }); + }); + + test("top level OR filter combined with implicit AND", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + OR: [ + { node: { title: { equals: "The Matrix" }, year: { equals: 2001 } } } + { node: { year: { equals: 2002 } } } + ] + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix Revelations", + }, + }, + ], + }, + }, + }); + }); + + test("top level OR filter combined with implicit AND and nested not", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} ( + where: { + OR: [ + { + NOT: { node: { runtime: { equals: 90.5 } } } + node: { title: { equals: "The Matrix" }, year: { equals: 1999 } } + } + { node: { year: { equals: 2002 } } } + ] + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix Revelations", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/nested/all.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/nested/all.int.test.ts new file mode 100644 index 0000000000..53a528cde9 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/nested/all.int.test.ts @@ -0,0 +1,209 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Relationship filters with all", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a1:${Actor} {name: "Keanu"}) + CREATE (a2:${Actor} {name: "Uneak"}) + CREATE (m1:${Movie} {title: "The Matrix"})<-[:ACTED_IN {year: 1999}]-(a1) + CREATE (m1)<-[:ACTED_IN {year: 2001}]-(a2) + CREATE (:${Movie} {title: "The Matrix Reloaded"})<-[:ACTED_IN {year: 2001}]-(a1) + CREATE (:${Movie} {title: "A very cool movie"})<-[:ACTED_IN {year: 1999}]-(a2) + CREATE (:${Movie} {title: "unknown movie"})<-[:ACTED_IN {year: 3000}]-(a2) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by nested node with all", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { all: { edges: { node: { name: { equals: "Keanu" } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix Reloaded", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested relationship properties with all", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { all: { edges: { properties: { year: { equals: 1999 } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "A very cool movie", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested relationship properties with all and OR operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { all: { edges: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix Reloaded", + }, + }, + { + node: { + title: "A very cool movie", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested node with all and NOT operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { all: { edges: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "A very cool movie", + }, + }, + { + node: { + title: "unknown movie", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/nested/none.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/nested/none.int.test.ts new file mode 100644 index 0000000000..2ce8aa30cf --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/nested/none.int.test.ts @@ -0,0 +1,209 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Relationship filters with none", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a1:${Actor} {name: "Keanu"}) + CREATE (a2:${Actor} {name: "Uneak"}) + CREATE (m1:${Movie} {title: "The Matrix"})<-[:ACTED_IN {year: 1999}]-(a1) + CREATE (m1)<-[:ACTED_IN {year: 1999}]-(a2) + CREATE (:${Movie} {title: "The Matrix Reloaded"})<-[:ACTED_IN {year: 2001}]-(a1) + CREATE (:${Movie} {title: "A very cool movie"})<-[:ACTED_IN {year: 1999}]-(a2) + CREATE (:${Movie} {title: "unknown movie"})<-[:ACTED_IN {year: 3000}]-(a2) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by nested node with none", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { none: { edges: { node: { name: { equals: "Keanu" } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "A very cool movie", + }, + }, + { + node: { + title: "unknown movie", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested relationship properties with none", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { none: { edges: { properties: { year: { equals: 1999 } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix Reloaded", + }, + }, + { + node: { + title: "unknown movie", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested relationship properties with none and OR operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { none: { edges: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "unknown movie", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested node with none and NOT operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { none: { edges: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix Reloaded", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/nested/single.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/nested/single.int.test.ts new file mode 100644 index 0000000000..4014963853 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/nested/single.int.test.ts @@ -0,0 +1,216 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Relationship filters with single", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a1:${Actor} {name: "Keanu"}) + CREATE (a2:${Actor} {name: "Uneak"}) + CREATE (anotherKeanu:${Actor} {name: "Keanu"}) + CREATE (m1:${Movie} {title: "The Matrix"})<-[:ACTED_IN {year: 1999}]-(a1) + CREATE (m1)<-[:ACTED_IN {year: 1999}]-(anotherKeanu) + CREATE (m1)<-[:ACTED_IN {year: 1999}]-(a2) + CREATE (:${Movie} {title: "The Matrix Reloaded"})<-[:ACTED_IN {year: 2001}]-(a1) + CREATE (:${Movie} {title: "A very cool movie"})<-[:ACTED_IN {year: 1999}]-(a2) + CREATE (:${Movie} {title: "unknown movie"})<-[:ACTED_IN {year: 3000}]-(a2) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by nested node with single", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { single: { edges: { node: { name: { equals: "Keanu" } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix Reloaded", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested relationship properties with single", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { single: { edges: { properties: { year: { equals: 1999 } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "A very cool movie", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested relationship properties with single and OR operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { single: { edges: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix Reloaded", + }, + }, + { + node: { + title: "A very cool movie", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested node with single and NOT operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { single: { edges: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "A very cool movie", + }, + }, + { + node: { + title: "unknown movie", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/nested/some.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/nested/some.int.test.ts new file mode 100644 index 0000000000..6c9d7e2985 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/nested/some.int.test.ts @@ -0,0 +1,229 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Relationship filters with some", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a1:${Actor} {name: "Keanu"}) + CREATE (a2:${Actor} {name: "Uneak"}) + CREATE (m1:${Movie} {title: "The Matrix"})<-[:ACTED_IN {year: 1999}]-(a1) + CREATE (m1)<-[:ACTED_IN {year: 1999}]-(a2) + CREATE (:${Movie} {title: "The Matrix Reloaded"})<-[:ACTED_IN {year: 2001}]-(a1) + CREATE (:${Movie} {title: "A very cool movie"})<-[:ACTED_IN {year: 1999}]-(a2) + CREATE (:${Movie} {title: "unknown movie"})<-[:ACTED_IN {year: 3000}]-(a2) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by nested node with some", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { some: { edges: { node: { name: { equals: "Keanu" } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix Reloaded", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested relationship properties with some", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { some: { edges: { properties: { year: { equals: 1999 } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "A very cool movie", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested relationship properties with some and OR operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { some: { edges: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix Reloaded", + }, + }, + { + node: { + title: "A very cool movie", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by nested node with some and NOT operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { node: { actors: { some: { edges: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "A very cool movie", + }, + }, + { + node: { + title: "unknown movie", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/null-filtering.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/null-filtering.int.test.ts new file mode 100644 index 0000000000..d007db5562 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/null-filtering.int.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Null filtering", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + id: String! + optional: String + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} { id: "id-1"}) + CREATE (:${Movie} { id: "id-2", optional: "My Option"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("equals null query", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + node: { optional: { equals: null } } + } + ) { + connection { + edges { + node { + id + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + id: "id-1", + }, + }, + ], + }, + }, + }); + }); + + test("equals null query with NOT", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + node: { optional: { NOT: { equals: null } } } + } + ) { + connection { + edges { + node { + id + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + id: "id-2", + }, + }, + ], + }, + }, + }); + }); + + test("null filter without operator should be ignored", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + node: { optional: null } + } + ) { + connection { + edges { + node { + id + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + id: "id-1", + }, + }, + { + node: { + id: "id-2", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/relationships/relationship-not.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/relationships/relationship-not.int.test.ts new file mode 100644 index 0000000000..019154f531 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/relationships/relationship-not.int.test.ts @@ -0,0 +1,137 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Relationship filters", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix", year: 1999, runtime: 90.5})<-[:ACTED_IN {year: 1999}]-(a:${Actor} {name: "Keanu"}) + CREATE (:${Movie} {title: "The Animatrix", year: 1999, runtime: 90.5})<-[:ACTED_IN {year: 2000}]-(:${Actor} {name: "Uneak"}) + CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001, runtime: 90.5})<-[:ACTED_IN {year: 2001}]-(a) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("NOT operator on nested edge properties", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + title + actors(where: { edges: { properties: { NOT: { year: { equals: 1999 } } } } }) { + connection { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + actors: { + connection: { + edges: [], + }, + }, + }, + }, + { + node: { + title: "The Animatrix", + actors: { + connection: { + edges: [ + { + node: { + name: "Uneak", + }, + }, + ], + }, + }, + }, + }, + { + node: { + title: "The Matrix Reloaded", + actors: { + connection: { + edges: [ + { + node: { + name: "Keanu", + }, + }, + ], + }, + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/relationships/relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/relationships/relationship.int.test.ts new file mode 100644 index 0000000000..b7ceca9996 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/relationships/relationship.int.test.ts @@ -0,0 +1,158 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Relationship filters", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix", year: 1999, runtime: 90.5})<-[:ACTED_IN {year: 1999}]-(a:${Actor} {name: "Keanu"}) + CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001, runtime: 90.5})<-[:ACTED_IN {year: 2001}]-(a) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("NOT operator on nested node", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + movies(where: { edges: { node: { title: { equals: "The Matrix Reloaded" } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix Reloaded", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("filter by edge properties", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + movies(where: { edges: { properties: { year: { equals: 1999 } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/top-level-filters.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/top-level-filters.int.test.ts new file mode 100644 index 0000000000..07fb4c6c48 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/top-level-filters.int.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Top level filters", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + year: Int! + runtime: Float! + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix", year: 1999, runtime: 90.5}) + CREATE (:${Movie} {title: "The Matrix Reloaded", year: 2001, runtime: 90.5}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to get a Movie", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { year: { equals: 1999 }, runtime: { equals: 90.5 } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/boolean/boolean-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/boolean/boolean-equals.int.test.ts new file mode 100644 index 0000000000..f4b01a6c2c --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/boolean/boolean-equals.int.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Boolean Filtering", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: Boolean + title: String! + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: true, title: "The Matrix"}) + CREATE (:${Movie} {value: false, title: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by true", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { equals: true } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + ], + }, + }, + }); + }); + + test("filter by false", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { equals: false } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "Bill And Ted", + }, + }, + ], + }, + }, + }); + }); + + test("filter by NOT", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { equals: true } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "Bill And Ted", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/array/cartesian-point-2d-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/array/cartesian-point-2d-equals.int.test.ts new file mode 100644 index 0000000000..ac8cb8f506 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/array/cartesian-point-2d-equals.int.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint 2d array EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 } as const; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: [CartesianPoint!]! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: [point($London), point($Paris) ]}) + CREATE (:${Location} { id: "2", value: [point($Rome)]}) + `, + { London, Rome, Paris } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + test("wgs-84-2d point filter by EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { equals: [{ x: ${London.x}, y: ${London.y} }, { x: ${Paris.x}, y: ${Paris.y} }] } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + + value: expect.toIncludeSameMembers([ + { + y: London.y, + x: London.x, + z: null, + crs: "cartesian", + }, + { + y: Paris.y, + x: Paris.x, + z: null, + crs: "cartesian", + }, + ]), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/array/cartesian-point-3d-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/array/cartesian-point-3d-equals.int.test.ts new file mode 100644 index 0000000000..a194ced96a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/array/cartesian-point-3d-equals.int.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint 3d array EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -0.127758, y: 51.507351, z: 0 } as const; + const Rome = { x: 12.496365, y: 41.902782, z: 0 } as const; + const Paris = { x: 2.352222, y: 48.856613, z: 0 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: [CartesianPoint!]! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: [point($London), point($Paris) ]}) + CREATE (:${Location} { id: "2", value: [point($Rome)]}) + `, + { London, Rome, Paris } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + test("wgs-84-2d point filter by EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { equals: [{ x: ${London.x}, y: ${London.y}, z: ${London.z} }, { x: ${Paris.x}, y: ${Paris.y}, z: ${Paris.z} }] } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: expect.toIncludeSameMembers([ + { + y: London.y, + x: London.x, + z: London.z, + crs: "cartesian-3d", + }, + { + y: Paris.y, + x: Paris.x, + z: Paris.z, + crs: "cartesian-3d", + }, + ]), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-equals.int.test.ts new file mode 100644 index 0000000000..bc893f7faf --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-equals.int.test.ts @@ -0,0 +1,158 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint 2d EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 } as const; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point filter by EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { equals: { x: ${London.x}, y: ${London.y} } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: null, + crs: "cartesian", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-2d point filter by NOT EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { equals: { x: ${Paris.x}, y: ${Paris.y} } } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: null, + crs: "cartesian", + }, + }, + }, + { + node: { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: null, + crs: "cartesian", + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-gt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-gt.int.test.ts new file mode 100644 index 0000000000..107934c922 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-gt.int.test.ts @@ -0,0 +1,150 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint 2d GT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 } as const; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point filter by GT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { gt: { point: { x: ${Paris.x}, y: ${Paris.y} }, distance: ${distance} } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: null, + crs: "cartesian", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-2d point filter by NOT GT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { gt: { point: { x: ${Paris.x}, y: ${Paris.y} }, distance: ${distance} } } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query); + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: null, + crs: "cartesian", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-in.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-in.int.test.ts new file mode 100644 index 0000000000..51c6addc59 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-in.int.test.ts @@ -0,0 +1,149 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint 2d IN", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 } as const; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point filter by IN", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { in: [{ x: ${Rome.x}, y: ${Rome.y} }, { x: ${Paris.x}, y: ${Paris.y} }] } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: {}, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: null, + crs: "cartesian", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-2d point filter by NOT IN", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { in: [{ x: ${Rome.x}, y: ${Rome.y} }, { x: ${Paris.x}, y: ${Paris.y} }] } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: {}, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: null, + crs: "cartesian", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-lt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-lt.int.test.ts new file mode 100644 index 0000000000..5c06c59a5a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-lt.int.test.ts @@ -0,0 +1,153 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint 2d LT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 } as const; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point filter by LT", async () => { + const query = /* GraphQL */ ` + query types($x: Float!, $y: Float!, $distance: Float!) { + ${Location.plural}(where: { edges: { node: { value: { lt: { point: { x: $x, y: $y }, distance: $distance } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: { x: Paris.x, y: Paris.y, distance }, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: null, + crs: "cartesian", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-2d point filter by NOT LT", async () => { + const query = /* GraphQL */ ` + query types($x: Float!, $y: Float!, $distance: Float!) { + ${Location.plural}(where: { edges: { node: { value: { NOT: { lt: { point: { x: $x, y: $y }, distance: $distance } } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: { x: Paris.x, y: Paris.y, distance }, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: null, + crs: "cartesian", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-equals.int.test.ts new file mode 100644 index 0000000000..746bb37732 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-equals.int.test.ts @@ -0,0 +1,159 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint 3d EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + + const London = { x: -14221.955504767046, y: 6711533.711877272, z: 0 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265, z: 0 } as const; + const Paris = { x: 261848.15527273554, y: 6250566.54904563, z: 0 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-3d point filter by EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { equals: { x: ${London.x}, y: ${London.y}, z: ${London.z} } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: London.z, + crs: "cartesian-3d", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-3d point filter by NOT EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { equals: { x: ${Paris.x}, y: ${Paris.y}, z: ${London.z} } } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: London.z, + crs: "cartesian-3d", + }, + }, + }, + { + node: { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: Rome.z, + crs: "cartesian-3d", + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-gt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-gt.int.test.ts new file mode 100644 index 0000000000..4e7035f41c --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-gt.int.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint 3d GT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272, z: 0 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265, z: 0 } as const; + const Paris = { x: 261848.15527273554, y: 6250566.54904563, z: 0 } as const; + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-3d point filter by GT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { gt: { point: { x: ${Paris.x}, y: ${Paris.y}, z: ${Paris.z} }, distance: ${distance} } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: Rome.z, + crs: "cartesian-3d", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-3d point filter by NOT GT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { gt: { point: { x: ${Paris.x}, y: ${Paris.y}, z: ${Paris.z} }, distance: ${distance} } } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: London.z, + crs: "cartesian-3d", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-lt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-lt.int.test.ts new file mode 100644 index 0000000000..dea40f7e62 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-lt.int.test.ts @@ -0,0 +1,149 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint 3d LT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272, z: 0 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265, z: 0 } as const; + const Paris = { x: 261848.15527273554, y: 6250566.54904563, z: 0 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-3d point filter by LT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { lt: { point: { x: ${Paris.x}, y: ${Paris.y}, z: ${Paris.z} }, distance: ${distance} } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: London.z, + crs: "cartesian-3d", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-3d point filter by NOT LT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { lt: { point: { x: ${Paris.x}, y: ${Paris.y}, z: ${Paris.z} }, distance: ${distance} } } } } } }) { + connection { + edges { + node { + id + value { + y + x + z + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: Rome.z, + crs: "cartesian-3d", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/date/array/date-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/date/array/date-equals.int.test.ts new file mode 100644 index 0000000000..afd4348f1b --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/date/array/date-equals.int.test.ts @@ -0,0 +1,101 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("Date array - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("date Equals", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + date: [Date!] + } + `; + + const date1 = new Date(1716904582368); + const date2 = new Date(1716900000000); + const date3 = new Date(1716904582369); + const dateList1 = [ + neo4jDriver.types.Date.fromStandardDate(date1), + neo4jDriver.types.Date.fromStandardDate(date3), + ]; + + const dateList2 = [neo4jDriver.types.Date.fromStandardDate(date2)]; + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", date: $dateList1}) + CREATE (:${Movie.name} {title: "The Matrix 2", date: $dateList2}) + `, + { dateList1, dateList2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const query = /* GraphQL */ ` + query movies($date1: Date!, $date3: Date!) { + ${Movie.plural}(where: { node: { date: { equals: [$date1, $date3] }}}) { + connection{ + edges { + node { + title + date + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query, { + variableValues: { + date1: dateList1[0]?.toString(), + date3: dateList1[1]?.toString(), + }, + }); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + date: [dateList1[0]?.toString(), dateList1[1]?.toString()], + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/date/date-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/date/date-equals.int.test.ts new file mode 100644 index 0000000000..e1722ae302 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/date/date-equals.int.test.ts @@ -0,0 +1,92 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Date - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("Date Equals", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + date: Date + } + `; + + const date1 = new Date(1716904582368); + const date2 = new Date(1736900000000); + const neoDate1 = neo4jDriver.types.Date.fromStandardDate(date1); + const neoDate2 = neo4jDriver.types.Date.fromStandardDate(date2); + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", date: $datetime1}) + CREATE (:${Movie.name} {title: "The Matrix 2", date: $datetime2}) + `, + { datetime1: neoDate1, datetime2: neoDate2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { date: { equals: "${neoDate1.toString()}" }} }) { + connection{ + edges { + node { + title + date + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + date: neoDate1.toString(), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/date/date-in.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/date/date-in.int.test.ts new file mode 100644 index 0000000000..2fa3a3e560 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/date/date-in.int.test.ts @@ -0,0 +1,157 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Date - IN", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("Date IN", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + date: Date + } + `; + + const date1 = new Date(1716904582368); + const date2 = new Date(1736900000000); + const date3 = new Date(1747900000000); + const neoDate1 = neo4jDriver.types.Date.fromStandardDate(date1); + const neoDate2 = neo4jDriver.types.Date.fromStandardDate(date2); + const neoDate3 = neo4jDriver.types.Date.fromStandardDate(date3); + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", date: $neoDate1}) + CREATE (:${Movie.name} {title: "The Matrix 2", date: $neoDate2}) + `, + { neoDate1, neoDate2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${ + Movie.plural + }(where: { node: { date: { in: ["${neoDate1.toString()}", "${neoDate3.toString()}"] }} }) { + connection{ + edges { + node { + title + date + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + date: neoDate1.toString(), + }, + }, + ], + }, + }, + }); + }); + + test("Date NOT IN", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + date: Date + } + `; + + const date1 = new Date(1716904582368); + const date2 = new Date(1736900000000); + const date3 = new Date(1747900000000); + const neoDate1 = neo4jDriver.types.Date.fromStandardDate(date1); + const neoDate2 = neo4jDriver.types.Date.fromStandardDate(date2); + const neoDate3 = neo4jDriver.types.Date.fromStandardDate(date3); + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", date: $neoDate1}) + CREATE (:${Movie.name} {title: "The Matrix 2", date: $neoDate2}) + `, + { neoDate1, neoDate2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${ + Movie.plural + }(where: { node: { date: { NOT: { in: ["${neoDate1.toString()}", "${neoDate3.toString()}"] } }} }) { + connection{ + edges { + node { + title + date + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 2", + date: neoDate2.toString(), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/date/date-lt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/date/date-lt.int.test.ts new file mode 100644 index 0000000000..390cd60613 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/date/date-lt.int.test.ts @@ -0,0 +1,92 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Date - LT", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("Date LT", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + date: Date + } + `; + + const date1 = new Date(1716904582368); + const date2 = new Date(1736900000000); + const neoDate1 = neo4jDriver.types.Date.fromStandardDate(date1); + const neoDate2 = neo4jDriver.types.Date.fromStandardDate(date2); + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", date: $neoDate1}) + CREATE (:${Movie.name} {title: "The Matrix 2", date: $neoDate2}) + `, + { neoDate1, neoDate2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { date: { lt: "${neoDate2.toString()}" }}}) { + connection{ + edges { + node { + title + date + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + date: neoDate1.toString(), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/datetime/array/datetime-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/datetime/array/datetime-equals.int.test.ts new file mode 100644 index 0000000000..3a4d7955b3 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/datetime/array/datetime-equals.int.test.ts @@ -0,0 +1,96 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("DateTime array - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("datetime equals to ISO string", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + datetime: [DateTime!] + } + `; + + const date1 = new Date(1716904582368); + const date2 = new Date(1716900000000); + const date3 = new Date(1716904582369); + const datetime1 = [ + neo4jDriver.types.DateTime.fromStandardDate(date1), + neo4jDriver.types.DateTime.fromStandardDate(date3), + ]; + const datetime2 = [neo4jDriver.types.DateTime.fromStandardDate(date2)]; + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", datetime: $datetime1}) + CREATE (:${Movie.name} {title: "The Matrix 2", datetime: $datetime2}) + `, + { datetime1, datetime2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${ + Movie.plural + }(where: { node: { datetime: { equals: ["${date1.toISOString()}", "${date3.toISOString()}"] }}}) { + connection{ + edges { + node { + title + datetime + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect((gqlResult.data as any)[Movie.plural]).toEqual({ + connection: { + edges: [ + { + node: { + title: "The Matrix", + datetime: [date1.toISOString(), date3.toISOString()], + }, + }, + ], + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/datetime/datetime-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/datetime/datetime-equals.int.test.ts new file mode 100644 index 0000000000..e4269b4970 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/datetime/datetime-equals.int.test.ts @@ -0,0 +1,90 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("DateTime - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("datetime equals to ISO string", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + datetime: DateTime + } + `; + + const date1 = new Date(1716904582368); + const date2 = new Date(1716900000000); + const datetime1 = neo4jDriver.types.DateTime.fromStandardDate(date1); + const datetime2 = neo4jDriver.types.DateTime.fromStandardDate(date2); + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", datetime: $datetime1}) + CREATE (:${Movie.name} {title: "The Matrix 2", datetime: $datetime2}) + `, + { datetime1, datetime2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { datetime: { equals: "${date1.toISOString()}" }}}) { + connection{ + edges { + node { + title + datetime + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect((gqlResult.data as any)[Movie.plural]).toEqual({ + connection: { + edges: [ + { + node: { + title: "The Matrix", + datetime: date1.toISOString(), + }, + }, + ], + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/duration/array/duration-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/duration/array/duration-equals.int.test.ts new file mode 100644 index 0000000000..7b49531b49 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/duration/array/duration-equals.int.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { Duration } from "neo4j-driver"; +import type { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("Duration array - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("duration Equals", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + duration: [Duration!] + } + `; + + const duration1Args = { months: 10, days: 10, seconds: 10, nanoseconds: 10 }; + const duration1 = new Duration( + duration1Args.months, + duration1Args.days, + duration1Args.seconds, + duration1Args.nanoseconds + ); + const duration2Args = { months: 4, days: 4, seconds: 4, nanoseconds: 4 }; + const duration2 = new Duration( + duration2Args.months, + duration2Args.days, + duration2Args.seconds, + duration2Args.nanoseconds + ); + + const duration3Args = { months: 4, days: 4, seconds: 4, nanoseconds: 4 }; + const duration3 = new Duration( + duration3Args.months, + duration3Args.days, + duration3Args.seconds, + duration3Args.nanoseconds + ); + + const dateList1 = [duration1, duration3]; + + const dateList2 = [duration2]; + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", duration: $dateList1}) + CREATE (:${Movie.name} {title: "The Matrix 2", duration: $dateList2}) + `, + { dateList1, dateList2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const query = /* GraphQL */ ` + query movies($date1: Duration!, $date3: Duration!) { + ${Movie.plural}(where: { node: { duration: { equals: [$date1, $date3] }} }) { + connection{ + edges { + node { + title + duration + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query, { + variableValues: { + date1: dateList1[0]?.toString(), + date3: dateList1[1]?.toString(), + }, + }); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + duration: [dateList1[0]?.toString(), dateList1[1]?.toString()], + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/duration/duration-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/duration/duration-equals.int.test.ts new file mode 100644 index 0000000000..2fb214828f --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/duration/duration-equals.int.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { Duration } from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Duration - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("Duration Equals", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + duration: Duration + } + `; + const duration1Args = { months: 10, days: 10, seconds: 10, nanoseconds: 10 }; + const duration1 = new Duration( + duration1Args.months, + duration1Args.days, + duration1Args.seconds, + duration1Args.nanoseconds + ); + const duration2Args = { months: 4, days: 4, seconds: 4, nanoseconds: 4 }; + const duration2 = new Duration( + duration2Args.months, + duration2Args.days, + duration2Args.seconds, + duration2Args.nanoseconds + ); + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", duration: $duration1}) + CREATE (:${Movie.name} {title: "The Matrix 2", duration: $duration2}) + `, + { duration1, duration2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { duration: { equals: "${duration1.toString()}" }} }) { + connection{ + edges { + node { + title + duration + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + duration: duration1.toString(), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/duration/duration-lt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/duration/duration-lt.int.test.ts new file mode 100644 index 0000000000..2c9ecabc53 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/duration/duration-lt.int.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Duration } from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Duration - LT", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("Duration LT", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + duration: Duration + } + `; + + const duration1Args = { months: 10, days: 10, seconds: 10, nanoseconds: 10 }; + const duration1 = new Duration( + duration1Args.months, + duration1Args.days, + duration1Args.seconds, + duration1Args.nanoseconds + ); + + const duration2Args = { months: 11, days: 11, seconds: 11, nanoseconds: 11 }; + const duration2 = new Duration( + duration2Args.months, + duration2Args.days, + duration2Args.seconds, + duration2Args.nanoseconds + ); + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", duration: $duration1}) + CREATE (:${Movie.name} {title: "The Matrix 2", duration: $duration2}) + `, + { duration1, duration2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { duration: { lt: "${duration2.toString()}" }} }) { + connection{ + edges { + node { + title + duration + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + duration: duration1.toString(), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/localdatetime/array/localdatetime-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/localdatetime/array/localdatetime-equals.int.test.ts new file mode 100644 index 0000000000..6d9e26324f --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/localdatetime/array/localdatetime-equals.int.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("LocalDateTime array - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("localDateTime equals", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + localDateTime: [LocalDateTime!] + } + `; + const date1 = new Date("2024-09-17T11:49:48.322Z"); + const localdatetime1 = neo4jDriver.types.LocalDateTime.fromStandardDate(date1); + + const date2 = new Date("2025-09-17T11:49:48.322Z"); + const localdatetime2 = neo4jDriver.types.LocalDateTime.fromStandardDate(date2); + + const date3 = new Date("2026-09-17T11:49:48.322Z"); + const localdatetime3 = neo4jDriver.types.LocalDateTime.fromStandardDate(date3); + + const dateList1 = [localdatetime1, localdatetime3]; + const dateList2 = [localdatetime2]; + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", localDateTime: $dateList1}) + CREATE (:${Movie.name} {title: "The Matrix 2", localDateTime: $dateList2}) + `, + { dateList1, dateList2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const query = /* GraphQL */ ` + query movies($date1: LocalDateTime!, $date3: LocalDateTime!) { + ${Movie.plural}(where: { node: { localDateTime: { equals: [$date1, $date3] }}}) { + connection{ + edges { + node { + title + localDateTime + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query, { + variableValues: { + date1: dateList1[0]?.toString(), + date3: dateList1[1]?.toString(), + }, + }); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + localDateTime: [dateList1[0]?.toString(), dateList1[1]?.toString()], + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/localdatetime/localdatetime-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/localdatetime/localdatetime-equals.int.test.ts new file mode 100644 index 0000000000..c37af05cd4 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/localdatetime/localdatetime-equals.int.test.ts @@ -0,0 +1,93 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("LocalDateTime - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("LocalDateTime equals", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + localDateTime: LocalDateTime + } + `; + + const date1 = new Date("2024-09-17T11:49:48.322Z"); + const localdatetime1 = neo4jDriver.types.LocalDateTime.fromStandardDate(date1); + + const date2 = new Date("2025-09-17T11:49:48.322Z"); + const localdatetime2 = neo4jDriver.types.LocalDateTime.fromStandardDate(date2); + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", localDateTime: $localdatetime1}) + CREATE (:${Movie.name} {title: "The Matrix 2", localDateTime: $localdatetime2}) + `, + { localdatetime1, localdatetime2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { localDateTime: { equals: "${localdatetime1.toString()}" }} }) { + connection{ + edges { + node { + title + localDateTime + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + localDateTime: localdatetime1.toString(), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/number/array/number-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/number/array/number-equals.int.test.ts new file mode 100644 index 0000000000..0e4ff87aae --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/number/array/number-equals.int.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering array - 'equals'", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + list: [${type}!]! + title: String! + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {list: [1999, 2000], title: "The Matrix"}) + CREATE (:${Movie} {list: [2001, 2000], title: "The Matrix 2"}) + CREATE (:${Movie} {list: [1999, 2000], title: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("list filter by 'equals'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { list: { equals: [2001, 2000] } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }); + }); + + test("List filter by NOT 'equals'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { list: { equals: [2001, 2000] } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "Bill And Ted", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/number/number-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/number/number-equals.int.test.ts new file mode 100644 index 0000000000..7c366ca142 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/number/number-equals.int.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'equals'", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: ${type} + title: String! + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: 1999, title: "The Matrix"}) + CREATE (:${Movie} {value: 2001, title: "The Matrix 2"}) + CREATE (:${Movie} {value: 1999, title: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by 'equals'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { equals: 2001 } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }); + }); + + test("filter by NOT 'equals'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { equals: 2001 } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "Bill And Ted", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/number/number-gt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/number/number-gt.int.test.ts new file mode 100644 index 0000000000..5e993cdb5f --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/number/number-gt.int.test.ts @@ -0,0 +1,188 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'gt' and 'gte'", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: ${type} + title: String! + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: 1999, title: "The Matrix"}) + CREATE (:${Movie} {value: 2001, title: "The Matrix 2"}) + CREATE (:${Movie} {value: 1998, title: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by 'gt'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { gt: 1999 } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }); + }); + + test("filter by NOT 'gt'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { gt: 1999 } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "Bill And Ted", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by 'gte'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { gte: 1999 } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by NOT 'gte'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { gte: 1999 } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "Bill And Ted", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/number/number-in.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/number/number-in.int.test.ts new file mode 100644 index 0000000000..79e73d5482 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/number/number-in.int.test.ts @@ -0,0 +1,119 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'in'", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: ${type} + title: String! + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: 1999, title: "The Matrix"}) + CREATE (:${Movie} {value: 2001, title: "The Matrix 2"}) + CREATE (:${Movie} {value: 1989, title: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by 'in'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { in: [1999, 2001] } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by NOT 'in'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { in: [1999, 2001] } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "Bill And Ted", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/number/number-lt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/number/number-lt.int.test.ts new file mode 100644 index 0000000000..3a1f3314d0 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/number/number-lt.int.test.ts @@ -0,0 +1,188 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: ${type} + title: String! + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: 1999, title: "The Matrix"}) + CREATE (:${Movie} {value: 2001, title: "The Matrix 2"}) + CREATE (:${Movie} {value: 1998, title: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by 'lt'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { lt: 1999 } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "Bill And Ted", + }, + }, + ], + }, + }, + }); + }); + + test("filter by NOT 'lt'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { lt: 1999 } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by 'lte'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { lte: 1999 } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "Bill And Ted", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by NOT 'lte'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { lte: 1999 } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/point/array/point-2d-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/point/array/point-2d-equals.int.test.ts new file mode 100644 index 0000000000..d4a56aff56 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/array/point-2d-equals.int.test.ts @@ -0,0 +1,110 @@ +/* + * 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 { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point 2d array EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782 } as const; + const Paris = { longitude: 2.352222, latitude: 48.856613 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: [Point!]! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: [point($London), point($Paris) ]}) + CREATE (:${Location} { id: "2", value: [point($Rome)]}) + `, + { London, Rome, Paris } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point filter by EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { equals: [{ longitude: ${London.longitude}, latitude: ${London.latitude} }, { longitude: ${Paris.longitude}, latitude: ${Paris.latitude} }] } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + + value: expect.toIncludeSameMembers([ + { + latitude: London.latitude, + longitude: London.longitude, + height: null, + crs: "wgs-84", + }, + { + latitude: Paris.latitude, + longitude: Paris.longitude, + height: null, + crs: "wgs-84", + }, + ]), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/point/array/point-3d-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/point/array/point-3d-equals.int.test.ts new file mode 100644 index 0000000000..d433fab32d --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/array/point-3d-equals.int.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point 3d array EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351, height: 24 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782, height: 35 } as const; + const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: [Point!]! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: [point($London), point($Paris) ]}) + CREATE (:${Location} { id: "2", value: [point($Rome)]}) + `, + { London, Rome, Paris } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + test("wgs-84-3d point filter by EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { equals: [{ longitude: ${London.longitude}, latitude: ${London.latitude}, height: ${London.height} }, { longitude: ${Paris.longitude}, latitude: ${Paris.latitude}, height: ${Paris.height} }] } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: expect.toIncludeSameMembers([ + { + latitude: London.latitude, + longitude: London.longitude, + height: London.height, + crs: "wgs-84-3d", + }, + { + latitude: Paris.latitude, + longitude: Paris.longitude, + height: Paris.height, + crs: "wgs-84-3d", + }, + ]), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-equals.int.test.ts new file mode 100644 index 0000000000..49827c7c26 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-equals.int.test.ts @@ -0,0 +1,158 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point 2d EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782 } as const; + const Paris = { longitude: 2.352222, latitude: 48.856613 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point filter by EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { equals: { longitude: ${London.longitude}, latitude: ${London.latitude} } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + + const equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: null, + crs: "wgs-84", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-2d point filter by NOT EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { equals: { longitude: ${Paris.longitude}, latitude: ${Paris.latitude} } } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + + const equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: null, + crs: "wgs-84", + }, + }, + }, + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: null, + crs: "wgs-84", + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-gt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-gt.int.test.ts new file mode 100644 index 0000000000..0d1ab22839 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-gt.int.test.ts @@ -0,0 +1,153 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point 2d GT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782 } as const; + const Paris = { longitude: 2.352222, latitude: 48.856613 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point filter by GT", async () => { + const query = /* GraphQL */ ` + query types($longitude: Float!, $latitude: Float!, $distance: Float!) { + ${Location.plural}(where: { edges: { node: { value: { gt: { point: { longitude: $longitude, latitude: $latitude }, distance: $distance } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: null, + crs: "wgs-84", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-2d point filter by NOT GT", async () => { + const query = /* GraphQL */ ` + query types($longitude: Float!, $latitude: Float!, $distance: Float!) { + ${Location.plural}(where: { edges: { node: { value: { NOT: { gt: { point: { longitude: $longitude, latitude: $latitude }, distance: $distance } } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: null, + crs: "wgs-84", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-in.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-in.int.test.ts new file mode 100644 index 0000000000..47a97ce27c --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-in.int.test.ts @@ -0,0 +1,149 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point 2d IN", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782 } as const; + const Paris = { longitude: 2.352222, latitude: 48.856613 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point filter by IN", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { in: [{ longitude: ${Rome.longitude}, latitude: ${Rome.latitude} }, { longitude: ${Paris.longitude}, latitude: ${Paris.latitude} }] } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: {}, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: null, + crs: "wgs-84", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-2d point filter by NOT IN", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { in: [{ longitude: ${Rome.longitude}, latitude: ${Rome.latitude} }, { longitude: ${Paris.longitude}, latitude: ${Paris.latitude} }] } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: {}, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: null, + crs: "wgs-84", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-lt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-lt.int.test.ts new file mode 100644 index 0000000000..9285007e48 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-lt.int.test.ts @@ -0,0 +1,153 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point 2d LT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782 } as const; + const Paris = { longitude: 2.352222, latitude: 48.856613 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point filter by LT", async () => { + const query = /* GraphQL */ ` + query types($longitude: Float!, $latitude: Float!, $distance: Float!) { + ${Location.plural}(where: { edges: { node: { value: { lt: { point: { longitude: $longitude, latitude: $latitude }, distance: $distance } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: null, + crs: "wgs-84", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-2d point filter by NOT LT", async () => { + const query = /* GraphQL */ ` + query types($longitude: Float!, $latitude: Float!, $distance: Float!) { + ${Location.plural}(where: { edges: { node: { value: { NOT: { lt: { point: { longitude: $longitude, latitude: $latitude }, distance: $distance } } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const queryResult = await testHelper.executeGraphQL(query, { + variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, + }); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: null, + crs: "wgs-84", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-equals.int.test.ts new file mode 100644 index 0000000000..70ec7bbd7e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-equals.int.test.ts @@ -0,0 +1,158 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point 3d EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351, height: 24 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782, height: 35 } as const; + const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-3d point filter by EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { equals: { longitude: ${London.longitude}, latitude: ${London.latitude}, height: ${London.height} } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: London.height, + crs: "wgs-84-3d", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-3d point filter by NOT EQ", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { equals: { longitude: ${Paris.longitude}, latitude: ${Paris.latitude}, height: ${London.height} } } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: London.height, + crs: "wgs-84-3d", + }, + }, + }, + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: Rome.height, + crs: "wgs-84-3d", + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-gt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-gt.int.test.ts new file mode 100644 index 0000000000..7084ac6a00 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-gt.int.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point 3d GT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351, height: 24 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782, height: 35 } as const; + const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 } as const; + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-3d point filter by GT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { gt: { point: { longitude: ${Paris.longitude}, latitude: ${Paris.latitude}, height: ${Paris.height} }, distance: ${distance} } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: Rome.height, + crs: "wgs-84-3d", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-3d point filter by NOT GT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { gt: { point: { longitude: ${Paris.longitude}, latitude: ${Paris.latitude}, height: ${Paris.height} }, distance: ${distance} } } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: London.height, + crs: "wgs-84-3d", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-lt.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-lt.int.test.ts new file mode 100644 index 0000000000..da1f19a3c6 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-lt.int.test.ts @@ -0,0 +1,148 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point 3d LT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351, height: 24 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782, height: 35 } as const; + const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 } as const; + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-3d point filter by LT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { lt: { point: { longitude: ${Paris.longitude}, latitude: ${Paris.latitude}, height: ${Paris.height} }, distance: ${distance} } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: London.height, + crs: "wgs-84-3d", + }, + }, + }, + ], + }, + }, + }); + }); + + test("wgs-84-3d point filter by NOT LT", async () => { + // distance is in meters + const distance = 1000 * 1000; // 1000 km + const query = /* GraphQL */ ` + query { + ${Location.plural}(where: { edges: { node: { value: { NOT: { lt: { point: { longitude: ${Paris.longitude}, latitude: ${Paris.latitude}, height: ${Paris.height} }, distance: ${distance} } } } } } }) { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + } + } + } + } + + } + } + `; + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: [ + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: Rome.height, + crs: "wgs-84-3d", + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/string/string-contains.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/string/string-contains.int.test.ts new file mode 100644 index 0000000000..1f40884515 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/string/string-contains.int.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe.each(["ID", "String"] as const)("%s Filtering - 'contains'", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: ${type} + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: "The Matrix"}) + CREATE (:${Movie} {value: "The Matrix 2"}) + CREATE (:${Movie} {value: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by 'contains'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { contains: "Matrix" } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + value: "The Matrix", + }, + }, + { + node: { + value: "The Matrix 2", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by NOT 'contains'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { contains: "Matrix" } } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + value: "Bill And Ted", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/string/string-ends-with-int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/string/string-ends-with-int.test.ts new file mode 100644 index 0000000000..bd8ab73b0b --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/string/string-ends-with-int.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe.each(["ID", "String"] as const)("%s Filtering - 'endsWith'", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: ${type} + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: "The Matrix"}) + CREATE (:${Movie} {value: "A cool return of The Matrix"}) + CREATE (:${Movie} {value: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by 'endsWith'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { endsWith: "The Matrix" } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + value: "The Matrix", + }, + }, + { + node: { + value: "A cool return of The Matrix", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by NOT 'endsWith'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { endsWith: "The Matrix" } } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + value: "Bill And Ted", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/string/string-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/string/string-equals.int.test.ts new file mode 100644 index 0000000000..e55b36f56e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/string/string-equals.int.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe.each(["ID", "String"] as const)("%s Filtering - 'equals'", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: ${type} + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: "The Matrix"}) + CREATE (:${Movie} {value: "The Matrix 2"}) + CREATE (:${Movie} {value: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by 'equals'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { equals: "The Matrix" } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + value: "The Matrix", + }, + }, + ], + }, + }, + }); + }); + + test("filter by NOT 'equals'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { equals: "The Matrix" } } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + value: "The Matrix 2", + }, + }, + { + node: { + value: "Bill And Ted", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/string/string-in.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/string/string-in.int.test.ts new file mode 100644 index 0000000000..c6ed46f1e4 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/string/string-in.int.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe.each(["ID", "String"] as const)("%s Filtering - 'in'", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: ${type} + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: "The Matrix"}) + CREATE (:${Movie} {value: "The Matrix 2"}) + CREATE (:${Movie} {value: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by 'in'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { in: ["The Matrix", "The Matrix 2"] } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + value: "The Matrix", + }, + }, + { + node: { + value: "The Matrix 2", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by NOT 'in'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { in: ["The Matrix", "The Matrix 2"] } } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + value: "Bill And Ted", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/string/string-starts-with.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/string/string-starts-with.int.test.ts new file mode 100644 index 0000000000..1fb90317a1 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/string/string-starts-with.int.test.ts @@ -0,0 +1,118 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe.each(["ID", "String"] as const)("%s Filtering - 'startsWith'", (type) => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + value: ${type} + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {value: "The Matrix"}) + CREATE (:${Movie} {value: "The Matrix 2"}) + CREATE (:${Movie} {value: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter by 'startsWith'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { value: { startsWith: "The" } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + value: "The Matrix", + }, + }, + { + node: { + value: "The Matrix 2", + }, + }, + ]), + }, + }, + }); + }); + + test("filter by NOT 'startsWith'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { NOT: { node: { value: { startsWith: "The" } } } }) { + connection { + edges { + node { + value + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + value: "Bill And Ted", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/time/array/time-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/time/array/time-equals.int.test.ts new file mode 100644 index 0000000000..140f495df7 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/time/array/time-equals.int.test.ts @@ -0,0 +1,101 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("Time array - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("time Equals", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + time: [Time!] + } + `; + + const date1 = new Date("2024-02-17T11:49:48.322Z"); + const date2 = new Date("2024-02-17T14:49:48.322Z"); + const date3 = new Date("2025-02-17T12:49:48.322Z"); + const dateList1 = [ + neo4jDriver.types.Time.fromStandardDate(date1), + neo4jDriver.types.Time.fromStandardDate(date3), + ]; + + const dateList2 = [neo4jDriver.types.Time.fromStandardDate(date2)]; + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", time: $dateList1}) + CREATE (:${Movie.name} {title: "The Matrix 2", time: $dateList2}) + `, + { dateList1, dateList2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const query = /* GraphQL */ ` + query movies($date1: Time!, $date3: Time!) { + ${Movie.plural}(where: { node: { time: { equals: [$date1, $date3] }} }) { + connection{ + edges { + node { + title + time + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query, { + variableValues: { + date1: dateList1[0]?.toString(), + date3: dateList1[1]?.toString(), + }, + }); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + time: [dateList1[0]?.toString(), dateList1[1]?.toString()], + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/types/time/time-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/time/time-equals.int.test.ts new file mode 100644 index 0000000000..07b1aa6ac1 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/time/time-equals.int.test.ts @@ -0,0 +1,92 @@ +/* + * 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 { Time } from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Time - Equals", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("Time Equals", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + title: String! + time: Time + } + `; + + const date1 = new Date("2024-02-17T11:49:48.322Z"); + const time1 = Time.fromStandardDate(date1); + + const date2 = new Date("2025-02-17T12:49:48.322Z"); + const time2 = Time.fromStandardDate(date2); + + await testHelper.executeCypher( + ` + CREATE (:${Movie.name} {title: "The Matrix", time: $time1}) + CREATE (:${Movie.name} {title: "The Matrix 2", time: $time2}) + `, + { time1, time2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { time: { equals: "${time1.toString()}" }}}) { + connection{ + edges { + node { + title + time + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + time: time1.toString(), + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/issues/190.int.test.ts b/packages/graphql/tests/api-v6/integration/issues/190.int.test.ts new file mode 100644 index 0000000000..81b1d4c5da --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/issues/190.int.test.ts @@ -0,0 +1,248 @@ +/* + * 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 "graphql-tag"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("https://github.com/neo4j/graphql/issues/190", () => { + let User: UniqueType; + let UserDemographics: UniqueType; + let typeDefs: DocumentNode; + + const testHelper = new TestHelper({ v6Api: true }); + + beforeAll(async () => { + User = testHelper.createUniqueType("User"); + UserDemographics = testHelper.createUniqueType("UserDemographics"); + + typeDefs = gql` + type ${User} @node { + client_id: String + uid: String + demographics: [${UserDemographics}!]! @relationship(type: "HAS_DEMOGRAPHIC", direction: OUT) + } + + type ${UserDemographics} @node { + client_id: String + type: String + value: String + users: [${User}!]! @relationship(type: "HAS_DEMOGRAPHIC", direction: IN) + } + `; + + await testHelper.executeCypher(` + CREATE (user1:${User} {uid: 'user1'}),(user2:${User} {uid: 'user2'}),(female:${UserDemographics}{type:'Gender',value:'Female'}),(male:${UserDemographics}{type:'Gender',value:'Male'}),(age:${UserDemographics}{type:'Age',value:'50+'}),(state:${UserDemographics}{type:'State',value:'VIC'}) + CREATE (user1)-[:HAS_DEMOGRAPHIC]->(female) + CREATE (user2)-[:HAS_DEMOGRAPHIC]->(male) + CREATE (user1)-[:HAS_DEMOGRAPHIC]->(age) + CREATE (user2)-[:HAS_DEMOGRAPHIC]->(age) + CREATE (user1)-[:HAS_DEMOGRAPHIC]->(state) + CREATE (user2)-[:HAS_DEMOGRAPHIC]->(state) + `); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("Example 1", async () => { + const query = /* GraphQL */ ` + query { + ${User.plural}(where: { node: { demographics: { some: { edges: {node: { type: { equals: "Gender" }, value: { equals: "Female" } } } } } } }) { + connection { + edges { + node { + uid + demographics { + connection { + edges { + node { + type + value + } + } + } + } + } + } + } + } + } + `; + + const result = await testHelper.executeGraphQL(query); + + expect(result.errors).toBeFalsy(); + + expect(result).toEqual({ + data: { + [User.plural]: { + connection: { + edges: [ + { + node: { + uid: "user1", + demographics: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + type: "State", + value: "VIC", + }, + }, + { + node: { + type: "Age", + value: "50+", + }, + }, + { + node: { + type: "Gender", + value: "Female", + }, + }, + ]), + }, + }, + }, + }, + ], + }, + }, + }, + }); + }); + + test("Example 2", async () => { + const query = /* GraphQL */ ` + query { + ${User.plural} ( + where: { + node: { + demographics: { + some: { + edges: { + node: { + OR: [{ type: {equals: "Gender"}, value:{equals: "Female"} }, { type: {equals: "State"} }, { type: {equals: "Age"} }] + } + } + } + } + } + } + ) { + connection { + edges { + node { + uid + demographics { + connection { + edges { + node { + type + value + } + } + } + } + } + } + } + } + } + `; + + const result = await testHelper.executeGraphQL(query); + + expect(result.errors).toBeFalsy(); + expect(result).toEqual({ + data: { + [User.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + uid: "user1", + demographics: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + type: "State", + value: "VIC", + }, + }, + { + node: { + type: "Age", + value: "50+", + }, + }, + { + node: { + type: "Gender", + value: "Female", + }, + }, + ]), + }, + }, + }, + }, + { + node: { + uid: "user2", + demographics: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + type: "State", + value: "VIC", + }, + }, + { + node: { + type: "Age", + value: "50+", + }, + }, + { + node: { + type: "Gender", + value: "Male", + }, + }, + ]), + }, + }, + }, + }, + ]), + }, + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/integration/issues/360.int.test.ts b/packages/graphql/tests/api-v6/integration/issues/360.int.test.ts similarity index 67% rename from packages/graphql/tests/integration/issues/360.int.test.ts rename to packages/graphql/tests/api-v6/integration/issues/360.int.test.ts index e21ad4890c..d2cc4bd8f6 100644 --- a/packages/graphql/tests/integration/issues/360.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/issues/360.int.test.ts @@ -17,12 +17,10 @@ * limitations under the License. */ -import { TestHelper } from "../../utils/tests-helper"; +import { TestHelper } from "../../../utils/tests-helper"; describe("https://github.com/neo4j/graphql/issues/360", () => { - const testHelper = new TestHelper(); - - beforeEach(() => {}); + const testHelper = new TestHelper({ v6Api: true }); afterEach(async () => { await testHelper.close(); @@ -32,7 +30,7 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { const type = testHelper.createUniqueType("Event"); const typeDefs = ` - type ${type.name} { + type ${type.name} @node { id: ID! name: String start: DateTime @@ -45,10 +43,16 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { typeDefs, }); - const query = ` + const query = /* GraphQL */ ` query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { - ${type.plural}(where: { AND: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { - id + ${type.plural}(where: { node: { AND: [{ start: { gte: $rangeStart } }, { start: { lte: $rangeEnd } }, { activity: { equals: $activity } }] } }) { + connection { + edges { + node { + id + } + } + } } } `; @@ -64,14 +68,20 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { const gqlResult = await testHelper.executeGraphQL(query); expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[type.plural]).toHaveLength(3); + expect(gqlResult.data).toEqual({ + [type.plural]: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }); }); test("should return all nodes when OR is used and members are optional", async () => { const type = testHelper.createUniqueType("Event"); const typeDefs = ` - type ${type.name} { + type ${type.name} @node { id: ID! name: String start: DateTime @@ -84,10 +94,16 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { typeDefs, }); - const query = ` + const query = /* GraphQL */ ` query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { - ${type.plural}(where: { OR: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { - id + ${type.plural}(where: { node: { OR: [{ start: { gte: $rangeStart } }, { start: { lte: $rangeEnd } }, { activity: { equals: $activity } }] } }) { + connection { + edges { + node { + id + } + } + } } } `; @@ -103,14 +119,20 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { const gqlResult = await testHelper.executeGraphQL(query); expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[type.plural]).toHaveLength(3); + expect(gqlResult.data).toEqual({ + [type.plural]: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }); }); test("should recreate given test in issue and return correct results", async () => { const type = testHelper.createUniqueType("Event"); - const typeDefs = ` - type ${type.name} { + const typeDefs = /* GraphQL */ ` + type ${type.name} @node { id: ID! name: String start: DateTime @@ -126,10 +148,16 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { const rangeStart = new Date().toISOString(); const rangeEnd = new Date().toISOString(); - const query = ` + const query = /* GraphQL */ ` query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { - ${type.plural}(where: { OR: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { - id + ${type.plural}(where: { node: { OR: [{ start: { gte: $rangeStart } }, { start: { lte: $rangeEnd } }, { activity: { equals: $activity } }] } }) { + connection { + edges { + node { + id + } + } + } } } `; @@ -148,6 +176,12 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { }); expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[type.plural]).toHaveLength(3); + expect(gqlResult.data).toEqual({ + [type.plural]: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }); }); }); diff --git a/packages/graphql/tests/integration/issues/433.int.test.ts b/packages/graphql/tests/api-v6/integration/issues/433.int.test.ts similarity index 58% rename from packages/graphql/tests/integration/issues/433.int.test.ts rename to packages/graphql/tests/api-v6/integration/issues/433.int.test.ts index be2b7935f7..ab0baebe60 100644 --- a/packages/graphql/tests/integration/issues/433.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/issues/433.int.test.ts @@ -18,11 +18,11 @@ */ import { generate } from "randomstring"; -import type { UniqueType } from "../../utils/graphql-types"; -import { TestHelper } from "../../utils/tests-helper"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; describe("https://github.com/neo4j/graphql/issues/433", () => { - const testHelper = new TestHelper(); + const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; let Person: UniqueType; let typeDefs: string; @@ -39,12 +39,12 @@ describe("https://github.com/neo4j/graphql/issues/433", () => { test("should recreate issue and return correct data", async () => { typeDefs = ` # Cannot use 'type Node' - type ${Movie} { + type ${Movie} @node { title: String actors: [${Person}!]! @relationship(type: "ACTED_IN", direction: IN) } - type ${Person} { + type ${Person} @node { name: String } `; @@ -61,14 +61,22 @@ describe("https://github.com/neo4j/graphql/issues/433", () => { const query = ` query { - ${Movie.plural}(where: {title: "${movieTitle}"}) { - title - actorsConnection(where: {}) { - edges { - node { - name + ${Movie.plural}(where: {node: {title: {equals: "${movieTitle}"}}}) { + connection { + edges { + node { + title + actors(where: {}) { + connection { + edges { + node { + name + } + } + } + } + } } - } } } } @@ -85,15 +93,23 @@ describe("https://github.com/neo4j/graphql/issues/433", () => { expect(result.errors).toBeFalsy(); - expect(result.data as any).toEqual({ - [Movie.plural]: [ - { - title: movieTitle, - actorsConnection: { - edges: [{ node: { name: personName } }], - }, + expect(result.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: movieTitle, + actors: { + connection: { + edges: [{ node: { name: personName } }], + }, + }, + }, + }, + ], }, - ], + }, }); }); }); diff --git a/packages/graphql/tests/integration/issues/560.int.test.ts b/packages/graphql/tests/api-v6/integration/issues/560.int.test.ts similarity index 53% rename from packages/graphql/tests/integration/issues/560.int.test.ts rename to packages/graphql/tests/api-v6/integration/issues/560.int.test.ts index ac998ae52b..4aa453b987 100644 --- a/packages/graphql/tests/integration/issues/560.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/issues/560.int.test.ts @@ -17,14 +17,12 @@ * limitations under the License. */ -import { gql } from "graphql-tag"; +import gql from "graphql-tag"; import { generate } from "randomstring"; -import { TestHelper } from "../../utils/tests-helper"; +import { TestHelper } from "../../../utils/tests-helper"; describe("https://github.com/neo4j/graphql/issues/560", () => { - const testHelper = new TestHelper(); - - beforeEach(() => {}); + const testHelper = new TestHelper({ v6Api: true }); afterEach(async () => { await testHelper.close(); @@ -34,7 +32,7 @@ describe("https://github.com/neo4j/graphql/issues/560", () => { const testLog = testHelper.createUniqueType("Log"); const typeDefs = gql` - type ${testLog.name} { + type ${testLog} @node { id: ID! location: Point } @@ -46,17 +44,23 @@ describe("https://github.com/neo4j/graphql/issues/560", () => { charset: "alphabetic", }); - const query = ` + const query = /* GraphQL */ ` query { ${testLog.plural} { - id - location { - longitude - latitude - height - crs - srid - } + connection { + edges { + node { + id + location { + longitude + latitude + height + crs + srid + } + } + } + } } } `; @@ -66,20 +70,21 @@ describe("https://github.com/neo4j/graphql/issues/560", () => { `); const result = await testHelper.executeGraphQL(query); - - if (result.errors) { - console.log(JSON.stringify(result.errors, null, 2)); - } - expect(result.errors).toBeFalsy(); - expect(result.data as any).toEqual({ - [testLog.plural]: [ - { - id: logId, - location: null, + expect(result.data).toEqual({ + [testLog.plural]: { + connection: { + edges: [ + { + node: { + id: logId, + location: null, + }, + }, + ], }, - ], + }, }); }); @@ -87,7 +92,7 @@ describe("https://github.com/neo4j/graphql/issues/560", () => { const testLog = testHelper.createUniqueType("Log"); const typeDefs = gql` - type ${testLog.name} { + type ${testLog.name} @node { id: ID! location: CartesianPoint } @@ -99,17 +104,23 @@ describe("https://github.com/neo4j/graphql/issues/560", () => { charset: "alphabetic", }); - const query = ` + const query = /* GraphQL */ ` query { ${testLog.plural} { - id - location { - x - y - z - crs - srid - } + connection { + edges { + node { + id + location { + x + y + z + crs + srid + } + } + } + } } } `; @@ -120,19 +131,21 @@ describe("https://github.com/neo4j/graphql/issues/560", () => { const result = await testHelper.executeGraphQL(query); - if (result.errors) { - console.log(JSON.stringify(result.errors, null, 2)); - } - expect(result.errors).toBeFalsy(); - expect(result.data as any).toEqual({ - [testLog.plural]: [ - { - id: logId, - location: null, + expect(result.data).toEqual({ + [testLog.plural]: { + connection: { + edges: [ + { + node: { + id: logId, + location: null, + }, + }, + ], }, - ], + }, }); }); }); diff --git a/packages/graphql/tests/api-v6/integration/issues/582.int.test.ts b/packages/graphql/tests/api-v6/integration/issues/582.int.test.ts new file mode 100644 index 0000000000..b1d717783a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/issues/582.int.test.ts @@ -0,0 +1,168 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("https://github.com/neo4j/graphql/issues/582", () => { + const testHelper = new TestHelper({ v6Api: true }); + let type: UniqueType; + let typeDefs: string; + let query: string; + + beforeAll(async () => { + type = testHelper.createUniqueType("Entity"); + + typeDefs = ` + type ${type.name} @node { + children: [${type.name}!]! @relationship(type: "EDGE", properties: "Edge", direction: OUT) + parents: [${type.name}!]! @relationship(type: "EDGE", properties: "Edge", direction: IN) + type: String! + } + + type Edge @relationshipProperties { + type: String! + } + `; + + query = ` + query ($where: ${type.name}OperationWhere) { + ${type.plural}(where: $where) { + connection { + edges { + node { + type + } + } + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (:${type.name} { type: "Cat" })-[:EDGE]->(:${type.name} { type: "Dog" })<-[:EDGE]-(:${type.name} { type: "Bird" })-[:EDGE]->(:${type.name} { type: "Fish" }) + ` + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should get all Cats where there exists at least one child Dog that has a Bird parent", async () => { + const gqlResult = await testHelper.executeGraphQL(query, { + variableValues: { + where: { + node: { + type: { equals: "Cat" }, + children: { + some: { + edges: { + node: { + type: { equals: "Dog" }, + parents: { + some: { + edges: { + node: { + type: { equals: "Bird" }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [type.plural]: { + connection: { + edges: [ + { + node: { + type: "Cat", + }, + }, + ], + }, + }, + }); + }); + + test("should get all Cats where there exists at least one child Dog that has a Bird parent which has a Fish child", async () => { + const gqlResult = await testHelper.executeGraphQL(query, { + variableValues: { + where: { + node: { + type: { equals: "Cat" }, + children: { + some: { + edges: { + node: { + type: { equals: "Dog" }, + parents: { + some: { + edges: { + node: { + type: { equals: "Bird" }, + children: { + some: { + edges: { + node: { + type: { equals: "Fish" }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [type.plural]: { + connection: { + edges: [ + { + node: { + type: "Cat", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/create.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/create.int.test.ts new file mode 100644 index 0000000000..270daf0a76 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/create.int.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { Integer } from "neo4j-driver"; +import type { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Top-Level Create", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + released: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should create two movies", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { title: "The Matrix" } }, + { node: { title: "The Matrix 2", released: 2001 } } + ]) { + info { + nodesCreated + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + + const cypherMatch = await testHelper.executeCypher( + ` + MATCH (m:${Movie}) + RETURN m + `, + {} + ); + const records = cypherMatch.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.toIncludeSameMembers([ + { m: expect.objectContaining({ properties: { title: "The Matrix" } }) }, + { m: expect.objectContaining({ properties: { title: "The Matrix 2", released: new Integer(2001) } }) }, + ]) + ); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/array/number-array.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/array/number-array.int.test.ts new file mode 100644 index 0000000000..4f000628ca --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/array/number-array.int.test.ts @@ -0,0 +1,92 @@ +/* + * 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 { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("Create Nodes with Numeric array fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + year: [Int!]! + rating: [Float!]! + viewings: [BigInt!]! + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to create nodes with Numeric fields", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { + node: { + year: [1999, 2000], + rating: [4.0, 5.0], + viewings: ["4294967297", "5294967297"], + } + } + { + node: { + year: [2001, 2002], + rating: [4.2, 5.2], + viewings: ["194967297", "194967292"], + } + } + ]) { + ${Movie.plural} { + year + rating + viewings + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { + year: [1999, 2000], + rating: [4.0, 5.0], + viewings: ["4294967297", "5294967297"], + }, + { + year: [2001, 2002], + rating: [4.2, 5.2], + viewings: ["194967297", "194967292"], + }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/array/temporal-array.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/array/temporal-array.int.test.ts new file mode 100644 index 0000000000..4953b59a1b --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/array/temporal-array.int.test.ts @@ -0,0 +1,137 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("Create Nodes with Temporal array fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + date: [Date!]! + dateTime: [DateTime!]! + localTime: [LocalTime!]! + localDateTime: [LocalDateTime!]! + time: [Time!]! + duration: [Duration!]! + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to create nodes with Temporal fields", async () => { + const date1 = new Date(1716904582368); + const date2 = new Date(1736900000000); + // DATETIME + const neoDateTime1 = date1.toISOString(); + const neoDateTime2 = date1.toISOString(); + + // DATE + const neoDate1 = neo4jDriver.types.Date.fromStandardDate(date1); + const neoDate2 = neo4jDriver.types.Date.fromStandardDate(date2); + + // TIME + const neoTime1 = neo4jDriver.Time.fromStandardDate(date1); + const neoTime2 = neo4jDriver.Time.fromStandardDate(date2); + + // LOCALTIME + const neoLocalTime1 = neo4jDriver.LocalTime.fromStandardDate(date1); + const neoLocalTime2 = neo4jDriver.LocalTime.fromStandardDate(date2); + + // LOCALDATETIME + const neoLocalDateTime1 = neo4jDriver.LocalDateTime.fromStandardDate(date1); + const neoLocalDateTime2 = neo4jDriver.LocalDateTime.fromStandardDate(date2); + + // Duration + const duration1 = new neo4jDriver.Duration(1, 2, 3, 4); + const duration2 = new neo4jDriver.Duration(5, 6, 7, 8); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { + node: { + date: ["${neoDate1.toString()}", "${neoDate1.toString()}"], + dateTime: ["${neoDateTime1.toString()}", "${neoDateTime1.toString()}"] + localTime: ["${neoLocalTime1.toString()}", "${neoLocalTime1.toString()}"] + localDateTime: ["${neoLocalDateTime1.toString()}", "${neoLocalDateTime1.toString()}"] + time: ["${neoTime1.toString()}", "${neoTime1.toString()}"], + duration: ["${duration1.toString()}", "${duration1.toString()}"] + } + } + { + node: { + date: ["${neoDate2.toString()}", "${neoDate2.toString()}"], + dateTime: ["${neoDateTime2.toString()}", "${neoDateTime2.toString()}"] + localTime: ["${neoLocalTime2.toString()}", "${neoLocalTime2.toString()}"] + localDateTime: ["${neoLocalDateTime2.toString()}", "${neoLocalDateTime2.toString()}"] + time: ["${neoTime2.toString()}", "${neoTime2.toString()}"], + duration: ["${duration2.toString()}", "${duration2.toString()}"] + } + } + ]) { + ${Movie.plural} { + date + dateTime + localTime + localDateTime + time + duration + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { + date: [neoDate1.toString(), neoDate1.toString()], + dateTime: [neoDateTime1.toString(), neoDateTime1.toString()], + localTime: [neoLocalTime1.toString(), neoLocalTime1.toString()], + localDateTime: [neoLocalDateTime1.toString(), neoLocalDateTime1.toString()], + time: [neoTime1.toString(), neoTime1.toString()], + duration: [duration1.toString(), duration1.toString()], + }, + { + date: [neoDate2.toString(), neoDate2.toString()], + dateTime: [neoDateTime2.toString(), neoDateTime2.toString()], + localTime: [neoLocalTime2.toString(), neoLocalTime2.toString()], + localDateTime: [neoLocalDateTime2.toString(), neoLocalDateTime2.toString()], + time: [neoTime2.toString(), neoTime2.toString()], + duration: [duration2.toString(), duration2.toString()], + }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/cartesian-point/cartesian-point-2d.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/cartesian-point/cartesian-point-2d.int.test.ts new file mode 100644 index 0000000000..a8fe3d5884 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/cartesian-point/cartesian-point-2d.int.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("Create Nodes with CartesianPoint 2d", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + + const London = { x: -14221.955504767046, y: 6711533.711877272 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should create nodes with wgs-84-2d point fields", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Location.operations.create}(input: [ + { node: { id: "1", value: { x: ${London.x}, y: ${London.y} } } } + { node: { id: "2", value: { x: ${Rome.x}, y: ${Rome.y} } } } + ]) + { + ${Location.plural} { + id + value { + x + y + crs + srid + } + } + } + + + } + `; + + const mutationResult = await testHelper.executeGraphQL(mutation); + expect(mutationResult.errors).toBeFalsy(); + expect(mutationResult.data).toEqual({ + [Location.operations.create]: { + [Location.plural]: expect.toIncludeSameMembers([ + { + id: "1", + value: { + y: London.y, + x: London.x, + crs: "cartesian", + srid: 7203, + }, + }, + { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + crs: "cartesian", + srid: 7203, + }, + }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/cartesian-point/cartesian-point-3d.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/cartesian-point/cartesian-point-3d.int.test.ts new file mode 100644 index 0000000000..460efdf29e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/cartesian-point/cartesian-point-3d.int.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("Create Nodes with CartesianPoint 3d", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + + const London = { x: -14221.955504767046, y: 6711533.711877272, z: 0 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265, z: 0 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should create nodes with wgs-84-3d point fields", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Location.operations.create}(input: [ + { node: { id: "1", value: { x: ${London.x}, y: ${London.y}, z: ${London.z} } } } + { node: { id: "2", value: { x: ${Rome.x}, y: ${Rome.y}, z: ${Rome.z} } } } + ]) + { + ${Location.plural} { + id + value { + x + y + z + crs + srid + } + } + } + + + } + `; + + const mutationResult = await testHelper.executeGraphQL(mutation); + expect(mutationResult.errors).toBeFalsy(); + expect(mutationResult.data).toEqual({ + [Location.operations.create]: { + [Location.plural]: expect.toIncludeSameMembers([ + { + id: "1", + value: { + y: London.y, + x: London.x, + z: London.z, + crs: "cartesian-3d", + srid: 9157, + }, + }, + { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: Rome.z, + crs: "cartesian-3d", + srid: 9157, + }, + }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/date.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/date.int.test.ts new file mode 100644 index 0000000000..d0ec2aab34 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/date.int.test.ts @@ -0,0 +1,73 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Create Nodes with Date fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + date: Date + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should be able to create nodes with date fields", async () => { + const date1 = new Date(1716904582368); + const date2 = new Date(1736900000000); + const neoDate1 = neo4jDriver.types.Date.fromStandardDate(date1); + const neoDate2 = neo4jDriver.types.Date.fromStandardDate(date2); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { date: "${neoDate1.toString()}" } } + { node: { date: "${neoDate2.toString()}" } } + ]) { + ${Movie.plural} { + date + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { date: neoDate1.toString() }, + { date: neoDate2.toString() }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/dateTime.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/dateTime.int.test.ts new file mode 100644 index 0000000000..8488a9b0e6 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/dateTime.int.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Create Nodes with DateTime fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + datetime: DateTime + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should be able to create nodes with DateTime fields", async () => { + const date1 = new Date(1716904582368); + const date2 = new Date(1796904582368); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { datetime: "${date1.toISOString()}" } } + { node: { datetime: "${date2.toISOString()}" } } + ]) { + ${Movie.plural} { + datetime + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { datetime: date1.toISOString() }, + { datetime: date2.toISOString() }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/duration.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/duration.int.test.ts new file mode 100644 index 0000000000..fb192393a0 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/duration.int.test.ts @@ -0,0 +1,70 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Create Nodes with Duration fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + duration: Duration + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + test("should be able to create nodes with Duration fields", async () => { + const duration1 = new neo4jDriver.Duration(1, 2, 3, 4); + const duration2 = new neo4jDriver.Duration(5, 6, 7, 8); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { duration: "${duration1.toString()}" } } + { node: { duration: "${duration2.toString()}" } } + ]) { + ${Movie.plural} { + duration + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { duration: duration1.toString() }, + { duration: duration2.toString() }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/localDateTime.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/localDateTime.int.test.ts new file mode 100644 index 0000000000..90ed66fc60 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/localDateTime.int.test.ts @@ -0,0 +1,74 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Create Nodes with LocalDateTime fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + localDateTime: LocalDateTime + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should be able to create nodes with LocalDateTime fields", async () => { + const time1 = new Date("2024-02-17T11:49:48.322Z"); + const time2 = new Date("2025-02-17T12:49:48.322Z"); + + const neoTime1 = neo4jDriver.LocalDateTime.fromStandardDate(time1); + const neoTime2 = neo4jDriver.LocalDateTime.fromStandardDate(time2); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { localDateTime: "${neoTime1.toString()}" } } + { node: { localDateTime: "${neoTime2.toString()}" } } + ]) { + ${Movie.plural} { + localDateTime + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { localDateTime: neoTime1.toString() }, + { localDateTime: neoTime2.toString() }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/localTime.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/localTime.int.test.ts new file mode 100644 index 0000000000..d55965cc16 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/localTime.int.test.ts @@ -0,0 +1,74 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Create Nodes with LocalTime fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + localTime: LocalTime + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should be able to create nodes with LocalTime fields", async () => { + const time1 = new Date("2024-02-17T11:49:48.322Z"); + const time2 = new Date("2025-02-17T12:49:48.322Z"); + + const neoTime1 = neo4jDriver.LocalTime.fromStandardDate(time1); + const neoTime2 = neo4jDriver.LocalTime.fromStandardDate(time2); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { localTime: "${neoTime1.toString()}" } } + { node: { localTime: "${neoTime2.toString()}" } } + ]) { + ${Movie.plural} { + localTime + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { localTime: neoTime1.toString() }, + { localTime: neoTime2.toString() }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/number.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/number.int.test.ts new file mode 100644 index 0000000000..944f644541 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/number.int.test.ts @@ -0,0 +1,73 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Create Nodes with Numeric fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + year: Int! + rating: Float! + viewings: BigInt! + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to create nodes with numeric fields", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { rating: 9.3, viewings: "900000000000000", year: 2000 } }, + { node: { rating: 8, viewings: "50000000000000", year: 2001 } } + ]) { + ${Movie.plural} { + year + rating + viewings + } + + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { rating: 9.3, viewings: "900000000000000", year: 2000 }, + { rating: 8, viewings: "50000000000000", year: 2001 }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-2d.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-2d.int.test.ts new file mode 100644 index 0000000000..37db56a6ba --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-2d.int.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("Create Nodes with Point 2d", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should create nodes with wgs-84-2d point fields", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Location.operations.create}(input: [ + { node: { id: "1", value: { longitude: ${London.longitude}, latitude: ${London.latitude} } } } + { node: { id: "2", value: { longitude: ${Rome.longitude}, latitude: ${Rome.latitude} } } } + ]) + { + ${Location.plural} { + id + value { + latitude + longitude + height + crs + srid + } + } + } + } + `; + + const mutationResult = await testHelper.executeGraphQL(mutation); + + expect(mutationResult.errors).toBeFalsy(); + expect(mutationResult.data).toEqual({ + [Location.operations.create]: { + [Location.plural]: expect.toIncludeSameMembers([ + { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: null, + crs: "wgs-84", + srid: 4326, + }, + }, + { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: null, + crs: "wgs-84", + srid: 4326, + }, + }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-3d.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-3d.int.test.ts new file mode 100644 index 0000000000..697863a41a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-3d.int.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { UniqueType } from "../../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../../utils/tests-helper"; + +describe("Create Nodes with Point 3d", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351, height: 24 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782, height: 35 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should create nodes with wgs-84-3d point fields", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Location.operations.create}(input: [ + { node: { id: "1", value: { longitude: ${London.longitude}, latitude: ${London.latitude}, height: ${London.height} } } } + { node: { id: "2", value: { longitude: ${Rome.longitude}, latitude: ${Rome.latitude}, height: ${Rome.height} } } } + ]) + { + ${Location.plural} { + id + value { + latitude + longitude + height + crs + srid + } + } + } + + + } + `; + + const mutationResult = await testHelper.executeGraphQL(mutation); + expect(mutationResult.errors).toBeFalsy(); + expect(mutationResult.data).toEqual({ + [Location.operations.create]: { + [Location.plural]: expect.toIncludeSameMembers([ + { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: London.height, + crs: "wgs-84-3d", + srid: 4979, + }, + }, + { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: Rome.height, + crs: "wgs-84-3d", + srid: 4979, + }, + }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/create/types/time.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/time.int.test.ts new file mode 100644 index 0000000000..e3e622fd15 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/time.int.test.ts @@ -0,0 +1,74 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Create Node with Time", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + time: Time + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should return a movie created with a Time parameter", async () => { + const time1 = new Date("2024-02-17T11:49:48.322Z"); + const time2 = new Date("2025-02-17T12:49:48.322Z"); + + const neoTime1 = neo4jDriver.Time.fromStandardDate(time1); + const neoTime2 = neo4jDriver.Time.fromStandardDate(time2); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { time: "${neoTime1.toString()}" } } + { node: { time: "${neoTime2.toString()}" } } + ]) { + ${Movie.plural} { + time + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { time: neoTime1.toString() }, + { time: neoTime2.toString() }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/mutations/update/update.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/update/update.int.test.ts new file mode 100644 index 0000000000..a1ed5c6b45 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/update/update.int.test.ts @@ -0,0 +1,191 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Top-Level Update", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + released: Int + actors: [${Actor}!]! @relationship(direction: IN, type: "ACTED_IN") + } + + type ${Actor} @node { + name: String! + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should update a movie", async () => { + await testHelper.executeCypher(` + CREATE(n:${Movie} {title: "The Matrix"}) + CREATE(:${Movie} {title: "The Matrix 2"}) + `); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.update}( + where: { + node: { + title: { + equals: "The Matrix" + } + } + } + input: + { node: { title: { set: "Another Movie"} } } + ) { + info { + nodesCreated + } + ${Movie.plural} { + title + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.update]: { + info: { + nodesCreated: 0, + }, + [Movie.plural]: [ + { + title: "Another Movie", + }, + ], + }, + }); + + const cypherMatch = await testHelper.executeCypher( + ` + MATCH (m:${Movie}) + RETURN {title: m.title} as m + `, + {} + ); + const records = cypherMatch.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.toIncludeSameMembers([ + { + m: { + title: "The Matrix 2", + }, + }, + { + m: { + title: "Another Movie", + }, + }, + ]) + ); + }); + + test("should update a movies with nested filter", async () => { + await testHelper.executeCypher(` + CREATE(n:${Movie} {title: "The Matrix"})<-[:ACTED_IN]-(:${Actor} {name: "Keanu"}) + CREATE(:${Movie} {title: "The Matrix 2"})<-[:ACTED_IN]-(:${Actor} {name: "Uneak"}) + `); + + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.update}( + where: { + node: { + actors: { + some: { + edges: { + node: { + name: {equals: "Keanu"} + } + } + } + } + } + } + input: + { node: { title: { set: "Another Movie"} } } + ) { + info { + nodesCreated + } + ${Movie.plural} { + title + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.update]: { + info: { + nodesCreated: 0, + }, + [Movie.plural]: [ + { + title: "Another Movie", + }, + ], + }, + }); + + const cypherMatch = await testHelper.executeCypher( + ` + MATCH (m:${Movie}) + RETURN {title: m.title} as m + `, + {} + ); + const records = cypherMatch.records.map((record) => record.toObject()); + expect(records).toEqual( + expect.toIncludeSameMembers([ + { + m: { + title: "The Matrix 2", + }, + }, + { + m: { + title: "Another Movie", + }, + }, + ]) + ); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/pagination/after.int.test.ts b/packages/graphql/tests/api-v6/integration/pagination/after.int.test.ts new file mode 100644 index 0000000000..dcbb3c0002 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/pagination/after.int.test.ts @@ -0,0 +1,146 @@ +/* + * 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 { offsetToCursor } from "graphql-relay"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Pagination with after", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + ratings: Int! + description: String + } + type ${Actor} @node { + name: String + age: Int + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + role: String + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a:${Movie} {title: "The Matrix", description: "DVD edition", ratings: 5}) + CREATE (b:${Movie} {title: "The Matrix", description: "Cinema edition", ratings: 4}) + CREATE (c:${Movie} {title: "The Matrix 2", ratings: 2}) + CREATE (d:${Movie} {title: "The Matrix 3", ratings: 4}) + CREATE (e:${Movie} {title: "The Matrix 4", ratings: 3}) + CREATE (keanu:${Actor} {name: "Keanu", age: 55}) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(a) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(b) + CREATE (keanu)-[:ACTED_IN {year: 2001, role: "Mr. Anderson"}]->(c) + CREATE (keanu)-[:ACTED_IN {year: 2003, role: "Neo"}]->(d) + CREATE (keanu)-[:ACTED_IN {year: 2021, role: "Neo"}]->(e) + + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("Get movies with after argument", async () => { + const afterCursor = offsetToCursor(2); + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(after: "${afterCursor}") { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }); + }); + + test("Get nested actors with after argument", async () => { + const afterArgument = offsetToCursor(2); + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + movies { + connection(after: "${afterArgument}") { + edges { + node { + title + } + } + } + } + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + movies: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/pagination/first-after.int.test.ts b/packages/graphql/tests/api-v6/integration/pagination/first-after.int.test.ts new file mode 100644 index 0000000000..a9a65f7088 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/pagination/first-after.int.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { offsetToCursor } from "graphql-relay"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Pagination with first and after", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix 1"}) + CREATE (:${Movie} {title: "The Matrix 2"}) + CREATE (:${Movie} {title: "The Matrix 3"}) + CREATE (:${Movie} {title: "The Matrix 4"}) + CREATE (:${Movie} {title: "The Matrix 5"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("Get movies with first and after argument", async () => { + const afterCursor = offsetToCursor(4); + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(first: 1, after: "${afterCursor}", sort: { node: { title: ASC } }) { + edges { + node { + title + } + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 5", + }, + }, + ], + pageInfo: { + endCursor: offsetToCursor(5), + hasNextPage: false, + hasPreviousPage: true, + startCursor: offsetToCursor(5), + }, + }, + }, + }); + }); + + test.todo("Get movies and nested actors with first and after"); +}); diff --git a/packages/graphql/tests/api-v6/integration/pagination/first.int.test.ts b/packages/graphql/tests/api-v6/integration/pagination/first.int.test.ts new file mode 100644 index 0000000000..c349d970fd --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/pagination/first.int.test.ts @@ -0,0 +1,155 @@ +/* + * 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 { offsetToCursor } from "graphql-relay"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Pagination with first", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + ratings: Int! + description: String + } + type ${Actor} @node { + name: String + age: Int + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + role: String + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a:${Movie} {title: "The Matrix", description: "DVD edition", ratings: 5}) + CREATE (b:${Movie} {title: "The Matrix", description: "Cinema edition", ratings: 4}) + CREATE (c:${Movie} {title: "The Matrix 2", ratings: 2}) + CREATE (d:${Movie} {title: "The Matrix 3", ratings: 4}) + CREATE (e:${Movie} {title: "The Matrix 4", ratings: 3}) + CREATE (keanu:${Actor} {name: "Keanu", age: 55}) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(a) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(b) + CREATE (keanu)-[:ACTED_IN {year: 2001, role: "Mr. Anderson"}]->(c) + CREATE (keanu)-[:ACTED_IN {year: 2003, role: "Neo"}]->(d) + CREATE (keanu)-[:ACTED_IN {year: 2021, role: "Neo"}]->(e) + + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("Get movies with first argument", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(first: 3) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toBeArrayOfSize(3), + pageInfo: { + endCursor: offsetToCursor(2), + hasNextPage: true, + hasPreviousPage: false, + startCursor: offsetToCursor(0), + }, + }, + }, + }); + }); + + test("Get nested actors with first argument", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + movies { + connection(first: 3) { + edges { + node { + title + } + } + } + } + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + movies: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/pagination/page-info.int.test.ts b/packages/graphql/tests/api-v6/integration/pagination/page-info.int.test.ts new file mode 100644 index 0000000000..2d8b389c53 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/pagination/page-info.int.test.ts @@ -0,0 +1,177 @@ +/* + * 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 { offsetToCursor } from "graphql-relay"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("PageInfo", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix 1"}) + CREATE (m:${Movie} {title: "The Matrix 2"}) + CREATE (:${Actor} { name: "Keanu" })-[:ACTED_IN]->(m) + CREATE (:${Actor} { name: "Carrie" })-[:ACTED_IN]->(m) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("Get pageInfo and cursor information on top level connection", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + pageInfo: { + hasPreviousPage: false, + hasNextPage: false, + startCursor: offsetToCursor(0), + endCursor: offsetToCursor(1), + }, + edges: [ + { + cursor: offsetToCursor(0), + }, + { + cursor: offsetToCursor(1), + }, + ], + }, + }, + }); + }); + + test("Get pageInfo and cursor information on nested level connection", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + actors { + connection { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + cursor + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + actors: { + connection: { + edges: [], + pageInfo: { + endCursor: null, + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + }, + }, + }, + }, + }, + { + node: { + actors: { + connection: { + edges: [ + { + cursor: offsetToCursor(0), + }, + { + cursor: offsetToCursor(1), + }, + ], + pageInfo: { + endCursor: offsetToCursor(1), + hasNextPage: false, + hasPreviousPage: false, + startCursor: offsetToCursor(0), + }, + }, + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts new file mode 100644 index 0000000000..fd3189df91 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.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 { offsetToCursor } from "graphql-relay"; +import type { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Query aliasing", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeEach(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = ` + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(direction: OUT, type: "DIRECTED", properties: "Directed") + } + + type Directed @relationshipProperties { + year: Int! + } + + type ${Movie} @node { + id: ID! + title: String + actors: [${Actor}!]! @relationship(direction: IN, type: "DIRECTED", properties: "Directed") + } + `; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + }); + + await testHelper.executeCypher( + ` + CREATE(m:${Movie} { title: "The Matrix", id: "1" })<-[:DIRECTED {year: 1999}]-(a:${Actor} {name: "Keanu"}) + CREATE(m2:${Movie} { title: "The Matrix Reloaded", id: "2" })<-[:DIRECTED {year: 2001}]-(a) + ` + ); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should be able to get a Movie with related actors and relationship properties with aliased fields", async () => { + const query = /* GraphQL */ ` + query { + myMovies: ${Movie.plural} { + c: connection { + e: edges { + n: node { + name: title + uid: id, + a: actors { + nc: connection { + ne: edges { + nn: node { + nodeName: name + }, + np: properties { + y:year + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + myMovies: { + c: { + e: expect.toIncludeSameMembers([ + { + n: { + name: "The Matrix", + uid: "1", + a: { + nc: { + ne: [ + { + nn: { nodeName: "Keanu" }, + np: { y: 1999 }, + }, + ], + }, + }, + }, + }, + { + n: { + name: "The Matrix Reloaded", + uid: "2", + a: { + nc: { + ne: [ + { + nn: { nodeName: "Keanu" }, + np: { y: 2001 }, + }, + ], + }, + }, + }, + }, + ]), + }, + }, + }); + }); + + test("should allow multiple aliases on the same connection", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + first: movies(where: { edges: { node: { id: {equals: "1" } } } }) { + connection { + edges { + node { + title + } + } + } + } + second: movies(where: { edges: { node: { id: {equals: "2" } } } }) { + connection { + edges { + node { + titleAlias: title + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + first: { + connection: { + edges: [{ node: { title: "The Matrix" } }], + }, + }, + second: { + connection: { + edges: [{ node: { titleAlias: "The Matrix Reloaded" } }], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should allow multiple aliases on relationship properties", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + movies { + connection { + edges { + properties { + year1: year + year2: year + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + movies: { + connection: { + edges: expect.arrayContaining([ + { + properties: { + year1: 2001, + year2: 2001, + }, + }, + { + properties: { + year1: 1999, + year2: 1999, + }, + }, + ]), + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("Should alias pageInfo and cursor", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + info: pageInfo { + previous: hasPreviousPage + next: hasNextPage + start: startCursor + end: endCursor + } + info2: pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + edges { + c: cursor + c2: cursor + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + info: { + previous: false, + next: false, + start: offsetToCursor(0), + end: offsetToCursor(1), + }, + info2: { + hasPreviousPage: false, + hasNextPage: false, + startCursor: offsetToCursor(0), + endCursor: offsetToCursor(1), + }, + edges: [ + { + c: offsetToCursor(0), + c2: offsetToCursor(0), + }, + { + c: offsetToCursor(1), + c2: offsetToCursor(1), + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/create/create.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/create/create.int.test.ts new file mode 100644 index 0000000000..a4d0d91564 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/create/create.int.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Top-Level Create projection", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + released: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should create two movies and project them", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { title: "The Matrix" } }, + { node: { title: "The Matrix 2", released: 2001 } } + ]) { + ${Movie.plural} { + title + released + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { title: "The Matrix", released: null }, + { title: "The Matrix 2", released: 2001 }, + ]), + }, + }); + }); + + test("should create two movies and project the info object", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { title: "The Matrix" } }, + { node: { title: "The Matrix 2", released: 2001 } } + ]) { + info { + nodesCreated + relationshipsCreated + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(mutation); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + info: { nodesCreated: 2, relationshipsCreated: 0 }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/empty.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/empty.int.test.ts new file mode 100644 index 0000000000..2a396bf67f --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/empty.int.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Simple Query with empty results", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to get a Movie", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + title + } + cursor + } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [], + pageInfo: { + hasPreviousPage: false, + hasNextPage: false, + startCursor: null, + endCursor: null, + }, + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/relationship.int.test.ts new file mode 100644 index 0000000000..d9c73ea786 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/relationship.int.test.ts @@ -0,0 +1,168 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Relationship simple query", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actors"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type ${Actor} @node { + name: String + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher( + ` + CREATE (movie:${Movie} {title: "The Matrix"})<-[:ACTED_IN {year: $year}]-(a:${Actor} {name: "Keanu"}) + `, + { + year: 1999, + } + ); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to get a Movie with related actors", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + title + actors { + connection { + edges { + node { + name + }, + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + actors: { + connection: { + edges: [ + { + node: { name: "Keanu" }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to get a Movie with related actors and relationship properties", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + title + actors { + connection { + edges { + node { + name + }, + properties { + year + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + actors: { + connection: { + edges: [ + { + node: { name: "Keanu" }, + properties: { year: 1999 }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/simple-query.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/simple-query.int.test.ts new file mode 100644 index 0000000000..454875e972 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/simple-query.int.test.ts @@ -0,0 +1,78 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Simple Query", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (movie:${Movie} {title: "The Matrix"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to get a Movie", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/types/array/number-array.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/types/array/number-array.int.test.ts new file mode 100644 index 0000000000..fe5b05c4b5 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/types/array/number-array.int.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Numeric array fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + year: [Int!]! + rating: [Float!]! + viewings: [BigInt!]! + + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (movie:${Movie} { + year: [1999], + rating: [4.0], + viewings: [4294967297] + }) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to get int and float fields", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + year + viewings + rating + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + year: [1999], + rating: [4.0], + viewings: ["4294967297"], + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/types/cartesian-point/cartesian-point-2d.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/types/cartesian-point/cartesian-point-2d.int.test.ts new file mode 100644 index 0000000000..36101dcbe2 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/types/cartesian-point/cartesian-point-2d.int.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("CartesianPoint 2d", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 } as const; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + y + x + z + crs + srid + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: null, + crs: "cartesian", + srid: 7203, + }, + }, + }, + { + node: { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: null, + crs: "cartesian", + srid: 7203, + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/types/cartesian-point/cartesian-point-3d.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/types/cartesian-point/cartesian-point-3d.int.test.ts new file mode 100644 index 0000000000..dc9f1a21ee --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/types/cartesian-point/cartesian-point-3d.int.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("CartesianPoint 3d", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + + const London = { x: -14221.955504767046, y: 6711533.711877272, z: 0 }; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265, z: 0 }; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: CartesianPoint! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-3d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + y + x + z + crs + srid + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + id: "1", + value: { + y: London.y, + x: London.x, + z: London.z, + crs: "cartesian-3d", + srid: 9157, + }, + }, + }, + { + node: { + id: "2", + value: { + y: Rome.y, + x: Rome.x, + z: Rome.z, + crs: "cartesian-3d", + srid: 9157, + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/types/datetime.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/types/datetime.int.test.ts new file mode 100644 index 0000000000..590ff22e25 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/types/datetime.int.test.ts @@ -0,0 +1,131 @@ +/* + * 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 neo4jDriver from "neo4j-driver"; +import type { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("DateTime", () => { + const testHelper = new TestHelper({ v6Api: true }); + let Movie: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("should return a movie created with a datetime parameter", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + datetime: DateTime + } + `; + + const date = new Date(1716904582368); + + const nDateTime = neo4jDriver.types.DateTime.fromStandardDate(date); + + await testHelper.executeCypher( + ` + CREATE (m:${Movie.name}) + SET m.datetime = $nDateTime + `, + { nDateTime } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + datetime + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect((gqlResult.data as any)[Movie.plural]).toEqual({ + connection: { + edges: [ + { + node: { + datetime: date.toISOString(), + }, + }, + ], + }, + }); + }); + + test("should return a movie created with a datetime with timezone", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + datetime: DateTime + } + `; + + const date = new Date(1716904582368); + await testHelper.executeCypher(` + CREATE (m:${Movie.name}) + SET m.datetime = datetime("${date.toISOString().replace("Z", "[Etc/UTC]")}") + `); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + datetime + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + + expect(gqlResult.errors).toBeFalsy(); + expect((gqlResult.data as any)[Movie.plural]).toEqual({ + connection: { + edges: [ + { + node: { + datetime: date.toISOString(), + }, + }, + ], + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/types/number.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/types/number.int.test.ts new file mode 100644 index 0000000000..9ffea3739c --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/types/number.int.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { UniqueType } from "../../../../utils/graphql-types"; +import { TestHelper } from "../../../../utils/tests-helper"; + +describe("Numeric fields", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + year: Int! + rating: Float! + viewings: BigInt! + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (movie:${Movie} {year: 1999, rating: 4.0, viewings: 4294967297 }) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to get int and float fields", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + year + rating + viewings + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + year: 1999, + rating: 4.0, + viewings: "4294967297", + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/types/point/point-2d.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/types/point/point-2d.int.test.ts new file mode 100644 index 0000000000..f4e7d3feda --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/types/point/point-2d.int.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Point 2d", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 } as const; + const Rome = { longitude: 12.496365, latitude: 41.902782 } as const; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-2d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + srid + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: null, + crs: "wgs-84", + srid: 4326, + }, + }, + }, + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: null, + crs: "wgs-84", + srid: 4326, + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/projection/types/point/point-3d.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/types/point/point-3d.int.test.ts new file mode 100644 index 0000000000..149a5c44e7 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/types/point/point-3d.int.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Point 3d", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351, height: 24 }; + const Rome = { longitude: 12.496365, latitude: 41.902782, height: 35 }; + + beforeEach(async () => { + Location = testHelper.createUniqueType("Location"); + + const typeDefs = /* GraphQL */ ` + type ${Location} @node { + id: ID! + value: Point! + } + `; + await testHelper.executeCypher( + ` + CREATE (:${Location} { id: "1", value: point($London)}) + CREATE (:${Location} { id: "2", value: point($Rome)}) + `, + { London, Rome } + ); + await testHelper.initNeo4jGraphQL({ typeDefs }); + }); + + afterEach(async () => { + await testHelper.close(); + }); + + test("wgs-84-3d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + srid + } + } + } + } + + } + } + `; + + const queryResult = await testHelper.executeGraphQL(query); + + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ + [Location.plural]: { + connection: { + edges: expect.toIncludeSameMembers([ + { + node: { + id: "1", + value: { + latitude: London.latitude, + longitude: London.longitude, + height: London.height, + crs: "wgs-84-3d", + srid: 4979, + }, + }, + }, + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: Rome.height, + crs: "wgs-84-3d", + srid: 4979, + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/sort/sort-filter.int.test.ts b/packages/graphql/tests/api-v6/integration/sort/sort-filter.int.test.ts new file mode 100644 index 0000000000..89886a55bb --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/sort/sort-filter.int.test.ts @@ -0,0 +1,263 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Sort with filter", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + ratings: Int! + description: String + } + type ${Actor} @node { + name: String + age: Int + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + role: String + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a:${Movie} {title: "The Matrix", description: "DVD edition", ratings: 5}) + CREATE (b:${Movie} {title: "The Matrix", description: "Cinema edition", ratings: 4}) + CREATE (c:${Movie} {title: "The Matrix 2", ratings: 2}) + CREATE (d:${Movie} {title: "The Matrix 3", ratings: 4}) + CREATE (e:${Movie} {title: "The Matrix 4", ratings: 3}) + CREATE (keanu:${Actor} {name: "Keanu", age: 55}) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(a) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(b) + CREATE (keanu)-[:ACTED_IN {year: 2001, role: "Mr. Anderson"}]->(c) + CREATE (keanu)-[:ACTED_IN {year: 2003, role: "Neo"}]->(d) + CREATE (keanu)-[:ACTED_IN {year: 2021, role: "Neo"}]->(e) + + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("filter and sort by ASC order and return filtered properties", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { title: { in: ["The Matrix 2", "The Matrix 4"] } } }) { + connection(sort: { node: { title: ASC } }) { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix 4", + }, + }, + ], + }, + }, + }); + }); + + test("filter and sort by DESC order and return filtered properties", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { node: { title: { in: ["The Matrix 2", "The Matrix 4"] } } }) { + connection(sort: { node: { title: DESC } }) { + edges { + node { + title + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 4", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }); + }); + + test("filter and sort nested by ASC order", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies( + where: { edges: { node: { title: { in: ["The Matrix 2", "The Matrix 3"] } } } } + ) { + connection(sort: { edges: { node: { title: ASC } } }) { + edges { + node { + title + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("filter and sort nested by DESC order", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies( + where: { edges: { node: { title: { in: ["The Matrix 2", "The Matrix 3"] } } } } + ) { + connection(sort: { edges: { node: { title: DESC } } }) { + edges { + node { + title + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/sort/sort-relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/sort/sort-relationship.int.test.ts new file mode 100644 index 0000000000..f407dc0ee4 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/sort/sort-relationship.int.test.ts @@ -0,0 +1,478 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Sort relationship", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + let Actor: UniqueType; + + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + ratings: Int! + description: String + } + type ${Actor} @node { + name: String + age: Int + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + role: String + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (a:${Movie} {title: "The Matrix", description: "DVD edition", ratings: 5}) + CREATE (b:${Movie} {title: "The Matrix", description: "Cinema edition", ratings: 4}) + CREATE (c:${Movie} {title: "The Matrix 2", ratings: 2}) + CREATE (d:${Movie} {title: "The Matrix 3", ratings: 4}) + CREATE (e:${Movie} {title: "The Matrix 4", ratings: 3}) + CREATE (keanu:${Actor} {name: "Keanu", age: 55}) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(a) + CREATE (keanu)-[:ACTED_IN {year: 1999, role: "Neo"}]->(b) + CREATE (keanu)-[:ACTED_IN {year: 2001, role: "Mr. Anderson"}]->(c) + CREATE (keanu)-[:ACTED_IN {year: 2003, role: "Neo"}]->(d) + CREATE (keanu)-[:ACTED_IN {year: 2021, role: "Neo"}]->(e) + + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to sort by ASC order", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: { edges: { node: { title: ASC } } }) { + edges { + node { + title + } + } + + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 4", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by DESC order", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: { edges: { node: { title: DESC } } }) { + edges { + node { + title + } + } + + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix 4", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by multiple criteria", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: [{edges: { node: { title: ASC } }}, {edges: { node: { ratings: DESC } }}]) { + edges { + node { + title + description + ratings + } + } + + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + description: "DVD edition", + ratings: 5, + }, + }, + { + node: { + title: "The Matrix", + description: "Cinema edition", + ratings: 4, + }, + }, + + { + node: { + title: "The Matrix 2", + description: null, + ratings: 2, + }, + }, + { + node: { + title: "The Matrix 3", + description: null, + ratings: 4, + }, + }, + { + node: { + title: "The Matrix 4", + description: null, + ratings: 3, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by relationship properties", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: [{edges:{ properties: { role: DESC } }}, {edges:{ properties: { year: ASC } }} ]) { + edges { + properties { + year + role + } + node { + title + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + properties: { year: 1999, role: "Neo" }, + node: { + title: "The Matrix", + }, + }, + { + properties: { year: 1999, role: "Neo" }, + node: { + title: "The Matrix", + }, + }, + { + properties: { year: 2003, role: "Neo" }, + node: { + title: "The Matrix 3", + }, + }, + { + properties: { year: 2021, role: "Neo" }, + node: { + title: "The Matrix 4", + }, + }, + { + properties: { year: 2001, role: "Mr. Anderson" }, + node: { + title: "The Matrix 2", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by relationship properties and node properties", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + connection { + edges { + node { + name + movies { + connection(sort: [ + {edges: { properties: { role: DESC } }}, + {edges: { node: { title: ASC } }}, + {edges: { properties: { year: DESC } }}, + {edges: { node: { description: ASC } }} + + ]) { + edges { + properties { + year + role + } + node { + title + description + } + } + } + } + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Actor.plural]: { + connection: { + edges: [ + { + node: { + name: "Keanu", + movies: { + connection: { + edges: [ + { + properties: { year: 1999, role: "Neo" }, + node: { + title: "The Matrix", + description: "Cinema edition", + }, + }, + { + properties: { year: 1999, role: "Neo" }, + node: { + title: "The Matrix", + description: "DVD edition", + }, + }, + + { + properties: { year: 2003, role: "Neo" }, + node: { + title: "The Matrix 3", + description: null, + }, + }, + { + properties: { year: 2021, role: "Neo" }, + node: { + title: "The Matrix 4", + description: null, + }, + }, + { + properties: { year: 2001, role: "Mr. Anderson" }, + node: { + title: "The Matrix 2", + description: null, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts b/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts new file mode 100644 index 0000000000..86bf34987e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts @@ -0,0 +1,217 @@ +/* + * 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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Sort", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + title: String! + ratings: Int! + description: String + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {title: "The Matrix", description: "DVD edition", ratings: 5}) + CREATE (:${Movie} {title: "The Matrix", description: "Cinema edition", ratings: 4}) + CREATE (:${Movie} {title: "The Matrix 2", ratings: 2}) + CREATE (:${Movie} {title: "The Matrix 3", ratings: 4}) + CREATE (:${Movie} {title: "The Matrix 4", ratings: 3}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to sort by ASC order", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(sort: { node: { title: ASC } }) { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 4", + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by DESC order", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(sort: { node: { title: DESC } }) { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix 4", + }, + }, + { + node: { + title: "The Matrix 3", + }, + }, + { + node: { + title: "The Matrix 2", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + { + node: { + title: "The Matrix", + }, + }, + ], + }, + }, + }); + }); + + test("should be able to sort by multiple criteria, not in the selection set", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(sort: [{ node: { title: ASC } }, { node: { ratings: DESC } }] ) { + edges { + node { + title + description + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + title: "The Matrix", + description: "DVD edition", + }, + }, + { + node: { + title: "The Matrix", + description: "Cinema edition", + }, + }, + + { + node: { + title: "The Matrix 2", + description: null, + }, + }, + { + node: { + title: "The Matrix 3", + description: null, + }, + }, + { + node: { + title: "The Matrix 4", + description: null, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts b/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts new file mode 100644 index 0000000000..f5d723bf6f --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts @@ -0,0 +1,224 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("@default on array fields", () => { + test("@default should add a default value in mutation inputs", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + id: [ID!]! @default(value: ["id"]) + title: [String!] @default(value: ["title"]) + year: [Int!] @default(value: [2021]) + length: [Float!] @default(value: [120.5]) + releasedDateTime: [DateTime!] @default(value: ["2021-01-01T00:00:00"]) + flags: [Boolean!] @default(value: [true, false]) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + input BooleanWhere { + AND: [BooleanWhere!] + NOT: BooleanWhere + OR: [BooleanWhere!] + equals: Boolean + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeListWhere { + equals: [DateTime!] + } + + input DateTimeUpdate { + set: DateTime + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + input FloatListWhere { + equals: [Float!] + } + + input FloatUpdate { + set: Float + } + + input IDListWhere { + equals: [ID!] + } + + input IDUpdate { + set: ID + } + + input IntListWhere { + equals: [Int!] + } + + input IntUpdate { + set: Int + } + + type Movie { + flags: [Boolean!] + id: [ID!]! + length: [Float!] + releasedDateTime: [DateTime!] + title: [String!] + year: [Int!] + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + type MovieCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + flags: [Boolean!] = [true, false] + id: [ID!]! = [\\"id\\"] + length: [Float!] = [120.5] + releasedDateTime: [DateTime!] = [\\"2021-01-01T00:00:00.000Z\\"] + title: [String!] = [\\"title\\"] + year: [Int!] = [2021] + } + + type MovieCreateResponse { + info: CreateInfo + movies: [Movie!]! + } + + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + flags: IntUpdate + id: IDUpdate + length: FloatUpdate + releasedDateTime: DateTimeUpdate + title: StringUpdate + year: IntUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + flags: BooleanWhere + id: IDListWhere + length: FloatListWhere + releasedDateTime: DateTimeListWhere + title: StringListWhere + year: IntListWhere + } + + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + input StringListWhere { + equals: [String!] + } + + input StringUpdate { + set: String + }" + `); + }); + test.todo("@default directive with relationship properties"); + test.todo("@default directive with user defined scalars"); +}); diff --git a/packages/graphql/tests/api-v6/schema/directives/default.test.ts b/packages/graphql/tests/api-v6/schema/directives/default.test.ts new file mode 100644 index 0000000000..9f1444d36e --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -0,0 +1,281 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("@default on fields", () => { + test("@default should add a default value in mutation inputs", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + id: ID! @default(value: "id") + title: String @default(value: "title") + year: Int @default(value: 2021) + length: Float @default(value: 120.5) + flag: Boolean @default(value: true) + releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + input BooleanWhere { + AND: [BooleanWhere!] + NOT: BooleanWhere + OR: [BooleanWhere!] + equals: Boolean + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeUpdate { + set: DateTime + } + + input DateTimeWhere { + AND: [DateTimeWhere!] + NOT: DateTimeWhere + OR: [DateTimeWhere!] + equals: DateTime + gt: DateTime + gte: DateTime + in: [DateTime!] + lt: DateTime + lte: DateTime + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + input FloatUpdate { + set: Float + } + + input FloatWhere { + AND: [FloatWhere!] + NOT: FloatWhere + OR: [FloatWhere!] + equals: Float + gt: Float + gte: Float + in: [Float!] + lt: Float + lte: Float + } + + input IDUpdate { + set: ID + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + input IntUpdate { + set: Int + } + + input IntWhere { + AND: [IntWhere!] + NOT: IntWhere + OR: [IntWhere!] + equals: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + } + + type Movie { + flag: Boolean + id: ID! + length: Float + releasedDateTime: DateTime + title: String + year: Int + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + flag: Boolean = true + id: ID! = \\"id\\" + length: Float = 120.5 + releasedDateTime: DateTime = \\"2021-01-01T00:00:00.000Z\\" + title: String = \\"title\\" + year: Int = 2021 + } + + type MovieCreateResponse { + info: CreateInfo + movies: [Movie!]! + } + + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieSort { + flag: SortDirection + id: SortDirection + length: SortDirection + releasedDateTime: SortDirection + title: SortDirection + year: SortDirection + } + + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + flag: IntUpdate + id: IDUpdate + length: FloatUpdate + releasedDateTime: DateTimeUpdate + title: StringUpdate + year: IntUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + flag: BooleanWhere + id: IDWhere + length: FloatWhere + releasedDateTime: DateTimeWhere + title: StringWhere + year: IntWhere + } + + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringUpdate { + set: String + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); + + test.todo("@default directive with relationship properties"); + test.todo("@default directive with user defined scalars"); +}); diff --git a/packages/graphql/tests/api-v6/schema/directives/id.test.ts b/packages/graphql/tests/api-v6/schema/directives/id.test.ts new file mode 100644 index 0000000000..59bd9ad6d8 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/id.test.ts @@ -0,0 +1,460 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("@id", () => { + test("should exclude @id marked field from Create and Update input", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + id: ID! @id + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + id: ID! @id + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + id: ID! @id + year: Int + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + type ActedIn { + id: ID! + year: Int + } + + input ActedInSort { + id: SortDirection + year: SortDirection + } + + input ActedInWhere { + AND: [ActedInWhere!] + NOT: ActedInWhere + OR: [ActedInWhere!] + id: IDWhere + year: IntWhere + } + + type Actor { + id: ID! + movies(where: ActorMoviesOperationWhere): ActorMoviesOperation + name: String + } + + type ActorConnection { + edges: [ActorEdge] + pageInfo: PageInfo + } + + input ActorConnectionSort { + node: ActorSort + } + + type ActorCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input ActorCreateInput { + node: ActorCreateNode! + } + + input ActorCreateNode { + name: String + } + + type ActorCreateResponse { + actors: [Actor!]! + info: CreateInfo + } + + input ActorDeleteInput { + node: ActorDeleteNode + } + + input ActorDeleteNode { + movies: ActorMoviesDeleteOperation + } + + type ActorEdge { + cursor: String + node: Actor + } + + type ActorMoviesConnection { + edges: [ActorMoviesEdge] + pageInfo: PageInfo + } + + input ActorMoviesConnectionSort { + edges: ActorMoviesEdgeSort + } + + input ActorMoviesDeleteInput { + input: MovieDeleteInput + where: ActorMoviesOperationWhere + } + + input ActorMoviesDeleteOperation { + delete: ActorMoviesDeleteInput + } + + type ActorMoviesEdge { + cursor: String + node: Movie + properties: ActedIn + } + + input ActorMoviesEdgeListWhere { + AND: [ActorMoviesEdgeListWhere!] + NOT: ActorMoviesEdgeListWhere + OR: [ActorMoviesEdgeListWhere!] + edges: ActorMoviesEdgeWhere + } + + input ActorMoviesEdgeSort { + node: MovieSort + properties: ActedInSort + } + + input ActorMoviesEdgeWhere { + AND: [ActorMoviesEdgeWhere!] + NOT: ActorMoviesEdgeWhere + OR: [ActorMoviesEdgeWhere!] + node: MovieWhere + properties: ActedInWhere + } + + input ActorMoviesNestedOperationWhere { + AND: [ActorMoviesNestedOperationWhere!] + NOT: ActorMoviesNestedOperationWhere + OR: [ActorMoviesNestedOperationWhere!] + all: ActorMoviesEdgeListWhere + none: ActorMoviesEdgeListWhere + single: ActorMoviesEdgeListWhere + some: ActorMoviesEdgeListWhere + } + + type ActorMoviesOperation { + connection(after: String, first: Int, sort: [ActorMoviesConnectionSort!]): ActorMoviesConnection + } + + input ActorMoviesOperationWhere { + AND: [ActorMoviesOperationWhere!] + NOT: ActorMoviesOperationWhere + OR: [ActorMoviesOperationWhere!] + edges: ActorMoviesEdgeWhere + } + + type ActorOperation { + connection(after: String, first: Int, sort: [ActorConnectionSort!]): ActorConnection + } + + input ActorOperationWhere { + AND: [ActorOperationWhere!] + NOT: ActorOperationWhere + OR: [ActorOperationWhere!] + node: ActorWhere + } + + input ActorSort { + id: SortDirection + name: SortDirection + } + + input ActorUpdateInput { + node: ActorUpdateNode! + } + + input ActorUpdateNode { + id: IDUpdate + name: StringUpdate + } + + type ActorUpdateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + + input ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + id: IDWhere + movies: ActorMoviesNestedOperationWhere + name: StringWhere + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + input IDUpdate { + set: ID + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + input IntWhere { + AND: [IntWhere!] + NOT: IntWhere + OR: [IntWhere!] + equals: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + } + + type Movie { + actors(where: MovieActorsOperationWhere): MovieActorsOperation + id: ID! + title: String + } + + type MovieActorsConnection { + edges: [MovieActorsEdge] + pageInfo: PageInfo + } + + input MovieActorsConnectionSort { + edges: MovieActorsEdgeSort + } + + input MovieActorsDeleteInput { + input: ActorDeleteInput + where: MovieActorsOperationWhere + } + + input MovieActorsDeleteOperation { + delete: MovieActorsDeleteInput + } + + type MovieActorsEdge { + cursor: String + node: Actor + properties: ActedIn + } + + input MovieActorsEdgeListWhere { + AND: [MovieActorsEdgeListWhere!] + NOT: MovieActorsEdgeListWhere + OR: [MovieActorsEdgeListWhere!] + edges: MovieActorsEdgeWhere + } + + input MovieActorsEdgeSort { + node: ActorSort + properties: ActedInSort + } + + input MovieActorsEdgeWhere { + AND: [MovieActorsEdgeWhere!] + NOT: MovieActorsEdgeWhere + OR: [MovieActorsEdgeWhere!] + node: ActorWhere + properties: ActedInWhere + } + + input MovieActorsNestedOperationWhere { + AND: [MovieActorsNestedOperationWhere!] + NOT: MovieActorsNestedOperationWhere + OR: [MovieActorsNestedOperationWhere!] + all: MovieActorsEdgeListWhere + none: MovieActorsEdgeListWhere + single: MovieActorsEdgeListWhere + some: MovieActorsEdgeListWhere + } + + type MovieActorsOperation { + connection(after: String, first: Int, sort: [MovieActorsConnectionSort!]): MovieActorsConnection + } + + input MovieActorsOperationWhere { + AND: [MovieActorsOperationWhere!] + NOT: MovieActorsOperationWhere + OR: [MovieActorsOperationWhere!] + edges: MovieActorsEdgeWhere + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + title: String + } + + type MovieCreateResponse { + info: CreateInfo + movies: [Movie!]! + } + + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + actors: MovieActorsDeleteOperation + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieSort { + id: SortDirection + title: SortDirection + } + + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + id: IDUpdate + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + actors: MovieActorsNestedOperationWhere + id: IDWhere + title: StringWhere + } + + type Mutation { + createActors(input: [ActorCreateInput!]!): ActorCreateResponse + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteActors(input: ActorDeleteInput, where: ActorOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse + updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + actors(where: ActorOperationWhere): ActorOperation + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringUpdate { + set: String + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts new file mode 100644 index 0000000000..11f823a1e8 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -0,0 +1,208 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("RelayId", () => { + test("relayId in a field", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + dbId: ID! @relayId + title: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + input GlobalIdWhere { + equals: String + } + + input IDUpdate { + set: ID + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + type Movie implements Node { + dbId: ID! + id: ID! + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + dbId: ID! + title: String + } + + type MovieCreateResponse { + info: CreateInfo + movies: [Movie!]! + } + + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieSort { + dbId: SortDirection + title: SortDirection + } + + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + dbId: IDUpdate + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + dbId: IDWhere + id: GlobalIdWhere + title: StringWhere + } + + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse + } + + interface Node { + id: ID! + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + \\"\\"\\"Fetches an object given its ID\\"\\"\\" + node(id: ID!): Node + } + + enum SortDirection { + ASC + DESC + } + + input StringUpdate { + set: String + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage-on-list-fields.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage-on-list-fields.test.ts new file mode 100644 index 0000000000..b26850ef65 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage-on-list-fields.test.ts @@ -0,0 +1,130 @@ +/* + * 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 { GraphQLError } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("invalid @default usage on List fields", () => { + test("@default should fail without a defined value", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type User @node { + name: [String!] @default + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([ + new GraphQLError( + 'Directive "@default" argument "value" of type "ScalarOrEnum!" is required, but it was not provided.' + ), + ]); + }); + + test.each([ + { + dataType: "[ID!]", + value: [1.2], + errorMsg: "@default.value on ID list fields must be a list of ID values", + }, + { + dataType: "[String!]", + value: [1.2], + errorMsg: "@default.value on String list fields must be a list of String values", + }, + { + dataType: "[Boolean!]", + value: [1.2], + errorMsg: "@default.value on Boolean list fields must be a list of Boolean values", + }, + { dataType: "[Int!]", value: 1.2, errorMsg: "@default.value on Int list fields must be a list of Int values" }, + { + dataType: "[Float!]", + value: ["stuff"], + errorMsg: "@default.value on Float list fields must be a list of Float values", + }, + { + dataType: "[DateTime!]", + value: ["dummy"], + errorMsg: "@default.value on DateTime list fields must be a list of DateTime values", + }, + ] as const)( + "@default should fail with an invalid $dataType value", + async ({ dataType, value: value, errorMsg }) => { + const stringValue = typeof value === "string" ? `"${value}"` : value; + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${stringValue}) + } + extend type User { + anotherField: ${dataType} @default(value: ${stringValue}) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).rejects.toEqual([new GraphQLError(errorMsg), new GraphQLError(errorMsg)]); + } + ); + + test.each([ + { + dataType: "[ID!]", + value: ["some-unique-id", "another-unique-id"], + }, + { + dataType: "[String!]", + value: ["dummyValue", "anotherDummyValue"], + }, + { + dataType: "[Boolean!]", + value: [false, true], + }, + { dataType: "[Int!]", value: [1, 3] }, + { dataType: "[Float!]", value: [1.2, 1.3] }, + { dataType: "[DateTime!]", value: ["2021-01-01T00:00:00", "2022-01-01T00:00:00"] }, + ] as const)("@default should not fail with a valid $dataType value", async ({ dataType, value }) => { + const fn = async () => { + const stringValue = value.map((v) => (typeof v === "string" ? `"${v}"` : v)).join(", "); + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: [${stringValue}]) + } + extend type User { + anotherField: ${dataType} @default(value: [${stringValue}]) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).resolves.not.toThrow(); + }); + + test.todo("add tests for LocalDateTime, Date, Time, LocalTime, Duration when supported"); + test.todo("@default with custom enum"); + test.todo("@default with user defined scalar"); +}); diff --git a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts new file mode 100644 index 0000000000..56dd419d9f --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts @@ -0,0 +1,124 @@ +/* + * 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 { GraphQLError } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("invalid @default usage", () => { + test("@default should fail without a defined value", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type User @node { + name: String @default + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([ + new GraphQLError( + 'Directive "@default" argument "value" of type "ScalarOrEnum!" is required, but it was not provided.' + ), + ]); + }); + + test.each([ + { + dataType: "ID", + value: 1.2, + errorMsg: "@default.value on ID fields must be of type ID", + }, + { + dataType: "String", + value: 1.2, + errorMsg: "@default.value on String fields must be of type String", + }, + { + dataType: "Boolean", + value: 1.2, + errorMsg: "@default.value on Boolean fields must be of type Boolean", + }, + { dataType: "Int", value: 1.2, errorMsg: "@default.value on Int fields must be of type Int" }, + { + dataType: "Float", + value: "stuff", + errorMsg: "@default.value on Float fields must be of type Float", + }, + { dataType: "DateTime", value: "dummy", errorMsg: "@default.value is not a valid DateTime" }, + ] as const)( + "@default should fail with an invalid $dataType value", + async ({ dataType, value: value, errorMsg }) => { + const fn = async () => { + const stringValue = typeof value === "string" ? `"${value}"` : value; + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${stringValue}) + } + extend type User { + anotherField: ${dataType} @default(value: ${stringValue}) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([new GraphQLError(errorMsg), new GraphQLError(errorMsg)]); + } + ); + + test.each([ + { + dataType: "ID", + value: "some-unique-id", + }, + { + dataType: "String", + value: "dummyValue", + }, + { + dataType: "Boolean", + value: false, + }, + { dataType: "Int", value: 1 }, + { dataType: "Float", value: 1.2 }, + { dataType: "DateTime", value: "2021-01-01T00:00:00" }, + ] as const)("@default should not fail with a valid $dataType value", async ({ dataType, value }) => { + const fn = async () => { + const stringValue = typeof value === "string" ? `"${value}"` : value; + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${stringValue}) + } + extend type User { + anotherField: ${dataType} @default(value: ${stringValue}) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).resolves.not.toThrow(); + }); + + test.todo("add tests for LocalDateTime, Date, Time, LocalTime, Duration when supported"); + test.todo("@default with custom enum"); + test.todo("@default with user defined scalar"); +}); diff --git a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-id.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-id.test.ts new file mode 100644 index 0000000000..f7636761dc --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-id.test.ts @@ -0,0 +1,112 @@ +/* + * 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 { GraphQLError } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("@id validation", () => { + test("should not raise for valid @id usage", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + id: ID! @id + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + id: ID! @id + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + id: ID! @id + year: Int + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).toResolve(); + }); + + test.each([ + { dataType: "String", errorMsg: "Cannot autogenerate a non ID field." }, + { dataType: "Float", errorMsg: "Cannot autogenerate a non ID field." }, + { dataType: "Date", errorMsg: "Cannot autogenerate a non ID field." }, + { dataType: "String!", errorMsg: "Cannot autogenerate a non ID field." }, + { dataType: "[String!]", errorMsg: "Cannot autogenerate an array." }, + { dataType: "[String!]!", errorMsg: "Cannot autogenerate an array." }, + ] as const)("should raise when @id is not defined on $dataType field", async ({ dataType, errorMsg }) => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + id: ID! + field: ${dataType} @id + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([new GraphQLError(errorMsg)]); + }); + + test("should raise when @id is not defined on an invalid field on a extension", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + id: ID! + } + extend type Movie { + field: String @id + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([new GraphQLError("Cannot autogenerate a non ID field.")]); + }); + + test("should raise when @id is not defined on an invalid field on a relationship property", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + id: ID! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + id: ID! + } + type ActedIn @relationshipProperties { + id: ID! + field: String @id + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([new GraphQLError("Cannot autogenerate a non ID field.")]); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-limit.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-limit.test.ts new file mode 100644 index 0000000000..664b011ed5 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-limit.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { GraphQLError } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("Limit validation", () => { + test("should not raise for valid @limit usage", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @limit(default: 10, max: 20) @node { + title: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).toResolve(); + }); + + test("should raise for invalid @limit usage, default > max", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @limit(default: 20, max: 10) @node { + title: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([ + new GraphQLError("@limit.max invalid value: 10. Must be greater than limit.default: 20."), + ]); + }); + + test("should raise for invalid @limit usage, default < 0", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @limit(default: -20, max: 10) @node { + title: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([ + new GraphQLError("@limit.default invalid value: -20. Must be greater than 0."), + ]); + }); + + test("should raise for invalid @limit usage, max < 0", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @limit(default: 10, max: -20) @node { + title: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([ + new GraphQLError("@limit.max invalid value: -20. Must be greater than 0."), + ]); + }); + + test("should raise for invalid @limit usage, max float", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @limit(max: 2.3) @node { + title: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual(new Error(`Argument "max" has invalid value 2.3.`)); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-list.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-list.test.ts new file mode 100644 index 0000000000..6d6ae9ce2c --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-list.test.ts @@ -0,0 +1,70 @@ +/* + * 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 { GraphQLError } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("List validation", () => { + test("should not raise for non nullable list of non nullable string", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: [String!]! + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).toResolve(); + }); + + test("should not raise for a list of non nullable string", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: [String!] + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).toResolve(); + }); + + test("should raise when for list of nullable element", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: [String]! + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([ + new GraphQLError("List of non-null elements are not supported. Found: [String]!"), + ]); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-relationship.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-relationship.test.ts new file mode 100644 index 0000000000..6bac73c3e9 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-relationship.test.ts @@ -0,0 +1,142 @@ +/* + * 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 { GraphQLError } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("Relationships validation", () => { + test("should raise if target is invalid, not nullable one to one", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + type Actor @node { + name: String + movies: Movie! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).rejects.toEqual([new GraphQLError("@relationship can only be used on List target")]); + }); + + test("should raise if target is invalid, nullable one to one", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + type Actor @node { + name: String + movies: Movie @relationship(type: "ACTED_IN", direction: OUT) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([new GraphQLError("@relationship can only be used on List target")]); + }); + + test("should raise if target is invalid, target is a scalar field", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + type Actor @node { + name: String + movies: [String]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([new GraphQLError("@relationship cannot be used with type: String")]); + }); + + test("should raise if target is valid, list of nullable target", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + type Actor @node { + name: String + movies: [Movie]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).rejects.toEqual([new GraphQLError("Invalid field type: List type relationship fields must be non-nullable and have non-nullable entries, please change type to [Movie!]!")]); + }); + + test("should not raise if target is valid, nullable list type", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + type Actor @node { + name: String + movies: [Movie!] @relationship(type: "ACTED_IN", direction: OUT) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).toResolve(); + }); + + test("should not raise if target is valid, not nullable list type", async () => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + + await expect(fn()).toResolve(); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts new file mode 100644 index 0000000000..51c3aa713e --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -0,0 +1,800 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../src"; +import { raiseOnInvalidSchema } from "../../utils/raise-on-invalid-schema"; + +describe("Relationships", () => { + test("Simple relationship without properties", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + type Actor { + movies(where: ActorMoviesOperationWhere): ActorMoviesOperation + name: String + } + + type ActorConnection { + edges: [ActorEdge] + pageInfo: PageInfo + } + + input ActorConnectionSort { + node: ActorSort + } + + type ActorCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input ActorCreateInput { + node: ActorCreateNode! + } + + input ActorCreateNode { + name: String + } + + type ActorCreateResponse { + actors: [Actor!]! + info: CreateInfo + } + + input ActorDeleteInput { + node: ActorDeleteNode + } + + input ActorDeleteNode { + movies: ActorMoviesDeleteOperation + } + + type ActorEdge { + cursor: String + node: Actor + } + + type ActorMoviesConnection { + edges: [ActorMoviesEdge] + pageInfo: PageInfo + } + + input ActorMoviesConnectionSort { + edges: ActorMoviesEdgeSort + } + + input ActorMoviesDeleteInput { + input: MovieDeleteInput + where: ActorMoviesOperationWhere + } + + input ActorMoviesDeleteOperation { + delete: ActorMoviesDeleteInput + } + + type ActorMoviesEdge { + cursor: String + node: Movie + } + + input ActorMoviesEdgeListWhere { + AND: [ActorMoviesEdgeListWhere!] + NOT: ActorMoviesEdgeListWhere + OR: [ActorMoviesEdgeListWhere!] + edges: ActorMoviesEdgeWhere + } + + input ActorMoviesEdgeSort { + node: MovieSort + } + + input ActorMoviesEdgeWhere { + AND: [ActorMoviesEdgeWhere!] + NOT: ActorMoviesEdgeWhere + OR: [ActorMoviesEdgeWhere!] + node: MovieWhere + } + + input ActorMoviesNestedOperationWhere { + AND: [ActorMoviesNestedOperationWhere!] + NOT: ActorMoviesNestedOperationWhere + OR: [ActorMoviesNestedOperationWhere!] + all: ActorMoviesEdgeListWhere + none: ActorMoviesEdgeListWhere + single: ActorMoviesEdgeListWhere + some: ActorMoviesEdgeListWhere + } + + type ActorMoviesOperation { + connection(after: String, first: Int, sort: [ActorMoviesConnectionSort!]): ActorMoviesConnection + } + + input ActorMoviesOperationWhere { + AND: [ActorMoviesOperationWhere!] + NOT: ActorMoviesOperationWhere + OR: [ActorMoviesOperationWhere!] + edges: ActorMoviesEdgeWhere + } + + type ActorOperation { + connection(after: String, first: Int, sort: [ActorConnectionSort!]): ActorConnection + } + + input ActorOperationWhere { + AND: [ActorOperationWhere!] + NOT: ActorOperationWhere + OR: [ActorOperationWhere!] + node: ActorWhere + } + + input ActorSort { + name: SortDirection + } + + input ActorUpdateInput { + node: ActorUpdateNode! + } + + input ActorUpdateNode { + name: StringUpdate + } + + type ActorUpdateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + + input ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + movies: ActorMoviesNestedOperationWhere + name: StringWhere + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + type Movie { + actors(where: MovieActorsOperationWhere): MovieActorsOperation + title: String + } + + type MovieActorsConnection { + edges: [MovieActorsEdge] + pageInfo: PageInfo + } + + input MovieActorsConnectionSort { + edges: MovieActorsEdgeSort + } + + input MovieActorsDeleteInput { + input: ActorDeleteInput + where: MovieActorsOperationWhere + } + + input MovieActorsDeleteOperation { + delete: MovieActorsDeleteInput + } + + type MovieActorsEdge { + cursor: String + node: Actor + } + + input MovieActorsEdgeListWhere { + AND: [MovieActorsEdgeListWhere!] + NOT: MovieActorsEdgeListWhere + OR: [MovieActorsEdgeListWhere!] + edges: MovieActorsEdgeWhere + } + + input MovieActorsEdgeSort { + node: ActorSort + } + + input MovieActorsEdgeWhere { + AND: [MovieActorsEdgeWhere!] + NOT: MovieActorsEdgeWhere + OR: [MovieActorsEdgeWhere!] + node: ActorWhere + } + + input MovieActorsNestedOperationWhere { + AND: [MovieActorsNestedOperationWhere!] + NOT: MovieActorsNestedOperationWhere + OR: [MovieActorsNestedOperationWhere!] + all: MovieActorsEdgeListWhere + none: MovieActorsEdgeListWhere + single: MovieActorsEdgeListWhere + some: MovieActorsEdgeListWhere + } + + type MovieActorsOperation { + connection(after: String, first: Int, sort: [MovieActorsConnectionSort!]): MovieActorsConnection + } + + input MovieActorsOperationWhere { + AND: [MovieActorsOperationWhere!] + NOT: MovieActorsOperationWhere + OR: [MovieActorsOperationWhere!] + edges: MovieActorsEdgeWhere + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + title: String + } + + type MovieCreateResponse { + info: CreateInfo + movies: [Movie!]! + } + + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + actors: MovieActorsDeleteOperation + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieSort { + title: SortDirection + } + + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + actors: MovieActorsNestedOperationWhere + title: StringWhere + } + + type Mutation { + createActors(input: [ActorCreateInput!]!): ActorCreateResponse + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteActors(input: ActorDeleteInput, where: ActorOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse + updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + actors(where: ActorOperationWhere): ActorOperation + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringUpdate { + set: String + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); + + test("Simple relationship with properties", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + type ActedIn { + year: Int + } + + input ActedInSort { + year: SortDirection + } + + input ActedInWhere { + AND: [ActedInWhere!] + NOT: ActedInWhere + OR: [ActedInWhere!] + year: IntWhere + } + + type Actor { + movies(where: ActorMoviesOperationWhere): ActorMoviesOperation + name: String + } + + type ActorConnection { + edges: [ActorEdge] + pageInfo: PageInfo + } + + input ActorConnectionSort { + node: ActorSort + } + + type ActorCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input ActorCreateInput { + node: ActorCreateNode! + } + + input ActorCreateNode { + name: String + } + + type ActorCreateResponse { + actors: [Actor!]! + info: CreateInfo + } + + input ActorDeleteInput { + node: ActorDeleteNode + } + + input ActorDeleteNode { + movies: ActorMoviesDeleteOperation + } + + type ActorEdge { + cursor: String + node: Actor + } + + type ActorMoviesConnection { + edges: [ActorMoviesEdge] + pageInfo: PageInfo + } + + input ActorMoviesConnectionSort { + edges: ActorMoviesEdgeSort + } + + input ActorMoviesDeleteInput { + input: MovieDeleteInput + where: ActorMoviesOperationWhere + } + + input ActorMoviesDeleteOperation { + delete: ActorMoviesDeleteInput + } + + type ActorMoviesEdge { + cursor: String + node: Movie + properties: ActedIn + } + + input ActorMoviesEdgeListWhere { + AND: [ActorMoviesEdgeListWhere!] + NOT: ActorMoviesEdgeListWhere + OR: [ActorMoviesEdgeListWhere!] + edges: ActorMoviesEdgeWhere + } + + input ActorMoviesEdgeSort { + node: MovieSort + properties: ActedInSort + } + + input ActorMoviesEdgeWhere { + AND: [ActorMoviesEdgeWhere!] + NOT: ActorMoviesEdgeWhere + OR: [ActorMoviesEdgeWhere!] + node: MovieWhere + properties: ActedInWhere + } + + input ActorMoviesNestedOperationWhere { + AND: [ActorMoviesNestedOperationWhere!] + NOT: ActorMoviesNestedOperationWhere + OR: [ActorMoviesNestedOperationWhere!] + all: ActorMoviesEdgeListWhere + none: ActorMoviesEdgeListWhere + single: ActorMoviesEdgeListWhere + some: ActorMoviesEdgeListWhere + } + + type ActorMoviesOperation { + connection(after: String, first: Int, sort: [ActorMoviesConnectionSort!]): ActorMoviesConnection + } + + input ActorMoviesOperationWhere { + AND: [ActorMoviesOperationWhere!] + NOT: ActorMoviesOperationWhere + OR: [ActorMoviesOperationWhere!] + edges: ActorMoviesEdgeWhere + } + + type ActorOperation { + connection(after: String, first: Int, sort: [ActorConnectionSort!]): ActorConnection + } + + input ActorOperationWhere { + AND: [ActorOperationWhere!] + NOT: ActorOperationWhere + OR: [ActorOperationWhere!] + node: ActorWhere + } + + input ActorSort { + name: SortDirection + } + + input ActorUpdateInput { + node: ActorUpdateNode! + } + + input ActorUpdateNode { + name: StringUpdate + } + + type ActorUpdateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + + input ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + movies: ActorMoviesNestedOperationWhere + name: StringWhere + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + input IntWhere { + AND: [IntWhere!] + NOT: IntWhere + OR: [IntWhere!] + equals: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + } + + type Movie { + actors(where: MovieActorsOperationWhere): MovieActorsOperation + title: String + } + + type MovieActorsConnection { + edges: [MovieActorsEdge] + pageInfo: PageInfo + } + + input MovieActorsConnectionSort { + edges: MovieActorsEdgeSort + } + + input MovieActorsDeleteInput { + input: ActorDeleteInput + where: MovieActorsOperationWhere + } + + input MovieActorsDeleteOperation { + delete: MovieActorsDeleteInput + } + + type MovieActorsEdge { + cursor: String + node: Actor + properties: ActedIn + } + + input MovieActorsEdgeListWhere { + AND: [MovieActorsEdgeListWhere!] + NOT: MovieActorsEdgeListWhere + OR: [MovieActorsEdgeListWhere!] + edges: MovieActorsEdgeWhere + } + + input MovieActorsEdgeSort { + node: ActorSort + properties: ActedInSort + } + + input MovieActorsEdgeWhere { + AND: [MovieActorsEdgeWhere!] + NOT: MovieActorsEdgeWhere + OR: [MovieActorsEdgeWhere!] + node: ActorWhere + properties: ActedInWhere + } + + input MovieActorsNestedOperationWhere { + AND: [MovieActorsNestedOperationWhere!] + NOT: MovieActorsNestedOperationWhere + OR: [MovieActorsNestedOperationWhere!] + all: MovieActorsEdgeListWhere + none: MovieActorsEdgeListWhere + single: MovieActorsEdgeListWhere + some: MovieActorsEdgeListWhere + } + + type MovieActorsOperation { + connection(after: String, first: Int, sort: [MovieActorsConnectionSort!]): MovieActorsConnection + } + + input MovieActorsOperationWhere { + AND: [MovieActorsOperationWhere!] + NOT: MovieActorsOperationWhere + OR: [MovieActorsOperationWhere!] + edges: MovieActorsEdgeWhere + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + title: String + } + + type MovieCreateResponse { + info: CreateInfo + movies: [Movie!]! + } + + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + actors: MovieActorsDeleteOperation + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieSort { + title: SortDirection + } + + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + actors: MovieActorsNestedOperationWhere + title: StringWhere + } + + type Mutation { + createActors(input: [ActorCreateInput!]!): ActorCreateResponse + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteActors(input: ActorDeleteInput, where: ActorOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse + updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + actors(where: ActorOperationWhere): ActorOperation + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringUpdate { + set: String + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts new file mode 100644 index 0000000000..106b64983b --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -0,0 +1,569 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../src"; +import { raiseOnInvalidSchema } from "../../utils/raise-on-invalid-schema"; + +describe("Simple Aura-API", () => { + test("single type", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + type Movie { + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + title: String + } + + type MovieCreateResponse { + info: CreateInfo + movies: [Movie!]! + } + + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieSort { + title: SortDirection + } + + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + title: StringWhere + } + + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringUpdate { + set: String + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); + + test("multiple types", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + } + type Actor @node { + name: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + type Actor { + name: String + } + + type ActorConnection { + edges: [ActorEdge] + pageInfo: PageInfo + } + + input ActorConnectionSort { + node: ActorSort + } + + type ActorCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input ActorCreateInput { + node: ActorCreateNode! + } + + input ActorCreateNode { + name: String + } + + type ActorCreateResponse { + actors: [Actor!]! + info: CreateInfo + } + + input ActorDeleteInput { + node: ActorDeleteNode + } + + input ActorDeleteNode { + _emptyInput: Boolean + } + + type ActorEdge { + cursor: String + node: Actor + } + + type ActorOperation { + connection(after: String, first: Int, sort: [ActorConnectionSort!]): ActorConnection + } + + input ActorOperationWhere { + AND: [ActorOperationWhere!] + NOT: ActorOperationWhere + OR: [ActorOperationWhere!] + node: ActorWhere + } + + input ActorSort { + name: SortDirection + } + + input ActorUpdateInput { + node: ActorUpdateNode! + } + + input ActorUpdateNode { + name: StringUpdate + } + + type ActorUpdateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + + input ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + name: StringWhere + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + type Movie { + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + title: String + } + + type MovieCreateResponse { + info: CreateInfo + movies: [Movie!]! + } + + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieSort { + title: SortDirection + } + + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + title: StringWhere + } + + type Mutation { + createActors(input: [ActorCreateInput!]!): ActorCreateResponse + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteActors(input: ActorDeleteInput, where: ActorOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse + updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + actors(where: ActorOperationWhere): ActorOperation + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringUpdate { + set: String + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); + + test("should ignore types without the @node directive", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + } + type AnotherNode { + name: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + type Movie { + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + title: String + } + + type MovieCreateResponse { + info: CreateInfo + movies: [Movie!]! + } + + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + node: MovieWhere + } + + input MovieSort { + title: SortDirection + } + + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + title: StringWhere + } + + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringUpdate { + set: String + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts new file mode 100644 index 0000000000..6ba7f68afc --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -0,0 +1,567 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("Scalars", () => { + test("should generate the right types for all the scalars", async () => { + const typeDefs = /* GraphQL */ ` + type NodeType @node { + stringList: [String!] + intList: [Int!] + floatList: [Float!] + idList: [ID!] + booleanList: [Boolean!] + dateList: [Date!] + dateTimeList: [DateTime!] + localDateTimeList: [LocalDateTime!] + durationList: [Duration!] + timeList: [Time!] + localTimeList: [LocalTime!] + bigIntList: [BigInt!] + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNodeProperties @relationshipProperties { + stringList: [String!] + intList: [Int!] + floatList: [Float!] + idList: [ID!] + booleanList: [Boolean!] + dateList: [Date!] + dateTimeList: [DateTime!] + localDateTimeList: [LocalDateTime!] + durationList: [Duration!] + timeList: [Time!] + localTimeList: [LocalTime!] + bigIntList: [BigInt!] + } + + type RelatedNode @node { + stringList: [String!] + intList: [Int!] + floatList: [Float!] + idList: [ID!] + booleanList: [Boolean!] + dateList: [Date!] + dateTimeList: [DateTime!] + localDateTimeList: [LocalDateTime!] + durationList: [Duration!] + timeList: [Time!] + localTimeList: [LocalTime!] + bigIntList: [BigInt!] + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + \\"\\"\\" + A BigInt value up to 64 bits in size, which can be a number or a string if used inline, or a string only if used as a variable. Always returned as a string. + \\"\\"\\" + scalar BigInt + + input BigIntListWhere { + equals: [BigInt!] + } + + input BigIntUpdate { + set: BigInt + } + + input BooleanWhere { + AND: [BooleanWhere!] + NOT: BooleanWhere + OR: [BooleanWhere!] + equals: Boolean + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" + scalar Date + + input DateListWhere { + equals: [Date!] + } + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeListWhere { + equals: [DateTime!] + } + + input DateTimeUpdate { + set: DateTime + } + + input DateUpdate { + set: Date + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" + scalar Duration + + input DurationListWhere { + equals: [Duration!] + } + + input DurationUpdate { + set: Duration + } + + input FloatListWhere { + equals: [Float!] + } + + input FloatUpdate { + set: Float + } + + input IDListWhere { + equals: [ID!] + } + + input IDUpdate { + set: ID + } + + input IntListWhere { + equals: [Int!] + } + + input IntUpdate { + set: Int + } + + \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" + scalar LocalDateTime + + input LocalDateTimeListWhere { + equals: [LocalDateTime!] + } + + input LocalDateTimeUpdate { + set: LocalDateTime + } + + \\"\\"\\" + A local time, represented as a time string without timezone information + \\"\\"\\" + scalar LocalTime + + input LocalTimeListWhere { + equals: [LocalTime!] + } + + input LocalTimeUpdate { + set: LocalTime + } + + type Mutation { + createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse + createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + deleteNodeTypes(input: NodeTypeDeleteInput, where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(input: RelatedNodeDeleteInput, where: RelatedNodeOperationWhere): DeleteResponse + updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse + } + + type NodeType { + bigIntList: [BigInt!] + booleanList: [Boolean!] + dateList: [Date!] + dateTimeList: [DateTime!] + durationList: [Duration!] + floatList: [Float!] + idList: [ID!] + intList: [Int!] + localDateTimeList: [LocalDateTime!] + localTimeList: [LocalTime!] + relatedNode(where: NodeTypeRelatedNodeOperationWhere): NodeTypeRelatedNodeOperation + stringList: [String!] + timeList: [Time!] + } + + type NodeTypeConnection { + edges: [NodeTypeEdge] + pageInfo: PageInfo + } + + type NodeTypeCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input NodeTypeCreateInput { + node: NodeTypeCreateNode! + } + + input NodeTypeCreateNode { + bigIntList: [BigInt!] + booleanList: [Boolean!] + dateList: [Date!] + dateTimeList: [DateTime!] + durationList: [Duration!] + floatList: [Float!] + idList: [ID!] + intList: [Int!] + localDateTimeList: [LocalDateTime!] + localTimeList: [LocalTime!] + stringList: [String!] + timeList: [Time!] + } + + type NodeTypeCreateResponse { + info: CreateInfo + nodeTypes: [NodeType!]! + } + + input NodeTypeDeleteInput { + node: NodeTypeDeleteNode + } + + input NodeTypeDeleteNode { + relatedNode: NodeTypeRelatedNodeDeleteOperation + } + + type NodeTypeEdge { + cursor: String + node: NodeType + } + + type NodeTypeOperation { + connection(after: String, first: Int): NodeTypeConnection + } + + input NodeTypeOperationWhere { + AND: [NodeTypeOperationWhere!] + NOT: NodeTypeOperationWhere + OR: [NodeTypeOperationWhere!] + node: NodeTypeWhere + } + + type NodeTypeRelatedNodeConnection { + edges: [NodeTypeRelatedNodeEdge] + pageInfo: PageInfo + } + + input NodeTypeRelatedNodeDeleteInput { + input: RelatedNodeDeleteInput + where: NodeTypeRelatedNodeOperationWhere + } + + input NodeTypeRelatedNodeDeleteOperation { + delete: NodeTypeRelatedNodeDeleteInput + } + + type NodeTypeRelatedNodeEdge { + cursor: String + node: RelatedNode + properties: RelatedNodeProperties + } + + input NodeTypeRelatedNodeEdgeListWhere { + AND: [NodeTypeRelatedNodeEdgeListWhere!] + NOT: NodeTypeRelatedNodeEdgeListWhere + OR: [NodeTypeRelatedNodeEdgeListWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeRelatedNodeEdgeWhere { + AND: [NodeTypeRelatedNodeEdgeWhere!] + NOT: NodeTypeRelatedNodeEdgeWhere + OR: [NodeTypeRelatedNodeEdgeWhere!] + node: RelatedNodeWhere + properties: RelatedNodePropertiesWhere + } + + input NodeTypeRelatedNodeNestedOperationWhere { + AND: [NodeTypeRelatedNodeNestedOperationWhere!] + NOT: NodeTypeRelatedNodeNestedOperationWhere + OR: [NodeTypeRelatedNodeNestedOperationWhere!] + all: NodeTypeRelatedNodeEdgeListWhere + none: NodeTypeRelatedNodeEdgeListWhere + single: NodeTypeRelatedNodeEdgeListWhere + some: NodeTypeRelatedNodeEdgeListWhere + } + + type NodeTypeRelatedNodeOperation { + connection(after: String, first: Int): NodeTypeRelatedNodeConnection + } + + input NodeTypeRelatedNodeOperationWhere { + AND: [NodeTypeRelatedNodeOperationWhere!] + NOT: NodeTypeRelatedNodeOperationWhere + OR: [NodeTypeRelatedNodeOperationWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeUpdateInput { + node: NodeTypeUpdateNode! + } + + input NodeTypeUpdateNode { + bigIntList: BigIntUpdate + booleanList: IntUpdate + dateList: DateUpdate + dateTimeList: DateTimeUpdate + durationList: DurationUpdate + floatList: FloatUpdate + idList: IDUpdate + intList: IntUpdate + localDateTimeList: LocalDateTimeUpdate + localTimeList: LocalTimeUpdate + stringList: StringUpdate + timeList: TimeUpdate + } + + type NodeTypeUpdateResponse { + info: NodeTypeCreateInfo + nodeTypes: [NodeType!]! + } + + input NodeTypeWhere { + AND: [NodeTypeWhere!] + NOT: NodeTypeWhere + OR: [NodeTypeWhere!] + bigIntList: BigIntListWhere + booleanList: BooleanWhere + dateList: DateListWhere + dateTimeList: DateTimeListWhere + durationList: DurationListWhere + floatList: FloatListWhere + idList: IDListWhere + intList: IntListWhere + localDateTimeList: LocalDateTimeListWhere + localTimeList: LocalTimeListWhere + relatedNode: NodeTypeRelatedNodeNestedOperationWhere + stringList: StringListWhere + timeList: TimeListWhere + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation + relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation + } + + type RelatedNode { + bigIntList: [BigInt!] + booleanList: [Boolean!] + dateList: [Date!] + dateTimeList: [DateTime!] + durationList: [Duration!] + floatList: [Float!] + idList: [ID!] + intList: [Int!] + localDateTimeList: [LocalDateTime!] + localTimeList: [LocalTime!] + stringList: [String!] + timeList: [Time!] + } + + type RelatedNodeConnection { + edges: [RelatedNodeEdge] + pageInfo: PageInfo + } + + type RelatedNodeCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input RelatedNodeCreateInput { + node: RelatedNodeCreateNode! + } + + input RelatedNodeCreateNode { + bigIntList: [BigInt!] + booleanList: [Boolean!] + dateList: [Date!] + dateTimeList: [DateTime!] + durationList: [Duration!] + floatList: [Float!] + idList: [ID!] + intList: [Int!] + localDateTimeList: [LocalDateTime!] + localTimeList: [LocalTime!] + stringList: [String!] + timeList: [Time!] + } + + type RelatedNodeCreateResponse { + info: CreateInfo + relatedNodes: [RelatedNode!]! + } + + input RelatedNodeDeleteInput { + node: RelatedNodeDeleteNode + } + + input RelatedNodeDeleteNode { + _emptyInput: Boolean + } + + type RelatedNodeEdge { + cursor: String + node: RelatedNode + } + + type RelatedNodeOperation { + connection(after: String, first: Int): RelatedNodeConnection + } + + input RelatedNodeOperationWhere { + AND: [RelatedNodeOperationWhere!] + NOT: RelatedNodeOperationWhere + OR: [RelatedNodeOperationWhere!] + node: RelatedNodeWhere + } + + type RelatedNodeProperties { + bigIntList: [BigInt!] + booleanList: [Boolean!] + dateList: [Date!] + dateTimeList: [DateTime!] + durationList: [Duration!] + floatList: [Float!] + idList: [ID!] + intList: [Int!] + localDateTimeList: [LocalDateTime!] + localTimeList: [LocalTime!] + stringList: [String!] + timeList: [Time!] + } + + input RelatedNodePropertiesWhere { + AND: [RelatedNodePropertiesWhere!] + NOT: RelatedNodePropertiesWhere + OR: [RelatedNodePropertiesWhere!] + bigIntList: BigIntListWhere + booleanList: BooleanWhere + dateList: DateListWhere + dateTimeList: DateTimeListWhere + durationList: DurationListWhere + floatList: FloatListWhere + idList: IDListWhere + intList: IntListWhere + localDateTimeList: LocalDateTimeListWhere + localTimeList: LocalTimeListWhere + stringList: StringListWhere + timeList: TimeListWhere + } + + input RelatedNodeUpdateInput { + node: RelatedNodeUpdateNode! + } + + input RelatedNodeUpdateNode { + bigIntList: BigIntUpdate + booleanList: IntUpdate + dateList: DateUpdate + dateTimeList: DateTimeUpdate + durationList: DurationUpdate + floatList: FloatUpdate + idList: IDUpdate + intList: IntUpdate + localDateTimeList: LocalDateTimeUpdate + localTimeList: LocalTimeUpdate + stringList: StringUpdate + timeList: TimeUpdate + } + + type RelatedNodeUpdateResponse { + info: RelatedNodeCreateInfo + relatedNodes: [RelatedNode!]! + } + + input RelatedNodeWhere { + AND: [RelatedNodeWhere!] + NOT: RelatedNodeWhere + OR: [RelatedNodeWhere!] + bigIntList: BigIntListWhere + booleanList: BooleanWhere + dateList: DateListWhere + dateTimeList: DateTimeListWhere + durationList: DurationListWhere + floatList: FloatListWhere + idList: IDListWhere + intList: IntListWhere + localDateTimeList: LocalDateTimeListWhere + localTimeList: LocalTimeListWhere + stringList: StringListWhere + timeList: TimeListWhere + } + + input StringListWhere { + equals: [String!] + } + + input StringUpdate { + set: String + } + + \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" + scalar Time + + input TimeListWhere { + equals: [Time!] + } + + input TimeUpdate { + set: Time + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts new file mode 100644 index 0000000000..34d50f0048 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -0,0 +1,604 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("Scalars", () => { + test("should generate the right types for all the scalars", async () => { + const typeDefs = /* GraphQL */ ` + type NodeType @node { + string: String! + int: Int! + float: Float! + id: ID! + boolean: Boolean! + bigInt: BigInt! + stringNullable: String + intNullable: Int + floatNullable: Float + idNullable: ID + booleanNullable: Boolean + bigIntNullable: BigInt + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNode @node { + string: String! + int: Int! + float: Float! + id: ID! + boolean: Boolean! + bigInt: BigInt! + stringNullable: String + intNullable: Int + floatNullable: Float + idNullable: ID + booleanNullable: Boolean + bigIntNullable: BigInt + } + + type RelatedNodeProperties @relationshipProperties { + string: String! + int: Int! + float: Float! + id: ID! + boolean: Boolean! + bigInt: BigInt! + stringNullable: String + intNullable: Int + floatNullable: Float + idNullable: ID + booleanNullable: Boolean + bigIntNullable: BigInt + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + \\"\\"\\" + A BigInt value up to 64 bits in size, which can be a number or a string if used inline, or a string only if used as a variable. Always returned as a string. + \\"\\"\\" + scalar BigInt + + input BigIntUpdate { + set: BigInt + } + + input BigIntWhere { + AND: [BigIntWhere!] + NOT: BigIntWhere + OR: [BigIntWhere!] + equals: BigInt + gt: BigInt + gte: BigInt + in: [BigInt!] + lt: BigInt + lte: BigInt + } + + input BooleanWhere { + AND: [BooleanWhere!] + NOT: BooleanWhere + OR: [BooleanWhere!] + equals: Boolean + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + input FloatUpdate { + set: Float + } + + input FloatWhere { + AND: [FloatWhere!] + NOT: FloatWhere + OR: [FloatWhere!] + equals: Float + gt: Float + gte: Float + in: [Float!] + lt: Float + lte: Float + } + + input IDUpdate { + set: ID + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + input IntUpdate { + set: Int + } + + input IntWhere { + AND: [IntWhere!] + NOT: IntWhere + OR: [IntWhere!] + equals: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + } + + type Mutation { + createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse + createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + deleteNodeTypes(input: NodeTypeDeleteInput, where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(input: RelatedNodeDeleteInput, where: RelatedNodeOperationWhere): DeleteResponse + updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse + } + + type NodeType { + bigInt: BigInt! + bigIntNullable: BigInt + boolean: Boolean! + booleanNullable: Boolean + float: Float! + floatNullable: Float + id: ID! + idNullable: ID + int: Int! + intNullable: Int + relatedNode(where: NodeTypeRelatedNodeOperationWhere): NodeTypeRelatedNodeOperation + string: String! + stringNullable: String + } + + type NodeTypeConnection { + edges: [NodeTypeEdge] + pageInfo: PageInfo + } + + input NodeTypeConnectionSort { + node: NodeTypeSort + } + + type NodeTypeCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input NodeTypeCreateInput { + node: NodeTypeCreateNode! + } + + input NodeTypeCreateNode { + bigInt: BigInt! + bigIntNullable: BigInt + boolean: Boolean! + booleanNullable: Boolean + float: Float! + floatNullable: Float + id: ID! + idNullable: ID + int: Int! + intNullable: Int + string: String! + stringNullable: String + } + + type NodeTypeCreateResponse { + info: CreateInfo + nodeTypes: [NodeType!]! + } + + input NodeTypeDeleteInput { + node: NodeTypeDeleteNode + } + + input NodeTypeDeleteNode { + relatedNode: NodeTypeRelatedNodeDeleteOperation + } + + type NodeTypeEdge { + cursor: String + node: NodeType + } + + type NodeTypeOperation { + connection(after: String, first: Int, sort: [NodeTypeConnectionSort!]): NodeTypeConnection + } + + input NodeTypeOperationWhere { + AND: [NodeTypeOperationWhere!] + NOT: NodeTypeOperationWhere + OR: [NodeTypeOperationWhere!] + node: NodeTypeWhere + } + + type NodeTypeRelatedNodeConnection { + edges: [NodeTypeRelatedNodeEdge] + pageInfo: PageInfo + } + + input NodeTypeRelatedNodeConnectionSort { + edges: NodeTypeRelatedNodeEdgeSort + } + + input NodeTypeRelatedNodeDeleteInput { + input: RelatedNodeDeleteInput + where: NodeTypeRelatedNodeOperationWhere + } + + input NodeTypeRelatedNodeDeleteOperation { + delete: NodeTypeRelatedNodeDeleteInput + } + + type NodeTypeRelatedNodeEdge { + cursor: String + node: RelatedNode + properties: RelatedNodeProperties + } + + input NodeTypeRelatedNodeEdgeListWhere { + AND: [NodeTypeRelatedNodeEdgeListWhere!] + NOT: NodeTypeRelatedNodeEdgeListWhere + OR: [NodeTypeRelatedNodeEdgeListWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeRelatedNodeEdgeSort { + node: RelatedNodeSort + properties: RelatedNodePropertiesSort + } + + input NodeTypeRelatedNodeEdgeWhere { + AND: [NodeTypeRelatedNodeEdgeWhere!] + NOT: NodeTypeRelatedNodeEdgeWhere + OR: [NodeTypeRelatedNodeEdgeWhere!] + node: RelatedNodeWhere + properties: RelatedNodePropertiesWhere + } + + input NodeTypeRelatedNodeNestedOperationWhere { + AND: [NodeTypeRelatedNodeNestedOperationWhere!] + NOT: NodeTypeRelatedNodeNestedOperationWhere + OR: [NodeTypeRelatedNodeNestedOperationWhere!] + all: NodeTypeRelatedNodeEdgeListWhere + none: NodeTypeRelatedNodeEdgeListWhere + single: NodeTypeRelatedNodeEdgeListWhere + some: NodeTypeRelatedNodeEdgeListWhere + } + + type NodeTypeRelatedNodeOperation { + connection(after: String, first: Int, sort: [NodeTypeRelatedNodeConnectionSort!]): NodeTypeRelatedNodeConnection + } + + input NodeTypeRelatedNodeOperationWhere { + AND: [NodeTypeRelatedNodeOperationWhere!] + NOT: NodeTypeRelatedNodeOperationWhere + OR: [NodeTypeRelatedNodeOperationWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeSort { + bigInt: SortDirection + bigIntNullable: SortDirection + boolean: SortDirection + booleanNullable: SortDirection + float: SortDirection + floatNullable: SortDirection + id: SortDirection + idNullable: SortDirection + int: SortDirection + intNullable: SortDirection + string: SortDirection + stringNullable: SortDirection + } + + input NodeTypeUpdateInput { + node: NodeTypeUpdateNode! + } + + input NodeTypeUpdateNode { + bigInt: BigIntUpdate + bigIntNullable: BigIntUpdate + boolean: IntUpdate + booleanNullable: IntUpdate + float: FloatUpdate + floatNullable: FloatUpdate + id: IDUpdate + idNullable: IDUpdate + int: IntUpdate + intNullable: IntUpdate + string: StringUpdate + stringNullable: StringUpdate + } + + type NodeTypeUpdateResponse { + info: NodeTypeCreateInfo + nodeTypes: [NodeType!]! + } + + input NodeTypeWhere { + AND: [NodeTypeWhere!] + NOT: NodeTypeWhere + OR: [NodeTypeWhere!] + bigInt: BigIntWhere + bigIntNullable: BigIntWhere + boolean: BooleanWhere + booleanNullable: BooleanWhere + float: FloatWhere + floatNullable: FloatWhere + id: IDWhere + idNullable: IDWhere + int: IntWhere + intNullable: IntWhere + relatedNode: NodeTypeRelatedNodeNestedOperationWhere + string: StringWhere + stringNullable: StringWhere + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation + relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation + } + + type RelatedNode { + bigInt: BigInt! + bigIntNullable: BigInt + boolean: Boolean! + booleanNullable: Boolean + float: Float! + floatNullable: Float + id: ID! + idNullable: ID + int: Int! + intNullable: Int + string: String! + stringNullable: String + } + + type RelatedNodeConnection { + edges: [RelatedNodeEdge] + pageInfo: PageInfo + } + + input RelatedNodeConnectionSort { + node: RelatedNodeSort + } + + type RelatedNodeCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input RelatedNodeCreateInput { + node: RelatedNodeCreateNode! + } + + input RelatedNodeCreateNode { + bigInt: BigInt! + bigIntNullable: BigInt + boolean: Boolean! + booleanNullable: Boolean + float: Float! + floatNullable: Float + id: ID! + idNullable: ID + int: Int! + intNullable: Int + string: String! + stringNullable: String + } + + type RelatedNodeCreateResponse { + info: CreateInfo + relatedNodes: [RelatedNode!]! + } + + input RelatedNodeDeleteInput { + node: RelatedNodeDeleteNode + } + + input RelatedNodeDeleteNode { + _emptyInput: Boolean + } + + type RelatedNodeEdge { + cursor: String + node: RelatedNode + } + + type RelatedNodeOperation { + connection(after: String, first: Int, sort: [RelatedNodeConnectionSort!]): RelatedNodeConnection + } + + input RelatedNodeOperationWhere { + AND: [RelatedNodeOperationWhere!] + NOT: RelatedNodeOperationWhere + OR: [RelatedNodeOperationWhere!] + node: RelatedNodeWhere + } + + type RelatedNodeProperties { + bigInt: BigInt! + bigIntNullable: BigInt + boolean: Boolean! + booleanNullable: Boolean + float: Float! + floatNullable: Float + id: ID! + idNullable: ID + int: Int! + intNullable: Int + string: String! + stringNullable: String + } + + input RelatedNodePropertiesSort { + bigInt: SortDirection + bigIntNullable: SortDirection + boolean: SortDirection + booleanNullable: SortDirection + float: SortDirection + floatNullable: SortDirection + id: SortDirection + idNullable: SortDirection + int: SortDirection + intNullable: SortDirection + string: SortDirection + stringNullable: SortDirection + } + + input RelatedNodePropertiesWhere { + AND: [RelatedNodePropertiesWhere!] + NOT: RelatedNodePropertiesWhere + OR: [RelatedNodePropertiesWhere!] + bigInt: BigIntWhere + bigIntNullable: BigIntWhere + boolean: BooleanWhere + booleanNullable: BooleanWhere + float: FloatWhere + floatNullable: FloatWhere + id: IDWhere + idNullable: IDWhere + int: IntWhere + intNullable: IntWhere + string: StringWhere + stringNullable: StringWhere + } + + input RelatedNodeSort { + bigInt: SortDirection + bigIntNullable: SortDirection + boolean: SortDirection + booleanNullable: SortDirection + float: SortDirection + floatNullable: SortDirection + id: SortDirection + idNullable: SortDirection + int: SortDirection + intNullable: SortDirection + string: SortDirection + stringNullable: SortDirection + } + + input RelatedNodeUpdateInput { + node: RelatedNodeUpdateNode! + } + + input RelatedNodeUpdateNode { + bigInt: BigIntUpdate + bigIntNullable: BigIntUpdate + boolean: IntUpdate + booleanNullable: IntUpdate + float: FloatUpdate + floatNullable: FloatUpdate + id: IDUpdate + idNullable: IDUpdate + int: IntUpdate + intNullable: IntUpdate + string: StringUpdate + stringNullable: StringUpdate + } + + type RelatedNodeUpdateResponse { + info: RelatedNodeCreateInfo + relatedNodes: [RelatedNode!]! + } + + input RelatedNodeWhere { + AND: [RelatedNodeWhere!] + NOT: RelatedNodeWhere + OR: [RelatedNodeWhere!] + bigInt: BigIntWhere + bigIntNullable: BigIntWhere + boolean: BooleanWhere + booleanNullable: BooleanWhere + float: FloatWhere + floatNullable: FloatWhere + id: IDWhere + idNullable: IDWhere + int: IntWhere + intNullable: IntWhere + string: StringWhere + stringNullable: StringWhere + } + + enum SortDirection { + ASC + DESC + } + + input StringUpdate { + set: String + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts new file mode 100644 index 0000000000..0c71dbf481 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -0,0 +1,376 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("Spatial Types", () => { + test("should generate the right types for all the spatial types", async () => { + const typeDefs = /* GraphQL */ ` + type NodeType @node { + cartesianPoint: CartesianPoint! + cartesianPointNullable: CartesianPoint + point: Point! + pointNullable: Point + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNode @node { + cartesianPoint: CartesianPoint! + cartesianPointNullable: CartesianPoint + point: Point! + pointNullable: Point + } + + type RelatedNodeProperties @relationshipProperties { + cartesianPoint: CartesianPoint! + cartesianPointNullable: CartesianPoint + point: Point! + pointNullable: Point + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + \\"\\"\\" + A point in a two- or three-dimensional Cartesian coordinate system or in a three-dimensional cylindrical coordinate system. For more information, see https://neo4j.com/docs/graphql/4/type-definitions/types/spatial/#cartesian-point + \\"\\"\\" + type CartesianPoint { + crs: String! + srid: Int! + x: Float! + y: Float! + z: Float + } + + \\"\\"\\"Input type for a cartesian point\\"\\"\\" + input CartesianPointInput { + x: Float! + y: Float! + z: Float + } + + input CartesianPointInputUpdate { + set: CartesianPointInput + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + type Mutation { + createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse + createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + deleteNodeTypes(input: NodeTypeDeleteInput, where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(input: RelatedNodeDeleteInput, where: RelatedNodeOperationWhere): DeleteResponse + updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse + } + + type NodeType { + cartesianPoint: CartesianPoint! + cartesianPointNullable: CartesianPoint + point: Point! + pointNullable: Point + relatedNode(where: NodeTypeRelatedNodeOperationWhere): NodeTypeRelatedNodeOperation + } + + type NodeTypeConnection { + edges: [NodeTypeEdge] + pageInfo: PageInfo + } + + type NodeTypeCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input NodeTypeCreateInput { + node: NodeTypeCreateNode! + } + + input NodeTypeCreateNode { + cartesianPoint: CartesianPointInput! + cartesianPointNullable: CartesianPointInput + point: PointInput! + pointNullable: PointInput + } + + type NodeTypeCreateResponse { + info: CreateInfo + nodeTypes: [NodeType!]! + } + + input NodeTypeDeleteInput { + node: NodeTypeDeleteNode + } + + input NodeTypeDeleteNode { + relatedNode: NodeTypeRelatedNodeDeleteOperation + } + + type NodeTypeEdge { + cursor: String + node: NodeType + } + + type NodeTypeOperation { + connection(after: String, first: Int): NodeTypeConnection + } + + input NodeTypeOperationWhere { + AND: [NodeTypeOperationWhere!] + NOT: NodeTypeOperationWhere + OR: [NodeTypeOperationWhere!] + node: NodeTypeWhere + } + + type NodeTypeRelatedNodeConnection { + edges: [NodeTypeRelatedNodeEdge] + pageInfo: PageInfo + } + + input NodeTypeRelatedNodeDeleteInput { + input: RelatedNodeDeleteInput + where: NodeTypeRelatedNodeOperationWhere + } + + input NodeTypeRelatedNodeDeleteOperation { + delete: NodeTypeRelatedNodeDeleteInput + } + + type NodeTypeRelatedNodeEdge { + cursor: String + node: RelatedNode + properties: RelatedNodeProperties + } + + input NodeTypeRelatedNodeEdgeListWhere { + AND: [NodeTypeRelatedNodeEdgeListWhere!] + NOT: NodeTypeRelatedNodeEdgeListWhere + OR: [NodeTypeRelatedNodeEdgeListWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeRelatedNodeEdgeWhere { + AND: [NodeTypeRelatedNodeEdgeWhere!] + NOT: NodeTypeRelatedNodeEdgeWhere + OR: [NodeTypeRelatedNodeEdgeWhere!] + node: RelatedNodeWhere + properties: RelatedNodePropertiesWhere + } + + input NodeTypeRelatedNodeNestedOperationWhere { + AND: [NodeTypeRelatedNodeNestedOperationWhere!] + NOT: NodeTypeRelatedNodeNestedOperationWhere + OR: [NodeTypeRelatedNodeNestedOperationWhere!] + all: NodeTypeRelatedNodeEdgeListWhere + none: NodeTypeRelatedNodeEdgeListWhere + single: NodeTypeRelatedNodeEdgeListWhere + some: NodeTypeRelatedNodeEdgeListWhere + } + + type NodeTypeRelatedNodeOperation { + connection(after: String, first: Int): NodeTypeRelatedNodeConnection + } + + input NodeTypeRelatedNodeOperationWhere { + AND: [NodeTypeRelatedNodeOperationWhere!] + NOT: NodeTypeRelatedNodeOperationWhere + OR: [NodeTypeRelatedNodeOperationWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeUpdateInput { + node: NodeTypeUpdateNode! + } + + input NodeTypeUpdateNode { + cartesianPoint: CartesianPointInputUpdate + cartesianPointNullable: CartesianPointInputUpdate + point: PointInputUpdate + pointNullable: PointInputUpdate + } + + type NodeTypeUpdateResponse { + info: NodeTypeCreateInfo + nodeTypes: [NodeType!]! + } + + input NodeTypeWhere { + AND: [NodeTypeWhere!] + NOT: NodeTypeWhere + OR: [NodeTypeWhere!] + relatedNode: NodeTypeRelatedNodeNestedOperationWhere + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + \\"\\"\\" + A point in a coordinate system. For more information, see https://neo4j.com/docs/graphql/4/type-definitions/types/spatial/#point + \\"\\"\\" + type Point { + crs: String! + height: Float + latitude: Float! + longitude: Float! + srid: Int! + } + + \\"\\"\\"Input type for a point\\"\\"\\" + input PointInput { + height: Float + latitude: Float! + longitude: Float! + } + + input PointInputUpdate { + set: PointInput + } + + type Query { + nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation + relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation + } + + type RelatedNode { + cartesianPoint: CartesianPoint! + cartesianPointNullable: CartesianPoint + point: Point! + pointNullable: Point + } + + type RelatedNodeConnection { + edges: [RelatedNodeEdge] + pageInfo: PageInfo + } + + type RelatedNodeCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input RelatedNodeCreateInput { + node: RelatedNodeCreateNode! + } + + input RelatedNodeCreateNode { + cartesianPoint: CartesianPointInput! + cartesianPointNullable: CartesianPointInput + point: PointInput! + pointNullable: PointInput + } + + type RelatedNodeCreateResponse { + info: CreateInfo + relatedNodes: [RelatedNode!]! + } + + input RelatedNodeDeleteInput { + node: RelatedNodeDeleteNode + } + + input RelatedNodeDeleteNode { + _emptyInput: Boolean + } + + type RelatedNodeEdge { + cursor: String + node: RelatedNode + } + + type RelatedNodeOperation { + connection(after: String, first: Int): RelatedNodeConnection + } + + input RelatedNodeOperationWhere { + AND: [RelatedNodeOperationWhere!] + NOT: RelatedNodeOperationWhere + OR: [RelatedNodeOperationWhere!] + node: RelatedNodeWhere + } + + type RelatedNodeProperties { + cartesianPoint: CartesianPoint! + cartesianPointNullable: CartesianPoint + point: Point! + pointNullable: Point + } + + input RelatedNodePropertiesWhere { + AND: [RelatedNodePropertiesWhere!] + NOT: RelatedNodePropertiesWhere + OR: [RelatedNodePropertiesWhere!] + } + + input RelatedNodeUpdateInput { + node: RelatedNodeUpdateNode! + } + + input RelatedNodeUpdateNode { + cartesianPoint: CartesianPointInputUpdate + cartesianPointNullable: CartesianPointInputUpdate + point: PointInputUpdate + pointNullable: PointInputUpdate + } + + type RelatedNodeUpdateResponse { + info: RelatedNodeCreateInfo + relatedNodes: [RelatedNode!]! + } + + input RelatedNodeWhere { + AND: [RelatedNodeWhere!] + NOT: RelatedNodeWhere + OR: [RelatedNodeWhere!] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts new file mode 100644 index 0000000000..d1c9612346 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -0,0 +1,534 @@ +/* + * 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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("Temporals", () => { + test("should generate the right types for all the temporal types", async () => { + const typeDefs = /* GraphQL */ ` + type NodeType @node { + date: Date + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNode @node { + date: Date + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + } + + type RelatedNodeProperties @relationshipProperties { + date: Date + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" + scalar Date + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeUpdate { + set: DateTime + } + + input DateTimeWhere { + AND: [DateTimeWhere!] + NOT: DateTimeWhere + OR: [DateTimeWhere!] + equals: DateTime + gt: DateTime + gte: DateTime + in: [DateTime!] + lt: DateTime + lte: DateTime + } + + input DateUpdate { + set: Date + } + + input DateWhere { + AND: [DateWhere!] + NOT: DateWhere + OR: [DateWhere!] + equals: Date + gt: Date + gte: Date + in: [Date!] + lt: Date + lte: Date + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" + scalar Duration + + input DurationUpdate { + set: Duration + } + + input DurationWhere { + AND: [DurationWhere!] + NOT: DurationWhere + OR: [DurationWhere!] + equals: Duration + gt: Duration + gte: Duration + in: [Duration!] + lt: Duration + lte: Duration + } + + \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" + scalar LocalDateTime + + input LocalDateTimeUpdate { + set: LocalDateTime + } + + input LocalDateTimeWhere { + AND: [LocalDateTimeWhere!] + NOT: LocalDateTimeWhere + OR: [LocalDateTimeWhere!] + equals: LocalDateTime + gt: LocalDateTime + gte: LocalDateTime + in: [LocalDateTime!] + lt: LocalDateTime + lte: LocalDateTime + } + + \\"\\"\\" + A local time, represented as a time string without timezone information + \\"\\"\\" + scalar LocalTime + + input LocalTimeUpdate { + set: LocalTime + } + + input LocalTimeWhere { + AND: [LocalTimeWhere!] + NOT: LocalTimeWhere + OR: [LocalTimeWhere!] + equals: LocalTime + gt: LocalTime + gte: LocalTime + in: [LocalTime!] + lt: LocalTime + lte: LocalTime + } + + type Mutation { + createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse + createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + deleteNodeTypes(input: NodeTypeDeleteInput, where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(input: RelatedNodeDeleteInput, where: RelatedNodeOperationWhere): DeleteResponse + updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse + } + + type NodeType { + date: Date + dateTime: DateTime + duration: Duration + localDateTime: LocalDateTime + localTime: LocalTime + relatedNode(where: NodeTypeRelatedNodeOperationWhere): NodeTypeRelatedNodeOperation + time: Time + } + + type NodeTypeConnection { + edges: [NodeTypeEdge] + pageInfo: PageInfo + } + + input NodeTypeConnectionSort { + node: NodeTypeSort + } + + type NodeTypeCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input NodeTypeCreateInput { + node: NodeTypeCreateNode! + } + + input NodeTypeCreateNode { + date: Date + dateTime: DateTime + duration: Duration + localDateTime: LocalDateTime + localTime: LocalTime + time: Time + } + + type NodeTypeCreateResponse { + info: CreateInfo + nodeTypes: [NodeType!]! + } + + input NodeTypeDeleteInput { + node: NodeTypeDeleteNode + } + + input NodeTypeDeleteNode { + relatedNode: NodeTypeRelatedNodeDeleteOperation + } + + type NodeTypeEdge { + cursor: String + node: NodeType + } + + type NodeTypeOperation { + connection(after: String, first: Int, sort: [NodeTypeConnectionSort!]): NodeTypeConnection + } + + input NodeTypeOperationWhere { + AND: [NodeTypeOperationWhere!] + NOT: NodeTypeOperationWhere + OR: [NodeTypeOperationWhere!] + node: NodeTypeWhere + } + + type NodeTypeRelatedNodeConnection { + edges: [NodeTypeRelatedNodeEdge] + pageInfo: PageInfo + } + + input NodeTypeRelatedNodeConnectionSort { + edges: NodeTypeRelatedNodeEdgeSort + } + + input NodeTypeRelatedNodeDeleteInput { + input: RelatedNodeDeleteInput + where: NodeTypeRelatedNodeOperationWhere + } + + input NodeTypeRelatedNodeDeleteOperation { + delete: NodeTypeRelatedNodeDeleteInput + } + + type NodeTypeRelatedNodeEdge { + cursor: String + node: RelatedNode + properties: RelatedNodeProperties + } + + input NodeTypeRelatedNodeEdgeListWhere { + AND: [NodeTypeRelatedNodeEdgeListWhere!] + NOT: NodeTypeRelatedNodeEdgeListWhere + OR: [NodeTypeRelatedNodeEdgeListWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeRelatedNodeEdgeSort { + node: RelatedNodeSort + properties: RelatedNodePropertiesSort + } + + input NodeTypeRelatedNodeEdgeWhere { + AND: [NodeTypeRelatedNodeEdgeWhere!] + NOT: NodeTypeRelatedNodeEdgeWhere + OR: [NodeTypeRelatedNodeEdgeWhere!] + node: RelatedNodeWhere + properties: RelatedNodePropertiesWhere + } + + input NodeTypeRelatedNodeNestedOperationWhere { + AND: [NodeTypeRelatedNodeNestedOperationWhere!] + NOT: NodeTypeRelatedNodeNestedOperationWhere + OR: [NodeTypeRelatedNodeNestedOperationWhere!] + all: NodeTypeRelatedNodeEdgeListWhere + none: NodeTypeRelatedNodeEdgeListWhere + single: NodeTypeRelatedNodeEdgeListWhere + some: NodeTypeRelatedNodeEdgeListWhere + } + + type NodeTypeRelatedNodeOperation { + connection(after: String, first: Int, sort: [NodeTypeRelatedNodeConnectionSort!]): NodeTypeRelatedNodeConnection + } + + input NodeTypeRelatedNodeOperationWhere { + AND: [NodeTypeRelatedNodeOperationWhere!] + NOT: NodeTypeRelatedNodeOperationWhere + OR: [NodeTypeRelatedNodeOperationWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeSort { + date: SortDirection + dateTime: SortDirection + duration: SortDirection + localDateTime: SortDirection + localTime: SortDirection + time: SortDirection + } + + input NodeTypeUpdateInput { + node: NodeTypeUpdateNode! + } + + input NodeTypeUpdateNode { + date: DateUpdate + dateTime: DateTimeUpdate + duration: DurationUpdate + localDateTime: LocalDateTimeUpdate + localTime: LocalTimeUpdate + time: TimeUpdate + } + + type NodeTypeUpdateResponse { + info: NodeTypeCreateInfo + nodeTypes: [NodeType!]! + } + + input NodeTypeWhere { + AND: [NodeTypeWhere!] + NOT: NodeTypeWhere + OR: [NodeTypeWhere!] + date: DateWhere + dateTime: DateTimeWhere + duration: DurationWhere + localDateTime: LocalDateTimeWhere + localTime: LocalTimeWhere + relatedNode: NodeTypeRelatedNodeNestedOperationWhere + time: TimeWhere + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation + relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation + } + + type RelatedNode { + date: Date + dateTime: DateTime + duration: Duration + localDateTime: LocalDateTime + localTime: LocalTime + time: Time + } + + type RelatedNodeConnection { + edges: [RelatedNodeEdge] + pageInfo: PageInfo + } + + input RelatedNodeConnectionSort { + node: RelatedNodeSort + } + + type RelatedNodeCreateInfo { + nodesCreated: Int! + nodesDelete: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + input RelatedNodeCreateInput { + node: RelatedNodeCreateNode! + } + + input RelatedNodeCreateNode { + date: Date + dateTime: DateTime + duration: Duration + localDateTime: LocalDateTime + localTime: LocalTime + time: Time + } + + type RelatedNodeCreateResponse { + info: CreateInfo + relatedNodes: [RelatedNode!]! + } + + input RelatedNodeDeleteInput { + node: RelatedNodeDeleteNode + } + + input RelatedNodeDeleteNode { + _emptyInput: Boolean + } + + type RelatedNodeEdge { + cursor: String + node: RelatedNode + } + + type RelatedNodeOperation { + connection(after: String, first: Int, sort: [RelatedNodeConnectionSort!]): RelatedNodeConnection + } + + input RelatedNodeOperationWhere { + AND: [RelatedNodeOperationWhere!] + NOT: RelatedNodeOperationWhere + OR: [RelatedNodeOperationWhere!] + node: RelatedNodeWhere + } + + type RelatedNodeProperties { + date: Date + dateTime: DateTime + duration: Duration + localDateTime: LocalDateTime + localTime: LocalTime + time: Time + } + + input RelatedNodePropertiesSort { + date: SortDirection + dateTime: SortDirection + duration: SortDirection + localDateTime: SortDirection + localTime: SortDirection + time: SortDirection + } + + input RelatedNodePropertiesWhere { + AND: [RelatedNodePropertiesWhere!] + NOT: RelatedNodePropertiesWhere + OR: [RelatedNodePropertiesWhere!] + date: DateWhere + dateTime: DateTimeWhere + duration: DurationWhere + localDateTime: LocalDateTimeWhere + localTime: LocalTimeWhere + time: TimeWhere + } + + input RelatedNodeSort { + date: SortDirection + dateTime: SortDirection + duration: SortDirection + localDateTime: SortDirection + localTime: SortDirection + time: SortDirection + } + + input RelatedNodeUpdateInput { + node: RelatedNodeUpdateNode! + } + + input RelatedNodeUpdateNode { + date: DateUpdate + dateTime: DateTimeUpdate + duration: DurationUpdate + localDateTime: LocalDateTimeUpdate + localTime: LocalTimeUpdate + time: TimeUpdate + } + + type RelatedNodeUpdateResponse { + info: RelatedNodeCreateInfo + relatedNodes: [RelatedNode!]! + } + + input RelatedNodeWhere { + AND: [RelatedNodeWhere!] + NOT: RelatedNodeWhere + OR: [RelatedNodeWhere!] + date: DateWhere + dateTime: DateTimeWhere + duration: DurationWhere + localDateTime: LocalDateTimeWhere + localTime: LocalTimeWhere + time: TimeWhere + } + + enum SortDirection { + ASC + DESC + } + + \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" + scalar Time + + input TimeUpdate { + set: Time + } + + input TimeWhere { + AND: [TimeWhere!] + NOT: TimeWhere + OR: [TimeWhere!] + equals: Time + gt: Time + gte: Time + in: [Time!] + lt: Time + lte: Time + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/combinations/create-node/create-node.test.ts b/packages/graphql/tests/api-v6/tck/combinations/create-node/create-node.test.ts new file mode 100644 index 0000000000..ec3d524aec --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/combinations/create-node/create-node.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Create with labels", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node(labels: ["Film", "ShortMovie"]) { + title: String! + released: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should create two movies and project them", async () => { + const mutation = /* GraphQL */ ` + mutation { + createMovies( + input: [ + { node: { title: "The Matrix" } } + { node: { title: "The Matrix Reloaded", released: 2001 } } + ] + ) { + movies { + title + released + } + } + } + `; + + const result = await translateQuery(neoSchema, mutation, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $param0 AS var0 + CALL { + WITH var0 + CREATE (this1:Film:ShortMovie) + SET + this1.title = var0.title, + this1.released = var0.released + RETURN this1 + } + WITH * + WITH collect({ node: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1 + RETURN collect({ node: { title: this1.title, released: this1.released, __resolveType: \\"Movie\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"title\\": \\"The Matrix\\" + }, + { + \\"title\\": \\"The Matrix Reloaded\\", + \\"released\\": { + \\"low\\": 2001, + \\"high\\": 0 + } + } + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/delete/delete.test.ts b/packages/graphql/tests/api-v6/tck/delete/delete.test.ts new file mode 100644 index 0000000000..91a0370c29 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/delete/delete.test.ts @@ -0,0 +1,66 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Top-Level Delete", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String! + released: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should delete one movie", async () => { + const mutation = /* GraphQL */ ` + mutation { + deleteMovies(where: { node: { title: { equals: "The Matrix" } } }) { + info { + nodesDeleted + relationshipsDeleted + } + } + } + `; + + const result = await translateQuery(neoSchema, mutation, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.title = $param0 + DETACH DELETE this0" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/directives/alias/create-alias.test.ts b/packages/graphql/tests/api-v6/tck/directives/alias/create-alias.test.ts new file mode 100644 index 0000000000..04924c1bb6 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/directives/alias/create-alias.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Create with @alias", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String! @alias(property: "name") + released: Int @alias(property: "year") + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should create two movies and project them", async () => { + const mutation = /* GraphQL */ ` + mutation { + createMovies( + input: [ + { node: { title: "The Matrix" } } + { node: { title: "The Matrix Reloaded", released: 2001 } } + ] + ) { + movies { + title + released + } + } + } + `; + + const result = await translateQuery(neoSchema, mutation, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $param0 AS var0 + CALL { + WITH var0 + CREATE (this1:Movie) + SET + this1.name = var0.title, + this1.year = var0.released + RETURN this1 + } + WITH * + WITH collect({ node: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1 + RETURN collect({ node: { title: this1.name, released: this1.year, __resolveType: \\"Movie\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"title\\": \\"The Matrix\\" + }, + { + \\"title\\": \\"The Matrix Reloaded\\", + \\"released\\": { + \\"low\\": 2001, + \\"high\\": 0 + } + } + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/directives/alias/query-alias.test.ts b/packages/graphql/tests/api-v6/tck/directives/alias/query-alias.test.ts new file mode 100644 index 0000000000..f768bce3c8 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/directives/alias/query-alias.test.ts @@ -0,0 +1,146 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Alias directive", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Director @node { + name: String + nameAgain: String @alias(property: "name") + movies: [Movie!]! @relationship(direction: OUT, type: "DIRECTED", properties: "Directed") + } + + type Directed @relationshipProperties { + year: Int! + movieYear: Int @alias(property: "year") + } + + type Movie @node { + title: String + titleAgain: String @alias(property: "title") + directors: [Director!]! @relationship(direction: IN, type: "DIRECTED", properties: "Directed") + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Query node with alias", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + titleAgain + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, titleAgain: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("Query node and relationship with alias", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + titleAgain + directors { + connection { + edges { + node { + name + nameAgain + } + properties { + year + movieYear + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:DIRECTED]-(this2:Director) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ properties: { year: this1.year, movieYear: this1.year }, node: { name: this2.name, nameAgain: this2.name, __resolveType: \\"Director\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, titleAgain: this0.title, directors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/directives/id/create-id.test.ts b/packages/graphql/tests/api-v6/tck/directives/id/create-id.test.ts new file mode 100644 index 0000000000..c9a23ed538 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/directives/id/create-id.test.ts @@ -0,0 +1,99 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Top-Level Create with autogenerated id", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + id: ID! @id + title: String! + released: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should create two movies", async () => { + const mutation = /* GraphQL */ ` + mutation { + createMovies( + input: [ + { node: { title: "The Matrix" } } + { node: { title: "The Matrix Reloaded", released: 2001 } } + ] + ) { + info { + nodesCreated + } + } + } + `; + + const result = await translateQuery(neoSchema, mutation, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $param0 AS var0 + CALL { + WITH var0 + CREATE (this1:Movie) + SET + this1.id = randomUUID(), + this1.title = var0.title, + this1.released = var0.released + RETURN this1 + } + WITH * + WITH collect({ node: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1 + RETURN collect({ node: { __id: id(this1), __resolveType: \\"Movie\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"title\\": \\"The Matrix\\" + }, + { + \\"title\\": \\"The Matrix Reloaded\\", + \\"released\\": { + \\"low\\": 2001, + \\"high\\": 0 + } + } + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/directives/limit/query-limit.test.ts b/packages/graphql/tests/api-v6/tck/directives/limit/query-limit.test.ts new file mode 100644 index 0000000000..276dce0efc --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/directives/limit/query-limit.test.ts @@ -0,0 +1,382 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("@limit directive", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node @limit(default: 3, max: 5) { + id: ID! + actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type Person @node @limit(default: 2) { + id: ID! + } + + type Show @node @limit(max: 2) { + id: ID! + } + + type Festival @node { + name: String! + shows: [Show!]! @relationship(type: "PART_OF", direction: IN) + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should limit the top level query with default value", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + id + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + LIMIT $param0 + RETURN collect({ node: { id: this0.id, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 3, + \\"high\\": 0 + } + }" + `); + }); + + test("should limit the top level query with max value if not default is available", async () => { + const query = /* GraphQL */ ` + query { + shows { + connection { + edges { + node { + id + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Show) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + LIMIT $param0 + RETURN collect({ node: { id: this0.id, __resolveType: \\"Show\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 2, + \\"high\\": 0 + } + }" + `); + }); + + test("should limit the top level query with max value if `first` provided is higher", async () => { + const query = /* GraphQL */ ` + query { + shows { + connection(first: 5) { + edges { + node { + id + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Show) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + LIMIT $param0 + RETURN collect({ node: { id: this0.id, __resolveType: \\"Show\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 2, + \\"high\\": 0 + } + }" + `); + }); + + test("should limit the normal field level query", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + id + actors { + connection { + edges { + node { + id + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + LIMIT $param0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Person) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + LIMIT $param1 + RETURN collect({ node: { id: this2.id, __resolveType: \\"Person\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { id: this0.id, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 3, + \\"high\\": 0 + }, + \\"param1\\": { + \\"low\\": 2, + \\"high\\": 0 + } + }" + `); + }); + + test("should override the default limit to the nested field if `first` provided", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + id + actors { + connection(first: 4) { + edges { + node { + id + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + LIMIT $param0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Person) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + LIMIT $param1 + RETURN collect({ node: { id: this2.id, __resolveType: \\"Person\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { id: this0.id, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 3, + \\"high\\": 0 + }, + \\"param1\\": { + \\"low\\": 4, + \\"high\\": 0 + } + }" + `); + }); + + test("should override the default limit to the nested field if `first` provided, honouring the `max` argument", async () => { + const query = /* GraphQL */ ` + query { + festivals { + connection { + edges { + node { + name + shows { + connection(first: 3) { + edges { + node { + id + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Festival) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:PART_OF]-(this2:Show) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + LIMIT $param0 + RETURN collect({ node: { id: this2.id, __resolveType: \\"Show\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { name: this0.name, shows: var4, __resolveType: \\"Festival\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 2, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/array/array-filters.test.ts b/packages/graphql/tests/api-v6/tck/filters/array/array-filters.test.ts new file mode 100644 index 0000000000..45122a56ce --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/array/array-filters.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Array filters", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + alternativeTitles: [String!] + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("array filters", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { alternativeTitles: { equals: ["potato"] } } }) { + connection { + edges { + node { + alternativeTitles + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.alternativeTitles = $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { alternativeTitles: this0.alternativeTitles, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + \\"potato\\" + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/logical-filters/and-filter.test.ts b/packages/graphql/tests/api-v6/tck/filters/logical-filters/and-filter.test.ts new file mode 100644 index 0000000000..24b30aa632 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/logical-filters/and-filter.test.ts @@ -0,0 +1,126 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("AND filters", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + year: Int + runtime: Float + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("AND logical filter in where", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { AND: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 100 } } }] } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE (this0.title = $param0 AND this0.year = $param1) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\", + \\"param1\\": { + \\"low\\": 100, + \\"high\\": 0 + } + }" + `); + }); + + test("AND logical filter in nodes", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { AND: [{ title: { equals: "The Matrix" } }, { year: { equals: 100 } }] } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE (this0.title = $param0 AND this0.year = $param1) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\", + \\"param1\\": { + \\"low\\": 100, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/logical-filters/not-filter.test.ts b/packages/graphql/tests/api-v6/tck/filters/logical-filters/not-filter.test.ts new file mode 100644 index 0000000000..73644ff926 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/logical-filters/not-filter.test.ts @@ -0,0 +1,120 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("NOT filters", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + year: Int + runtime: Float + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("NOT logical filter in where", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { NOT: { node: { title: { equals: "The Matrix" } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE NOT (this0.title = $param0) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\" + }" + `); + }); + + test("NOT logical filter on nodes", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { NOT: { title: { equals: "The Matrix" }, year: { equals: 100 } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE NOT (this0.title = $param0 AND this0.year = $param1) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\", + \\"param1\\": { + \\"low\\": 100, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/logical-filters/or-filter.test.ts b/packages/graphql/tests/api-v6/tck/filters/logical-filters/or-filter.test.ts new file mode 100644 index 0000000000..8c1125c566 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/logical-filters/or-filter.test.ts @@ -0,0 +1,183 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("OR filters", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + year: Int + runtime: Float + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("OR logical filter in where", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { OR: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 100 } } }] } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE (this0.title = $param0 OR this0.year = $param1) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\", + \\"param1\\": { + \\"low\\": 100, + \\"high\\": 0 + } + }" + `); + }); + + test("OR logical filter in nodes", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { OR: [{ title: { equals: "The Matrix" } }, { year: { equals: 100 } }] } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE (this0.title = $param0 OR this0.year = $param1) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\", + \\"param1\\": { + \\"low\\": 100, + \\"high\\": 0 + } + }" + `); + }); + + test("top level OR filter combined with implicit AND and nested not", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { + OR: [ + { + NOT: { node: { runtime: { equals: 2 } } } + node: { title: { equals: "The Matrix" }, year: { equals: 2 } } + } + { node: { year: { equals: 100 } } } + ] + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE ((NOT (this0.runtime = $param0) AND (this0.title = $param1 AND this0.year = $param2)) OR this0.year = $param3) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": 2, + \\"param1\\": \\"The Matrix\\", + \\"param2\\": { + \\"low\\": 2, + \\"high\\": 0 + }, + \\"param3\\": { + \\"low\\": 100, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts b/packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts new file mode 100644 index 0000000000..5a8602e91c --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts @@ -0,0 +1,197 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Nested Filters with all", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + type ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("query nested relationship with all filter", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { actors: { all: { edges: { node: { name: { equals: "Keanu" } } } } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE (EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE this2.name = $param0 + } AND NOT (EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE NOT (this2.name = $param0) + })) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\" + }" + `); + }); + + test("query nested relationship properties with all filter", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { actors: { all: { edges: { properties: { year: { equals: 1999 } } } } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE (EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE this1.year = $param0 + } AND NOT (EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE NOT (this1.year = $param0) + })) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 1999, + \\"high\\": 0 + } + }" + `); + }); + + test("query nested relationship with all filter and OR operator", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { + node: { + actors: { + all: { + edges: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "eeves" } } } + ] + } + } + } + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE (EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE (this2.name = $param0 OR this2.name ENDS WITH $param1) + } AND NOT (EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE NOT (this2.name = $param0 OR this2.name ENDS WITH $param1) + })) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\", + \\"param1\\": \\"eeves\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts b/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts new file mode 100644 index 0000000000..23810c3244 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts @@ -0,0 +1,188 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Nested Filters with none", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + type ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("query nested relationship with none filter", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { actors: { none: { edges: { node: { name: { equals: "Keanu" } } } } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE NOT (EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE this2.name = $param0 + }) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\" + }" + `); + }); + + test("query nested relationship properties with none filter", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { actors: { none: { edges: { properties: { year: { equals: 1999 } } } } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE NOT (EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE this1.year = $param0 + }) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 1999, + \\"high\\": 0 + } + }" + `); + }); + + test("query nested relationship with none filter and OR operator", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { + node: { + actors: { + some: { + edges: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "eeves" } } } + ] + } + } + } + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE (this2.name = $param0 OR this2.name ENDS WITH $param1) + } + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\", + \\"param1\\": \\"eeves\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts b/packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts new file mode 100644 index 0000000000..2aff3b39c5 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/nested/single.test.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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Nested Filters with single", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + type ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("query nested relationship with single filter", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { actors: { single: { edges: { node: { name: { equals: "Keanu" } } } } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE single(this1 IN [(this0)<-[this2:ACTED_IN]-(this1:Actor) WHERE this1.name = $param0 | 1] WHERE true) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\" + }" + `); + }); + + test("query nested relationship properties with single filter", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { actors: { single: { edges: { properties: { year: { equals: 1999 } } } } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE single(this2 IN [(this0)<-[this1:ACTED_IN]-(this2:Actor) WHERE this1.year = $param0 | 1] WHERE true) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 1999, + \\"high\\": 0 + } + }" + `); + }); + + test("query nested relationship with single filter and OR operator", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { + node: { + actors: { + single: { + edges: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "eeves" } } } + ] + } + } + } + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE single(this1 IN [(this0)<-[this2:ACTED_IN]-(this1:Actor) WHERE (this1.name = $param0 OR this1.name ENDS WITH $param1) | 1] WHERE true) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\", + \\"param1\\": \\"eeves\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts b/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts new file mode 100644 index 0000000000..c88db78416 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts @@ -0,0 +1,188 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Nested Filters with some", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + type ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("query nested relationship with some filter", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { actors: { some: { edges: { node: { name: { equals: "Keanu" } } } } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE this2.name = $param0 + } + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\" + }" + `); + }); + + test("query nested relationship properties with some filter", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { actors: { some: { edges: { properties: { year: { equals: 1999 } } } } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE this1.year = $param0 + } + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 1999, + \\"high\\": 0 + } + }" + `); + }); + + test("query nested relationship with some filter and OR operator", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { + node: { + actors: { + some: { + edges: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "eeves" } } } + ] + } + } + } + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE EXISTS { + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE (this2.name = $param0 OR this2.name ENDS WITH $param1) + } + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\", + \\"param1\\": \\"eeves\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/relationships/filters-on-relationships.test.ts b/packages/graphql/tests/api-v6/tck/filters/relationships/filters-on-relationships.test.ts new file mode 100644 index 0000000000..a2460147c1 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/relationships/filters-on-relationships.test.ts @@ -0,0 +1,239 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Relationship", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + type ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("query relationship nodes", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors(where: { edges: { node: { name: { equals: "Keanu" } } } }) { + connection { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE this2.name = $param0 + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\" + }" + `); + }); + + test("query relationship properties", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors(where: { edges: { properties: { year: { equals: 1999 } } } }) { + connection { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE this1.year = $param0 + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 1999, + \\"high\\": 0 + } + }" + `); + }); + test("query relationship properties with node", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors( + where: { + edges: { + properties: { year: { equals: 1999 } } + node: { name: { equals: "Keanu" } } + } + } + ) { + connection { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE (this2.name = $param0 AND this1.year = $param1) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Keanu\\", + \\"param1\\": { + \\"low\\": 1999, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/relationships/relationship-not.test.ts b/packages/graphql/tests/api-v6/tck/filters/relationships/relationship-not.test.ts new file mode 100644 index 0000000000..05fa0bbf34 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/relationships/relationship-not.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("NOT filters", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + type ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("NOT operator on nested edge properties", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors(where: { edges: { properties: { NOT: { year: { equals: 1999 } } } } }) { + connection { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WHERE NOT (this1.year = $param0) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 1999, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/top-level-filters.test.ts b/packages/graphql/tests/api-v6/tck/filters/top-level-filters.test.ts new file mode 100644 index 0000000000..20b5173acc --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/top-level-filters.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Top level filters", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + year: Int + runtime: Float + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Query scalar types", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { + node: { title: { equals: "The Matrix" }, year: { equals: 100 }, runtime: { equals: 90.5 } } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE (this0.title = $param0 AND this0.year = $param1 AND this0.runtime = $param2) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\", + \\"param1\\": { + \\"low\\": 100, + \\"high\\": 0 + }, + \\"param2\\": 90.5 + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/cartesian-filters.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/cartesian-filters.test.ts new file mode 100644 index 0000000000..aa3a0ded88 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/cartesian-filters.test.ts @@ -0,0 +1,485 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("CartesianPoint filters", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Location @node { + id: String + value: CartesianPoint + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("CartesianPoint EQUALS", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { equals: { x: 1.0, y: 2.0 } } } }) { + connection { + edges { + node { + value { + x + y + crs + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE this0.value = point($param0) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value, crs: this0.value.crs } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"x\\": 1, + \\"y\\": 2 + } + }" + `); + }); + + test("Simple Point NOT EQUALS", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { NOT: { equals: { x: 1.0, y: 2.0 } } } }) { + connection { + edges { + node { + value { + x + y + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE NOT (this0.value = point($param0)) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"x\\": 1, + \\"y\\": 2 + } + }" + `); + }); + + test("Simple Point IN", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { in: [{ x: 1.0, y: 2.0 }] } } }) { + connection { + edges { + node { + value { + x + y + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE this0.value IN [var1 IN $param0 | point(var1)] + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"x\\": 1, + \\"y\\": 2 + } + ] + }" + `); + }); + + test("Simple Point NOT IN", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { NOT: { in: [{ x: 1.0, y: 2.0 }] } } } }) { + connection { + edges { + node { + value { + x + y + crs + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE NOT (this0.value IN [var1 IN $param0 | point(var1)]) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value, crs: this0.value.crs } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"x\\": 1, + \\"y\\": 2 + } + ] + }" + `); + }); + + test("Simple Point LT", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { lt: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { + connection { + edges { + node { + value { + x + y + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) < $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"x\\": 1.1, + \\"y\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); + + test("Simple Point LTE", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { lte: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { + connection { + edges { + node { + value { + x + y + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) <= $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"x\\": 1.1, + \\"y\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); + + test("Simple Point GT", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { gt: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { + connection { + edges { + node { + value { + x + y + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) > $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"x\\": 1.1, + \\"y\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); + + test("Simple Point GTE", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { gte: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { + connection { + edges { + node { + value { + x + y + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) >= $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"x\\": 1.1, + \\"y\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); + + test("Simple Point DISTANCE EQ", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { distance: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { + connection { + edges { + node { + value { + x + y + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) = $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"x\\": 1.1, + \\"y\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/number/number-equals.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/number/number-equals.test.ts new file mode 100644 index 0000000000..7a32e29e25 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/number/number-equals.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("Number filtering - equals", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + rating: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("filter by 'equals'", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { rating: { equals: 10 } } }) { + connection { + edges { + node { + rating + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.rating = $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { rating: this0.rating, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 10, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/number/number-gt.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/number/number-gt.test.ts new file mode 100644 index 0000000000..6a25e90495 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/number/number-gt.test.ts @@ -0,0 +1,120 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("Number filtering - greater than", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + rating: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("filter by 'gt'", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { rating: { gt: 10 } } }) { + connection { + edges { + node { + rating + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.rating > $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { rating: this0.rating, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 10, + \\"high\\": 0 + } + }" + `); + }); + + test("filter by 'gte'", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { rating: { gte: 10 } } }) { + connection { + edges { + node { + rating + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.rating >= $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { rating: this0.rating, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 10, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/number/number-lt.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/number/number-lt.test.ts new file mode 100644 index 0000000000..e83a5058b8 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/number/number-lt.test.ts @@ -0,0 +1,120 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("Number filtering - less than", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + rating: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("filter by 'lt'", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { rating: { lt: 10 } } }) { + connection { + edges { + node { + rating + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.rating < $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { rating: this0.rating, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 10, + \\"high\\": 0 + } + }" + `); + }); + + test("filter by 'lte'", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { rating: { lte: 10 } } }) { + connection { + edges { + node { + rating + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.rating <= $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { rating: this0.rating, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 10, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/point-filters.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/point-filters.test.ts new file mode 100644 index 0000000000..b66d350959 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/point-filters.test.ts @@ -0,0 +1,497 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +// Skip Spatial types waiting for the new operator design +// eslint-disable-next-line jest/no-disabled-tests +describe.skip("Point filters", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Location @node { + id: String + value: Point + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Simple Point EQUALS", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { equals: { longitude: 1.0, latitude: 2.0 } } } }) { + connection { + edges { + node { + value { + longitude + latitude + crs + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE this0.value = point($param0) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value, crs: this0.value.crs } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"longitude\\": 1, + \\"latitude\\": 2 + } + }" + `); + }); + + test("Simple Point NOT EQUALS", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { NOT: { equals: { longitude: 1.0, latitude: 2.0 } } } } }) { + connection { + edges { + node { + value { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE NOT (this0.value = point($param0)) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"longitude\\": 1, + \\"latitude\\": 2 + } + }" + `); + }); + + test("Simple Point IN", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { in: [{ longitude: 1.0, latitude: 2.0 }] } } }) { + connection { + edges { + node { + value { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE this0.value IN [var1 IN $param0 | point(var1)] + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"longitude\\": 1, + \\"latitude\\": 2 + } + ] + }" + `); + }); + + test("Simple Point NOT IN", async () => { + const query = /* GraphQL */ ` + { + locations(where: { node: { value: { NOT: { in: [{ longitude: 1.0, latitude: 2.0 }] } } } }) { + connection { + edges { + node { + value { + longitude + latitude + crs + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE NOT (this0.value IN [var1 IN $param0 | point(var1)]) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value, crs: this0.value.crs } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"longitude\\": 1, + \\"latitude\\": 2 + } + ] + }" + `); + }); + + test("Simple Point LT", async () => { + const query = /* GraphQL */ ` + { + locations( + where: { node: { value: { lt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } + ) { + connection { + edges { + node { + value { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) < $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"longitude\\": 1.1, + \\"latitude\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); + + test("Simple Point LTE", async () => { + const query = /* GraphQL */ ` + { + locations( + where: { node: { value: { lte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } + ) { + connection { + edges { + node { + value { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) <= $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"longitude\\": 1.1, + \\"latitude\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); + + test("Simple Point GT", async () => { + const query = /* GraphQL */ ` + { + locations( + where: { node: { value: { gt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } + ) { + connection { + edges { + node { + value { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) > $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"longitude\\": 1.1, + \\"latitude\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); + + test("Simple Point GTE", async () => { + const query = /* GraphQL */ ` + { + locations( + where: { node: { value: { gte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } + ) { + connection { + edges { + node { + value { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) >= $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"longitude\\": 1.1, + \\"latitude\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); + + test("Simple Point DISTANCE EQ", async () => { + const query = /* GraphQL */ ` + { + locations( + where: { + node: { value: { distance: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } + } + ) { + connection { + edges { + node { + value { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WHERE point.distance(this0.value, point($param0.point)) = $param0.distance + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"point\\": { + \\"longitude\\": 1.1, + \\"latitude\\": 2.2 + }, + \\"distance\\": 3.3 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/string/string-contains.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/string/string-contains.test.ts new file mode 100644 index 0000000000..fe0063ec6a --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/string/string-contains.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("String filtering - contains", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("filter by 'contains'", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { title: { contains: "Mat" } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.title CONTAINS $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Mat\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/string/string-ends-with.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/string/string-ends-with.test.ts new file mode 100644 index 0000000000..dac8433f4b --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/string/string-ends-with.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("String filtering - endsWith", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("filter by 'endsWith'", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { title: { endsWith: "The" } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.title ENDS WITH $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/string/string-in.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/string/string-in.test.ts new file mode 100644 index 0000000000..12c049c491 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/string/string-in.test.ts @@ -0,0 +1,79 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("String filtering - in", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("filter by 'in'", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { title: { in: ["The Matrix", "The Matrix 2"] } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.title IN $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + \\"The Matrix\\", + \\"The Matrix 2\\" + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/string/string-starts-with.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/string/string-starts-with.test.ts new file mode 100644 index 0000000000..4c323a032d --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/string/string-starts-with.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("String filtering - startsWith", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("filter by 'startsWith'", async () => { + const query = /* GraphQL */ ` + query { + movies(where: { node: { title: { startsWith: "The" } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.title STARTS WITH $param0 + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/time/temporals-array.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/time/temporals-array.test.ts new file mode 100644 index 0000000000..4450a55236 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/time/temporals-array.test.ts @@ -0,0 +1,420 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("Temporal types", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type TypeNode @node { + dateTime: [DateTime!] + localDateTime: [LocalDateTime!] + duration: [Duration!] + time: [Time!] + localTime: [LocalTime!] + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNodeProperties @relationshipProperties { + dateTime: [DateTime!] + localDateTime: [LocalDateTime!] + duration: [Duration!] + time: [Time!] + localTime: [LocalTime!] + } + + type RelatedNode @node { + dateTime: [DateTime!] + localDateTime: [LocalDateTime!] + duration: [Duration!] + time: [Time!] + localTime: [LocalTime!] + } + `; + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should filter temporal array types - Top-Level", async () => { + const query = /* GraphQL */ ` + query { + typeNodes( + where: { + node: { + dateTime: { equals: ["2015-06-24T12:50:35.556+0100"] } + localDateTime: { equals: ["2003-09-14T12:00:00"] } + duration: { equals: ["P1Y"] } + time: { equals: ["22:00:15.555"] } + localTime: { equals: ["12:50:35.556"] } + } + } + ) { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WHERE (this0.dateTime = $param0 AND this0.localDateTime = $param1 AND this0.duration = $param2 AND this0.time = $param3 AND this0.localTime = $param4) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { dateTime: [var1 IN this0.dateTime | apoc.date.convertFormat(toString(var1), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: this0.localDateTime, duration: this0.duration, time: this0.time, localTime: this0.localTime, __resolveType: \\"TypeNode\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"year\\": 2015, + \\"month\\": 6, + \\"day\\": 24, + \\"hour\\": 11, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param1\\": [ + { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + } + ], + \\"param2\\": [ + { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } + ], + \\"param3\\": [ + { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param4\\": [ + { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + ] + }" + `); + }); + + test("should filter temporal array types - Nested", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + relatedNode( + where: { + edges: { + node: { + dateTime: { equals: ["2015-06-24T12:50:35.556+0100"] } + localDateTime: { equals: ["2003-09-14T12:00:00"] } + duration: { equals: ["P1Y"] } + time: { equals: ["22:00:15.555"] } + localTime: { equals: ["12:50:35.556"] } + } + } + } + ) { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) + WHERE (this2.dateTime = $param0 AND this2.localDateTime = $param1 AND this2.duration = $param2 AND this2.time = $param3 AND this2.localTime = $param4) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { dateTime: [var3 IN this2.dateTime | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: this2.localDateTime, duration: this2.duration, time: this2.time, localTime: this2.localTime, __resolveType: \\"RelatedNode\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 + } + RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 + } + RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"year\\": 2015, + \\"month\\": 6, + \\"day\\": 24, + \\"hour\\": 11, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param1\\": [ + { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + } + ], + \\"param2\\": [ + { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } + ], + \\"param3\\": [ + { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param4\\": [ + { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + ] + }" + `); + }); + + test("should filter temporal array types - Relationship properties", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + relatedNode( + where: { + edges: { + properties: { + dateTime: { equals: ["2015-06-24T12:50:35.556+0100"] } + localDateTime: { equals: ["2003-09-14T12:00:00"] } + duration: { equals: ["P1Y"] } + time: { equals: ["22:00:15.555"] } + localTime: { equals: ["12:50:35.556"] } + } + } + } + ) { + connection { + edges { + properties { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) + WHERE (this1.dateTime = $param0 AND this1.localDateTime = $param1 AND this1.duration = $param2 AND this1.time = $param3 AND this1.localTime = $param4) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ properties: { dateTime: [var3 IN this1.dateTime | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: this1.localDateTime, duration: this1.duration, time: this1.time, localTime: this1.localTime }, node: { __id: id(this2), __resolveType: \\"RelatedNode\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 + } + RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 + } + RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"year\\": 2015, + \\"month\\": 6, + \\"day\\": 24, + \\"hour\\": 11, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param1\\": [ + { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + } + ], + \\"param2\\": [ + { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } + ], + \\"param3\\": [ + { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param4\\": [ + { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/time/temporals-filters.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/time/temporals-filters.test.ts new file mode 100644 index 0000000000..63a885aacf --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/time/temporals-filters.test.ts @@ -0,0 +1,390 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("Temporal types", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type TypeNode @node { + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNodeProperties @relationshipProperties { + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + } + + type RelatedNode @node { + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + } + `; + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should filter temporal types - Top-Level", async () => { + const query = /* GraphQL */ ` + query { + typeNodes( + where: { + node: { + dateTime: { equals: "2015-06-24T12:50:35.556+0100" } + localDateTime: { gt: "2003-09-14T12:00:00" } + duration: { gte: "P1Y" } + time: { lt: "22:00:15.555" } + localTime: { lte: "12:50:35.556" } + } + } + ) { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WHERE (this0.dateTime = $param0 AND this0.localDateTime > $param1 AND (datetime() + this0.duration) >= (datetime() + $param2) AND this0.time < $param3 AND this0.localTime <= $param4) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { dateTime: apoc.date.convertFormat(toString(this0.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: this0.localDateTime, duration: this0.duration, time: this0.time, localTime: this0.localTime, __resolveType: \\"TypeNode\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"year\\": 2015, + \\"month\\": 6, + \\"day\\": 24, + \\"hour\\": 11, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000, + \\"timeZoneOffsetSeconds\\": 0 + }, + \\"param1\\": { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + }, + \\"param2\\": { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + }, + \\"param3\\": { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + }, + \\"param4\\": { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + }" + `); + }); + + test("should filter temporal types - Nested", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + relatedNode( + where: { + edges: { + node: { + dateTime: { equals: "2015-06-24T12:50:35.556+0100" } + localDateTime: { gt: "2003-09-14T12:00:00" } + duration: { gte: "P1Y" } + time: { lt: "22:00:15.555" } + localTime: { lte: "12:50:35.556" } + } + } + } + ) { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) + WHERE (this2.dateTime = $param0 AND this2.localDateTime > $param1 AND (datetime() + this2.duration) >= (datetime() + $param2) AND this2.time < $param3 AND this2.localTime <= $param4) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { dateTime: apoc.date.convertFormat(toString(this2.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: this2.localDateTime, duration: this2.duration, time: this2.time, localTime: this2.localTime, __resolveType: \\"RelatedNode\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { relatedNode: var4, __resolveType: \\"TypeNode\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"year\\": 2015, + \\"month\\": 6, + \\"day\\": 24, + \\"hour\\": 11, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000, + \\"timeZoneOffsetSeconds\\": 0 + }, + \\"param1\\": { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + }, + \\"param2\\": { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + }, + \\"param3\\": { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + }, + \\"param4\\": { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + }" + `); + }); + + test("should filter temporal types - Relationship properties", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + relatedNode( + where: { + edges: { + properties: { + dateTime: { equals: "2015-06-24T12:50:35.556+0100" } + localDateTime: { gt: "2003-09-14T12:00:00" } + duration: { gte: "P1Y" } + time: { lt: "22:00:15.555" } + localTime: { lte: "12:50:35.556" } + } + } + } + ) { + connection { + edges { + properties { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) + WHERE (this1.dateTime = $param0 AND this1.localDateTime > $param1 AND (datetime() + this1.duration) >= (datetime() + $param2) AND this1.time < $param3 AND this1.localTime <= $param4) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ properties: { dateTime: apoc.date.convertFormat(toString(this1.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: this1.localDateTime, duration: this1.duration, time: this1.time, localTime: this1.localTime }, node: { __id: id(this2), __resolveType: \\"RelatedNode\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { relatedNode: var4, __resolveType: \\"TypeNode\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"year\\": 2015, + \\"month\\": 6, + \\"day\\": 24, + \\"hour\\": 11, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000, + \\"timeZoneOffsetSeconds\\": 0 + }, + \\"param1\\": { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + }, + \\"param2\\": { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + }, + \\"param3\\": { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + }, + \\"param4\\": { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/mutations/create/create.test.ts b/packages/graphql/tests/api-v6/tck/mutations/create/create.test.ts new file mode 100644 index 0000000000..9c48f6b30f --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/mutations/create/create.test.ts @@ -0,0 +1,97 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Top-Level Create", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String! + released: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should create two movies", async () => { + const mutation = /* GraphQL */ ` + mutation { + createMovies( + input: [ + { node: { title: "The Matrix" } } + { node: { title: "The Matrix Reloaded", released: 2001 } } + ] + ) { + info { + nodesCreated + } + } + } + `; + + const result = await translateQuery(neoSchema, mutation, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $param0 AS var0 + CALL { + WITH var0 + CREATE (this1:Movie) + SET + this1.title = var0.title, + this1.released = var0.released + RETURN this1 + } + WITH * + WITH collect({ node: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1 + RETURN collect({ node: { __id: id(this1), __resolveType: \\"Movie\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"title\\": \\"The Matrix\\" + }, + { + \\"title\\": \\"The Matrix Reloaded\\", + \\"released\\": { + \\"low\\": 2001, + \\"high\\": 0 + } + } + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts b/packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts new file mode 100644 index 0000000000..1386a38cbc --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Top-Level Update", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String! + released: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should update movie", async () => { + const mutation = /* GraphQL */ ` + mutation UpdateMovies { + updateMovies( + input: { node: { title: { set: "The Matrix" } } } + where: { node: { title: { equals: "Matrix" } } } + ) { + movies { + title + } + } + } + `; + + const result = await translateQuery(neoSchema, mutation, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WHERE this0.title = $param0 + SET + this0.title = $param1 + WITH * + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"Matrix\\", + \\"param1\\": \\"The Matrix\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/pagination/after.test.ts b/packages/graphql/tests/api-v6/tck/pagination/after.test.ts new file mode 100644 index 0000000000..f2a5dbfe51 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/pagination/after.test.ts @@ -0,0 +1,94 @@ +/* + * 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 { offsetToCursor } from "graphql-relay"; +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Pagination - After argument", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Director @node { + name: String + movies: [Movie!]! @relationship(direction: OUT, type: "DIRECTED", properties: "Directed") + } + + type Directed @relationshipProperties { + year: Int! + movieYear: Int @alias(property: "year") + } + + type Movie @node { + title: String + directors: [Director!]! @relationship(direction: IN, type: "DIRECTED", properties: "Directed") + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Query top level with after", async () => { + const cursor = offsetToCursor(10); + + const query = /* GraphQL */ ` + query { + movies { + connection(after: "${cursor}") { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + SKIP $param0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 10, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/pagination/first-after.test.ts b/packages/graphql/tests/api-v6/tck/pagination/first-after.test.ts new file mode 100644 index 0000000000..75b648af74 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/pagination/first-after.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { offsetToCursor } from "graphql-relay"; +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Pagination - First argument", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Director @node { + name: String + movies: [Movie!]! @relationship(direction: OUT, type: "DIRECTED", properties: "Directed") + } + + type Directed @relationshipProperties { + year: Int! + movieYear: Int @alias(property: "year") + } + + type Movie @node { + title: String + directors: [Director!]! @relationship(direction: IN, type: "DIRECTED", properties: "Directed") + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Get movies with first and after argument", async () => { + const cursor = offsetToCursor(5); + + const query = /* GraphQL */ ` + query { + movies { + connection(first: 10, after: "${cursor}", sort: { node: { title: ASC } }) { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.title ASC + SKIP $param0 + LIMIT $param1 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 5, + \\"high\\": 0 + }, + \\"param1\\": { + \\"low\\": 10, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/pagination/first.test.ts b/packages/graphql/tests/api-v6/tck/pagination/first.test.ts new file mode 100644 index 0000000000..5cbe2e6ee6 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/pagination/first.test.ts @@ -0,0 +1,91 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Pagination - First argument", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Director @node { + name: String + movies: [Movie!]! @relationship(direction: OUT, type: "DIRECTED", properties: "Directed") + } + + type Directed @relationshipProperties { + year: Int! + movieYear: Int @alias(property: "year") + } + + type Movie @node { + title: String + directors: [Director!]! @relationship(direction: IN, type: "DIRECTED", properties: "Directed") + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Query top level with first", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection(first: 10) { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + LIMIT $param0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 10, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/projection/aliasing.test.ts b/packages/graphql/tests/api-v6/tck/projection/aliasing.test.ts new file mode 100644 index 0000000000..9830be0662 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/aliasing.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Aliasing", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + type ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should query relationship properties with aliased fields", async () => { + const query = /* GraphQL */ ` + query { + myMovies: movies { + connection { + edges { + node { + movieTitle: title + actors { + connection { + edges { + properties { + releaseYear: year + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ properties: { releaseYear: this1.year }, node: { __id: id(this2), __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { movieTitle: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/projection/create/create.test.ts b/packages/graphql/tests/api-v6/tck/projection/create/create.test.ts new file mode 100644 index 0000000000..b4e6704eb8 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/create/create.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Create Projection", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String! + released: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should create two movies and project them", async () => { + const mutation = /* GraphQL */ ` + mutation { + createMovies( + input: [ + { node: { title: "The Matrix" } } + { node: { title: "The Matrix Reloaded", released: 2001 } } + ] + ) { + movies { + title + released + } + } + } + `; + + const result = await translateQuery(neoSchema, mutation, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "UNWIND $param0 AS var0 + CALL { + WITH var0 + CREATE (this1:Movie) + SET + this1.title = var0.title, + this1.released = var0.released + RETURN this1 + } + WITH * + WITH collect({ node: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1 + RETURN collect({ node: { title: this1.title, released: this1.released, __resolveType: \\"Movie\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"title\\": \\"The Matrix\\" + }, + { + \\"title\\": \\"The Matrix Reloaded\\", + \\"released\\": { + \\"low\\": 2001, + \\"high\\": 0 + } + } + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts b/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts new file mode 100644 index 0000000000..da17c28b22 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts @@ -0,0 +1,156 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Relationship", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + type Actor @node { + name: String + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + type ActedIn @relationshipProperties { + year: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should query a relationship", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("should query relationship properties", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection { + edges { + properties { + year + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ properties: { year: this1.year }, node: { __id: id(this2), __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/projection/simple-query.test.ts b/packages/graphql/tests/api-v6/tck/projection/simple-query.test.ts new file mode 100644 index 0000000000..a742612f2a --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/simple-query.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Simple Query", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String! + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Simple", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/projection/types/array/temporals-array.test.ts b/packages/graphql/tests/api-v6/tck/projection/types/array/temporals-array.test.ts new file mode 100644 index 0000000000..d33efa3e6c --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/types/array/temporals-array.test.ts @@ -0,0 +1,213 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; + +describe("Temporal types", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type TypeNode @node { + dateTime: [DateTime!] + localDateTime: [LocalDateTime!] + duration: [Duration!] + time: [Time!] + localTime: [LocalTime!] + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNodeProperties @relationshipProperties { + dateTime: [DateTime!] + localDateTime: [LocalDateTime!] + duration: [Duration!] + time: [Time!] + localTime: [LocalTime!] + } + + type RelatedNode @node { + dateTime: [DateTime!] + localDateTime: [LocalDateTime!] + duration: [Duration!] + time: [Time!] + localTime: [LocalTime!] + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should be possible to querying temporal fields - Top-Level", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { dateTime: [var1 IN this0.dateTime | apoc.date.convertFormat(toString(var1), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: this0.localDateTime, duration: this0.duration, time: this0.time, localTime: this0.localTime, __resolveType: \\"TypeNode\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("should be possible to querying temporal fields - Nested", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + relatedNode { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { dateTime: [var3 IN this2.dateTime | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: this2.localDateTime, duration: this2.duration, time: this2.time, localTime: this2.localTime, __resolveType: \\"RelatedNode\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 + } + RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 + } + RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("should be possible to querying temporal fields - Relationship properties", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + relatedNode { + connection { + edges { + properties { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ properties: { dateTime: [var3 IN this1.dateTime | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: this1.localDateTime, duration: this1.duration, time: this1.time, localTime: this1.localTime }, node: { __id: id(this2), __resolveType: \\"RelatedNode\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 + } + RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 + } + RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/projection/types/cartesian.test.ts b/packages/graphql/tests/api-v6/tck/projection/types/cartesian.test.ts new file mode 100644 index 0000000000..2025f2f235 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/types/cartesian.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Cartesian projection", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Location @node { + id: String + value: CartesianPoint + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("point coordinates", async () => { + const query = /* GraphQL */ ` + { + locations { + connection { + edges { + node { + value { + x + y + z + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("point crs", async () => { + const query = /* GraphQL */ ` + { + locations { + connection { + edges { + node { + value { + x + y + z + crs + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value, crs: this0.value.crs } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("point srid", async () => { + const query = /* GraphQL */ ` + { + locations { + connection { + edges { + node { + value { + x + y + z + srid + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/projection/types/point.test.ts b/packages/graphql/tests/api-v6/tck/projection/types/point.test.ts new file mode 100644 index 0000000000..c534378c69 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/types/point.test.ts @@ -0,0 +1,161 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Point projection", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Location @node { + id: String + value: Point + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("point coordinates", async () => { + const query = /* GraphQL */ ` + { + locations { + connection { + edges { + node { + value { + longitude + latitude + height + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("point crs", async () => { + const query = /* GraphQL */ ` + { + locations { + connection { + edges { + node { + value { + longitude + latitude + height + crs + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value, crs: this0.value.crs } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("point srid", async () => { + const query = /* GraphQL */ ` + { + locations { + connection { + edges { + node { + value { + longitude + latitude + height + srid + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Location) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } + ELSE NULL + END, __resolveType: \\"Location\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts b/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts new file mode 100644 index 0000000000..d09f681b47 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts @@ -0,0 +1,213 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; + +describe("Temporal types", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type TypeNode @node { + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNodeProperties @relationshipProperties { + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + } + + type RelatedNode @node { + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should be possible to querying temporal fields - Top Level", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { dateTime: apoc.date.convertFormat(toString(this0.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: this0.localDateTime, duration: this0.duration, time: this0.time, localTime: this0.localTime, __resolveType: \\"TypeNode\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("should be possible to querying temporal fields - Nested", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + relatedNode { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { dateTime: apoc.date.convertFormat(toString(this2.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: this2.localDateTime, duration: this2.duration, time: this2.time, localTime: this2.localTime, __resolveType: \\"RelatedNode\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { relatedNode: var4, __resolveType: \\"TypeNode\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("should be possible to querying temporal fields - Relationship properties", async () => { + const query = /* GraphQL */ ` + query { + typeNodes { + connection { + edges { + node { + relatedNode { + connection { + edges { + properties { + dateTime + localDateTime + duration + time + localTime + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ properties: { dateTime: apoc.date.convertFormat(toString(this1.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: this1.localDateTime, duration: this1.duration, time: this1.time, localTime: this1.localTime }, node: { __id: id(this2), __resolveType: \\"RelatedNode\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { relatedNode: var4, __resolveType: \\"TypeNode\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/sort/sort-alias.test.ts b/packages/graphql/tests/api-v6/tck/sort/sort-alias.test.ts new file mode 100644 index 0000000000..245be651f3 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/sort/sort-alias.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Sort with alias", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String @alias(property: "movieTitle") + ratings: Int! @alias(property: "movieRatings") + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Top-Level sort", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection(sort: { node: { title: DESC } }) { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.movieTitle DESC + RETURN collect({ node: { title: this0.movieTitle, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("Top-Level multiple sort fields", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection(sort: [{ node: { title: DESC } }, { node: { ratings: DESC } }]) { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.movieTitle DESC, this0.movieRatings DESC + RETURN collect({ node: { title: this0.movieTitle, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/sort/sort-relationship-alias.test.ts b/packages/graphql/tests/api-v6/tck/sort/sort-relationship-alias.test.ts new file mode 100644 index 0000000000..05e2818208 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/sort/sort-relationship-alias.test.ts @@ -0,0 +1,287 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Sort relationship with alias", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + ratings: Int! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Actor @node { + name: String @alias(property: "actorName") + age: Int @alias(property: "actorAge") + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int @alias(property: "actedInYear") + role: String + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Relationship sort", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection(sort: { edges: { node: { name: DESC } } }) { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + ORDER BY this2.actorName DESC + RETURN collect({ node: { name: this2.actorName, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("Relationship sort multiple sort fields", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection( + sort: [{ edges: { node: { name: DESC } } }, { edges: { node: { age: DESC } } }] + ) { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + ORDER BY this2.actorName DESC, this2.actorAge DESC + RETURN collect({ node: { name: this2.actorName, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("Relationship sort on relationship properties", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection(sort: { edges: { properties: { year: DESC } } }) { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + ORDER BY this1.actedInYear DESC + RETURN collect({ node: { name: this2.actorName, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("should respect input order on sorting", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection( + sort: [ + { edges: { properties: { year: DESC } } } + { edges: { node: { name: ASC } } } + { edges: { properties: { role: ASC } } } + ] + ) { + edges { + node { + age + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + ORDER BY this1.actedInYear DESC, this2.actorName ASC, this1.role ASC + RETURN collect({ node: { age: this2.actorAge, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/sort/sort-relationship.test.ts b/packages/graphql/tests/api-v6/tck/sort/sort-relationship.test.ts new file mode 100644 index 0000000000..7d01b3c208 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/sort/sort-relationship.test.ts @@ -0,0 +1,287 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Sort relationship", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + ratings: Int! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Actor @node { + name: String + age: Int + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + type ActedIn @relationshipProperties { + year: Int + role: String + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Relationship sort", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection(sort: { edges: { node: { name: DESC } } }) { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + ORDER BY this2.name DESC + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("Relationship sort multiple sort fields", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection( + sort: [{ edges: { node: { name: DESC } } }, { edges: { node: { age: DESC } } }] + ) { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + ORDER BY this2.name DESC, this2.age DESC + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("Relationship sort on relationship properties", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection(sort: { edges: { properties: { year: DESC } } }) { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + ORDER BY this1.year DESC + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("Should respect input order on sorting", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection( + sort: [ + { edges: { properties: { year: DESC } } } + { edges: { node: { name: ASC } } } + { edges: { properties: { role: ASC } } } + ] + ) { + edges { + node { + age + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + WITH * + ORDER BY this1.year DESC, this2.name ASC, this1.role ASC + RETURN collect({ node: { age: this2.age, __resolveType: \\"Actor\\" } }) AS var3 + } + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 + } + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 + } + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/sort/sort.test.ts b/packages/graphql/tests/api-v6/tck/sort/sort.test.ts new file mode 100644 index 0000000000..08174e251c --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/sort/sort.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; + +describe("Sort", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + ratings: Int! + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Top-Level sort", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection(sort: { node: { title: DESC } }) { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.title DESC + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); + + test("Top-Level multiple sort fields", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection(sort: [{ node: { title: DESC } }, { node: { ratings: DESC } }]) { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.title DESC, this0.ratings DESC + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/integration/aliasing.int.test.ts b/packages/graphql/tests/integration/aliasing.int.test.ts deleted file mode 100644 index e007341de3..0000000000 --- a/packages/graphql/tests/integration/aliasing.int.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { generate } from "randomstring"; -import type { UniqueType } from "../utils/graphql-types"; -import { TestHelper } from "../utils/tests-helper"; - -describe("Aliasing", () => { - const testHelper = new TestHelper(); - - let Movie: UniqueType; - let id: string; - let budget: number; - let boxOffice: number; - - beforeAll(async () => { - Movie = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${Movie} { - id: ID! - budget: Int! - boxOffice: Float! - } - `; - - id = generate({ readable: false }); - budget = 63; - boxOffice = 465.3; - await testHelper.initNeo4jGraphQL({ typeDefs }); - - await testHelper.executeCypher( - ` - CREATE (movie:${Movie}) - SET movie += $properties - `, - { - properties: { - id, - boxOffice, - budget, - }, - } - ); - }); - - afterAll(async () => { - await testHelper.close(); - }); - - test("should correctly alias an ID field", async () => { - const query = ` - query ($id: ID!) { - ${Movie.plural}(where: { id: $id }) { - aliased: id - budget - boxOffice - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query, { - variableValues: { id }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult?.data as any)[Movie.plural][0]).toEqual({ - aliased: id, - budget, - boxOffice, - }); - }); - - test("should correctly alias an Int field", async () => { - const query = ` - query ($id: ID!) { - ${Movie.plural}(where: { id: $id }) { - id - aliased: budget - boxOffice - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query, { - variableValues: { id }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult?.data as any)[Movie.plural][0]).toEqual({ - id, - aliased: budget, - boxOffice, - }); - }); - - test("should correctly alias an Float field", async () => { - const query = ` - query ($id: ID!) { - ${Movie.plural}(where: { id: $id }) { - id - budget - aliased: boxOffice - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query, { - variableValues: { id }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult?.data as any)[Movie.plural][0]).toEqual({ - id, - budget, - aliased: boxOffice, - }); - }); -}); diff --git a/packages/graphql/tests/integration/connections/alias.int.test.ts b/packages/graphql/tests/integration/connections/alias.int.test.ts deleted file mode 100644 index 8642be8c3d..0000000000 --- a/packages/graphql/tests/integration/connections/alias.int.test.ts +++ /dev/null @@ -1,886 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { gql } from "graphql-tag"; -import { generate } from "randomstring"; -import type { UniqueType } from "../../utils/graphql-types"; -import { TestHelper } from "../../utils/tests-helper"; - -describe("Connections Alias", () => { - const testHelper = new TestHelper(); - - let typeMovie: UniqueType; - let typeActor: UniqueType; - - beforeEach(() => { - typeMovie = testHelper.createUniqueType("Movie"); - typeActor = testHelper.createUniqueType("Actor"); - }); - - afterEach(async () => { - await testHelper.close(); - }); - - // using totalCount as the bear minimal selection - test("should alias top level connection field and return correct totalCount", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actors: actorsConnection { - totalCount - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect(result.data as any).toEqual({ - [typeMovie.plural]: [{ actors: { totalCount: 3 } }], - }); - }); - - test("should alias totalCount", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection { - count: totalCount - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect(result.data as any).toEqual({ - [typeMovie.plural]: [{ actorsConnection: { count: 3 } }], - }); - }); - - // using hasNextPage as the bear minimal selection - test("should alias pageInfo top level key", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection { - pi:pageInfo { - hasNextPage - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect(result.data as any).toEqual({ - [typeMovie.plural]: [{ actorsConnection: { pi: { hasNextPage: false } } }], - }); - }); - - test("should alias startCursor", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection { - pageInfo { - sc:startCursor - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.pageInfo.sc).toBeDefined(); - }); - - test("should alias endCursor", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection { - pageInfo { - ec:endCursor - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.pageInfo.ec).toBeDefined(); - }); - - test("should alias hasPreviousPage", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection { - pageInfo { - hPP:hasPreviousPage - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.pageInfo.hPP).toBeDefined(); - }); - - test("should alias hasNextPage", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection(first: 1) { - pageInfo { - hNP:hasNextPage - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.pageInfo.hNP).toBeDefined(); - }); - - test("should alias the top level edges key", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection(first: 1) { - e:edges { - cursor - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.e[0].cursor).toBeDefined(); - }); - - test("should alias cursor", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection(first: 1) { - edges { - c:cursor - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.edges[0].c).toBeDefined(); - }); - - test("should alias the top level node key", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection(first: 1) { - edges { - n:node { - name - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.edges[0].n).toBeDefined(); - }); - - test("should alias a property on the node", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection(first: 1) { - edges { - node { - n:name - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN]-(:${typeActor.name} {name: randomUUID()}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.edges[0].node.n).toBeDefined(); - }); - - test("should alias a property on the relationship", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") - } - - type ActedIn @relationshipProperties { - roles: [String]! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - actorsConnection(first: 1) { - edges { - r:properties { - r:roles - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN {roles: [randomUUID()]}]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN {roles: [randomUUID()]}]-(:${typeActor.name} {name: randomUUID()}) - CREATE (m)<-[:ACTED_IN {roles: [randomUUID()]}]-(:${typeActor.name} {name: randomUUID()}) - `, - { - movieTitle, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.edges[0].r.r).toBeDefined(); - }); - - test("should alias many keys on a connection", async () => { - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") - } - - type ${typeActor.name} { - name: String! - } - - type ActedIn @relationshipProperties { - roles: [String]! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - const actorName = generate({ - charset: "alphabetic", - }); - const roles = [ - generate({ - charset: "alphabetic", - }), - ]; - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - title - connection:actorsConnection { - tC:totalCount - edges { - n:node { - n:name - } - p:properties { - r:roles - } - } - page:pageInfo { - hNP:hasNextPage - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (m:${typeMovie.name} {title: $movieTitle}) - CREATE (m)<-[:ACTED_IN {roles: $roles}]-(:${typeActor.name} {name: $actorName}) - `, - { - movieTitle, - actorName, - roles, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect(result.data as any).toEqual({ - [typeMovie.plural]: [ - { - title: movieTitle, - connection: { - tC: 1, - edges: [{ n: { n: actorName }, p: { r: roles } }], - page: { - hNP: false, - }, - }, - }, - ], - }); - }); - - test("should allow multiple aliases on the same connection", async () => { - const typeDefs = gql` - type Post { - title: String! - comments: [Comment!]! @relationship(type: "HAS_COMMENT", direction: OUT) - } - type Comment { - flag: Boolean! - post: Post! @relationship(type: "HAS_COMMENT", direction: IN) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const postTitle = generate({ charset: "alphabetic" }); - - const flags = [true, true, false]; - - const flaggedCount = flags.filter((flag) => flag).length; - const unflaggedCount = flags.filter((flag) => !flag).length; - - const query = ` - { - posts(where: { title: "${postTitle}"}) { - flagged: commentsConnection(where: { node: { flag: true } }) { - edges { - node { - flag - } - } - } - unflagged: commentsConnection(where: { node: { flag: false } }) { - edges { - node { - flag - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (post:Post {title: $postTitle}) - FOREACH(flag in $flags | - CREATE (:Comment {flag: flag})<-[:HAS_COMMENT]-(post) - ) - `, - { - postTitle, - flags, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any).posts[0].flagged.edges).toContainEqual({ node: { flag: true } }); - expect((result.data as any).posts[0].flagged.edges).toHaveLength(flaggedCount); - expect((result.data as any).posts[0].unflagged.edges).toContainEqual({ node: { flag: false } }); - expect((result.data as any).posts[0].unflagged.edges).toHaveLength(unflaggedCount); - }); - - test("should allow alias on nested connections", async () => { - const movieTitle = "The Matrix"; - const actorName = "Keanu Reeves"; - const screenTime = 120; - - const typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - screenTime: Int! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - title - actorsConnection(where: { node: { name: "${actorName}" } }) { - edges { - properties { - screenTime - } - node { - name - b: moviesConnection(where: { node: { title: "${movieTitle}"}}) { - edges { - node { - title - a: actors { - name - } - } - } - } - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (movie:${typeMovie.name} {title: $movieTitle}) - CREATE (actor:${typeActor.name} {name: $actorName}) - CREATE (actor)-[:ACTED_IN {screenTime: $screenTime}]->(movie) - `, - { - movieTitle, - actorName, - screenTime, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.edges[0].node.b).toEqual({ - edges: [ - { - node: { - title: movieTitle, - a: [ - { - name: actorName, - }, - ], - }, - }, - ], - }); - }); -}); diff --git a/packages/graphql/tests/integration/connections/filtering.int.test.ts b/packages/graphql/tests/integration/connections/filtering.int.test.ts deleted file mode 100644 index 1de0cd48bd..0000000000 --- a/packages/graphql/tests/integration/connections/filtering.int.test.ts +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { gql } from "graphql-tag"; -import type { UniqueType } from "../../utils/graphql-types"; -import { TestHelper } from "../../utils/tests-helper"; - -describe("Connections Filtering", () => { - const testHelper = new TestHelper(); - let actorType: UniqueType; - let movieType: UniqueType; - - beforeEach(async () => { - movieType = testHelper.createUniqueType("Movie"); - actorType = testHelper.createUniqueType("Actor"); - - const typeDefs = gql` - type ${movieType} { - title: String! - actors: [${actorType}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${actorType} { - name: String! - movies: [${movieType}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - await testHelper.initNeo4jGraphQL({ typeDefs }); - }); - - afterEach(async () => { - await testHelper.close(); - }); - - test("should allow where clause on relationship property of node", async () => { - const movieTitle = "My title"; - const actorOneName = "Arthur"; - const actorTwoName = "Zaphod"; - - const query = ` - query ($movieTitle: String!) { - ${movieType.plural}(where: { title: $movieTitle }) { - actorsConnection(where: {node: {movies: { title: $movieTitle } } }) { - edges { - node { - name - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (movie:${movieType} {title: $movieTitle}) - CREATE (actorOne:${actorType} {name: $actorOneName}) - CREATE (actorTwo:${actorType} {name: $actorTwoName}) - CREATE (actorOne)-[:ACTED_IN]->(movie)<-[:ACTED_IN]-(actorTwo) - `, - { - movieTitle, - actorOneName, - actorTwoName, - } - ); - const result = await testHelper.executeGraphQL(query, { - variableValues: { - movieTitle, - }, - }); - expect(result.errors).toBeFalsy(); - expect((result?.data as any)?.[movieType.plural][0].actorsConnection.edges).toContainEqual({ - node: { name: actorOneName }, - }); - expect((result?.data as any)?.[movieType.plural][0].actorsConnection.edges).toContainEqual({ - node: { name: actorTwoName }, - }); - }); - - it("allows for OR boolean operators on nested connections filters", async () => { - const movieTitle = "My title"; - const actor1Name = "Arthur"; - const actor2Name = "Zaphod"; - - const query = ` - query { - ${movieType.plural} (where: {actorsConnection: { OR: [{ node: { name: "${actor1Name}" } }, { node: { name: "${actor2Name}" } }]}}){ - actorsConnection { - edges { - node { - name - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (movie:${movieType} {title: $movieTitle}) - CREATE (actorOne:${actorType} {name: $actor1Name}) - CREATE (actorTwo:${actorType} {name: $actor2Name}) - CREATE (actorOne)-[:ACTED_IN]->(movie)<-[:ACTED_IN]-(actorTwo) - `, - { - actor1Name, - actor2Name, - movieTitle, - } - ); - const result = await testHelper.executeGraphQL(query); - expect(result.errors).toBeFalsy(); - expect(result?.data?.[movieType.plural]).toEqual([ - { - actorsConnection: { - edges: expect.toIncludeSameMembers([ - { - node: { name: actor1Name }, - }, - { - node: { name: actor2Name }, - }, - ]), - }, - }, - ]); - }); - - it("allows for NOT boolean operators on nested connections filters", async () => { - const movieTitle = "My title"; - const actor1Name = "Arthur"; - const actor2Name = "Zaphod"; - - const query = ` - query { - ${movieType.plural} (where: {actorsConnection: { NOT: { node: { name: "${actor1Name}" } } } }){ - actorsConnection { - edges { - node { - name - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (movie:${movieType} {title: $movieTitle}) - CREATE (actorOne:${actorType} {name: $actor1Name}) - CREATE (actorTwo:${actorType} {name: $actor2Name}) - CREATE (actorOne)-[:ACTED_IN]->(movie)<-[:ACTED_IN]-(actorTwo) - `, - { - actor1Name, - actor2Name, - movieTitle, - } - ); - const result = await testHelper.executeGraphQL(query); - expect(result.errors).toBeFalsy(); - expect(result?.data?.[movieType.plural]).toEqual([ - { - actorsConnection: { - edges: expect.toIncludeSameMembers([ - { - node: { name: actor1Name }, - }, - { - node: { name: actor2Name }, - }, - ]), - }, - }, - ]); - }); - - it("allows for NOT boolean operators on connection projection filters", async () => { - const movieTitle = "My title"; - const actor1Name = "Arthur"; - const actor2Name = "Zaphod"; - - const query = ` - query { - ${movieType.plural}{ - actorsConnection(where: { NOT: { node: { name: "${actor1Name}" } } }){ - edges { - node { - name - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (movie:${movieType} {title: $movieTitle}) - CREATE (actorOne:${actorType} {name: $actor1Name}) - CREATE (actorTwo:${actorType} {name: $actor2Name}) - CREATE (actorOne)-[:ACTED_IN]->(movie)<-[:ACTED_IN]-(actorTwo) - `, - { - actor1Name, - actor2Name, - movieTitle, - } - ); - const result = await testHelper.executeGraphQL(query); - expect(result.errors).toBeFalsy(); - expect(result?.data?.[movieType.plural]).toEqual([ - { - actorsConnection: { - edges: expect.toIncludeSameMembers([ - { - node: { name: actor2Name }, - }, - ]), - }, - }, - ]); - }); -}); diff --git a/packages/graphql/tests/integration/connections/nested.int.test.ts b/packages/graphql/tests/integration/connections/nested.int.test.ts deleted file mode 100644 index e77625c678..0000000000 --- a/packages/graphql/tests/integration/connections/nested.int.test.ts +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { gql } from "graphql-tag"; -import type { UniqueType } from "../../utils/graphql-types"; -import { TestHelper } from "../../utils/tests-helper"; - -describe("Connections Alias", () => { - const testHelper = new TestHelper(); - - let typeMovie: UniqueType; - let typeActor: UniqueType; - - const movieTitle = "Forrest Gump"; - const actorName = "Tom Hanks"; - const screenTime = 120; - - beforeEach(async () => { - typeMovie = testHelper.createUniqueType("Movie"); - typeActor = testHelper.createUniqueType("Actor"); - - const typeDefs = gql` - type ${typeMovie} { - title: String! - actors: [${typeActor}!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type ${typeActor} { - name: String! - movies: [${typeMovie}!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - screenTime: Int! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - }); - - afterEach(async () => { - await testHelper.close(); - }); - - test("should allow nested connections", async () => { - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - title - actorsConnection(where: { node: { name: "${actorName}" } }) { - edges { - properties { - screenTime - } - node { - name - moviesConnection { - edges { - node { - title - actors { - name - } - } - } - } - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (movie:${typeMovie} {title: $movieTitle}) - CREATE (actor:${typeActor} {name: $actorName}) - CREATE (actor)-[:ACTED_IN {screenTime: $screenTime}]->(movie) - `, - { - movieTitle, - actorName, - screenTime, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.edges[0].node.moviesConnection).toEqual({ - edges: [ - { - node: { - title: movieTitle, - actors: [ - { - name: actorName, - }, - ], - }, - }, - ], - }); - }); - - test("should allow where clause on nested connections", async () => { - const query = ` - { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - title - actorsConnection(where: { node: { name: "${actorName}" } }) { - edges { - properties { - screenTime - } - node { - name - moviesConnection(where: { node: { title: "${movieTitle}" } }) { - edges { - node { - title - actors { - name - } - } - } - } - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (movie:${typeMovie} {title: $movieTitle}) - CREATE (actor:${typeActor} {name: $actorName}) - CREATE (actor)-[:ACTED_IN {screenTime: $screenTime}]->(movie) - `, - { - movieTitle, - actorName, - screenTime, - } - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - expect((result.data as any)[typeMovie.plural][0].actorsConnection.edges[0].node.moviesConnection).toEqual({ - edges: [ - { - node: { - title: movieTitle, - actors: [ - { - name: actorName, - }, - ], - }, - }, - ], - }); - }); -}); diff --git a/packages/graphql/tests/integration/default-values.int.test.ts b/packages/graphql/tests/integration/cypher-default-values.int.test.ts similarity index 98% rename from packages/graphql/tests/integration/default-values.int.test.ts rename to packages/graphql/tests/integration/cypher-default-values.int.test.ts index 79eb832a1d..4c4505255f 100644 --- a/packages/graphql/tests/integration/default-values.int.test.ts +++ b/packages/graphql/tests/integration/cypher-default-values.int.test.ts @@ -21,7 +21,7 @@ import { generate } from "randomstring"; import type { UniqueType } from "../utils/graphql-types"; import { TestHelper } from "../utils/tests-helper"; -describe("Default values", () => { +describe("@cypher default values", () => { const testHelper = new TestHelper(); let Movie: UniqueType; diff --git a/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts b/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts index b912955f8c..f6efa1e5d4 100644 --- a/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts +++ b/packages/graphql/tests/integration/directives/alias/nodes.int.test.ts @@ -58,92 +58,8 @@ describe("@alias directive", () => { await testHelper.close(); }); - test("Create mutation with alias referring to existing field, include both fields as inputs", async () => { - const userMutation = ` - mutation { - ${typeDirector.operations.create}(input: [{ name: "Tim Burton", nameAgain: "Timmy Burton" }]) { - ${typeDirector.plural} { - name - nameAgain - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(userMutation); - - expect(gqlResult.errors).toBeDefined(); - expect(gqlResult.errors).toHaveLength(1); - expect(gqlResult.errors?.[0]?.message).toBe( - `Conflicting modification of [[name]], [[nameAgain]] on type ${typeDirector.name}` - ); - expect(gqlResult?.data?.[typeDirector.operations.create]?.[typeDirector.plural]).toBeUndefined(); - }); - test("Create mutation with alias referring to existing field, include only field as inputs", async () => { - const userMutation = ` - mutation { - ${typeDirector.operations.create}(input: [{ name: "Tim Burton" }]) { - ${typeDirector.plural} { - name - nameAgain - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(userMutation); - - expect(gqlResult.errors).toBeUndefined(); - expect(gqlResult?.data?.[typeDirector.operations.create]?.[typeDirector.plural]).toEqual([ - { - name: "Tim Burton", - nameAgain: "Tim Burton", - }, - ]); - }); - test("Create mutation with alias referring to existing field, include only alias field as inputs", async () => { - const userMutation = ` - mutation { - ${typeDirector.operations.create}(input: [{ nameAgain: "Timmy Burton" }]) { - ${typeDirector.plural} { - name - nameAgain - } - } - } - `; - const gqlResult = await testHelper.executeGraphQL(userMutation); - - expect(gqlResult.errors).toBeUndefined(); - expect(gqlResult?.data?.[typeDirector.operations.create]?.[typeDirector.plural]).toEqual([ - { - name: "Timmy Burton", - nameAgain: "Timmy Burton", - }, - ]); - }); - test("Create mutation with alias referring to existing field, include both bad and good inputs", async () => { - const userMutation = ` - mutation { - ${typeDirector.operations.create}(input: [{ name: "Tim Burton", nameAgain: "Timmy Burton" }, { name: "Someone" }]) { - ${typeDirector.plural} { - name - nameAgain - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(userMutation); - - expect(gqlResult.errors).toBeDefined(); - expect(gqlResult.errors).toHaveLength(1); - expect(gqlResult.errors?.[0]?.message).toBe( - `Conflicting modification of [[name]], [[nameAgain]] on type ${typeDirector.name}` - ); - expect(gqlResult?.data?.[typeDirector.operations.create]?.[typeDirector.plural]).toBeUndefined(); - }); + test("Create mutation with alias on connection referring to existing field, include only field as inputs", async () => { const userMutation = ` mutation { diff --git a/packages/graphql/tests/integration/directives/cypher/cypher.int.test.ts b/packages/graphql/tests/integration/directives/cypher/cypher.int.test.ts new file mode 100644 index 0000000000..0a7e157c7f --- /dev/null +++ b/packages/graphql/tests/integration/directives/cypher/cypher.int.test.ts @@ -0,0 +1,899 @@ +/* + * 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 { generate } from "randomstring"; +import { createBearerToken } from "../../../utils/create-bearer-token"; +import { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("cypher directive", () => { + const testHelper = new TestHelper(); + + afterEach(async () => { + await testHelper.close(); + }); + + describe(`Top level cypher`, () => { + describe("Query", () => { + let Movie: UniqueType; + let Actor: UniqueType; + let Director: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + Director = testHelper.createUniqueType("Director"); + }); + + test("should query custom query and return relationship data", async () => { + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Query { + customMovies(title: String!): [${Movie}] + @cypher( + statement: """ + MATCH (m:${Movie} {title: $title}) + RETURN m + """, + columnName: "m" + ) + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const source = ` + query($title: String!) { + customMovies(title: $title) { + title + actors { + name + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (:${Movie} {title: $title})<-[:ACTED_IN]-(:${Actor} {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await testHelper.executeGraphQL(source, { + variableValues: { title: movieTitle }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).customMovies).toEqual([ + { title: movieTitle, actors: [{ name: actorName }] }, + ]); + }); + + test("should query custom query and return relationship data with custom where on field", async () => { + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Query { + customMovies(title: String!): [${Movie}] @cypher(statement: """ + MATCH (m:${Movie} {title: $title}) + RETURN m + """, + columnName: "m" + ) + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const source = ` + query($title: String!, $name: String) { + customMovies(title: $title) { + title + actors(where: {name: $name}) { + name + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (:${Movie} {title: $title})<-[:ACTED_IN]-(:${Actor} {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await testHelper.executeGraphQL(source, { + variableValues: { title: movieTitle, name: actorName }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).customMovies).toEqual([ + { title: movieTitle, actors: [{ name: actorName }] }, + ]); + }); + + test("should query custom query and return relationship data with auth", async () => { + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type JWT @jwt { + roles: [String!]! + } + + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} @authorization(validate: [{ operations: [READ], where: { jwt: { roles_INCLUDES:"admin" } } }]) { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Query { + customMovies(title: String!): [${Movie}] @cypher(statement: """ + MATCH (m:${Movie} {title: $title}) + RETURN m + """, + columnName: "m") + } + `; + + const secret = "secret"; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + features: { authorization: { key: "secret" } }, + }); + + const source = ` + query($title: String!, $name: String) { + customMovies(title: $title) { + title + actors(where: {name: $name}) { + name + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (:${Movie} {title: $title})<-[:ACTED_IN]-(:${Actor} {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const token = createBearerToken(secret); + + const gqlResult = await testHelper.executeGraphQLWithToken(source, token, { + variableValues: { title: movieTitle, name: actorName }, + }); + + expect((gqlResult.errors as any[])[0].message).toBe("Forbidden"); + }); + + test("should query multiple nodes and return relationship data", async () => { + const movieTitle1 = generate({ + charset: "alphabetic", + }); + const movieTitle2 = generate({ + charset: "alphabetic", + }); + const movieTitle3 = generate({ + charset: "alphabetic", + }); + + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Query { + customMovies(titles: [String!]!): [${Movie}] @cypher(statement: """ + MATCH (m:${Movie}) + WHERE m.title in $titles + RETURN m + """, + columnName: "m" + ) + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const source = ` + query($titles: [String!]!) { + customMovies(titles: $titles) { + title + actors { + name + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (:${Movie} {title: $title1})<-[:ACTED_IN]-(a:${Actor} {name: $name}) + CREATE (:${Movie} {title: $title2})<-[:ACTED_IN]-(a) + CREATE (:${Movie} {title: $title3})<-[:ACTED_IN]-(a) + `, + { + title1: movieTitle1, + title2: movieTitle2, + title3: movieTitle3, + name: actorName, + } + ); + + const gqlResult = await testHelper.executeGraphQL(source, { + variableValues: { titles: [movieTitle1, movieTitle2, movieTitle3] }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).customMovies).toHaveLength(3); + expect((gqlResult?.data as any).customMovies).toContainEqual({ + title: movieTitle1, + actors: [{ name: actorName }], + }); + expect((gqlResult?.data as any).customMovies).toContainEqual({ + title: movieTitle2, + actors: [{ name: actorName }], + }); + expect((gqlResult?.data as any).customMovies).toContainEqual({ + title: movieTitle3, + actors: [{ name: actorName }], + }); + }); + + test("should query multiple connection fields on a type", async () => { + const title = generate({ + charset: "alphabetic", + }); + + const actorName = generate({ + charset: "alphabetic", + }); + const directorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + directors: [${Director}!]! @relationship(type: "DIRECTED", direction: IN) + } + + type ${Actor} { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type ${Director} { + name: String! + movies: [${Movie}!]! @relationship(type: "DIRECTED", direction: OUT) + } + + type Query { + movie(title: String!): ${Movie} + @cypher( + statement: """ + MATCH (m:${Movie}) + WHERE m.title = $title + RETURN m + """, + columnName: "m" + ) + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const source = ` + query($title: String!) { + movie(title: $title) { + title + actorsConnection { + edges { + node { + name + } + } + } + directorsConnection { + edges { + node { + name + } + } + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (m:${Movie} {title: $title})<-[:ACTED_IN]-(:${Actor} {name: $actorName}) + CREATE (m)<-[:DIRECTED]-(:${Director} {name: $directorName}) + `, + { + title, + actorName, + directorName, + } + ); + + const gqlResult = await testHelper.executeGraphQL(source, { + variableValues: { title }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect(gqlResult?.data?.movie).toEqual({ + title, + actorsConnection: { + edges: [{ node: { name: actorName } }], + }, + directorsConnection: { + edges: [{ node: { name: directorName } }], + }, + }); + }); + }); + + describe("Mutation", () => { + let Movie: UniqueType; + let Actor: UniqueType; + + beforeEach(() => { + Movie = testHelper.createUniqueType("Movie"); + Actor = testHelper.createUniqueType("Actor"); + }); + + test("should query custom mutation and return relationship data", async () => { + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Mutation { + customMovies(title: String!): [${Movie}] + @cypher( + statement: """ + MATCH (m:${Movie} {title: $title}) + RETURN m + """, + columnName: "m" + ) + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const source = ` + mutation($title: String!) { + customMovies(title: $title) { + title + actors { + name + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (:${Movie} {title: $title})<-[:ACTED_IN]-(:${Actor} {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await testHelper.executeGraphQL(source, { + variableValues: { title: movieTitle }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).customMovies).toEqual([ + { title: movieTitle, actors: [{ name: actorName }] }, + ]); + }); + + test("should query custom mutation and return relationship data with custom where on field", async () => { + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Mutation { + customMovies(title: String!): [${Movie}] @cypher(statement: """ + MATCH (m:${Movie} {title: $title}) + RETURN m + """, + columnName: "m") + } + `; + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const source = ` + mutation($title: String!, $name: String) { + customMovies(title: $title) { + title + actors(where: {name: $name}) { + name + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (:${Movie} {title: $title})<-[:ACTED_IN]-(:${Actor} {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await testHelper.executeGraphQL(source, { + variableValues: { title: movieTitle }, + }); + + expect(gqlResult.errors).toBeFalsy(); + + expect((gqlResult?.data as any).customMovies).toEqual([ + { title: movieTitle, actors: [{ name: actorName }] }, + ]); + }); + + test("should query custom mutation and return relationship data with auth", async () => { + const movieTitle = generate({ + charset: "alphabetic", + }); + const actorName = generate({ + charset: "alphabetic", + }); + + const typeDefs = ` + type JWT @jwt { + roles: [String!]! + } + + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) + } + + type ${Actor} @authorization(validate: [{ operations: [READ], where: { jwt: { roles_INCLUDES: "admin" } } }]) { + name: String! + movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Mutation { + customMovies(title: String!): [${Movie}] @cypher(statement: """ + MATCH (m:${Movie} {title: $title}) + RETURN m + """, + columnName: "m") + } + `; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + features: { authorization: { key: "secret" } }, + }); + + const source = ` + mutation($title: String!, $name: String) { + customMovies(title: $title) { + title + actors(where: {name: $name}) { + name + } + } + } + `; + + await testHelper.executeCypher( + ` + CREATE (:${Movie} {title: $title})<-[:ACTED_IN]-(:${Actor} {name: $name}) + `, + { + title: movieTitle, + name: actorName, + } + ); + + const gqlResult = await testHelper.executeGraphQL(source, { + variableValues: { title: movieTitle }, + }); + + expect((gqlResult.errors as any[])[0].message).toBe("Forbidden"); + }); + }); + }); + + describe("Field level cypher", () => { + // Reproduces https://github.com/neo4j/graphql/issues/444 with default value test + describe("Null Values with default value", () => { + const townId = generate({ charset: "alphabetic" }); + const destinationId = generate({ charset: "alphabetic" }); + + const defaultPreposition = generate({ charset: "alphabetic" }); + const testCaseName = generate({ charset: "alphabetic" }); + let Destination: UniqueType; + let Town: UniqueType; + + beforeEach(async () => { + Destination = testHelper.createUniqueType("Destination"); + Town = testHelper.createUniqueType("Town"); + + const typeDefs = ` + type ${Destination} { + id: ID! + preposition(caseName: String = null): String! @cypher(statement: "RETURN coalesce($caseName, '${defaultPreposition}') as result", columnName: "result") + } + + type Query { + townDestinationList(id: ID!): [${Destination}] @cypher(statement: """ + MATCH (town:${Town} {id:$id}) + OPTIONAL MATCH (town)<-[:BELONGS_TO]-(destination:${Destination}) + RETURN destination + """, + columnName: "destination") + } + `; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + }); + + await testHelper.executeCypher( + ` + CREATE (t:${Town} {id: $townId}) + MERGE (t)<-[:BELONGS_TO]-(:${Destination} {id: $destinationId}) + `, + { + townId, + destinationId, + } + ); + }); + + test("should return default value", async () => { + const source = ` + query($id: ID!) { + townDestinationList(id: $id) { + id + preposition + } + } + `; + + const expectedTownDestinationList = [{ id: destinationId, preposition: defaultPreposition }]; + + // Schema with default value + const gqlResultWithDefaultValue = await testHelper.executeGraphQL(source, { + variableValues: { id: townId }, + }); + + expect(gqlResultWithDefaultValue.errors).toBeFalsy(); + + expect((gqlResultWithDefaultValue?.data as any).townDestinationList).toEqual( + expectedTownDestinationList + ); + }); + + test("should return test value", async () => { + const source = ` + query($id: ID!, $caseName: String) { + townDestinationList(id: $id) { + id + preposition(caseName: $caseName) + } + } + `; + + const expectedTownDestinationList = [{ id: destinationId, preposition: testCaseName }]; + + // Schema with default value + const gqlResultWithDefaultValue = await testHelper.executeGraphQL(source, { + variableValues: { id: townId, caseName: testCaseName }, + }); + + expect(gqlResultWithDefaultValue.errors).toBeFalsy(); + + expect((gqlResultWithDefaultValue?.data as any).townDestinationList).toEqual( + expectedTownDestinationList + ); + }); + }); + + describe("Null Values without default value", () => { + const townId = generate({ charset: "alphabetic" }); + const destinationId = generate({ charset: "alphabetic" }); + + const defaultPreposition = generate({ charset: "alphabetic" }); + const testCaseName = generate({ charset: "alphabetic" }); + let Destination: UniqueType; + let Town: UniqueType; + + beforeEach(async () => { + Destination = testHelper.createUniqueType("Destination"); + Town = testHelper.createUniqueType("Town"); + + const typeDefs = ` + type ${Destination} { + id: ID! + preposition(caseName: String): String! @cypher(statement: "RETURN coalesce($caseName, '${defaultPreposition}') as result", columnName: "result") + } + + type Query { + townDestinationList(id: ID!): [${Destination}] @cypher(statement: """ + MATCH (town:${Town} {id:$id}) + OPTIONAL MATCH (town)<-[:BELONGS_TO]-(destination:${Destination}) + RETURN destination + """, + columnName: "destination") + } + `; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + }); + + await testHelper.executeCypher( + ` + CREATE (t:${Town} {id: $townId}) + MERGE (t)<-[:BELONGS_TO]-(:${Destination} {id: $destinationId}) + `, + { + townId, + destinationId, + } + ); + }); + + test("should return default value", async () => { + const source = ` + query($id: ID!) { + townDestinationList(id: $id) { + id + preposition + } + } + `; + + const expectedTownDestinationList = [{ id: destinationId, preposition: defaultPreposition }]; + + // Schema with default value + const gqlResultWithMissingValue = await testHelper.executeGraphQL(source, { + variableValues: { id: townId }, + }); + + expect(gqlResultWithMissingValue.errors).toBeFalsy(); + + expect((gqlResultWithMissingValue?.data as any).townDestinationList).toEqual( + expectedTownDestinationList + ); + }); + + test("should return test value", async () => { + const source = ` + query($id: ID!, $caseName: String) { + townDestinationList(id: $id) { + id + preposition(caseName: $caseName) + } + } + `; + + const expectedTownDestinationList = [{ id: destinationId, preposition: testCaseName }]; + + // Schema with default value + const gqlResultWithMissingValue = await testHelper.executeGraphQL(source, { + variableValues: { id: townId, caseName: testCaseName }, + }); + + expect(gqlResultWithMissingValue.errors).toBeFalsy(); + + expect((gqlResultWithMissingValue?.data as any).townDestinationList).toEqual( + expectedTownDestinationList + ); + }); + }); + + describe("Union type", () => { + const secret = "secret"; + + const userId = generate({ charset: "alphabetic" }); + + let Post: UniqueType; + let Movie: UniqueType; + let User: UniqueType; + + beforeEach(async () => { + Post = new UniqueType("Post"); + Movie = new UniqueType("Movie"); + User = new UniqueType("User"); + + const typeDefs = ` + union PostMovieUser = ${Post} | ${Movie} | ${User} + + type ${Post} { + name: String + } + + type ${Movie} { + name: String + } + + type ${User} { + id: ID @id @unique + updates: [PostMovieUser!]! + @cypher( + statement: """ + MATCH (this:${User})-[:WROTE]->(wrote:${Post}) + RETURN wrote + LIMIT 5 + """, + columnName: "wrote" + ) + } + `; + + await testHelper.initNeo4jGraphQL({ + typeDefs, + features: { authorization: { key: "secret" } }, + }); + + await testHelper.executeCypher( + ` + CREATE (p:${Post} {name: "Postname"}) + CREATE (m:${Movie} {name: "Moviename"}) + CREATE (u:${User} {id: "${userId}"}) + CREATE (u)-[:WROTE]->(p) + CREATE (u)-[:WATCHED]->(m) + ` + ); + }); + + test("should return __typename", async () => { + const source = ` + query { + ${User.plural} (where: { id: "${userId}" }) { + updates { + __typename + } + } + } + `; + + const token = createBearerToken(secret); + const gqlResult = await testHelper.executeGraphQLWithToken(source, token); + + expect(gqlResult.errors).toBeUndefined(); + expect(gqlResult?.data).toEqual({ + [User.plural]: [ + { + updates: [ + { + __typename: `${Post}`, + }, + ], + }, + ], + }); + }); + }); + }); +}); diff --git a/packages/graphql/tests/integration/directives/default.int.test.ts b/packages/graphql/tests/integration/directives/default.int.test.ts index 46951cddc3..675b96b45f 100644 --- a/packages/graphql/tests/integration/directives/default.int.test.ts +++ b/packages/graphql/tests/integration/directives/default.int.test.ts @@ -27,72 +27,6 @@ describe("@default directive", () => { await testHelper.close(); }); - describe("with primitive fields", () => { - test("on non-primitive field should throw an error", async () => { - const typeDefs = ` - type User { - name: String! - location: Point! @default(value: "default") - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ - typeDefs, - }); - - await expect(neoSchema.getSchema()).rejects.toIncludeSameMembers([ - new GraphQLError("@default is not supported by Spatial types."), - ]); - }); - - test("with an argument with a type which doesn't match the field should throw an error", async () => { - const typeDefs = ` - type User { - name: String! @default(value: 2) - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ - typeDefs, - }); - - await expect(neoSchema.getSchema()).rejects.toIncludeSameMembers([ - new GraphQLError("@default.value on String fields must be of type String"), - ]); - }); - - test("on a DateTime with an invalid value should throw an error", async () => { - const typeDefs = ` - type User { - verifiedAt: DateTime! @default(value: "Not a date") - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ - typeDefs, - }); - - await expect(neoSchema.getSchema()).rejects.toIncludeSameMembers([ - new GraphQLError("@default.value is not a valid DateTime"), - ]); - }); - - test("on primitive field should not throw an error", async () => { - const typeDefs = ` - type User { - name: String! - location: String! @default(value: "somewhere") - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ - typeDefs, - }); - - await expect(neoSchema.getSchema()).resolves.not.toThrow(); - }); - }); - describe("with enum fields", () => { test("on enum field with incorrect value should throw an error", async () => { const typeDefs = ` diff --git a/packages/graphql/tests/integration/directives/relayId/relayId-projection-with-database-name.int.test.ts b/packages/graphql/tests/integration/directives/relayId/relayId-projection-with-database-name.int.test.ts deleted file mode 100644 index 4134691634..0000000000 --- a/packages/graphql/tests/integration/directives/relayId/relayId-projection-with-database-name.int.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { generate } from "randomstring"; -import { toGlobalId } from "../../../../src/utils/global-ids"; -import { TestHelper } from "../../../utils/tests-helper"; - -describe("RelayId projection with different database name", () => { - const testHelper = new TestHelper(); - let movieDatabaseID: string; - let genreDatabaseID: string; - let actorDatabaseID: string; - - const Movie = testHelper.createUniqueType("Movie"); - const Genre = testHelper.createUniqueType("Genre"); - const Actor = testHelper.createUniqueType("Actor"); - - beforeAll(async () => { - const typeDefs = ` - type ${Movie} { - dbId: ID! @id @unique @relayId @alias(property: "serverId") - title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${Genre} { - dbId: ID! @id @unique @relayId @alias(property: "serverId") - name: String! - } - - type ${Actor} { - dbId: ID! @id @unique @relayId @alias(property: "serverId") - name: String! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const randomID = generate({ charset: "alphabetic" }); - const randomID2 = generate({ charset: "alphabetic" }); - const randomID3 = generate({ charset: "alphabetic" }); - await testHelper.executeCypher(` - CREATE (m:${Movie.name} { title: "Movie1", serverId: "${randomID}" }) - CREATE (g:${Genre.name} { name: "Action", serverId: "${randomID2}" }) - CREATE (o:${Actor.name} { name: "Keanu", serverId: "${randomID3}" }) - CREATE (m)-[:HAS_GENRE]->(g) - CREATE (m)-[:ACTED_IN]->(o) - `); - movieDatabaseID = randomID; - genreDatabaseID = randomID2; - actorDatabaseID = randomID3; - }); - - afterAll(async () => { - await testHelper.close(); - }); - - test("should return the correct relayId ids using the simple API", async () => { - const query = ` - query { - ${Movie.plural} { - id - dbId - title - genre { - id - dbId - name - } - actors { - id - dbId - name - } - } - } - `; - - const queryResult = await testHelper.executeGraphQL(query); - expect(queryResult.errors).toBeUndefined(); - - expect(queryResult.data?.[Movie.plural]).toEqual([ - { - id: expect.toBeString(), - dbId: expect.toBeString(), - title: "Movie1", - genre: { - id: expect.toBeString(), - dbId: expect.toBeString(), - name: "Action", - }, - actors: [ - { - id: expect.toBeString(), - dbId: expect.toBeString(), - name: "Keanu", - }, - ], - }, - ]); - const id = (queryResult.data as any)[Movie.plural][0].id; - const dbId = (queryResult.data as any)?.[Movie.plural][0].dbId; - expect(dbId).toBe(movieDatabaseID); - expect(id).toBe(toGlobalId({ typeName: Movie.name, field: "dbId", id: dbId })); - - const genreId = (queryResult.data as any)?.[Movie.plural][0].genre?.id; - const genreDbId = (queryResult.data as any)?.[Movie.plural][0].genre?.dbId; - expect(genreDbId).toBe(genreDatabaseID); - expect(genreId).toBe(toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDbId })); - - const actorId = (queryResult.data as any)?.[Movie.plural][0].actors[0]?.id; - const actorDbId = (queryResult.data as any)?.[Movie.plural][0].actors[0]?.dbId; - expect(actorDbId).toBe(actorDatabaseID); - expect(actorId).toBe(toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDbId })); - }); - - test("should return the correct relayId ids using the connection API", async () => { - const connectionQuery = ` - query { - ${Movie.operations.connection} { - totalCount - edges { - node { - id - dbId - title - genre { - id - dbId - name - } - actors { - id - dbId - name - } - } - } - } - } - `; - - const connectionQueryResult = await testHelper.executeGraphQL(connectionQuery); - - expect(connectionQueryResult.errors).toBeUndefined(); - expect(connectionQueryResult.data).toBeDefined(); - - expect(connectionQueryResult.data?.[Movie.operations.connection]).toEqual({ - edges: [ - { - node: { - id: expect.toBeString(), - dbId: expect.toBeString(), - title: "Movie1", - genre: { - id: expect.toBeString(), - dbId: expect.toBeString(), - name: "Action", - }, - actors: [ - { - id: expect.toBeString(), - dbId: expect.toBeString(), - name: "Keanu", - }, - ], - }, - }, - ], - totalCount: 1, - }); - const id = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.id; - const dbId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.dbId; - expect(dbId).toBe(movieDatabaseID); - expect(id).toBe(toGlobalId({ typeName: Movie.name, field: "dbId", id: dbId })); - - const genreId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.genre?.id; - const genreDbId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.genre - ?.dbId; - expect(genreDbId).toBe(genreDatabaseID); - expect(genreId).toBe(toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDbId })); - - const actorId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.actors[0] - ?.id; - const actorDbId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.actors[0] - ?.dbId; - expect(actorDbId).toBe(actorDatabaseID); - expect(actorId).toBe(toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDbId })); - }); -}); diff --git a/packages/graphql/tests/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts b/packages/graphql/tests/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts deleted file mode 100644 index 6d9612979a..0000000000 --- a/packages/graphql/tests/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { generate } from "randomstring"; -import { toGlobalId } from "../../../../src/utils/global-ids"; -import { TestHelper } from "../../../utils/tests-helper"; - -// used to confirm the issue: https://github.com/neo4j/graphql/issues/4158 -describe("RelayId projection with GraphQL field alias", () => { - const testHelper = new TestHelper(); - let movieDatabaseID: string; - let genreDatabaseID: string; - let actorDatabaseID: string; - - const Movie = testHelper.createUniqueType("Movie"); - const Genre = testHelper.createUniqueType("Genre"); - const Actor = testHelper.createUniqueType("Actor"); - - beforeAll(async () => { - const typeDefs = ` - type ${Movie} { - dbId: ID! @id @unique @relayId - title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${Genre} { - dbId: ID! @id @unique @relayId - name: String! - } - - type ${Actor} { - dbId: ID! @id @unique @relayId - name: String! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const randomID = generate({ charset: "alphabetic" }); - const randomID2 = generate({ charset: "alphabetic" }); - const randomID3 = generate({ charset: "alphabetic" }); - await testHelper.executeCypher(` - CREATE (m:${Movie.name} { title: "Movie1", dbId: "${randomID}" }) - CREATE (g:${Genre.name} { name: "Action", dbId: "${randomID2}" }) - CREATE (o:${Actor.name} { name: "Keanu", dbId: "${randomID3}" }) - CREATE (m)-[:HAS_GENRE]->(g) - CREATE (m)-[:ACTED_IN]->(o) - `); - movieDatabaseID = randomID; - genreDatabaseID = randomID2; - actorDatabaseID = randomID3; - }); - - afterAll(async () => { - await testHelper.close(); - }); - - test("should return the correct relayId ids using the simple API", async () => { - const query = ` - query { - ${Movie.plural} { - testAliasID: id - testAliasDBID: dbId - title - genre { - testAliasID: id - testAliasDBID: dbId - name - } - actors { - testAliasID: id - testAliasDBID: dbId - name - } - } - } - `; - - const queryResult = await testHelper.executeGraphQL(query); - expect(queryResult.errors).toBeUndefined(); - - expect(queryResult.data?.[Movie.plural]).toEqual([ - { - testAliasID: expect.toBeString(), - testAliasDBID: expect.toBeString(), - title: "Movie1", - genre: { - testAliasID: expect.toBeString(), - testAliasDBID: expect.toBeString(), - name: "Action", - }, - actors: [ - { - testAliasID: expect.toBeString(), - testAliasDBID: expect.toBeString(), - name: "Keanu", - }, - ], - }, - ]); - const id = (queryResult.data as any)[Movie.plural][0].testAliasID; - const dbId = (queryResult.data as any)?.[Movie.plural][0].testAliasDBID; - expect(dbId).toBe(movieDatabaseID); - expect(id).toBe(toGlobalId({ typeName: Movie.name, field: "dbId", id: dbId })); - - const genreId = (queryResult.data as any)?.[Movie.plural][0].genre?.testAliasID; - const genreDbId = (queryResult.data as any)?.[Movie.plural][0].genre?.testAliasDBID; - expect(genreDbId).toBe(genreDatabaseID); - expect(genreId).toBe(toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDbId })); - - const actorId = (queryResult.data as any)?.[Movie.plural][0].actors[0]?.testAliasID; - const actorDbId = (queryResult.data as any)?.[Movie.plural][0].actors[0]?.testAliasDBID; - expect(actorDbId).toBe(actorDatabaseID); - expect(actorId).toBe(toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDbId })); - }); - - test("should return the correct relayId ids using the connection API", async () => { - const connectionQuery = ` - query { - ${Movie.operations.connection} { - totalCount - edges { - node { - testAliasID: id - testAliasDBID: dbId - title - genre { - testAliasID: id - testAliasDBID: dbId - name - } - actors { - testAliasID: id - testAliasDBID: dbId - name - } - } - } - } - } - `; - - const connectionQueryResult = await testHelper.executeGraphQL(connectionQuery); - - expect(connectionQueryResult.errors).toBeUndefined(); - expect(connectionQueryResult.data).toBeDefined(); - - expect(connectionQueryResult.data?.[Movie.operations.connection]).toEqual({ - edges: [ - { - node: { - testAliasID: expect.toBeString(), - testAliasDBID: expect.toBeString(), - title: "Movie1", - genre: { - testAliasID: expect.toBeString(), - testAliasDBID: expect.toBeString(), - name: "Action", - }, - actors: [ - { - testAliasID: expect.toBeString(), - testAliasDBID: expect.toBeString(), - name: "Keanu", - }, - ], - }, - }, - ], - totalCount: 1, - }); - const id = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.testAliasID; - const dbId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.testAliasDBID; - expect(dbId).toBe(movieDatabaseID); - expect(id).toBe(toGlobalId({ typeName: Movie.name, field: "dbId", id: dbId })); - - const genreId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.genre - ?.testAliasID; - const genreDbId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.genre - ?.testAliasDBID; - expect(genreDbId).toBe(genreDatabaseID); - expect(genreId).toBe(toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDbId })); - - const actorId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.actors[0] - ?.testAliasID; - const actorDbId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.actors[0] - ?.testAliasDBID; - expect(actorDbId).toBe(actorDatabaseID); - expect(actorId).toBe(toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDbId })); - }); -}); diff --git a/packages/graphql/tests/integration/directives/relayId/relayId-projection.int.test.ts b/packages/graphql/tests/integration/directives/relayId/relayId-projection.int.test.ts deleted file mode 100644 index 7367b710d1..0000000000 --- a/packages/graphql/tests/integration/directives/relayId/relayId-projection.int.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { generate } from "randomstring"; -import { toGlobalId } from "../../../../src/utils/global-ids"; -import { TestHelper } from "../../../utils/tests-helper"; - -describe("RelayId projection", () => { - const testHelper = new TestHelper(); - let movieDatabaseID: string; - let genreDatabaseID: string; - let actorDatabaseID: string; - - const Movie = testHelper.createUniqueType("Movie"); - const Genre = testHelper.createUniqueType("Genre"); - const Actor = testHelper.createUniqueType("Actor"); - - beforeAll(async () => { - const typeDefs = ` - type ${Movie} { - dbId: ID! @id @unique @relayId - title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - - type ${Genre} { - dbId: ID! @id @unique @relayId - name: String! - } - - type ${Actor} { - dbId: ID! @id @unique @relayId - name: String! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - const randomID = generate({ charset: "alphabetic" }); - const randomID2 = generate({ charset: "alphabetic" }); - const randomID3 = generate({ charset: "alphabetic" }); - await testHelper.executeCypher(` - CREATE (m:${Movie.name} { title: "Movie1", dbId: "${randomID}" }) - CREATE (g:${Genre.name} { name: "Action", dbId: "${randomID2}" }) - CREATE (o:${Actor.name} { name: "Keanu", dbId: "${randomID3}" }) - CREATE (m)-[:HAS_GENRE]->(g) - CREATE (m)-[:ACTED_IN]->(o) - `); - movieDatabaseID = randomID; - genreDatabaseID = randomID2; - actorDatabaseID = randomID3; - }); - - afterAll(async () => { - await testHelper.close(); - }); - - test("should return the correct relayId ids using the simple API", async () => { - const query = ` - query { - ${Movie.plural} { - id - dbId - title - genre { - id - dbId - name - } - actors { - id - dbId - name - } - } - } - `; - - const queryResult = await testHelper.executeGraphQL(query); - expect(queryResult.errors).toBeUndefined(); - - expect(queryResult.data?.[Movie.plural]).toEqual([ - { - id: expect.toBeString(), - dbId: expect.toBeString(), - title: "Movie1", - genre: { - id: expect.toBeString(), - dbId: expect.toBeString(), - name: "Action", - }, - actors: [ - { - id: expect.toBeString(), - dbId: expect.toBeString(), - name: "Keanu", - }, - ], - }, - ]); - const id = (queryResult.data as any)[Movie.plural][0].id; - const dbId = (queryResult.data as any)?.[Movie.plural][0].dbId; - expect(dbId).toBe(movieDatabaseID); - expect(id).toBe(toGlobalId({ typeName: Movie.name, field: "dbId", id: dbId })); - - const genreId = (queryResult.data as any)?.[Movie.plural][0].genre?.id; - const genreDbId = (queryResult.data as any)?.[Movie.plural][0].genre?.dbId; - expect(genreDbId).toBe(genreDatabaseID); - expect(genreId).toBe(toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDbId })); - - const actorId = (queryResult.data as any)?.[Movie.plural][0].actors[0]?.id; - const actorDbId = (queryResult.data as any)?.[Movie.plural][0].actors[0]?.dbId; - expect(actorDbId).toBe(actorDatabaseID); - expect(actorId).toBe(toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDbId })); - }); - - test("should return the correct relayId ids using the connection API", async () => { - const connectionQuery = ` - query { - ${Movie.operations.connection} { - totalCount - edges { - node { - id - dbId - title - genre { - id - dbId - name - } - actors { - id - dbId - name - } - } - } - } - } - `; - - const connectionQueryResult = await testHelper.executeGraphQL(connectionQuery); - - expect(connectionQueryResult.errors).toBeUndefined(); - expect(connectionQueryResult.data).toBeDefined(); - - expect(connectionQueryResult.data?.[Movie.operations.connection]).toEqual({ - edges: [ - { - node: { - id: expect.toBeString(), - dbId: expect.toBeString(), - title: "Movie1", - genre: { - id: expect.toBeString(), - dbId: expect.toBeString(), - name: "Action", - }, - actors: [ - { - id: expect.toBeString(), - dbId: expect.toBeString(), - name: "Keanu", - }, - ], - }, - }, - ], - totalCount: 1, - }); - const id = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.id; - const dbId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.dbId; - expect(dbId).toBe(movieDatabaseID); - expect(id).toBe(toGlobalId({ typeName: Movie.name, field: "dbId", id: dbId })); - - const genreId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.genre?.id; - const genreDbId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.genre - ?.dbId; - expect(genreDbId).toBe(genreDatabaseID); - expect(genreId).toBe(toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDbId })); - - const actorId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.actors[0] - ?.id; - const actorDbId = (connectionQueryResult.data as any)?.[Movie.operations.connection]?.edges[0]?.node?.actors[0] - ?.dbId; - expect(actorDbId).toBe(actorDatabaseID); - expect(actorId).toBe(toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDbId })); - }); -}); diff --git a/packages/graphql/tests/integration/directives/unique.int.test.ts b/packages/graphql/tests/integration/directives/unique.int.test.ts deleted file mode 100644 index 557826bc85..0000000000 --- a/packages/graphql/tests/integration/directives/unique.int.test.ts +++ /dev/null @@ -1,873 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { gql } from "graphql-tag"; -import type { Driver } from "neo4j-driver"; -import { generate } from "randomstring"; -import type { Neo4jDatabaseInfo } from "../../../src/classes/Neo4jDatabaseInfo"; -import { isMultiDbUnsupportedError } from "../../utils/is-multi-db-unsupported-error"; -import { TestHelper } from "../../utils/tests-helper"; - -describe("assertIndexesAndConstraints/unique", () => { - const testHelper = new TestHelper(); - let driver: Driver; - let databaseName: string; - let MULTIDB_SUPPORT = true; - let dbInfo: Neo4jDatabaseInfo; - - beforeAll(async () => { - dbInfo = await testHelper.getDatabaseInfo(); - - databaseName = generate({ readable: true, charset: "alphabetic" }); - - try { - await testHelper.createDatabase(databaseName); - } catch (e) { - if (e instanceof Error) { - if (isMultiDbUnsupportedError(e)) { - // No multi-db support, so we skip tests - MULTIDB_SUPPORT = false; - await testHelper.close(); - } else { - throw e; - } - } - } - }); - - beforeEach(async () => { - if (MULTIDB_SUPPORT) { - driver = await testHelper.getDriver(); - } - }); - - afterEach(async () => { - if (MULTIDB_SUPPORT) { - await testHelper.close(); - } - }); - - afterAll(async () => { - if (MULTIDB_SUPPORT) { - await testHelper.dropDatabase(); - await testHelper.close(); - } - }); - - test("should create a constraint if it doesn't exist and specified in options, and then throw an error in the event of constraint validation", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const isbn = generate({ readable: true }); - const title = generate({ readable: true }); - - const typeDefs = gql` - type Book { - isbn: String! @unique - title: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - sessionConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - const result = await testHelper.executeCypher(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter((record) => record.labelsOrTypes.includes("Book")) - ).toHaveLength(1); - - const mutation = ` - mutation CreateBooks($isbn: String!, $title: String!) { - createBooks(input: [{ isbn: $isbn, title: $title }]) { - books { - isbn - title - } - } - } - `; - - const createResult = await testHelper.executeGraphQL(mutation, { - variableValues: { - isbn, - title, - }, - }); - - expect(createResult.errors).toBeFalsy(); - - expect(createResult.data).toEqual({ - createBooks: { books: [{ isbn, title }] }, - }); - - const errorResult = await testHelper.executeGraphQL(mutation, { - variableValues: { - isbn, - title, - }, - }); - - expect(errorResult.errors).toHaveLength(1); - expect(errorResult.errors?.[0]?.message).toBe("Constraint validation failed"); - }); - - describe("@unique", () => { - test("should throw an error when all necessary constraints do not exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique - title: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, sessionConfig: { database: databaseName } }) - ).rejects.toThrow(`Missing constraint for ${type.name}.isbn`); - }); - - test("should throw an error when all necessary constraints do not exist when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique @alias(property: "internationalStandardBookNumber") - title: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, sessionConfig: { database: databaseName } }) - ).rejects.toThrow(`Missing constraint for ${type.name}.internationalStandardBookNumber`); - }); - - test("should not throw an error when all necessary constraints exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique - title: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const cypher = `CREATE CONSTRAINT ${type.name}_isbn ${dbInfo.gte("4.4") ? "FOR" : "ON"} (n:${type.name}) ${ - dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT" - } n.isbn IS UNIQUE`; - - await testHelper.executeCypher(cypher); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, sessionConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should not throw an error when all necessary constraints exist when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique @alias(property: "internationalStandardBookNumber") - title: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const cypher = `CREATE CONSTRAINT ${type.name}_isbn ${dbInfo.gte("4.4") ? "FOR" : "ON"} (n:${type.name}) ${ - dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT" - } n.internationalStandardBookNumber IS UNIQUE`; - - await testHelper.executeCypher(cypher); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, sessionConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should create a constraint if it doesn't exist and specified in options", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique - title: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - sessionConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - const result = await testHelper.executeCypher(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter((record) => record.labelsOrTypes.includes(type.name)) - ).toHaveLength(1); - }); - - test("should create a constraint if it doesn't exist and specified in options when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("Book"); - - const typeDefs = ` - type ${type.name} { - isbn: String! @unique @alias(property: "internationalStandardBookNumber") - title: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - sessionConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - const result = await testHelper.executeCypher(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter( - (record) => - record.labelsOrTypes.includes(type.name) && - record.properties.includes("internationalStandardBookNumber") - ) - ).toHaveLength(1); - }); - - test("should not throw if constraint exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = testHelper.createUniqueType("Base"); - const additionalType = testHelper.createUniqueType("Additional"); - - const typeDefs = ` - type ${baseType.name} @node(labels: ["${baseType.name}", "${additionalType.name}"]) { - someIntProperty: Int! - title: String! @unique - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_title ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.title IS UNIQUE; - `; - - await testHelper.executeCypher(createConstraintCypher); - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - sessionConfig: { database: databaseName }, - }) - ).resolves.not.toThrow(); - }); - - test("should not create new constraint if constraint exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = testHelper.createUniqueType("Base"); - const additionalType = testHelper.createUniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(labels: ["${baseType.name}", "${additionalType.name}"]) { - someIntProperty: Int! - title: String! @unique - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_title ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.title IS UNIQUE; - `; - - const showConstraintsCypher = "SHOW UNIQUE CONSTRAINTS"; - - await testHelper.executeCypher(createConstraintCypher); - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - sessionConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const dbConstraintsResult = (await testHelper.executeCypher(showConstraintsCypher)).records.map( - (record) => { - return record.toObject(); - } - ); - - expect( - dbConstraintsResult.filter( - (record) => record.labelsOrTypes.includes(baseType.name) && record.properties.includes("title") - ) - ).toHaveLength(0); - - expect( - dbConstraintsResult.filter( - (record) => - record.labelsOrTypes.includes(additionalType.name) && record.properties.includes("title") - ) - ).toHaveLength(1); - }); - - test("should not allow creating duplicate @unique properties when constraint is on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = testHelper.createUniqueType("Base"); - const additionalType = testHelper.createUniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(labels: ["${baseType.name}", "${additionalType.name}"]) { - someStringProperty: String! @unique @alias(property: "someAlias") - title: String! - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_someAlias ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.someAlias IS UNIQUE; - `; - - await testHelper.executeCypher(createConstraintCypher); - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const mutation = ` - mutation { - ${baseType.operations.create}(input: [ - { - someStringProperty: "notUnique", - title: "someTitle" - }, - { - someStringProperty: "notUnique", - title: "someTitle2" - }, - ]) { - ${baseType.plural} { - someStringProperty - } - } - } - `; - - const query = ` - query { - ${baseType.plural} { - someStringProperty - } - } - `; - - const mutationGqlResult = await testHelper.executeGraphQL(mutation); - - const queryGqlResult = await testHelper.executeGraphQL(query); - - expect((mutationGqlResult?.errors as any[])[0].message).toBe("Constraint validation failed"); - - expect(queryGqlResult.errors).toBeFalsy(); - expect(queryGqlResult.data?.[baseType.plural]).toBeArrayOfSize(0); - }); - - test("should not allow updating to duplicate @unique properties when constraint is on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = testHelper.createUniqueType("Base"); - const additionalType = testHelper.createUniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(labels: ["${baseType.name}", "${additionalType.name}"]) { - someStringProperty: String! @unique @alias(property: "someAlias") - title: String! - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_someAlias ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.someAlias IS UNIQUE; - `; - - await testHelper.executeCypher(createConstraintCypher); - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const uniqueVal1 = "someVal1"; - const uniqueVal2 = "someUniqueVal2"; - - const createMutation = ` - mutation { - ${baseType.operations.create}(input: [ - { - someStringProperty: "${uniqueVal1}", - title: "someTitle" - }, - { - someStringProperty: "${uniqueVal2}", - title: "someTitle2" - }, - ]) { - ${baseType.plural} { - someStringProperty - } - } - } - `; - - const updateMutation = ` - mutation { - ${baseType.operations.update}(update: { - someStringProperty: "notUnique" - }) { - ${baseType.plural} { - someStringProperty - } - } - } - `; - - const query = ` - query { - ${baseType.plural} { - someStringProperty - } - } - `; - - const createGqlResult = await testHelper.executeGraphQL(createMutation); - const updateGqlResult = await testHelper.executeGraphQL(updateMutation); - const queryGqlResult = await testHelper.executeGraphQL(query); - - expect(createGqlResult?.errors).toBeFalsy(); - expect((updateGqlResult?.errors as any[])[0].message).toBe("Constraint validation failed"); - - expect(queryGqlResult.errors).toBeFalsy(); - expect(queryGqlResult.data?.[baseType.plural]).toIncludeSameMembers([ - { - someStringProperty: uniqueVal1, - }, - { - someStringProperty: uniqueVal2, - }, - ]); - }); - }); - - describe("@id", () => { - test("should throw an error when all necessary constraints do not exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id @unique - name: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, sessionConfig: { database: databaseName } }) - ).rejects.toThrow(`Missing constraint for ${type.name}.id`); - }); - - test("should throw an error when all necessary constraints do not exist when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id @unique @alias(property: "identifier") - name: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, sessionConfig: { database: databaseName } }) - ).rejects.toThrow(`Missing constraint for ${type.name}.identifier`); - }); - - test("should not throw an error when all necessary constraints exist", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id @unique - name: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const cypher = `CREATE CONSTRAINT ${type.name}_id ${dbInfo.gte("4.4") ? "FOR" : "ON"} (n:${type.name}) ${ - dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT" - } n.id IS UNIQUE`; - - await testHelper.executeCypher(cypher); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, sessionConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should not throw an error when all necessary constraints exist when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id @unique @alias(property: "identifier") - name: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - const cypher = `CREATE CONSTRAINT ${type.name}_id ${dbInfo.gte("4.4") ? "FOR" : "ON"} (n:${type.name}) ${ - dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT" - } n.identifier IS UNIQUE`; - - await testHelper.executeCypher(cypher); - - await expect( - neoSchema.assertIndexesAndConstraints({ driver, sessionConfig: { database: databaseName } }) - ).resolves.not.toThrow(); - }); - - test("should create a constraint if it doesn't exist and specified in options", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id @unique - name: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - sessionConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - const result = await testHelper.executeCypher(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter((record) => record.labelsOrTypes.includes(type.name)) - ).toHaveLength(1); - }); - - test("should create a constraint if it doesn't exist and specified in options when used with @alias", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const type = testHelper.createUniqueType("User"); - - const typeDefs = ` - type ${type.name} { - id: ID! @id @unique @alias(property: "identifier") - name: String! - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - sessionConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const cypher = "SHOW UNIQUE CONSTRAINTS"; - - const result = await testHelper.executeCypher(cypher); - - expect( - result.records - .map((record) => { - return record.toObject(); - }) - .filter( - (record) => record.labelsOrTypes.includes(type.name) && record.properties.includes("identifier") - ) - ).toHaveLength(1); - }); - - test("should not throw if constraint exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = testHelper.createUniqueType("Base"); - const additionalType = testHelper.createUniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(labels: ["${baseType.name}", "${additionalType.name}"]) @mutation(operations: []) { - someIdProperty: ID! @id @unique @alias(property: "someAlias") - title: String! - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_someAlias ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.someAlias IS UNIQUE; - `; - - await testHelper.executeCypher(createConstraintCypher); - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - sessionConfig: { database: databaseName }, - }) - ).resolves.not.toThrow(); - }); - - test("should not create new constraint if constraint exists on an additional label", async () => { - // Skip if multi-db not supported - if (!MULTIDB_SUPPORT) { - console.log("MULTIDB_SUPPORT NOT AVAILABLE - SKIPPING"); - return; - } - - const baseType = testHelper.createUniqueType("Base"); - const additionalType = testHelper.createUniqueType("Additional"); - const typeDefs = ` - type ${baseType.name} @node(labels: ["${baseType.name}", "${additionalType.name}"]) @mutation(operations: []) { - someIdProperty: ID! @id @unique @alias(property: "someAlias") - title: String! - } - `; - - const createConstraintCypher = ` - CREATE CONSTRAINT ${baseType.name}_unique_someAlias ${dbInfo.gte("4.4") ? "FOR" : "ON"} (r:${ - additionalType.name - }) - ${dbInfo.gte("4.4") ? "REQUIRE" : "ASSERT"} r.someAlias IS UNIQUE; - `; - - const showConstraintsCypher = "SHOW UNIQUE CONSTRAINTS"; - - await testHelper.executeCypher(createConstraintCypher); - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - await neoSchema.getSchema(); - - await expect( - neoSchema.assertIndexesAndConstraints({ - driver, - sessionConfig: { database: databaseName }, - options: { create: true }, - }) - ).resolves.not.toThrow(); - - const dbConstraintsResult = (await testHelper.executeCypher(showConstraintsCypher)).records.map( - (record) => { - return record.toObject(); - } - ); - - expect( - dbConstraintsResult.filter( - (record) => record.labelsOrTypes.includes(baseType.name) && record.properties.includes("someAlias") - ) - ).toHaveLength(0); - - expect( - dbConstraintsResult.filter( - (record) => - record.labelsOrTypes.includes(additionalType.name) && record.properties.includes("someAlias") - ) - ).toHaveLength(1); - }); - }); -}); diff --git a/packages/graphql/tests/integration/field-filtering.int.test.ts b/packages/graphql/tests/integration/field-filtering.int.test.ts deleted file mode 100644 index af6e7638fd..0000000000 --- a/packages/graphql/tests/integration/field-filtering.int.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { gql } from "graphql-tag"; -import { generate } from "randomstring"; -import { TestHelper } from "../utils/tests-helper"; - -describe("field-filtering", () => { - const testHelper = new TestHelper(); - - afterAll(async () => { - await testHelper.close(); - }); - - test("should use connection filter on field", async () => { - const Movie = testHelper.createUniqueType("Movie"); - const Series = testHelper.createUniqueType("Series"); - const Genre = testHelper.createUniqueType("Genre"); - - const typeDefs = gql` - type ${Movie} { - title: String! - genres: [${Genre}!]! @relationship(type: "IN_GENRE", direction: OUT) - } - - type ${Genre} { - name: String! - series: [${Series}!]! @relationship(type: "IN_SERIES", direction: OUT) - } - - type ${Series} { - name: String! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieTitle = generate({ - charset: "alphabetic", - }); - - const genreName1 = generate({ - charset: "alphabetic", - }); - const genreName2 = generate({ - charset: "alphabetic", - }); - - const seriesName = generate({ - charset: "alphabetic", - }); - - const query = ` - { - ${Movie.plural}(where: { title: "${movieTitle}" }) { - title - genres(where: { seriesConnection: { node: { name: "${seriesName}" } } }) { - name - series { - name - } - } - } - } - `; - - const cypher = ` - CREATE (m:${Movie} {title:$movieTitle})-[:IN_GENRE]->(:${Genre} {name:$genreName1})-[:IN_SERIES]->(:${Series} {name:$seriesName}) - CREATE (m)-[:IN_GENRE]->(:${Genre} {name:$genreName2}) - `; - - await testHelper.executeCypher(cypher, { movieTitle, genreName1, seriesName, genreName2 }); - - const gqlResult = await testHelper.executeGraphQL(query); - - if (gqlResult.errors) { - console.log(JSON.stringify(gqlResult.errors, null, 2)); - } - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[Movie.plural]).toEqual([ - { title: movieTitle, genres: [{ name: genreName1, series: [{ name: seriesName }] }] }, - ]); - }); -}); diff --git a/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts b/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts index 4697f9329b..8ce6fa425a 100644 --- a/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts +++ b/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts @@ -18,7 +18,6 @@ */ import { generate } from "randomstring"; -import type { UniqueType } from "../../utils/graphql-types"; import { TestHelper } from "../../utils/tests-helper"; describe("Advanced Filtering", () => { @@ -34,54 +33,6 @@ describe("Advanced Filtering", () => { }); describe.each(["ID", "String"] as const)("%s Filtering", (type) => { - test("should find Movies IN strings", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = generate({ - readable: true, - charset: "alphabetic", - }); - - const randomValue1 = generate({ - readable: true, - charset: "alphabetic", - }); - - const randomValue2 = generate({ - readable: true, - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - `, - { value } - ); - - const query = ` - { - ${randomType.plural}(where: { property_IN: ["${value}", "${randomValue1}", "${randomValue2}"] }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(value); - }); - test("should find Movies REGEX", async () => { const randomType = testHelper.createUniqueType("Movie"); @@ -128,2052 +79,253 @@ describe("Advanced Filtering", () => { expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); expect((gqlResult.data as any)[randomType.plural][0].property).toBe(`${value}${value}`); }); + }); - test("should find Movies NOT string", async () => { - const randomType = testHelper.createUniqueType("Movie"); + describe("String Filtering", () => { + test("should find Movies GT string", async () => { + const movieType = testHelper.createUniqueType("Movie"); const typeDefs = ` - type ${randomType.name} { - property: ${type} + type ${movieType.name} { + title: String } `; - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = generate({ - readable: true, - charset: "alphabetic", + await testHelper.initNeo4jGraphQL({ + features: { + filters: { + String: { + LT: true, + GT: true, + LTE: true, + GTE: true, + }, + }, + }, + typeDefs, }); - const randomValue1 = generate({ - readable: true, - charset: "alphabetic", - }); + const animatrix = "The Animatrix"; + const matrix = "The Matrix"; + const matrixReloaded = "The Matrix Reloaded"; + const matrixRevolutions = "The Matrix Revolutions"; await testHelper.executeCypher( ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $randomValue1}) + CREATE (:${movieType.name} {title: $animatrix}) + CREATE (:${movieType.name} {title: $matrix}) + CREATE (:${movieType.name} {title: $matrixReloaded}) + CREATE (:${movieType.name} {title: $matrixRevolutions}) `, - { value, randomValue1 } + { animatrix, matrix, matrixReloaded, matrixRevolutions } ); const query = ` { - ${randomType.plural}(where: { property_NOT: "${randomValue1}" }) { - property + ${movieType.plural}(where: { title_GT: "${matrix}" }) { + title } } `; const gqlResult = await testHelper.executeGraphQL(query); - expect(gqlResult.errors).toBeUndefined(); + if (gqlResult.errors) { + console.log(JSON.stringify(gqlResult.errors, null, 2)); + } - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); + expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(value); + expect((gqlResult.data as any)[movieType.plural]).toHaveLength(2); + expect((gqlResult.data as any)[movieType.plural]).toEqual( + expect.arrayContaining([{ title: matrixReloaded }, { title: matrixRevolutions }]) + ); }); - test("should find Movies NOT_IN strings", async () => { - const randomType = testHelper.createUniqueType("Movie"); + test("should find Movies LT string", async () => { + const movieType = testHelper.createUniqueType("Movie"); const typeDefs = ` - type ${randomType.name} { - property: ${type} + type ${movieType.name} { + title: String } `; - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = generate({ - readable: true, - charset: "alphabetic", - }); - - const randomValue1 = generate({ - readable: true, - charset: "alphabetic", + await testHelper.initNeo4jGraphQL({ + features: { + filters: { + String: { + LT: true, + GT: true, + LTE: true, + GTE: true, + }, + }, + }, + typeDefs, }); - const randomValue2 = generate({ - readable: true, - charset: "alphabetic", - }); + const matrix = "The Matrix"; + const matrixReloaded = "The Matrix Reloaded"; + const matrixRevolutions = "The Matrix Revolutions"; + const matrixResurrections = "The Matrix Resurrections"; await testHelper.executeCypher( ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $randomValue1}) - CREATE (:${randomType.name} {property: $randomValue2}) + CREATE (:${movieType.name} {title: $matrix}) + CREATE (:${movieType.name} {title: $matrixReloaded}) + CREATE (:${movieType.name} {title: $matrixRevolutions}) + CREATE (:${movieType.name} {title: $matrixResurrections}) `, - { value, randomValue1, randomValue2 } + { matrix, matrixReloaded, matrixRevolutions, matrixResurrections } ); const query = ` { - ${randomType.plural}(where: { property_NOT_IN: ["${randomValue1}", "${randomValue2}"] }) { - property + ${movieType.plural}(where: { title_LT: "${matrixRevolutions}" }) { + title } } `; const gqlResult = await testHelper.executeGraphQL(query); - expect(gqlResult.errors).toBeUndefined(); + if (gqlResult.errors) { + console.log(JSON.stringify(gqlResult.errors, null, 2)); + } - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); + expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(value); + expect((gqlResult.data as any)[movieType.plural]).toHaveLength(3); + expect((gqlResult.data as any)[movieType.plural]).toEqual( + expect.arrayContaining([{ title: matrix }, { title: matrixReloaded }, { title: matrixResurrections }]) + ); }); - test("should find Movies CONTAINS string", async () => { - const randomType = testHelper.createUniqueType("Movie"); + test("should find Movies GTE string", async () => { + const movieType = testHelper.createUniqueType("Movie"); const typeDefs = ` - type ${randomType.name} { - property: ${type} + type ${movieType.name} { + title: String } `; - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = generate({ - readable: true, - charset: "alphabetic", + await testHelper.initNeo4jGraphQL({ + features: { + filters: { + String: { + LT: true, + GT: true, + LTE: true, + GTE: true, + }, + }, + }, + typeDefs, }); - const superValue = `${value}${value}`; + const animatrix = "The Animatrix"; + const matrix = "The Matrix"; + const matrixReloaded = "The Matrix Reloaded"; + const matrixRevolutions = "The Matrix Revolutions"; await testHelper.executeCypher( ` - CREATE (:${randomType.name} {property: $superValue}) - CREATE (:${randomType.name} {property: $superValue}) - CREATE (:${randomType.name} {property: $superValue}) + CREATE (:${movieType.name} {title: $animatrix}) + CREATE (:${movieType.name} {title: $matrix}) + CREATE (:${movieType.name} {title: $matrixReloaded}) + CREATE (:${movieType.name} {title: $matrixRevolutions}) `, - { superValue } + { animatrix, matrix, matrixReloaded, matrixRevolutions } ); const query = ` { - ${randomType.plural}(where: { property_CONTAINS: "${value}" }) { - property + ${movieType.plural}(where: { title_GTE: "${matrix}" }) { + title } } `; const gqlResult = await testHelper.executeGraphQL(query); - expect(gqlResult.errors).toBeUndefined(); + if (gqlResult.errors) { + console.log(JSON.stringify(gqlResult.errors, null, 2)); + } - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(3); + expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(superValue); + expect((gqlResult.data as any)[movieType.plural]).toHaveLength(3); + expect((gqlResult.data as any)[movieType.plural]).toEqual( + expect.arrayContaining([{ title: matrix }, { title: matrixReloaded }, { title: matrixRevolutions }]) + ); }); - test("should find Movies NOT_CONTAINS string", async () => { - const randomType = testHelper.createUniqueType("Movie"); + test("should find Movies LTE string", async () => { + const movieType = testHelper.createUniqueType("Movie"); const typeDefs = ` - type ${randomType.name} { - property: ${type} + type ${movieType.name} { + title: String } `; - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = generate({ - readable: true, - charset: "alphabetic", + await testHelper.initNeo4jGraphQL({ + features: { + filters: { + String: { + LT: true, + GT: true, + LTE: true, + GTE: true, + }, + }, + }, + typeDefs, }); - const notValue = generate({ - readable: true, - charset: "alphabetic", - }); + const matrix = "The Matrix"; + const matrixReloaded = "The Matrix Reloaded"; + const matrixRevolutions = "The Matrix Revolutions"; + const matrixResurrections = "The Matrix Resurrections"; await testHelper.executeCypher( ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $notValue}) - CREATE (:${randomType.name} {property: $notValue}) + CREATE (:${movieType.name} {title: $matrix}) + CREATE (:${movieType.name} {title: $matrixReloaded}) + CREATE (:${movieType.name} {title: $matrixRevolutions}) + CREATE (:${movieType.name} {title: $matrixResurrections}) + `, - { value, notValue } + { matrix, matrixReloaded, matrixRevolutions, matrixResurrections } ); const query = ` { - ${randomType.plural}(where: { property_NOT_CONTAINS: "${notValue}" }) { - property + ${movieType.plural}(where: { title_LTE: "${matrixRevolutions}" }) { + title } } `; const gqlResult = await testHelper.executeGraphQL(query); - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(value); - }); - - test("should find Movies STARTS_WITH string", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = generate({ - readable: true, - charset: "alphabetic", - }); + if (gqlResult.errors) { + console.log(JSON.stringify(gqlResult.errors, null, 2)); + } - const superValue = `${value}${value}`; + expect(gqlResult.errors).toBeUndefined(); - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $superValue}) - CREATE (:${randomType.name} {property: $superValue}) - CREATE (:${randomType.name} {property: $superValue}) - `, - { superValue } - ); - - const query = ` - { - ${randomType.plural}(where: { property_STARTS_WITH: "${value}" }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(3); - - ((gqlResult.data as any)[randomType.plural] as any[]).forEach((x) => { - expect(x.property).toEqual(superValue); - }); - }); - - test("should find Movies NOT_STARTS_WITH string", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = generate({ - readable: true, - charset: "alphabetic", - }); - - const notValue = generate({ - readable: true, - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $notValue}) - CREATE (:${randomType.name} {property: $notValue}) - `, - { value, notValue } - ); - - const query = ` - { - ${randomType.plural}(where: { property_NOT_STARTS_WITH: "${notValue}" }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - }); - - test("should find Movies ENDS_WITH string", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = generate({ - readable: true, - charset: "alphabetic", - }); - - const notValue = generate({ - readable: true, - charset: "alphabetic", - }); - - const superValue = `${value}${value}`; - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $notValue}) - CREATE (:${randomType.name} {property: $superValue}) - `, - { value, notValue, superValue } - ); - - const query = ` - { - ${randomType.plural}(where: { property_ENDS_WITH: "${value}" }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(2); - }); - - test("should find Movies NOT_ENDS_WITH string", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = generate({ - readable: true, - charset: "alphabetic", - }); - - const notValue = generate({ - readable: true, - charset: "alphabetic", - }); - - const superValue = `${value}${value}`; - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $notValue}) - CREATE (:${randomType.name} {property: $superValue}) - `, - { value, notValue, superValue } - ); - - const query = ` - { - ${randomType.plural}(where: { property_NOT_ENDS_WITH: "${value}" }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(notValue); - }); - }); - - describe("String Filtering", () => { - test("should find Movies GT string", async () => { - const movieType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${movieType.name} { - title: String - } - `; - - await testHelper.initNeo4jGraphQL({ - features: { - filters: { - String: { - LT: true, - GT: true, - LTE: true, - GTE: true, - }, - }, - }, - typeDefs, - }); - - const animatrix = "The Animatrix"; - const matrix = "The Matrix"; - const matrixReloaded = "The Matrix Reloaded"; - const matrixRevolutions = "The Matrix Revolutions"; - - await testHelper.executeCypher( - ` - CREATE (:${movieType.name} {title: $animatrix}) - CREATE (:${movieType.name} {title: $matrix}) - CREATE (:${movieType.name} {title: $matrixReloaded}) - CREATE (:${movieType.name} {title: $matrixRevolutions}) - `, - { animatrix, matrix, matrixReloaded, matrixRevolutions } - ); - - const query = ` - { - ${movieType.plural}(where: { title_GT: "${matrix}" }) { - title - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - if (gqlResult.errors) { - console.log(JSON.stringify(gqlResult.errors, null, 2)); - } - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[movieType.plural]).toHaveLength(2); - expect((gqlResult.data as any)[movieType.plural]).toEqual( - expect.arrayContaining([{ title: matrixReloaded }, { title: matrixRevolutions }]) - ); - }); - - test("should find Movies LT string", async () => { - const movieType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${movieType.name} { - title: String - } - `; - - await testHelper.initNeo4jGraphQL({ - features: { - filters: { - String: { - LT: true, - GT: true, - LTE: true, - GTE: true, - }, - }, - }, - typeDefs, - }); - - const matrix = "The Matrix"; - const matrixReloaded = "The Matrix Reloaded"; - const matrixRevolutions = "The Matrix Revolutions"; - const matrixResurrections = "The Matrix Resurrections"; - - await testHelper.executeCypher( - ` - CREATE (:${movieType.name} {title: $matrix}) - CREATE (:${movieType.name} {title: $matrixReloaded}) - CREATE (:${movieType.name} {title: $matrixRevolutions}) - CREATE (:${movieType.name} {title: $matrixResurrections}) - `, - { matrix, matrixReloaded, matrixRevolutions, matrixResurrections } - ); - - const query = ` - { - ${movieType.plural}(where: { title_LT: "${matrixRevolutions}" }) { - title - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - if (gqlResult.errors) { - console.log(JSON.stringify(gqlResult.errors, null, 2)); - } - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[movieType.plural]).toHaveLength(3); + expect((gqlResult.data as any)[movieType.plural]).toHaveLength(4); expect((gqlResult.data as any)[movieType.plural]).toEqual( - expect.arrayContaining([{ title: matrix }, { title: matrixReloaded }, { title: matrixResurrections }]) + expect.arrayContaining([ + { title: matrix }, + { title: matrixReloaded }, + { title: matrixRevolutions }, + { title: matrixResurrections }, + ]) ); }); - - test("should find Movies GTE string", async () => { - const movieType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${movieType.name} { - title: String - } - `; - - await testHelper.initNeo4jGraphQL({ - features: { - filters: { - String: { - LT: true, - GT: true, - LTE: true, - GTE: true, - }, - }, - }, - typeDefs, - }); - - const animatrix = "The Animatrix"; - const matrix = "The Matrix"; - const matrixReloaded = "The Matrix Reloaded"; - const matrixRevolutions = "The Matrix Revolutions"; - - await testHelper.executeCypher( - ` - CREATE (:${movieType.name} {title: $animatrix}) - CREATE (:${movieType.name} {title: $matrix}) - CREATE (:${movieType.name} {title: $matrixReloaded}) - CREATE (:${movieType.name} {title: $matrixRevolutions}) - `, - { animatrix, matrix, matrixReloaded, matrixRevolutions } - ); - - const query = ` - { - ${movieType.plural}(where: { title_GTE: "${matrix}" }) { - title - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - if (gqlResult.errors) { - console.log(JSON.stringify(gqlResult.errors, null, 2)); - } - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[movieType.plural]).toHaveLength(3); - expect((gqlResult.data as any)[movieType.plural]).toEqual( - expect.arrayContaining([{ title: matrix }, { title: matrixReloaded }, { title: matrixRevolutions }]) - ); - }); - - test("should find Movies LTE string", async () => { - const movieType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${movieType.name} { - title: String - } - `; - - await testHelper.initNeo4jGraphQL({ - features: { - filters: { - String: { - LT: true, - GT: true, - LTE: true, - GTE: true, - }, - }, - }, - typeDefs, - }); - - const matrix = "The Matrix"; - const matrixReloaded = "The Matrix Reloaded"; - const matrixRevolutions = "The Matrix Revolutions"; - const matrixResurrections = "The Matrix Resurrections"; - - await testHelper.executeCypher( - ` - CREATE (:${movieType.name} {title: $matrix}) - CREATE (:${movieType.name} {title: $matrixReloaded}) - CREATE (:${movieType.name} {title: $matrixRevolutions}) - CREATE (:${movieType.name} {title: $matrixResurrections}) - - `, - { matrix, matrixReloaded, matrixRevolutions, matrixResurrections } - ); - - const query = ` - { - ${movieType.plural}(where: { title_LTE: "${matrixRevolutions}" }) { - title - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - if (gqlResult.errors) { - console.log(JSON.stringify(gqlResult.errors, null, 2)); - } - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[movieType.plural]).toHaveLength(4); - expect((gqlResult.data as any)[movieType.plural]).toEqual( - expect.arrayContaining([ - { title: matrix }, - { title: matrixReloaded }, - { title: matrixRevolutions }, - { title: matrixResurrections }, - ]) - ); - }); - }); - - describe.each(["Int", "Float"] as const)("%s Filtering", (type) => { - test("should find Movies NOT number", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - let property: number; - - if (type === "Int") { - property = Math.floor(Math.random() * 9999); - } else { - property = Math.floor(Math.random() * 9999) + 0.5; - } - - let notProperty: number; - - if (type === "Int") { - notProperty = Math.floor(Math.random() * 9999); - } else { - notProperty = Math.floor(Math.random() * 9999) + 0.5; - } - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $property}) - CREATE (:${randomType.name} {property: $notProperty}) - `, - { property, notProperty } - ); - - const query = ` - { - ${randomType.plural}(where: { property_NOT: ${notProperty} }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(property); - }); - - test("should find Movies IN numbers", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - let value: number; - - if (type === "Int") { - value = Math.floor(Math.random() * 9999); - } else { - value = Math.floor(Math.random() * 9999) + 0.5; - } - - let randomValue1: number; - - if (type === "Int") { - randomValue1 = Math.floor(Math.random() * 9999); - } else { - randomValue1 = Math.floor(Math.random() * 9999) + 0.5; - } - - let randomValue2: number; - - if (type === "Int") { - randomValue2 = Math.floor(Math.random() * 9999); - } else { - randomValue2 = Math.floor(Math.random() * 9999) + 0.5; - } - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - `, - { value } - ); - - const query = ` - { - ${randomType.plural}(where: { property_IN: [${value}, ${randomValue1}, ${randomValue2}] }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(value); - }); - - test("should find Movies NOT_IN numbers", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - let value: number; - - if (type === "Int") { - value = Math.floor(Math.random() * 9999); - } else { - value = Math.floor(Math.random() * 9999) + 0.5; - } - - let randomValue1: number; - - if (type === "Int") { - randomValue1 = Math.floor(Math.random() * 99999); - } else { - randomValue1 = Math.floor(Math.random() * 99999) + 0.5; - } - - let randomValue2: number; - - if (type === "Int") { - randomValue2 = Math.floor(Math.random() * 99999); - } else { - randomValue2 = Math.floor(Math.random() * 99999) + 0.5; - } - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $randomValue1}) - CREATE (:${randomType.name} {property: $randomValue2}) - `, - { value, randomValue1, randomValue2 } - ); - - const query = ` - { - ${randomType.plural}(where: { property_NOT_IN: [${randomValue1}, ${randomValue2}] }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(value); - }); - - test("should find Movies LT number", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - let value: number; - - if (type === "Int") { - value = Math.floor(Math.random() * 9999); - } else { - value = Math.floor(Math.random() * 9999) + 0.5; - } - - const lessThanValue = value - (value + 1); - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $lessThanValue}) - `, - { value, lessThanValue } - ); - - const query = ` - { - ${randomType.plural}(where: { property_LT: ${lessThanValue + 1} }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(lessThanValue); - }); - - test("should find Movies LTE number", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - let value: number; - - if (type === "Int") { - value = Math.floor(Math.random() * 9999); - } else { - value = Math.floor(Math.random() * 9999) + 0.5; - } - - const lessThanValue = value - (value + 1); - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $lessThanValue}) - `, - { value, lessThanValue } - ); - - const query = ` - { - ${randomType.plural}(where: { property_LTE: ${value} }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(2); - }); - - test("should find Movies GT number", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - let value: number; - - if (type === "Int") { - value = Math.floor(Math.random() * 9999); - } else { - value = Math.floor(Math.random() * 9999) + 0.5; - } - - const graterThanValue = value + 1; - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $graterThanValue}) - `, - { value, graterThanValue } - ); - - const query = ` - { - ${randomType.plural}(where: { property_GT: ${graterThanValue - 1} }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(graterThanValue); - }); - - test("should find Movies GTE number", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: ${type} - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - let value: number; - - if (type === "Int") { - value = Math.floor(Math.random() * 9999); - } else { - value = Math.floor(Math.random() * 9999) + 0.5; - } - - const greaterThan = value + 1; - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $greaterThan}) - `, - { value, greaterThan } - ); - - const query = ` - { - ${randomType.plural}(where: { property_GTE: ${value} }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(2); - }); - }); - - describe("Boolean Filtering", () => { - test("should find Movies equality equality", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: Boolean - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = false; - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - `, - { value } - ); - - const query = ` - { - ${randomType.plural}(where: { property: false }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(1); - }); - - test("should find Movies NOT boolean", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - property: Boolean - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const value = false; - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - `, - { value } - ); - - const query = ` - { - ${randomType.plural}(where: { property_NOT: false }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(0); - }); - }); - - describe("Relationship/Connection Filtering", () => { - describe("equality", () => { - test("should find using relationship equality on node", async () => { - const randomType1 = testHelper.createUniqueType("Movie"); - const randomType2 = testHelper.createUniqueType("Genre"); - - const typeDefs = ` - type ${randomType1.name} { - id: ID - ${randomType2.plural}: [${randomType2.name}!]! @relationship(type: "IN_GENRE", direction: OUT) - } - - type ${randomType2.name} { - id: ID - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const rootId = generate({ - charset: "alphabetic", - }); - - const relationId = generate({ - charset: "alphabetic", - }); - - const randomId = generate({ - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (root:${randomType1.name} {id: $rootId}) - CREATE (:${randomType1.name} {id: $randomId}) - CREATE (relation:${randomType2.name} {id: $relationId}) - CREATE (:${randomType2.name} {id: $randomId}) - MERGE (root)-[:IN_GENRE]->(relation) - `, - { rootId, relationId, randomId } - ); - - const query = ` - { - ${randomType1.plural}(where: { ${randomType2.plural}: { id: "${relationId}" } }) { - id - ${randomType2.plural} { - id - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType1.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType1.plural][0]).toMatchObject({ - id: rootId, - [randomType2.plural]: [{ id: relationId }], - }); - }); - - test("should find using equality on node using connection", async () => { - const Movie = testHelper.createUniqueType("Movie"); - const Genre = testHelper.createUniqueType("Genre"); - - const typeDefs = ` - type ${Movie} { - id: ID - genres: [${Genre}!]! @relationship(type: "IN_GENRE", direction: OUT) - } - - type ${Genre} { - id: ID - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieId = generate({ - charset: "alphabetic", - }); - - const genreId = generate({ - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (:${Movie} {id: $movieId})-[:IN_GENRE]->(:${Genre} {id:$genreId}) - `, - { movieId, genreId } - ); - - const query = ` - { - ${Movie.plural}(where: { genresConnection: { node: { id: "${genreId}" } } }) { - id - genres { - id - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect(gqlResult.data as any).toEqual({ - [Movie.plural]: [ - { - id: movieId, - genres: [{ id: genreId }], - }, - ], - }); - }); - - test("should find using equality on relationship using connection", async () => { - const Movie = testHelper.createUniqueType("Movie"); - const Genre = testHelper.createUniqueType("Genre"); - - const typeDefs = ` - type ${Movie} { - id: ID - genres: [${Genre}!]! @relationship(type: "IN_GENRE", direction: OUT, properties: "ActedIn") - } - - type ${Genre} { - id: ID - } - - type ActedIn @relationshipProperties { - id: String - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieId = generate({ - charset: "alphabetic", - }); - - const genreId = generate({ - charset: "alphabetic", - }); - - const actedInId = generate({ - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (movie:${Movie} {id: $movieId})-[:IN_GENRE {id:$actedInId}]->(:${Genre} {id:$genreId}) - `, - { movieId, genreId, actedInId } - ); - - const query = ` - { - ${Movie.plural}(where: { genresConnection: { edge: { id: "${actedInId}" } } }) { - id - genres { - id - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - expect(gqlResult.data as any).toEqual({ - [Movie.plural]: [ - { - id: movieId, - genres: [{ id: genreId }], - }, - ], - }); - }); - - test("should find relationship and node property equality using connection", async () => { - const Movie = testHelper.createUniqueType("Movie"); - const Genre = testHelper.createUniqueType("Genre"); - - const typeDefs = ` - type ${Movie} { - id: ID - genres: [${Genre}!]! @relationship(type: "IN_GENRE", direction: OUT, properties: "ActedIn") - } - - type ${Genre} { - id: ID - } - - type ActedIn @relationshipProperties { - id: String - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieId = generate({ - charset: "alphabetic", - }); - - const genreId = generate({ - charset: "alphabetic", - }); - - const actedInId = generate({ - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (:${Movie} {id: $movieId})-[:IN_GENRE {id:$actedInId}]->(:${Genre} {id:$genreId}) - `, - { movieId, genreId, actedInId } - ); - - const query = ` - { - ${Movie.plural}(where: { genresConnection: { node: { id: "${genreId}" } edge: { id: "${actedInId}" } } }) { - id - genres { - id - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect(gqlResult.data as any).toEqual({ - [Movie.plural]: [ - { - id: movieId, - genres: [{ id: genreId }], - }, - ], - }); - }); - }); - - describe("NOT", () => { - test("should find using NOT on relationship", async () => { - const randomType1 = testHelper.createUniqueType("Movie"); - const randomType2 = testHelper.createUniqueType("Genre"); - - const typeDefs = ` - type ${randomType1.name} { - id: ID - ${randomType2.plural}: [${randomType2.name}!]! @relationship(type: "IN_GENRE", direction: OUT) - } - - type ${randomType2.name} { - id: ID - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const rootId1 = generate({ - charset: "alphabetic", - }); - const rootId2 = generate({ - charset: "alphabetic", - }); - - const relationId1 = generate({ - charset: "alphabetic", - }); - const relationId2 = generate({ - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (root1:${randomType1.name} {id: $rootId1}) - CREATE (root2:${randomType1.name} {id: $rootId2}) - CREATE (relation1:${randomType2.name} {id: $relationId1}) - CREATE (relation2:${randomType2.name} {id: $relationId2}) - MERGE (root1)-[:IN_GENRE]->(relation1) - MERGE (root2)-[:IN_GENRE]->(relation2) - `, - { rootId1, rootId2, relationId1, relationId2 } - ); - - const query = ` - { - ${randomType1.plural}(where: { ${randomType2.plural}_NOT: { id: "${relationId2}" } }) { - id - ${randomType2.plural} { - id - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType1.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType1.plural][0]).toMatchObject({ - id: rootId1, - [randomType2.plural]: [{ id: relationId1 }], - }); - }); - - test("should find using NOT on connections", async () => { - const randomType1 = testHelper.createUniqueType("Movie"); - const randomType2 = testHelper.createUniqueType("Genre"); - - const typeDefs = ` - type ${randomType1.name} { - id: ID - ${randomType2.plural}: [${randomType2.name}!]! @relationship(type: "IN_GENRE", direction: OUT) - } - - type ${randomType2.name} { - id: ID - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const rootId1 = generate({ - charset: "alphabetic", - }); - const rootId2 = generate({ - charset: "alphabetic", - }); - - const relationId1 = generate({ - charset: "alphabetic", - }); - const relationId2 = generate({ - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (root1:${randomType1.name} {id: $rootId1})-[:IN_GENRE]->(relation1:${randomType2.name} {id: $relationId1}) - CREATE (root2:${randomType1.name} {id: $rootId2})-[:IN_GENRE]->(relation2:${randomType2.name} {id: $relationId2}) - `, - { rootId1, rootId2, relationId1, relationId2 } - ); - - const query = ` - { - ${randomType1.plural}(where: { ${randomType2.plural}Connection_NOT: { node: { id: "${relationId2}" } } }) { - id - ${randomType2.plural} { - id - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType1.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType1.plural][0]).toMatchObject({ - id: rootId1, - [randomType2.plural]: [{ id: relationId1 }], - }); - }); - - test("should find using relationship properties and connections", async () => { - const randomType1 = testHelper.createUniqueType("Movie"); - const randomType2 = testHelper.createUniqueType("Genre"); - - const typeDefs = ` - type ${randomType1.name} { - id: ID - ${randomType2.plural}: [${randomType2.name}!]! @relationship(type: "IN_GENRE", direction: OUT, properties: "ActedIn") - } - - type ${randomType2.name} { - id: ID - } - - type ActedIn @relationshipProperties { - id: ID - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const rootId1 = generate({ - charset: "alphabetic", - }); - const rootId2 = generate({ - charset: "alphabetic", - }); - - const relationId1 = generate({ - charset: "alphabetic", - }); - const relationId2 = generate({ - charset: "alphabetic", - }); - const actedInId = generate({ - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (:${randomType1.name} {id: $rootId1})-[:IN_GENRE {id: $actedInId}]->(:${randomType2.name} {id: $relationId1}) - CREATE (:${randomType1.name} {id: $rootId2})-[:IN_GENRE {id: randomUUID()}]->(:${randomType2.name} {id: $relationId2}) - `, - { rootId1, rootId2, relationId1, relationId2, actedInId } - ); - - const query = ` - { - ${randomType1.plural}(where: { ${randomType2.plural}Connection_NOT: { edge: { id: "${actedInId}" } } }) { - id - ${randomType2.plural} { - id - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType1.plural]).toHaveLength(1); - expect((gqlResult.data as any)[randomType1.plural][0]).toMatchObject({ - id: rootId2, - [randomType2.plural]: [{ id: relationId2 }], - }); - }); - }); - - describe("List Predicates", () => { - let Movie: UniqueType; - let Actor: UniqueType; - - const movies = [ - ...Array(4) - .fill(null) - .map((_, i) => ({ id: generate(), budget: (i + 1) ** 2 })), - ]; - const actors = [ - ...Array(4) - .fill(null) - .map((_, i) => ({ id: generate(), flag: i % 2 === 0 })), - ]; - - beforeEach(async () => { - Movie = testHelper.createUniqueType("Movie"); - Actor = testHelper.createUniqueType("Actor"); - - const typeDefs = ` - type ${Movie} { - id: ID! @id @unique - budget: Int! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${Actor} { - id: ID! @id @unique - flag: Boolean! - actedIn: [${Movie}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - await testHelper.executeCypher( - ` - CREATE (m1:${Movie}) SET m1 = $movies[0] - CREATE (m2:${Movie}) SET m2 = $movies[1] - CREATE (m3:${Movie}) SET m3 = $movies[2] - CREATE (m4:${Movie}) SET m4 = $movies[3] - CREATE (a1:${Actor}) SET a1 = $actors[0] - CREATE (a2:${Actor}) SET a2 = $actors[1] - CREATE (a3:${Actor}) SET a3 = $actors[2] - CREATE (a4:${Actor}) SET a4 = $actors[3] - MERGE (a1)-[:ACTED_IN]->(m1)<-[:ACTED_IN]-(a3) - MERGE (a2)-[:ACTED_IN]->(m2)<-[:ACTED_IN]-(a3) - MERGE (a2)-[:ACTED_IN]->(m3)<-[:ACTED_IN]-(a4) - MERGE (a1)-[:ACTED_IN]->(m4)<-[:ACTED_IN]-(a2) - MERGE (a3)-[:ACTED_IN]->(m4) - `, - { movies, actors } - ); - }); - - describe("on relationship", () => { - function generateQuery(predicate: "ALL" | "NONE" | "SINGLE" | "SOME") { - return ` - query($movieIds: [ID!]!) { - ${Movie.plural}(where: { AND: [{ id_IN: $movieIds }, { actors_${predicate}: { flag_NOT: false } }] }) { - id - actors(where: { flag_NOT: false }) { - id - flag - } - } - } - `; - } - - test("ALL", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("ALL"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[0]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - }); - - test("NONE", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("NONE"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[2]?.id, - actors: [], - }); - }); - - test("SINGLE", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("SINGLE"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[1]?.id, - actors: expect.toIncludeSameMembers([actors[2]]), - }); - }); - - test("SOME", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("SOME"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(3); - expect(gqlMovies).toContainEqual({ - id: movies[0]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - expect(gqlMovies).toContainEqual({ - id: movies[1]?.id, - actors: expect.toIncludeSameMembers([actors[2]]), - }); - expect(gqlMovies).toContainEqual({ - id: movies[3]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - }); - }); - - describe("on relationship using NOT operator", () => { - const generateQuery = (predicate: "ALL" | "NONE" | "SINGLE" | "SOME") => ` - query($movieIds: [ID!]!) { - ${Movie.plural}(where: { AND: [{ id_IN: $movieIds }, { actors_${predicate}: { NOT: { flag: false } } }] }) { - id - actors(where: { NOT: { flag: false } }) { - id - flag - } - } - } - `; - - test("ALL", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("ALL"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[0]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - }); - - test("NONE", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("NONE"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[2]?.id, - actors: [], - }); - }); - - test("SINGLE", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("SINGLE"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[1]?.id, - actors: expect.toIncludeSameMembers([actors[2]]), - }); - }); - - test("SOME", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("SOME"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(3); - expect(gqlMovies).toContainEqual({ - id: movies[0]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - expect(gqlMovies).toContainEqual({ - id: movies[1]?.id, - actors: expect.toIncludeSameMembers([actors[2]]), - }); - expect(gqlMovies).toContainEqual({ - id: movies[3]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - }); - }); - - describe("on connection", () => { - const generateQuery = (predicate: "ALL" | "NONE" | "SINGLE" | "SOME") => ` - query($movieIds: [ID!]!) { - ${Movie.plural}(where: { AND: [{ id_IN: $movieIds }, { actorsConnection_${predicate}: { node: { flag_NOT: false } } }] }) { - id - actors(where: {flag_NOT: false}) { - id - flag - } - } - } - `; - - test("ALL", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("ALL"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[0]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - }); - - test("NONE", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("NONE"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[2]?.id, - actors: [], - }); - }); - - test("SINGLE", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("SINGLE"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[1]?.id, - actors: expect.toIncludeSameMembers([actors[2]]), - }); - }); - - test("SOME", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("SOME"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(3); - expect(gqlMovies).toContainEqual({ - id: movies[0]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - expect(gqlMovies).toContainEqual({ - id: movies[1]?.id, - actors: expect.toIncludeSameMembers([actors[2]]), - }); - expect(gqlMovies).toContainEqual({ - id: movies[3]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - }); - }); - - describe("on connection using NOT operator", () => { - const generateQuery = (predicate: "ALL" | "NONE" | "SINGLE" | "SOME") => ` - query($movieIds: [ID!]!) { - ${Movie.plural}(where: { AND: [{ id_IN: $movieIds }, { actorsConnection_${predicate}: { node: { NOT: { flag: false } } } }] }) { - id - actors(where: { NOT: { flag: false }}) { - id - flag - } - } - } - `; - - test("ALL", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("ALL"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[0]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - }); - - test("NONE", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("NONE"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[2]?.id, - actors: [], - }); - }); - - test("SINGLE", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("SINGLE"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(1); - expect(gqlMovies).toContainEqual({ - id: movies[1]?.id, - actors: expect.toIncludeSameMembers([actors[2]]), - }); - }); - - test("SOME", async () => { - const gqlResult = await testHelper.executeGraphQL(generateQuery("SOME"), { - variableValues: { movieIds: movies.map(({ id }) => id) }, - }); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovies = gqlResult.data?.[Movie.plural]; - - expect(gqlMovies).toHaveLength(3); - expect(gqlMovies).toContainEqual({ - id: movies[0]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - expect(gqlMovies).toContainEqual({ - id: movies[1]?.id, - actors: expect.toIncludeSameMembers([actors[2]]), - }); - expect(gqlMovies).toContainEqual({ - id: movies[3]?.id, - actors: expect.toIncludeSameMembers([actors[0], actors[2]]), - }); - }); - }); - }); - - test("should test for not null", async () => { - const randomType1 = testHelper.createUniqueType("Movie"); - const randomType2 = testHelper.createUniqueType("Genre"); - - const typeDefs = ` - type ${randomType1.name} { - id: ID - ${randomType2.plural}: [${randomType2.name}!]! @relationship(type: "IN_GENRE", direction: OUT) - } - - type ${randomType2.name} { - id: ID - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const rootId = generate({ - charset: "alphabetic", - }); - - const relationId = generate({ - charset: "alphabetic", - }); - - const randomId = generate({ - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (root:${randomType1.name} {id: $rootId}) - CREATE (:${randomType1.name} {id: $randomId}) - CREATE (relation:${randomType2.name} {id: $relationId}) - CREATE (:${randomType2.name} {id: $randomId}) - MERGE (root)-[:IN_GENRE]->(relation) - `, - { rootId, relationId, randomId } - ); - - const nullQuery = ` - { - ${randomType1.plural}(where: { ${randomType2.plural}: null }) { - id - } - } - `; - - // Test null checking (nodes without any related nodes on the specified field) - - const nullResult = await testHelper.executeGraphQL(nullQuery); - - expect(nullResult.errors).toBeUndefined(); - - expect((nullResult.data as any)[randomType1.plural]).toHaveLength(1); - expect((nullResult.data as any)[randomType1.plural][0]).toMatchObject({ - id: randomId, - }); - - // Test not null checking (nodes without any related nodes on the specified field) - - const notNullQuery = ` - { - ${randomType1.plural}(where: { ${randomType2.plural}_NOT: null }) { - id - } - } - `; - - const notNullResult = await testHelper.executeGraphQL(notNullQuery); - - expect(notNullResult.errors).toBeUndefined(); - - expect((notNullResult.data as any)[randomType1.plural]).toHaveLength(1); - expect((notNullResult.data as any)[randomType1.plural][0]).toMatchObject({ - id: rootId, - }); - }); - }); - - describe("NULL Filtering", () => { - // TODO: split in 2 tests - test("should work for existence and non-existence", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} { - id: String! - optional: String - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const id1 = generate({ - readable: true, - charset: "alphabetic", - }); - - const id2 = generate({ - readable: true, - charset: "alphabetic", - }); - - const optionalValue = generate({ - readable: true, - charset: "alphabetic", - }); - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {id: $id1}) - CREATE (:${randomType.name} {id: $id2, optional: $optionalValue}) - `, - { id1, id2, optionalValue } - ); - - // Test NULL checking - - const nullQuery = ` - { - ${randomType.plural}(where: { optional: null }) { - id - } - } - `; - - const nullResult = await testHelper.executeGraphQL(nullQuery); - - expect(nullResult.errors).toBeUndefined(); - - expect((nullResult.data as any)[randomType.plural]).toHaveLength(1); - - expect((nullResult.data as any)[randomType.plural][0].id).toEqual(id1); - - // Test NOT NULL checking - - const notNullQuery = ` - { - ${randomType.plural}(where: { optional_NOT: null }) { - id - } - } - `; - - const notNullResult = await testHelper.executeGraphQL(notNullQuery); - - expect(notNullResult.errors).toBeUndefined(); - - expect((notNullResult.data as any)[randomType.plural]).toHaveLength(1); - - expect((notNullResult.data as any)[randomType.plural][0].id).toEqual(id2); - }); }); }); diff --git a/packages/graphql/tests/integration/filtering/operations.int.test.ts b/packages/graphql/tests/integration/filtering/operations.int.test.ts deleted file mode 100644 index 07231ff40f..0000000000 --- a/packages/graphql/tests/integration/filtering/operations.int.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { UniqueType } from "../../utils/graphql-types"; -import { TestHelper } from "../../utils/tests-helper"; - -describe("Filtering Operations", () => { - const testHelper = new TestHelper(); - let personType: UniqueType; - let movieType: UniqueType; - - beforeEach(async () => { - personType = testHelper.createUniqueType("Person"); - movieType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${personType} { - name: String! - age: Int! - movies: [${movieType}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${movieType} { - title: String! - released: Int! - actors: [${personType}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.executeCypher(`CREATE (:${movieType} {title: "The Matrix", released: 1999}) - CREATE (:${movieType} {title: "The Italian Job", released: 1969}) - CREATE (:${movieType} {title: "The Italian Job", released: 2003}) - CREATE (:${movieType} {title: "The Lion King", released: 1994}) - `); - - await testHelper.initNeo4jGraphQL({ typeDefs }); - }); - - afterEach(async () => { - await testHelper.close(); - }); - - it("Combine AND and OR operations", async () => { - const query = ` - query { - ${movieType.plural}(where: { OR: [{ title: "The Italian Job", released: 2003 }, { title: "The Lion King" }] }) { - title - released - } - } - `; - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeUndefined(); - - const moviesResult = result.data?.[movieType.plural]; - expect(moviesResult).toEqual( - expect.toIncludeSameMembers([ - { title: "The Italian Job", released: 2003 }, - { title: "The Lion King", released: 1994 }, - ]) - ); - }); -}); diff --git a/packages/graphql/tests/integration/find.int.test.ts b/packages/graphql/tests/integration/find.int.test.ts deleted file mode 100644 index e0ef134f1c..0000000000 --- a/packages/graphql/tests/integration/find.int.test.ts +++ /dev/null @@ -1,503 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { generate } from "randomstring"; -import type { UniqueType } from "../utils/graphql-types"; -import { TestHelper } from "../utils/tests-helper"; - -describe("find", () => { - const testHelper = new TestHelper(); - let Movie: UniqueType; - let Actor: UniqueType; - let User: UniqueType; - - beforeEach(() => { - Movie = testHelper.createUniqueType("Movie"); - Actor = testHelper.createUniqueType("Actor"); - User = testHelper.createUniqueType("User"); - }); - - afterEach(async () => { - await testHelper.close(); - }); - - test("should find Movie by id", async () => { - const typeDefs = ` - type ${Actor} { - name: String - movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${Movie} { - id: ID! - title: String! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - - const id = generate({ - charset: "alphabetic", - }); - - const query = ` - query($id: ID){ - ${Movie.plural}(where: {id: $id}){ - id - } - } - `; - - await neoSchema.checkNeo4jCompat(); - - await testHelper.executeCypher(`CREATE (:${Movie} {id: $id}), (:${Movie} {id: $id}), (:${Movie} {id: $id})`, { - id, - }); - - const result = await testHelper.executeGraphQL(query, { - variableValues: { id }, - }); - - expect(result.errors).toBeFalsy(); - - expect(result?.data?.[Movie.plural]).toEqual([{ id }, { id }, { id }]); - }); - - test("should find Move by id and limit", async () => { - const typeDefs = ` - type ${Actor} { - name: String - movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${Movie} { - id: ID! - title: String! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const id = generate({ - charset: "alphabetic", - }); - - const query = ` - query($id: ID){ - ${Movie.plural}(where: {id: $id}, options: {limit: 2}){ - id - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (:${Movie} {id: $id}), (:${Movie} {id: $id}), (:${Movie} {id: $id}) - `, - { id } - ); - - const result = await testHelper.executeGraphQL(query, { - variableValues: { id }, - }); - - expect(result.errors).toBeFalsy(); - - expect(result?.data?.[Movie.plural]).toEqual([{ id }, { id }]); - }); - - test("should find Movie IN ids", async () => { - const typeDefs = ` - type ${Actor} { - name: String - movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${Movie} { - id: ID! - title: String! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - const id1 = generate({ - charset: "alphabetic", - }); - const id2 = generate({ - charset: "alphabetic", - }); - const id3 = generate({ - charset: "alphabetic", - }); - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = ` - query($ids: [ID!]){ - ${Movie.plural}(where: {id_IN: $ids}){ - id - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (:${Movie} {id: $id1}), (:${Movie} {id: $id2}), (:${Movie} {id: $id3}) - `, - { id1, id2, id3 } - ); - - const result = await testHelper.executeGraphQL(query, { - variableValues: { ids: [id1, id2, id3] }, - }); - - expect(result.errors).toBeFalsy(); - - (result?.data as any)?.[Movie.plural].forEach((e: { id: string }) => { - expect([id1, id2, id3].includes(e.id)).toBeTruthy(); - }); - }); - - test("should find Movie IN ids with one other param", async () => { - const typeDefs = ` - type ${Actor} { - name: String - movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${Movie} { - id: ID! - title: String! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const id1 = generate({ - charset: "alphabetic", - }); - const id2 = generate({ - charset: "alphabetic", - }); - const id3 = generate({ - charset: "alphabetic", - }); - const title = generate({ - charset: "alphabetic", - }); - - const query = ` - query($ids: [ID!], $title: String){ - ${Movie.plural}(where: {id_IN: $ids, title: $title}){ - id - title - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (:${User} {id: $id1, title: $title}), (:${User} {id: $id2, title: $title}), (:${User} {id: $id3, title: $title}) - `, - { id1, id2, id3, title } - ); - - const result = await testHelper.executeGraphQL(query, { - variableValues: { ids: [id1, id2, id3], title }, - }); - - expect(result.errors).toBeFalsy(); - - (result?.data as any)?.[Movie.plural].forEach((e: { id: string; title: string }) => { - expect([id1, id2, id3].includes(e.id)).toBeTruthy(); - expect(e.title).toEqual(title); - }); - }); - - test("should find Movie IN id and many Movie.actor IN id", async () => { - const typeDefs = ` - type ${Actor} { - id: ID! - movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${Movie} { - id: ID! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieId1 = generate({ - charset: "alphabetic", - }); - const movieId2 = generate({ - charset: "alphabetic", - }); - const movieId3 = generate({ - charset: "alphabetic", - }); - - const actorId1 = generate({ - charset: "alphabetic", - }); - const actorId2 = generate({ - charset: "alphabetic", - }); - const actorId3 = generate({ - charset: "alphabetic", - }); - - const query = ` - query($movieIds: [ID!], $actorIds: [ID!]){ - ${Movie.plural}(where: {id_IN: $movieIds}){ - id - actors(where: {id_IN: $actorIds}){ - id - movies { - id - actors { - id - } - } - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (:${Movie} {id: $movieId1})-[:ACTED_IN]->(:${Actor} {id: $actorId1}), - (:${Movie} {id: $movieId2})-[:ACTED_IN]->(:${Actor} {id: $actorId2}), - (:${Movie} {id: $movieId3})-[:ACTED_IN]->(:${Actor} {id: $actorId3}) - `, - { - movieId1, - movieId2, - movieId3, - actorId1, - actorId2, - actorId3, - } - ); - - const result = await testHelper.executeGraphQL(query, { - variableValues: { - movieIds: [movieId1, movieId2, movieId3], - actorIds: [actorId1, actorId2, actorId3], - }, - }); - - expect(result.errors).toBeFalsy(); - - (result?.data as any)?.[Movie.plural].forEach( - (movie: { id: string; title: string; actors: { id: string }[] }) => { - expect([movieId1, movieId2, movieId3].includes(movie.id)).toBeTruthy(); - - let expected: any; - - switch (movie.id) { - case movieId1: - expected = [ - { - id: actorId1, - movies: [ - { - id: movieId1, - actors: [{ id: actorId1 }], - }, - ], - }, - ]; - break; - case movieId2: - expected = [ - { - id: actorId2, - movies: [ - { - id: movieId2, - actors: [{ id: actorId2 }], - }, - ], - }, - ]; - break; - case movieId3: - expected = [ - { - id: actorId3, - movies: [ - { - id: movieId3, - actors: [{ id: actorId3 }], - }, - ], - }, - ]; - break; - default: - throw new Error("Fail"); - } - - expect(movie.actors).toEqual(expected); - } - ); - }); - - test("should find Movie and populate nested cypher query", async () => { - const typeDefs = ` - type ${Actor} { - id: ID - } - - type ${Movie} { - id: ID! - actors(actorIds: [ID!]): [${Actor}!]! @cypher( - statement: """ - MATCH (a:${Actor}) - WHERE a.id IN $actorIds - RETURN a - """, - columnName: "a" - ) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const movieId1 = generate({ - charset: "alphabetic", - }); - const movieId2 = generate({ - charset: "alphabetic", - }); - const movieId3 = generate({ - charset: "alphabetic", - }); - - const actorId1 = generate({ - charset: "alphabetic", - }); - const actorId2 = generate({ - charset: "alphabetic", - }); - const actorId3 = generate({ - charset: "alphabetic", - }); - - const query = ` - query($movieIds: [ID!], $actorIds: [ID!]){ - ${Movie.plural}(where: {id_IN: $movieIds}){ - id - actors(actorIds: $actorIds) { - id - } - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (:${Movie} {id: $movieId1}), - (:${Movie} {id: $movieId2}), - (:${Movie} {id: $movieId3}), - (:${Actor} {id: $actorId1}), - (:${Actor} {id: $actorId2}), - (:${Actor} {id: $actorId3}) - `, - { - movieId1, - movieId2, - movieId3, - actorId1, - actorId2, - actorId3, - } - ); - - const result = await testHelper.executeGraphQL(query, { - variableValues: { movieIds: [movieId1, movieId2, movieId3], actorIds: [actorId1, actorId2, actorId3] }, - }); - - expect(result.errors).toBeFalsy(); - - (result?.data as any)?.[Movie.plural].forEach((movie: { id: string; actors: { id: string }[] }) => { - expect([movieId1, movieId2, movieId3].includes(movie.id)).toBeTruthy(); - - movie.actors.forEach((actor) => { - expect([actorId1, actorId2, actorId3].includes(actor.id)).toBeTruthy(); - }); - }); - }); - - test("should use OR and find Movie by id or title", async () => { - const typeDefs = ` - type ${Actor} { - name: String - movies: [${Movie}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${Movie} { - id: ID! - title: String! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) - mainActor: ${Actor}! @relationship(type: "MAIN_ACTOR", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const id = generate({ - charset: "alphabetic", - }); - - const title = generate({ - charset: "alphabetic", - }); - - const query = ` - query($movieWhere: ${Movie}Where){ - ${Movie.plural}(where: $movieWhere){ - id - title - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (:${Movie} {id: $id, title: $title}) - `, - { id, title } - ); - - const result = await testHelper.executeGraphQL(query, { - variableValues: { movieWhere: { OR: [{ title, id }] } }, - }); - - expect(result.errors).toBeFalsy(); - - expect(result?.data?.[Movie.plural]).toEqual([{ id, title }]); - }); -}); diff --git a/packages/graphql/tests/integration/info.int.test.ts b/packages/graphql/tests/integration/info.int.test.ts index e808c238a5..45b88a1176 100644 --- a/packages/graphql/tests/integration/info.int.test.ts +++ b/packages/graphql/tests/integration/info.int.test.ts @@ -24,70 +24,15 @@ import { TestHelper } from "../utils/tests-helper"; describe("info", () => { const testHelper = new TestHelper(); let Movie: UniqueType; - let Actor: UniqueType; beforeEach(() => { Movie = testHelper.createUniqueType("Movie"); - Actor = testHelper.createUniqueType("Actor"); }); afterEach(async () => { await testHelper.close(); }); - test("should return info from a create mutation", async () => { - const typeDefs = ` - type ${Actor} { - name: String! - } - - type ${Movie} { - title: String! - actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const title = generate({ - charset: "alphabetic", - }); - const name = generate({ - charset: "alphabetic", - }); - - const query = ` - mutation($title: String!, $name: String!) { - ${Movie.operations.create}(input: [{ title: $title, actors: { create: [{ node: { name: $name } }] } }]) { - info { - bookmark - nodesCreated - relationshipsCreated - } - ${Movie.plural} { - title - actors { - name - } - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query, { - variableValues: { title, name }, - }); - - expect(gqlResult.errors).toBeFalsy(); - - expect(typeof (gqlResult?.data as any)?.[Movie.operations.create].info.bookmark).toBe("string"); - expect((gqlResult?.data as any)?.[Movie.operations.create].info.nodesCreated).toBe(2); - expect((gqlResult?.data as any)?.[Movie.operations.create].info.relationshipsCreated).toBe(1); - expect((gqlResult?.data as any)?.[Movie.operations.create][Movie.plural]).toEqual([ - { title, actors: [{ name }] }, - ]); - }); - test("should return info from a delete mutation", async () => { const typeDefs = ` type ${Movie} { diff --git a/packages/graphql/tests/integration/issues/190.int.test.ts b/packages/graphql/tests/integration/issues/190.int.test.ts deleted file mode 100644 index 044647ec12..0000000000 --- a/packages/graphql/tests/integration/issues/190.int.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentNode } from "graphql"; -import { gql } from "graphql-tag"; -import type { UniqueType } from "../../utils/graphql-types"; -import { TestHelper } from "../../utils/tests-helper"; - -describe("https://github.com/neo4j/graphql/issues/190", () => { - let User: UniqueType; - let UserDemographics: UniqueType; - let typeDefs: DocumentNode; - - const testHelper = new TestHelper(); - - beforeAll(async () => { - User = testHelper.createUniqueType("User"); - UserDemographics = testHelper.createUniqueType("UserDemographics"); - - typeDefs = gql` - type ${User} { - client_id: String - uid: String - demographics: [${UserDemographics}!]! @relationship(type: "HAS_DEMOGRAPHIC", direction: OUT) - } - - type ${UserDemographics} { - client_id: String - type: String - value: String - users: [${User}!]! @relationship(type: "HAS_DEMOGRAPHIC", direction: IN) - } - `; - - await testHelper.executeCypher(` - CREATE (user1:${User} {uid: 'user1'}),(user2:${User} {uid: 'user2'}),(female:${UserDemographics}{type:'Gender',value:'Female'}),(male:${UserDemographics}{type:'Gender',value:'Male'}),(age:${UserDemographics}{type:'Age',value:'50+'}),(state:${UserDemographics}{type:'State',value:'VIC'}) - CREATE (user1)-[:HAS_DEMOGRAPHIC]->(female) - CREATE (user2)-[:HAS_DEMOGRAPHIC]->(male) - CREATE (user1)-[:HAS_DEMOGRAPHIC]->(age) - CREATE (user2)-[:HAS_DEMOGRAPHIC]->(age) - CREATE (user1)-[:HAS_DEMOGRAPHIC]->(state) - CREATE (user2)-[:HAS_DEMOGRAPHIC]->(state) - `); - await testHelper.initNeo4jGraphQL({ typeDefs }); - }); - - afterAll(async () => { - await testHelper.close(); - }); - - test("Example 1", async () => { - const query = /* GraphQL */ ` - query { - ${User.plural}(where: { demographics: { type: "Gender", value: "Female" } }) { - uid - demographics { - type - value - } - } - } - `; - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - expect((result?.data as any)?.[User.plural][0].uid).toBe("user1"); - expect((result?.data as any)?.[User.plural][0].demographics).toHaveLength(3); - expect((result?.data as any)?.[User.plural][0].demographics).toContainEqual({ - type: "Age", - value: "50+", - }); - expect((result?.data as any)?.[User.plural][0].demographics).toContainEqual({ - type: "Gender", - value: "Female", - }); - expect((result?.data as any)?.[User.plural][0].demographics).toContainEqual({ - type: "State", - value: "VIC", - }); - }); - - test("Example 2", async () => { - const query = /* GraphQL */ ` - query { - ${User.plural}( - where: { - demographics: { OR: [{ type: "Gender", value: "Female" }, { type: "State" }, { type: "Age" }] } - } - ) { - uid - demographics { - type - value - } - } - } - `; - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - - expect((result?.data as any)?.[User.plural]).toHaveLength(2); - - expect((result?.data as any)?.[User.plural].filter((u) => u.uid === "user1")[0].demographics).toHaveLength(3); - expect((result?.data as any)?.[User.plural].filter((u) => u.uid === "user1")[0].demographics).toContainEqual({ - type: "Gender", - value: "Female", - }); - expect((result?.data as any)?.[User.plural].filter((u) => u.uid === "user1")[0].demographics).toContainEqual({ - type: "State", - value: "VIC", - }); - expect((result?.data as any)?.[User.plural].filter((u) => u.uid === "user1")[0].demographics).toContainEqual({ - type: "Age", - value: "50+", - }); - - expect((result?.data as any)?.[User.plural].filter((u) => u.uid === "user2")[0].demographics).toHaveLength(3); - expect((result?.data as any)?.[User.plural].filter((u) => u.uid === "user2")[0].demographics).toContainEqual({ - type: "Gender", - value: "Male", - }); - expect((result?.data as any)?.[User.plural].filter((u) => u.uid === "user2")[0].demographics).toContainEqual({ - type: "State", - value: "VIC", - }); - expect((result?.data as any)?.[User.plural].filter((u) => u.uid === "user2")[0].demographics).toContainEqual({ - type: "Age", - value: "50+", - }); - }); -}); diff --git a/packages/graphql/tests/integration/issues/582.int.test.ts b/packages/graphql/tests/integration/issues/582.int.test.ts deleted file mode 100644 index bbf3f59179..0000000000 --- a/packages/graphql/tests/integration/issues/582.int.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { UniqueType } from "../../utils/graphql-types"; -import { TestHelper } from "../../utils/tests-helper"; - -describe("https://github.com/neo4j/graphql/issues/582", () => { - const testHelper = new TestHelper(); - let type: UniqueType; - let typeDefs: string; - let query: string; - - beforeAll(async () => { - type = testHelper.createUniqueType("Entity"); - - typeDefs = ` - type ${type.name} { - children: [${type.name}!]! @relationship(type: "EDGE", properties: "Edge", direction: OUT) - parents: [${type.name}!]! @relationship(type: "EDGE", properties: "Edge", direction: IN) - type: String! - } - - type Edge @relationshipProperties { - type: String! - } - `; - - query = ` - query ($where: ${type.name}Where) { - ${type.plural}(where: $where) { - type - } - } - `; - - await testHelper.executeCypher( - ` - CREATE (:${type.name} { type: "Cat" })-[:EDGE]->(:${type.name} { type: "Dog" })<-[:EDGE]-(:${type.name} { type: "Bird" })-[:EDGE]->(:${type.name} { type: "Fish" }) - ` - ); - await testHelper.initNeo4jGraphQL({ typeDefs }); - }); - - afterAll(async () => { - await testHelper.close(); - }); - - test("should get all Cats where there exists at least one child Dog that has a Bird parent", async () => { - const gqlResult = await testHelper.executeGraphQL(query, { - variableValues: { - where: { - type: "Cat", - childrenConnection: { - node: { - type: "Dog", - parentsConnection: { - node: { - type: "Bird", - }, - }, - }, - }, - }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - - expect((gqlResult?.data?.[type.plural] as any[])[0]).toMatchObject({ - type: "Cat", - }); - }); - - test("should get all Cats where there exists at least one child Dog that has a Bird parent which has a Fish child", async () => { - const gqlResult = await testHelper.executeGraphQL(query, { - variableValues: { - where: { - type: "Cat", - childrenConnection: { - node: { - type: "Dog", - parentsConnection: { - node: { - type: "Bird", - childrenConnection: { - node: { - type: "Fish", - }, - }, - }, - }, - }, - }, - }, - }, - }); - - expect(gqlResult.errors).toBeFalsy(); - - expect((gqlResult?.data?.[type.plural] as any[])[0]).toMatchObject({ - type: "Cat", - }); - }); -}); diff --git a/packages/graphql/tests/integration/relationship-properties/read.int.test.ts b/packages/graphql/tests/integration/relationship-properties/read.int.test.ts deleted file mode 100644 index 605f682e4e..0000000000 --- a/packages/graphql/tests/integration/relationship-properties/read.int.test.ts +++ /dev/null @@ -1,617 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DocumentNode } from "graphql"; -import { offsetToCursor } from "graphql-relay"; -import { gql } from "graphql-tag"; -import { generate } from "randomstring"; -import { UniqueType } from "../../utils/graphql-types"; -import { TestHelper } from "../../utils/tests-helper"; - -describe("Relationship properties - read", () => { - const testHelper = new TestHelper(); - - let typeMovie: UniqueType; - let typeActor: UniqueType; - let typeDefs: DocumentNode; - - const movieTitle = generate({ charset: "alphabetic" }); - const actorA = `a${generate({ charset: "alphabetic" })}`; - const actorB = `b${generate({ charset: "alphabetic" })}`; - const actorC = `c${generate({ charset: "alphabetic" })}`; - const actorD = `d${generate({ charset: "alphabetic" })}`; - - beforeEach(async () => { - typeMovie = new UniqueType("Movie"); - typeActor = new UniqueType("Actor"); - - await testHelper.executeCypher( - ` - CREATE (:${typeActor.name} { name: '${actorA}' })-[:ACTED_IN { screenTime: 105 }]->(m:${typeMovie.name} { title: '${movieTitle}'}) - CREATE (m)<-[:ACTED_IN { screenTime: 105 }]-(:${typeActor.name} { name: '${actorB}' }) - CREATE (m)<-[:ACTED_IN { screenTime: 5 }]-(:${typeActor.name} { name: '${actorC}' }) - ` - ); - - typeDefs = gql` - type ${typeMovie.name} { - title: String! - actors: [${typeActor.name}!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type ${typeActor.name} { - name: String! - movies: [${typeMovie.name}!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - screenTime: Int! - } - `; - }); - - afterEach(async () => { - await testHelper.close(); - }); - - test("Projecting node and relationship properties with no arguments", async () => { - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = ` - query { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - title - actorsConnection { - totalCount - edges { - properties { - screenTime - } - node { - name - } - } - pageInfo { - hasNextPage - } - } - } - } - `; - - await neoSchema.checkNeo4jCompat(); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - - expect((result?.data as any)?.[typeMovie.plural]).toHaveLength(1); - - expect((result?.data as any)?.[typeMovie.plural][0].actorsConnection.totalCount).toBe(3); - expect((result?.data as any)?.[typeMovie.plural][0].actorsConnection.pageInfo).toEqual({ - hasNextPage: false, - }); - - expect((result?.data as any)?.[typeMovie.plural][0].actorsConnection.edges).toHaveLength(3); - expect((result?.data as any)?.[typeMovie.plural][0].actorsConnection.edges).toContainEqual({ - properties: { screenTime: 5 }, - node: { - name: actorC, - }, - }); - expect((result?.data as any)?.[typeMovie.plural][0].actorsConnection.edges).toContainEqual({ - properties: { screenTime: 105 }, - node: { - name: actorB, - }, - }); - expect((result?.data as any)?.[typeMovie.plural][0].actorsConnection.edges).toContainEqual({ - properties: { screenTime: 105 }, - node: { - name: actorA, - }, - }); - }); - - test("With `where` argument", async () => { - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = ` - query { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - title - actorsConnection( - where: { AND: [{ edge: { screenTime_GT: 60 } }, { node: { name_STARTS_WITH: "a" } }] } - ) { - totalCount - edges { - cursor - properties { - screenTime - } - node { - name - } - } - pageInfo { - hasNextPage - } - } - } - } - `; - - await neoSchema.checkNeo4jCompat(); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - - expect((result?.data as any)?.[typeMovie.plural]).toEqual([ - { - title: movieTitle, - actorsConnection: { - totalCount: 1, - edges: [ - { - cursor: offsetToCursor(0), - properties: { screenTime: 105 }, - node: { - name: actorA, - }, - }, - ], - pageInfo: { - hasNextPage: false, - }, - }, - }, - ]); - }); - - test("With `sort` argument", async () => { - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = ` - query ConnectionWithSort($nameSort: SortDirection) { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - title - actorsConnection( - sort: [{ edge: { screenTime: DESC } }, { node: { name: $nameSort } }] - ) { - totalCount - edges { - cursor - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - await neoSchema.checkNeo4jCompat(); - - const ascResult = await testHelper.executeGraphQL(query, { - variableValues: { nameSort: "ASC" }, - }); - - expect(ascResult.errors).toBeFalsy(); - - expect(ascResult?.data?.[typeMovie.plural]).toEqual([ - { - title: movieTitle, - actorsConnection: { - totalCount: 3, - edges: [ - { - cursor: offsetToCursor(0), - properties: { screenTime: 105 }, - node: { - name: actorA, - }, - }, - { - cursor: offsetToCursor(1), - properties: { screenTime: 105 }, - node: { - name: actorB, - }, - }, - { - cursor: offsetToCursor(2), - properties: { screenTime: 5 }, - node: { - name: actorC, - }, - }, - ], - }, - }, - ]); - - const descResult = await testHelper.executeGraphQL(query, { - variableValues: { nameSort: "DESC" }, - }); - - expect(descResult.errors).toBeFalsy(); - - expect(descResult?.data?.[typeMovie.plural]).toEqual([ - { - title: movieTitle, - actorsConnection: { - totalCount: 3, - edges: [ - { - cursor: offsetToCursor(0), - properties: { screenTime: 105 }, - node: { - name: actorB, - }, - }, - { - cursor: offsetToCursor(1), - properties: { screenTime: 105 }, - node: { - name: actorA, - }, - }, - { - cursor: offsetToCursor(2), - properties: { screenTime: 5 }, - node: { - name: actorC, - }, - }, - ], - }, - }, - ]); - }); - - test("With `sort` argument ordered", async () => { - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = ` - query { - ${typeMovie.plural} { - actorsConnection( - sort: [{ edge: { screenTime: DESC } }, { node: { name: ASC } }] - ) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const queryReverse = ` - query { - ${typeMovie.plural} { - actorsConnection( - sort: [{ node: { name: ASC } }, { edge: { screenTime: DESC } }] - ) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - await neoSchema.checkNeo4jCompat(); - - await testHelper.executeCypher( - ` - MATCH (m:${typeMovie.name} { title: '${movieTitle}'}) - CREATE (m)<-[:ACTED_IN { screenTime: 106 }]-(:${typeActor.name} { name: '${actorD}' }) - ` - ); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - - expect((result?.data as any)?.[typeMovie.plural]).toEqual([ - { - actorsConnection: { - edges: [ - { - properties: { screenTime: 106 }, - node: { - name: actorD, - }, - }, - { - properties: { screenTime: 105 }, - node: { - name: actorA, - }, - }, - { - properties: { screenTime: 105 }, - node: { - name: actorB, - }, - }, - { - properties: { screenTime: 5 }, - node: { - name: actorC, - }, - }, - ], - }, - }, - ]); - - const reverseResult = await testHelper.executeGraphQL(queryReverse); - - expect(reverseResult.errors).toBeFalsy(); - - expect(reverseResult?.data?.[typeMovie.plural]).toEqual([ - { - actorsConnection: { - edges: [ - { - properties: { screenTime: 105 }, - node: { - name: actorA, - }, - }, - { - properties: { screenTime: 105 }, - node: { - name: actorB, - }, - }, - { - properties: { screenTime: 5 }, - node: { - name: actorC, - }, - }, - { - properties: { screenTime: 106 }, - node: { - name: actorD, - }, - }, - ], - }, - }, - ]); - }); - - test("With `where` and `sort` arguments", async () => { - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = ` - query ConnectionWithSort($nameSort: SortDirection) { - ${typeMovie.plural}(where: { title: "${movieTitle}" }) { - title - actorsConnection( - where: { edge: { screenTime_GT: 60 } } - sort: [{ node: { name: $nameSort } }] - ) { - totalCount - edges { - properties { - screenTime - } - node { - name - } - } - pageInfo { - hasNextPage - } - } - } - } - `; - - await neoSchema.checkNeo4jCompat(); - - const ascResult = await testHelper.executeGraphQL(query, { - variableValues: { nameSort: "ASC" }, - }); - - expect(ascResult.errors).toBeFalsy(); - - expect(ascResult?.data?.[typeMovie.plural]).toEqual([ - { - title: movieTitle, - actorsConnection: { - totalCount: 2, - edges: [ - { - properties: { screenTime: 105 }, - node: { - name: actorA, - }, - }, - { - properties: { screenTime: 105 }, - node: { - name: actorB, - }, - }, - ], - pageInfo: { - hasNextPage: false, - }, - }, - }, - ]); - - const descResult = await testHelper.executeGraphQL(query, { - variableValues: { nameSort: "DESC" }, - }); - - expect(descResult.errors).toBeFalsy(); - - expect(descResult?.data?.[typeMovie.plural]).toEqual([ - { - title: movieTitle, - actorsConnection: { - totalCount: 2, - edges: [ - { - properties: { screenTime: 105 }, - node: { - name: actorB, - }, - }, - { - properties: { screenTime: 105 }, - node: { - name: actorA, - }, - }, - ], - pageInfo: { - hasNextPage: false, - }, - }, - }, - ]); - }); - - test("Projecting a connection from a relationship with no argument", async () => { - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = ` - query { - ${typeActor.plural}(where: { name: "${actorA}" }) { - name - movies { - title - actorsConnection { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - } - `; - - await neoSchema.checkNeo4jCompat(); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - - expect((result?.data as any)?.[typeActor.plural]).toHaveLength(1); - expect((result?.data as any)?.[typeActor.plural][0].name).toEqual(actorA); - - expect((result?.data as any)?.[typeActor.plural][0].movies).toHaveLength(1); - - expect((result?.data as any)?.[typeActor.plural][0].movies[0].actorsConnection.edges).toHaveLength(3); - expect((result?.data as any)?.[typeActor.plural][0].movies[0].actorsConnection.edges).toContainEqual({ - properties: { screenTime: 5 }, - node: { - name: actorC, - }, - }); - expect((result?.data as any)?.[typeActor.plural][0].movies[0].actorsConnection.edges).toContainEqual({ - properties: { screenTime: 105 }, - node: { - name: actorB, - }, - }); - expect((result?.data as any)?.[typeActor.plural][0].movies[0].actorsConnection.edges).toContainEqual({ - properties: { screenTime: 105 }, - node: { - name: actorA, - }, - }); - }); - - test("Projecting a connection from a relationship with `where` argument", async () => { - const neoSchema = await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = ` - query { - ${typeActor.plural}(where: { name: "${actorA}" }) { - name - movies { - title - actorsConnection(where: { node: { name_NOT: "${actorA}" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - } - `; - - await neoSchema.checkNeo4jCompat(); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - - expect((result?.data as any)?.[typeActor.plural]).toHaveLength(1); - expect((result?.data as any)?.[typeActor.plural][0].name).toEqual(actorA); - - expect((result?.data as any)?.[typeActor.plural][0].movies).toHaveLength(1); - - expect((result?.data as any)?.[typeActor.plural][0].movies[0].actorsConnection.edges).toHaveLength(2); - expect((result?.data as any)?.[typeActor.plural][0].movies[0].actorsConnection.edges).toContainEqual({ - properties: { screenTime: 5 }, - node: { - name: actorC, - }, - }); - expect((result?.data as any)?.[typeActor.plural][0].movies[0].actorsConnection.edges).toContainEqual({ - properties: { screenTime: 105 }, - node: { - name: actorB, - }, - }); - }); -}); diff --git a/packages/graphql/tests/integration/rfcs/query-limits.int.test.ts b/packages/graphql/tests/integration/rfcs/query-limits.int.test.ts deleted file mode 100644 index 13fa48a2f9..0000000000 --- a/packages/graphql/tests/integration/rfcs/query-limits.int.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { generate } from "randomstring"; -import { TestHelper } from "../../utils/tests-helper"; - -describe("integration/rfcs/query-limits", () => { - const testHelper = new TestHelper(); - - beforeEach(() => {}); - - afterEach(async () => { - await testHelper.close(); - }); - - describe("Top Level Query Limits", () => { - test("should limit the top level query", async () => { - const randomType = testHelper.createUniqueType("Movie"); - - const typeDefs = ` - type ${randomType.name} @limit(default: 2) { - id: ID! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - await testHelper.executeCypher( - ` - WITH [1,2,3,4,5] AS iterate - UNWIND iterate AS i - CREATE (:${randomType.name} {id: randomUUID()}) - `, - {} - ); - - const query = ` - { - ${randomType.plural} { - id - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - if (gqlResult.errors) { - console.log(JSON.stringify(gqlResult.errors, null, 2)); - } - - expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(2); - }); - }); - - describe("Field Level Query Limits", () => { - test("should limit the normal field level query", async () => { - const randomType1 = testHelper.createUniqueType("Movie"); - const randomType2 = testHelper.createUniqueType("Person"); - const movieId = generate({ - charset: "alphabetic", - }); - - const typeDefs = ` - type ${randomType1.name} { - id: ID! - actors: [${randomType2.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${randomType2.name} @limit(default: 3) { - id: ID! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - await testHelper.executeCypher( - ` - CREATE (movie:${randomType1.name} {id: "${movieId}"}) - WITH movie, [1,2,3,4,5] AS iterate - UNWIND iterate AS i - MERGE (movie)<-[:ACTED_IN]-(:${randomType2.name} {id: randomUUID()}) - `, - {} - ); - - const query = ` - { - ${randomType1.plural} { - id - actors { - id - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - if (gqlResult.errors) { - console.log(JSON.stringify(gqlResult.errors, null, 2)); - } - - expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[randomType1.plural][0].actors).toHaveLength(3); - }); - - test("should limit the connection field level query", async () => { - const randomType1 = testHelper.createUniqueType("Movie"); - const randomType2 = testHelper.createUniqueType("Person"); - const movieId = generate({ - charset: "alphabetic", - }); - - const typeDefs = ` - type ${randomType1.name} { - id: ID! - actors: [${randomType2.name}!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type ${randomType2.name} @limit(default: 4) { - id: ID! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - await testHelper.executeCypher( - ` - CREATE (movie:${randomType1.name} {id: "${movieId}"}) - WITH movie, [1,2,3,4,5] AS iterate - UNWIND iterate AS i - MERGE (movie)<-[:ACTED_IN]-(:${randomType2.name} {id: randomUUID()}) - `, - {} - ); - - const query = ` - { - ${randomType1.plural} { - id - actorsConnection { - edges { - node { - id - } - } - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - if (gqlResult.errors) { - console.log(JSON.stringify(gqlResult.errors, null, 2)); - } - - expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[randomType1.plural][0].actorsConnection.edges).toHaveLength(4); - }); - }); -}); diff --git a/packages/graphql/tests/integration/root-connections.int.test.ts b/packages/graphql/tests/integration/root-connections.int.test.ts deleted file mode 100644 index f099ebbdbd..0000000000 --- a/packages/graphql/tests/integration/root-connections.int.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { generate } from "randomstring"; -import type { UniqueType } from "../utils/graphql-types"; -import { TestHelper } from "../utils/tests-helper"; - -describe("root-connections", () => { - const testHelper: TestHelper = new TestHelper(); - - let pilotType: UniqueType; - let aircraftType: UniqueType; - - beforeEach(async () => { - pilotType = testHelper.createUniqueType("Pilot"); - aircraftType = testHelper.createUniqueType("Aircraft"); - - const typeDefs = ` - type ${pilotType.name} { - name: String - aircraft: [${aircraftType.name}!]! @relationship(type: "FLIES_IN", direction: IN) - } - - type ${aircraftType.name} { - id: ID! - name: String! - pilots: [${pilotType.name}!]! @relationship(type: "FLIES_IN", direction: OUT) - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - }); - - afterEach(async () => { - await testHelper.close(); - }); - - test("should return an empty array of edges and a totalCount of zero when there are no records", async () => { - const query = ` - query { - ${aircraftType.operations.connection} { - totalCount - edges { - cursor - node { - id - } - } - } - } - `; - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - expect(result?.data?.[aircraftType.operations.connection]).toEqual({ - totalCount: 0, - edges: [], - }); - }); - test("should return an array of edges and the correct totalCount", async () => { - const dummyAircrafts = [...Array(20).keys()].map(() => ({ - id: generate({ charset: "alphabetic " }), - name: generate({ charset: "alphabetic" }), - })); - - const query = ` - query { - ${aircraftType.operations.connection} { - totalCount - edges { - cursor - node { - id - name - } - } - } - } - `; - - const create = ` - mutation($input: [${aircraftType.name}CreateInput!]!) { - ${aircraftType.operations.create}(input: $input) { - ${aircraftType.plural} { - id - } - } - } - `; - - await testHelper.executeGraphQL(create, { - variableValues: { input: dummyAircrafts }, - }); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - expect(result?.data?.[aircraftType.operations.connection]).toEqual({ - totalCount: 20, - edges: expect.toIncludeAllMembers( - dummyAircrafts.map((node) => ({ - cursor: expect.any(String), - node, - })) - ), - }); - }); - test("should correctly produce edges when sort and limit are used", async () => { - const dummyAircrafts = [...Array(20).keys()].map(() => ({ - id: generate({ charset: "alphabetic", readable: true }), - name: generate({ charset: "alphabetic", readable: true }), - })); - - const sortedAircrafts = dummyAircrafts.sort((a, b) => { - if (a.name < b.name) return -1; - if (a.name > b.name) return 1; - return 0; - }); - - const query = ` - query { - ${aircraftType.operations.connection}(first: 10, sort: [{ name: ASC }]) { - totalCount - edges { - cursor - node { - id - name - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - `; - - const create = ` - mutation($input: [${aircraftType.name}CreateInput!]!) { - ${aircraftType.operations.create}(input: $input) { - ${aircraftType.plural} { - id - } - } - } - `; - - await testHelper.executeGraphQL(create, { - variableValues: { input: dummyAircrafts }, - }); - - const result = await testHelper.executeGraphQL(query); - - expect(result.errors).toBeFalsy(); - expect(result?.data?.[aircraftType.operations.connection]).toEqual({ - totalCount: 20, - edges: sortedAircrafts.slice(0, 10).map((node) => ({ - cursor: expect.any(String), - node, - })), - pageInfo: { - hasNextPage: true, - endCursor: "YXJyYXljb25uZWN0aW9uOjk=", - }, - }); - }); - test("should calculate the correct cursors when the first argument is provided as a parameter", async () => { - const dummyAircrafts = [...Array(20).keys()].map(() => ({ - id: generate({ charset: "alphabetic", readable: true }), - name: generate({ charset: "alphabetic", readable: true }), - })); - - const query = ` - query($first: Int) { - ${aircraftType.operations.connection}(first: $first) { - totalCount - edges { - cursor - node { - id - name - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - `; - - const create = ` - mutation($input: [${aircraftType.name}CreateInput!]!) { - ${aircraftType.operations.create}(input: $input) { - ${aircraftType.plural} { - id - } - } - } - `; - - await testHelper.executeGraphQL(create, { - variableValues: { input: dummyAircrafts }, - }); - - const result = await testHelper.executeGraphQL(query, { - variableValues: { first: 10 }, - }); - - expect(result.errors).toBeFalsy(); - expect(result?.data?.[aircraftType.operations.connection]).toEqual({ - totalCount: 20, - edges: expect.toBeArrayOfSize(10), - pageInfo: { - hasNextPage: true, - endCursor: "YXJyYXljb25uZWN0aW9uOjk=", - }, - }); - }); -}); diff --git a/packages/graphql/tests/integration/sort.int.test.ts b/packages/graphql/tests/integration/sort.int.test.ts index 2f02211f44..8de043b78f 100644 --- a/packages/graphql/tests/integration/sort.int.test.ts +++ b/packages/graphql/tests/integration/sort.int.test.ts @@ -138,123 +138,6 @@ describe("sort", () => { }); describe("on top level", () => { - describe("primitive fields", () => { - const gqlResultByTypeFromSource = (source: string) => (direction: "ASC" | "DESC") => - testHelper.executeGraphQL(source, { - variableValues: { movieIds: movies.map(({ id }) => id), direction }, - }); - - describe("with field in selection set", () => { - const queryWithTitle = ` - query ($movieIds: [ID!]!, $direction: SortDirection!) { - ${movieType.plural}( - where: { id_IN: $movieIds }, - options: { sort: [{ title: $direction }] } - ) { - id - title - } - } - `; - - const gqlResultByType = gqlResultByTypeFromSource(queryWithTitle); - - test("ASC", async () => { - const gqlResult = await gqlResultByType("ASC"); - - expect(gqlResult.errors).toBeUndefined(); - - const { [movieType.plural]: gqlMovies } = gqlResult.data as any; - - expect(gqlMovies[0].id).toBe(movies[0].id); - expect(gqlMovies[1].id).toBe(movies[1].id); - }); - test("DESC", async () => { - const gqlResult = await gqlResultByType("DESC"); - - expect(gqlResult.errors).toBeUndefined(); - - const { [movieType.plural]: gqlMovies } = gqlResult.data as any; - - expect(gqlMovies[0].id).toBe(movies[1].id); - expect(gqlMovies[1].id).toBe(movies[0].id); - }); - }); - - describe("with field aliased in selection set", () => { - const queryWithTitle = ` - query ($movieIds: [ID!]!, $direction: SortDirection!) { - ${movieType.plural}( - where: { id_IN: $movieIds }, - options: { sort: [{ title: $direction }] } - ) { - id - aliased: title - } - } - `; - - const gqlResultByType = gqlResultByTypeFromSource(queryWithTitle); - - test("ASC", async () => { - const gqlResult = await gqlResultByType("ASC"); - - expect(gqlResult.errors).toBeUndefined(); - - const { [movieType.plural]: gqlMovies } = gqlResult.data as any; - - expect(gqlMovies[0].id).toBe(movies[0].id); - expect(gqlMovies[1].id).toBe(movies[1].id); - }); - test("DESC", async () => { - const gqlResult = await gqlResultByType("DESC"); - - expect(gqlResult.errors).toBeUndefined(); - - const { [movieType.plural]: gqlMovies } = gqlResult.data as any; - - expect(gqlMovies[0].id).toBe(movies[1].id); - expect(gqlMovies[1].id).toBe(movies[0].id); - }); - }); - - describe("with field not in selection set", () => { - const queryWithoutTitle = ` - query ($movieIds: [ID!]!, $direction: SortDirection!) { - ${movieType.plural}( - where: { id_IN: $movieIds }, - options: { sort: [{ title: $direction }] } - ) { - id - } - } - `; - - const gqlResultByType = gqlResultByTypeFromSource(queryWithoutTitle); - - test("ASC", async () => { - const gqlResult = await gqlResultByType("ASC"); - - expect(gqlResult.errors).toBeUndefined(); - - const { [movieType.plural]: gqlMovies } = gqlResult.data as any; - - expect(gqlMovies[0].id).toBe(movies[0].id); - expect(gqlMovies[1].id).toBe(movies[1].id); - }); - test("DESC", async () => { - const gqlResult = await gqlResultByType("DESC"); - - expect(gqlResult.errors).toBeUndefined(); - - const { [movieType.plural]: gqlMovies } = gqlResult.data as any; - - expect(gqlMovies[0].id).toBe(movies[1].id); - expect(gqlMovies[1].id).toBe(movies[0].id); - }); - }); - }); - describe("cypher fields", () => { const gqlResultByTypeFromSource = (source: string) => (direction: "ASC" | "DESC") => testHelper.executeGraphQL(source, { @@ -431,138 +314,6 @@ describe("sort", () => { variableValues: { movieId: movies[1].id, actorIds: actors.map(({ id }) => id), direction }, }); - describe("primitive fields", () => { - describe("with field in selection set", () => { - const queryWithName = ` - query($movieId: ID!, $actorIds: [ID!]!, $direction: SortDirection!) { - ${movieType.plural}(where: { id: $movieId }) { - id - actors(where: { id_IN: $actorIds }, options: { sort: [{ name: $direction }] }) { - id - name - } - } - } - `; - const gqlResultByType = gqlResultByTypeFromSource(queryWithName); - - test("ASC", async () => { - const gqlResult = await gqlResultByType("ASC"); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovie: { id: string; actors: Array<{ id: string; name: string }> } = ( - gqlResult.data as any - )?.[movieType.plural][0]; - expect(gqlMovie).toBeDefined(); - - expect(gqlMovie.id).toBe(movies[1].id); - expect(gqlMovie.actors[0]?.id).toBe(actors[0].id); - expect(gqlMovie.actors[1]?.id).toBe(actors[1].id); - }); - test("DESC", async () => { - const gqlResult = await gqlResultByType("DESC"); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovie: { id: string; actors: Array<{ id: string; name: string }> } = ( - gqlResult.data as any - )?.[movieType.plural][0]; - expect(gqlMovie).toBeDefined(); - - expect(gqlMovie.id).toBe(movies[1].id); - expect(gqlMovie.actors[0]?.id).toBe(actors[1].id); - expect(gqlMovie.actors[1]?.id).toBe(actors[0].id); - }); - }); - - describe("with field aliased in selection set", () => { - const queryWithName = ` - query($movieId: ID!, $actorIds: [ID!]!, $direction: SortDirection!) { - ${movieType.plural}(where: { id: $movieId }) { - id - actors(where: { id_IN: $actorIds }, options: { sort: [{ name: $direction }] }) { - id - aliased: name - } - } - } - `; - const gqlResultByType = gqlResultByTypeFromSource(queryWithName); - - test("ASC", async () => { - const gqlResult = await gqlResultByType("ASC"); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovie: { id: string; actors: Array<{ id: string; name: string }> } = ( - gqlResult.data as any - )?.[movieType.plural][0]; - expect(gqlMovie).toBeDefined(); - - expect(gqlMovie.id).toBe(movies[1].id); - expect(gqlMovie.actors[0]?.id).toBe(actors[0].id); - expect(gqlMovie.actors[1]?.id).toBe(actors[1].id); - }); - test("DESC", async () => { - const gqlResult = await gqlResultByType("DESC"); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovie: { id: string; actors: Array<{ id: string; name: string }> } = ( - gqlResult.data as any - )?.[movieType.plural][0]; - expect(gqlMovie).toBeDefined(); - - expect(gqlMovie.id).toBe(movies[1].id); - expect(gqlMovie.actors[0]?.id).toBe(actors[1].id); - expect(gqlMovie.actors[1]?.id).toBe(actors[0].id); - }); - }); - - describe("with field not in selection set", () => { - const queryWithoutName = ` - query($movieId: ID!, $actorIds: [ID!]!, $direction: SortDirection!) { - ${movieType.plural}(where: { id: $movieId }) { - id - actors(where: { id_IN: $actorIds }, options: { sort: [{ name: $direction }] }) { - id - } - } - } - `; - const gqlResultByType = gqlResultByTypeFromSource(queryWithoutName); - test("ASC", async () => { - const gqlResult = await gqlResultByType("ASC"); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovie: { id: string; actors: Array<{ id: string }> } = (gqlResult.data as any)?.[ - movieType.plural - ][0]; - expect(gqlMovie).toBeDefined(); - - expect(gqlMovie.id).toBe(movies[1].id); - expect(gqlMovie.actors[0]?.id).toBe(actors[0].id); - expect(gqlMovie.actors[1]?.id).toBe(actors[1].id); - }); - test("DESC", async () => { - const gqlResult = await gqlResultByType("DESC"); - - expect(gqlResult.errors).toBeUndefined(); - - const gqlMovie: { id: string; actors: Array<{ id: string }> } = (gqlResult.data as any)?.[ - movieType.plural - ][0]; - expect(gqlMovie).toBeDefined(); - - expect(gqlMovie.id).toBe(movies[1].id); - expect(gqlMovie.actors[0]?.id).toBe(actors[1].id); - expect(gqlMovie.actors[1]?.id).toBe(actors[0].id); - }); - }); - }); - describe("cypher fields", () => { // Actor 1 has 3 totalScreenTime // Actor 2 has 1 totalScreenTime @@ -707,169 +458,5 @@ describe("sort", () => { }); }); }); - - it("sort with skip and limit on relationship", async () => { - const query = ` - query { - ${movieType.plural} { - actors(options: { limit: 1, offset: 1, sort: { name: ASC } }) { - name - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - expect((gqlResult.data as any)[movieType.plural]).toEqual( - expect.toIncludeSameMembers([{ actors: [] }, { actors: [{ name: actors[1].name }] }]) - ); - }); - }); - - describe("on interface relationship", () => { - describe("primitive fields", () => { - const gqlResultByTypeFromSource = (source: string) => (direction: "ASC" | "DESC") => - testHelper.executeGraphQL(source, { - variableValues: { actorId: actors[0].id, direction }, - }); - describe("with field in selection set", () => { - const queryWithSortField = ` - query ($actorId: ID!, $direction: SortDirection!) { - ${actorType.plural}(where: { id: $actorId }) { - id - actedIn(options: { sort: [{ title: $direction }] }) { - id - title - } - } - } - `; - const gqlResultByType = gqlResultByTypeFromSource(queryWithSortField); - test("ASC", async () => { - const gqlResult = await gqlResultByType("ASC"); - - expect(gqlResult.errors).toBeUndefined(); - - const [gqlActor] = gqlResult.data?.[actorType.plural] as any[]; - expect(gqlActor.id).toEqual(actors[0].id); - - const { actedIn: production } = gqlActor; - expect(production).toHaveLength(4); - expect(production[0].id).toBe(movies[0].id); - expect(production[1].id).toBe(movies[1].id); - expect(production[2].id).toBe(series[0].id); - expect(production[3].id).toBe(series[1].id); - }); - - test("DESC", async () => { - const gqlResult = await gqlResultByType("DESC"); - - expect(gqlResult.errors).toBeUndefined(); - - const [gqlActor] = gqlResult.data?.[actorType.plural] as any[]; - expect(gqlActor.id).toEqual(actors[0].id); - - const { actedIn: production } = gqlActor; - expect(gqlActor.actedIn).toHaveLength(4); - expect(production[0].id).toBe(series[1].id); - expect(production[1].id).toBe(series[0].id); - expect(production[2].id).toBe(movies[1].id); - expect(production[3].id).toBe(movies[0].id); - }); - }); - - describe("with field aliased in selection set", () => { - const queryWithAliasedSortField = ` - query ($actorId: ID!, $direction: SortDirection!) { - ${actorType.plural}(where: { id: $actorId }) { - id - actedIn(options: { sort: [{ title: $direction }] }) { - id - aliased: title - } - } - } - `; - const gqlResultByType = gqlResultByTypeFromSource(queryWithAliasedSortField); - test("ASC", async () => { - const gqlResult = await gqlResultByType("ASC"); - - expect(gqlResult.errors).toBeUndefined(); - - const [gqlActor] = gqlResult.data?.[actorType.plural] as any[]; - expect(gqlActor.id).toEqual(actors[0].id); - - const { actedIn: production } = gqlActor; - expect(production).toHaveLength(4); - expect(production[0].id).toBe(movies[0].id); - expect(production[1].id).toBe(movies[1].id); - expect(production[2].id).toBe(series[0].id); - expect(production[3].id).toBe(series[1].id); - }); - - test("DESC", async () => { - const gqlResult = await gqlResultByType("DESC"); - - expect(gqlResult.errors).toBeUndefined(); - - const [gqlActor] = gqlResult.data?.[actorType.plural] as any[]; - expect(gqlActor.id).toEqual(actors[0].id); - - const { actedIn: production } = gqlActor; - expect(gqlActor.actedIn).toHaveLength(4); - expect(production[0].id).toBe(series[1].id); - expect(production[1].id).toBe(series[0].id); - expect(production[2].id).toBe(movies[1].id); - expect(production[3].id).toBe(movies[0].id); - }); - }); - - describe("with field not in selection set", () => { - const queryWithOutSortField = ` - query ($actorId: ID!, $direction: SortDirection!) { - ${actorType.plural}(where: { id: $actorId }) { - id - actedIn(options: { sort: [{ title: $direction }] }) { - id - } - } - } - `; - const gqlResultByType = gqlResultByTypeFromSource(queryWithOutSortField); - test("ASC", async () => { - const gqlResult = await gqlResultByType("ASC"); - - expect(gqlResult.errors).toBeUndefined(); - - const [gqlActor] = gqlResult.data?.[actorType.plural] as any[]; - expect(gqlActor.id).toEqual(actors[0].id); - - const { actedIn: production } = gqlActor; - expect(production).toHaveLength(4); - expect(production[0].id).toBe(movies[0].id); - expect(production[1].id).toBe(movies[1].id); - expect(production[2].id).toBe(series[0].id); - expect(production[3].id).toBe(series[1].id); - }); - - test("DESC", async () => { - const gqlResult = await gqlResultByType("DESC"); - - expect(gqlResult.errors).toBeUndefined(); - - const [gqlActor] = gqlResult.data?.[actorType.plural] as any[]; - expect(gqlActor.id).toEqual(actors[0].id); - - const { actedIn: production } = gqlActor; - expect(gqlActor.actedIn).toHaveLength(4); - expect(production[0].id).toBe(series[1].id); - expect(production[1].id).toBe(series[0].id); - expect(production[2].id).toBe(movies[1].id); - expect(production[3].id).toBe(movies[0].id); - }); - }); - }); }); }); diff --git a/packages/graphql/tests/integration/types/date.int.test.ts b/packages/graphql/tests/integration/types/date.int.test.ts index b996974c20..946edebd08 100644 --- a/packages/graphql/tests/integration/types/date.int.test.ts +++ b/packages/graphql/tests/integration/types/date.int.test.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import neo4jDriver from "neo4j-driver"; +import type neo4jDriver from "neo4j-driver"; import { generate } from "randomstring"; import type { UniqueType } from "../../utils/graphql-types"; import { TestHelper } from "../../utils/tests-helper"; @@ -129,45 +129,6 @@ describe("Date", () => { }); }); - describe("find", () => { - test("should find a movie (with a Date)", async () => { - const typeDefs = /* GraphQL */ ` - type ${Movie.name} { - date: Date - } - `; - - const date = new Date(); - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const query = /* GraphQL */ ` - query { - ${Movie.plural}(where: { date: "${date.toISOString()}" }) { - date - } - } - `; - - const nDate = neo4jDriver.types.Date.fromStandardDate(date); - - await testHelper.executeCypher( - ` - CREATE (m:${Movie.name}) - SET m.date = $nDate - `, - { nDate } - ); - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data as any)[Movie.plural][0]).toEqual({ - date: date.toISOString().split("T")[0], - }); - }); - }); - describe("update", () => { test("should update a movie (with a Date)", async () => { const typeDefs = /* GraphQL */ ` diff --git a/packages/graphql/tests/integration/types/duration.int.test.ts b/packages/graphql/tests/integration/types/duration.int.test.ts index d1452459a0..d62198e222 100644 --- a/packages/graphql/tests/integration/types/duration.int.test.ts +++ b/packages/graphql/tests/integration/types/duration.int.test.ts @@ -229,51 +229,6 @@ describe("Duration", () => { }); describe("filter", () => { - test("should filter based on duration equality", async () => { - const typeDefs = /* GraphQL */ ` - type ${Movie} { - id: ID! - duration: Duration! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const id = generate({ readable: false }); - const days = 4; - const duration = `P${days}D`; - const parsedDuration = parseDuration(duration); - const neo4jDuration = new neo4jDriver.types.Duration(0, days, 0, 0); - - await testHelper.executeCypher( - ` - CREATE (movie:${Movie}) - SET movie = $movie - `, - { movie: { id, duration: neo4jDuration } } - ); - - const query = /* GraphQL */ ` - query ($id: ID!, $duration: Duration!) { - ${Movie.plural}(where: { id: $id, duration: $duration }) { - id - duration - } - } - `; - - const graphqlResult = await testHelper.executeGraphQL(query, { - variableValues: { id, duration }, - }); - - expect(graphqlResult.errors).toBeFalsy(); - - const graphqlMovie: { id: string; duration: string } = (graphqlResult.data as any)[Movie.plural][0]; - expect(graphqlMovie).toBeDefined(); - expect(graphqlMovie.id).toEqual(id); - expect(parseDuration(graphqlMovie.duration)).toStrictEqual(parsedDuration); - }); - test.each(["LT", "LTE", "GT", "GTE"])( "should filter based on duration comparison, for filter: %s", async (filter) => { @@ -323,7 +278,7 @@ describe("Duration", () => { SET medium = $medium CREATE (short:${Movie}) SET short = $short - `, + `, { long: { id: longId, duration: neo4jLong }, medium: { id: mediumId, duration: neo4jMedium }, diff --git a/packages/graphql/tests/integration/types/localdatetime.int.test.ts b/packages/graphql/tests/integration/types/localdatetime.int.test.ts index dd0a09440a..7c928008e3 100644 --- a/packages/graphql/tests/integration/types/localdatetime.int.test.ts +++ b/packages/graphql/tests/integration/types/localdatetime.int.test.ts @@ -207,41 +207,6 @@ describe("LocalDateTime", () => { }); describe("filter", () => { - test("should filter based on localDT equality", async () => { - const id = generate({ readable: false }); - const date = new Date("2024-09-17T11:49:48.322Z"); - const localDT = date.toISOString().split("Z")[0]; - const neo4jLocalDateTime = neo4jDriver.types.LocalDateTime.fromStandardDate(date); - const parsedLocalDateTime = parseLocalDateTime(localDT); - - await testHelper.executeCypher( - ` - CREATE (movie:${Movie}) - SET movie = $movie - `, - { movie: { id, localDT: neo4jLocalDateTime } } - ); - - const query = /* GraphQL */ ` - query ($localDT: LocalDateTime!) { - ${Movie.plural}(where: { localDT: $localDT }) { - id - localDT - } - } - `; - - const graphqlResult = await testHelper.executeGraphQL(query, { - variableValues: { localDT }, - }); - - expect(graphqlResult.errors).toBeFalsy(); - - const graphqlMovie: { id: string; localDT: string } = (graphqlResult.data as any)[Movie.plural][0]; - expect(graphqlMovie).toBeDefined(); - expect(graphqlMovie.id).toEqual(id); - expect(parseLocalDateTime(graphqlMovie.localDT)).toStrictEqual(parsedLocalDateTime); - }); test.each(["LT", "LTE", "GT", "GTE"])( "should filter based on localDT comparison, for filter %s", async (filter) => { diff --git a/packages/graphql/tests/integration/types/point.int.test.ts b/packages/graphql/tests/integration/types/point.int.test.ts index e9aa3c7d79..9b36d27216 100644 --- a/packages/graphql/tests/integration/types/point.int.test.ts +++ b/packages/graphql/tests/integration/types/point.int.test.ts @@ -305,265 +305,4 @@ describe("Point", () => { expect((result.records[0] as any).toObject().p.location.z).toEqual(height); expect((result.records[0] as any).toObject().p.location.srid).toEqual(int(4979)); }); - - test("enables query of a node with a wgs-84 point", async () => { - // Create node - const id = "f8a5a58a-7380-4a39-9103-07a2c0528d8e"; - const size = 31364; - const longitude = parseFloat("62.5196"); - const latitude = parseFloat("-41.1021"); - - const result = await testHelper.executeCypher(` - CALL { - CREATE (p:${Photograph}) - SET p.id = "${id}" - SET p.size = ${size} - SET p.location = point({longitude: ${longitude}, latitude: ${latitude}}) - RETURN p - } - - RETURN p { .id, .size, .location } AS p - `); - - expect((result.records[0] as any).toObject().p.location.x).toEqual(longitude); - expect((result.records[0] as any).toObject().p.location.y).toEqual(latitude); - - // Test equality - const photographsEqualsQuery = /* GraphQL */ ` - query Photographs($longitude: Float!, $latitude: Float!) { - ${Photograph.plural}(where: { location: { longitude: $longitude, latitude: $latitude } }) { - id - size - location { - latitude - longitude - height - crs - } - } - } - `; - - const equalsResult = await testHelper.executeGraphQL(photographsEqualsQuery, { - variableValues: { longitude, latitude }, - }); - - expect(equalsResult.errors).toBeFalsy(); - expect((equalsResult.data as any)[Photograph.plural][0]).toEqual({ - id, - size, - location: { - latitude, - longitude, - height: null, - crs: "wgs-84", - }, - }); - - // Test IN functionality - const photographsInQuery = /* GraphQL */ ` - query Photographs($locations: [PointInput!]) { - ${Photograph.plural}(where: { location_IN: $locations }) { - id - size - location { - latitude - longitude - height - crs - } - } - } - `; - - const inResult = await testHelper.executeGraphQL(photographsInQuery, { - variableValues: { - locations: [ - { longitude, latitude }, - { - longitude: parseFloat("-156.8208"), - latitude: parseFloat("64.9108"), - }, - ], - }, - }); - - expect(inResult.errors).toBeFalsy(); - expect((inResult.data as any)[Photograph.plural]).toContainEqual({ - id, - size, - location: { - latitude, - longitude, - height: null, - crs: "wgs-84", - }, - }); - - // Test NOT IN functionality - const photographsNotInQuery = /* GraphQL */ ` - query Photographs($locations: [PointInput!]) { - ${Photograph.plural}(where: { location_NOT_IN: $locations }) { - id - size - location { - latitude - longitude - height - crs - } - } - } - `; - - const notInResult = await testHelper.executeGraphQL(photographsNotInQuery, { - variableValues: { - locations: [ - { - longitude: parseFloat("147.0866"), - latitude: parseFloat("-64.3432"), - }, - { - longitude: parseFloat("-97.4775"), - latitude: parseFloat("-61.2485"), - }, - ], - }, - }); - - expect(notInResult.errors).toBeFalsy(); - expect((notInResult.data as any)[Photograph.plural]).toContainEqual({ - id, - size, - location: { - latitude, - longitude, - height: null, - crs: "wgs-84", - }, - }); - - // Test less than - const photographsLessThanQuery = /* GraphQL */ ` - query Photographs($longitude: Float!, $latitude: Float!) { - ${Photograph.plural}( - where: { location_LT: { point: { longitude: $longitude, latitude: $latitude }, distance: 1000000 } } - ) { - id - size - location { - latitude - longitude - height - crs - } - } - } - `; - - const lessThanResult = await testHelper.executeGraphQL(photographsLessThanQuery, { - variableValues: { longitude, latitude: latitude + 1 }, - }); - - expect(lessThanResult.errors).toBeFalsy(); - expect((lessThanResult.data as any)[Photograph.plural]).toContainEqual({ - id, - size, - location: { - latitude, - longitude, - height: null, - crs: "wgs-84", - }, - }); - - // Test greater than - const photographsGreaterThanQuery = /* GraphQL */ ` - query Photographs($longitude: Float!, $latitude: Float!) { - ${Photograph.plural}( - where: { location_GT: { point: { longitude: $longitude, latitude: $latitude }, distance: 1 } } - ) { - id - size - location { - latitude - longitude - height - crs - } - } - } - `; - - const greaterThanResult = await testHelper.executeGraphQL(photographsGreaterThanQuery, { - variableValues: { longitude, latitude: latitude + 1 }, - }); - - expect(greaterThanResult.errors).toBeFalsy(); - expect((greaterThanResult.data as any)[Photograph.plural]).toContainEqual({ - id, - size, - location: { - latitude, - longitude, - height: null, - crs: "wgs-84", - }, - }); - }); - - test("enables query for equality of a node with a wgs-84-3d point", async () => { - const id = "3019fe82-5231-4103-8662-39c1fcc7d50c"; - const size = 99119; - const longitude = parseFloat("125.6358"); - const latitude = parseFloat("-7.2045"); - const height = 0.6950517320074141; - - const result = await testHelper.executeCypher(` - CALL { - CREATE (p:${Photograph}) - SET p.id = "${id}" - SET p.size = ${size} - SET p.location = point({longitude: ${longitude}, latitude: ${latitude}, height: ${height}}) - RETURN p - } - - RETURN p { .id, .size, .location } AS p - `); - - expect((result.records[0] as any).toObject().p.location.x).toEqual(longitude); - expect((result.records[0] as any).toObject().p.location.y).toEqual(latitude); - expect((result.records[0] as any).toObject().p.location.z).toEqual(height); - - const photographsQuery = /* GraphQL */ ` - query Photographs($longitude: Float!, $latitude: Float!, $height: Float) { - ${Photograph.plural}(where: { location: { longitude: $longitude, latitude: $latitude, height: $height } }) { - id - size - location { - latitude - longitude - height - crs - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(photographsQuery, { - variableValues: { longitude, latitude, height }, - }); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data as any)[Photograph.plural][0]).toEqual({ - id, - size, - location: { - latitude, - longitude, - height, - crs: "wgs-84-3d", - }, - }); - }); }); diff --git a/packages/graphql/tests/integration/types/time.int.test.ts b/packages/graphql/tests/integration/types/time.int.test.ts index 277a8ad236..0a8870b952 100644 --- a/packages/graphql/tests/integration/types/time.int.test.ts +++ b/packages/graphql/tests/integration/types/time.int.test.ts @@ -216,49 +216,6 @@ describe("Time", () => { }); describe("filter", () => { - test("should filter based on time equality", async () => { - const typeDefs = /* GraphQL */ ` - type ${Movie} { - id: ID! - time: Time! - } - `; - - await testHelper.initNeo4jGraphQL({ typeDefs }); - - const id = generate({ readable: false }); - const date = new Date("2024-02-17T11:49:48.322Z"); - const time = date.toISOString().split("T")[1]; - const neo4jTime = Time.fromStandardDate(date); - const parsedTime = parseTime(time); - - await testHelper.executeCypher( - ` - CREATE (movie:${Movie}) - SET movie = $movie - `, - { movie: { id, time: neo4jTime } } - ); - - const query = /* GraphQL */ ` - query ($time: Time!) { - ${Movie.plural}(where: { time: $time }) { - id - time - } - } - `; - - const graphqlResult = await testHelper.executeGraphQL(query, { variableValues: { time } }); - - expect(graphqlResult.errors).toBeFalsy(); - - const graphqlMovie: { id: string; time: string } = (graphqlResult.data as any)[Movie.plural][0]; - expect(graphqlMovie).toBeDefined(); - expect(graphqlMovie.id).toEqual(id); - expect(parseTime(graphqlMovie.time)).toStrictEqual(parsedTime); - }); - test.each(["LT", "LTE", "GT", "GTE"])( "should filter based on time comparison for filter: %s", async (filter) => { 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 index 7b760480b9..4f7daa8ae8 100644 --- a/packages/graphql/tests/integration/unwind-create/unwind-create.int.test.ts +++ b/packages/graphql/tests/integration/unwind-create/unwind-create.int.test.ts @@ -31,61 +31,6 @@ describe("unwind-create", () => { await testHelper.close(); }); - test("should create a batch of movies", async () => { - const Movie = new UniqueType("Movie"); - - const typeDefs = ` - type ${Movie} { - id: ID! - } - `; - - await testHelper.initNeo4jGraphQL({ 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 - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query, { - variableValues: { id, id2 }, - }); - - expect(gqlResult.errors).toBeFalsy(); - - expect(gqlResult?.data?.[Movie.operations.create]?.[Movie.plural]).toEqual( - expect.arrayContaining([{ id }, { id: id2 }]) - ); - - const reFind = await testHelper.executeCypher( - ` - 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 } }) }, - ]) - ); - }); - test("should create a batch of movies with nested actors", async () => { const Movie = new UniqueType("Movie"); const Actor = new UniqueType("Actor"); diff --git a/packages/graphql/tests/schema/issues/200.test.ts b/packages/graphql/tests/schema/issues/200.test.ts index 9e834fc5b9..4e3a6f6fdb 100644 --- a/packages/graphql/tests/schema/issues/200.test.ts +++ b/packages/graphql/tests/schema/issues/200.test.ts @@ -18,11 +18,11 @@ */ import { printSchemaWithDirectives } from "@graphql-tools/utils"; -import { lexicographicSortSchema } from "graphql/utilities"; import { gql } from "graphql-tag"; +import { lexicographicSortSchema } from "graphql/utilities"; import { Neo4jGraphQL } from "../../../src"; -describe("200", () => { +describe("https://github.com/neo4j/graphql/issues/200", () => { test("Preserve schema array non null", async () => { const typeDefs = gql` type Category { diff --git a/packages/graphql/tests/tck/advanced-filtering.test.ts b/packages/graphql/tests/tck/advanced-filtering.test.ts index b70b4e7608..c842bd26c3 100644 --- a/packages/graphql/tests/tck/advanced-filtering.test.ts +++ b/packages/graphql/tests/tck/advanced-filtering.test.ts @@ -65,32 +65,6 @@ describe("Cypher Advanced Filtering", () => { unsetTestEnvVars(undefined); }); - test("IN", async () => { - const query = /* GraphQL */ ` - { - movies(where: { _id_IN: ["123"] }) { - _id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this._id IN $param0 - RETURN this { ._id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": [ - \\"123\\" - ] - }" - `); - }); - test("REGEX", async () => { const query = /* GraphQL */ ` { @@ -114,796 +88,4 @@ describe("Cypher Advanced Filtering", () => { }" `); }); - - test("NOT", async () => { - const query = /* GraphQL */ ` - { - movies(where: { id_NOT: "123" }) { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE NOT (this.id = $param0) - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\" - }" - `); - }); - - test("NOT_IN", async () => { - const query = /* GraphQL */ ` - { - movies(where: { id_NOT_IN: ["123"] }) { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE NOT (this.id IN $param0) - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": [ - \\"123\\" - ] - }" - `); - }); - - test("CONTAINS", async () => { - const query = /* GraphQL */ ` - { - movies(where: { id_CONTAINS: "123" }) { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.id CONTAINS $param0 - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\" - }" - `); - }); - - test("NOT_CONTAINS", async () => { - const query = /* GraphQL */ ` - { - movies(where: { id_NOT_CONTAINS: "123" }) { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE NOT (this.id CONTAINS $param0) - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\" - }" - `); - }); - - test("STARTS_WITH", async () => { - const query = /* GraphQL */ ` - { - movies(where: { id_STARTS_WITH: "123" }) { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.id STARTS WITH $param0 - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\" - }" - `); - }); - - test("NOT_STARTS_WITH", async () => { - const query = /* GraphQL */ ` - { - movies(where: { id_NOT_STARTS_WITH: "123" }) { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE NOT (this.id STARTS WITH $param0) - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\" - }" - `); - }); - - test("ENDS_WITH", async () => { - const query = /* GraphQL */ ` - { - movies(where: { id_ENDS_WITH: "123" }) { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.id ENDS WITH $param0 - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\" - }" - `); - }); - - test("NOT_ENDS_WITH", async () => { - const query = /* GraphQL */ ` - { - movies(where: { id_NOT_ENDS_WITH: "123" }) { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE NOT (this.id ENDS WITH $param0) - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"123\\" - }" - `); - }); - - test("LT", async () => { - const query = /* GraphQL */ ` - { - movies(where: { actorCount_LT: 123 }) { - actorCount - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.actorCount < $param0 - RETURN this { .actorCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 123, - \\"high\\": 0 - } - }" - `); - }); - - test("LT BigInt", async () => { - const query = /* GraphQL */ ` - { - movies(where: { budget_LT: 9223372036854775807 }) { - budget - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.budget < $param0 - RETURN this { .budget } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": -1, - \\"high\\": 2147483647 - } - }" - `); - }); - - test("LT String", async () => { - const query = /* GraphQL */ ` - { - movies(where: { title_LT: "The Matrix Revolutions" }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title < $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"The Matrix Revolutions\\" - }" - `); - }); - - test("LTE", async () => { - const query = /* GraphQL */ ` - { - movies(where: { actorCount_LTE: 123 }) { - actorCount - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.actorCount <= $param0 - RETURN this { .actorCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 123, - \\"high\\": 0 - } - }" - `); - }); - - test("LTE BigInt", async () => { - const query = /* GraphQL */ ` - { - movies(where: { budget_LTE: 9223372036854775807 }) { - budget - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.budget <= $param0 - RETURN this { .budget } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": -1, - \\"high\\": 2147483647 - } - }" - `); - }); - - test("LTE String", async () => { - const query = /* GraphQL */ ` - { - movies(where: { title_LTE: "The Matrix Revolutions" }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title <= $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"The Matrix Revolutions\\" - }" - `); - }); - - test("GT", async () => { - const query = /* GraphQL */ ` - { - movies(where: { actorCount_GT: 123 }) { - actorCount - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.actorCount > $param0 - RETURN this { .actorCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 123, - \\"high\\": 0 - } - }" - `); - }); - - test("GT BigInt", async () => { - const query = /* GraphQL */ ` - { - movies(where: { budget_GT: 9223372036854775000 }) { - budget - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.budget > $param0 - RETURN this { .budget } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": -808, - \\"high\\": 2147483647 - } - }" - `); - }); - - test("GT String", async () => { - const query = /* GraphQL */ ` - { - movies(where: { title_GT: "The Matrix Revolutions" }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title > $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"The Matrix Revolutions\\" - }" - `); - }); - - test("GTE", async () => { - const query = /* GraphQL */ ` - { - movies(where: { actorCount_GTE: 123 }) { - actorCount - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.actorCount >= $param0 - RETURN this { .actorCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 123, - \\"high\\": 0 - } - }" - `); - }); - - test("GTE BigInt", async () => { - const query = /* GraphQL */ ` - { - movies(where: { budget_GTE: 9223372036854775000 }) { - budget - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.budget >= $param0 - RETURN this { .budget } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": -808, - \\"high\\": 2147483647 - } - }" - `); - }); - - test("GTE String", async () => { - const query = /* GraphQL */ ` - { - movies(where: { title_GTE: "The Matrix Revolutions" }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title >= $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"The Matrix Revolutions\\" - }" - `); - }); - - describe("Relationships", () => { - test("equality", async () => { - const query = /* GraphQL */ ` - { - movies(where: { genres: { name: "some genre" } }) { - actorCount - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE EXISTS { - MATCH (this)-[:IN_GENRE]->(this0:Genre) - WHERE this0.name = $param0 - } - RETURN this { .actorCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - - test("NOT", async () => { - const query = /* GraphQL */ ` - { - movies(where: { genres_NOT: { name: "some genre" } }) { - actorCount - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE NOT (EXISTS { - MATCH (this)-[:IN_GENRE]->(this0:Genre) - WHERE this0.name = $param0 - }) - RETURN this { .actorCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - - describe("List Predicates", () => { - const generateQuery = (operator: "ALL" | "NONE" | "SINGLE" | "SOME"): string => { - const query = /* GraphQL */ ` - { - movies(where: { genres_${operator}: { name: "some genre" } }) { - actorCount - } - } - `; - return query; - }; - test("ALL", async () => { - const query = generateQuery("ALL"); - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE (EXISTS { - MATCH (this)-[:IN_GENRE]->(this0:Genre) - WHERE this0.name = $param0 - } AND NOT (EXISTS { - MATCH (this)-[:IN_GENRE]->(this0:Genre) - WHERE NOT (this0.name = $param0) - })) - RETURN this { .actorCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - test("NONE", async () => { - const query = generateQuery("NONE"); - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE NOT (EXISTS { - MATCH (this)-[:IN_GENRE]->(this0:Genre) - WHERE this0.name = $param0 - }) - RETURN this { .actorCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - test("SINGLE", async () => { - const query = generateQuery("SINGLE"); - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE single(this0 IN [(this)-[:IN_GENRE]->(this0:Genre) WHERE this0.name = $param0 | 1] WHERE true) - RETURN this { .actorCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - test("SOME", async () => { - const query = generateQuery("SOME"); - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE EXISTS { - MATCH (this)-[:IN_GENRE]->(this0:Genre) - WHERE this0.name = $param0 - } - RETURN this { .actorCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - }); - }); - - describe("Connections", () => { - test("Node and relationship properties equality", async () => { - const query = /* GraphQL */ ` - { - movies(where: { genresConnection: { node: { name: "some genre" } } }) { - actorCount - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE EXISTS { - MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - WHERE this1.name = $param0 - } - RETURN this { .actorCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - - test("Node and relationship properties NOT", async () => { - const query = /* GraphQL */ ` - { - movies(where: { genresConnection_NOT: { node: { name: "some genre" } } }) { - actorCount - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE NOT (EXISTS { - MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - WHERE this1.name = $param0 - }) - RETURN this { .actorCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - - describe("List Predicates", () => { - const generateQuery = (operator: "ALL" | "NONE" | "SINGLE" | "SOME"): string => { - const query = /* GraphQL */ ` - { - movies(where: { genresConnection_${operator}: { node: { name: "some genre" } } }) { - actorCount - } - } - `; - return query; - }; - test("ALL", async () => { - const query = generateQuery("ALL"); - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE (EXISTS { - MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - WHERE this1.name = $param0 - } AND NOT (EXISTS { - MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - WHERE NOT (this1.name = $param0) - })) - RETURN this { .actorCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - test("NONE", async () => { - const query = generateQuery("NONE"); - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE NOT (EXISTS { - MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - WHERE this1.name = $param0 - }) - RETURN this { .actorCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - test("SINGLE", async () => { - const query = generateQuery("SINGLE"); - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE single(this0 IN [(this)-[this1:IN_GENRE]->(this0:Genre) WHERE this0.name = $param0 | 1] WHERE true) - RETURN this { .actorCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - test("SOME", async () => { - const query = generateQuery("SOME"); - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE EXISTS { - MATCH (this)-[this0:IN_GENRE]->(this1:Genre) - WHERE this1.name = $param0 - } - RETURN this { .actorCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some genre\\" - }" - `); - }); - }); - }); }); diff --git a/packages/graphql/tests/tck/alias.test.ts b/packages/graphql/tests/tck/alias.test.ts deleted file mode 100644 index 818fcbe01d..0000000000 --- a/packages/graphql/tests/tck/alias.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../src"; -import { formatCypher, formatParams, translateQuery } from "./utils/tck-test-utils"; - -describe("Cypher Alias", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Actor { - name: String! - } - - type Movie { - id: ID - releaseDate: DateTime! - location: Point! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) - custom: [Movie!]! - @cypher( - statement: """ - MATCH (m:Movie) - RETURN m - """ - columnName: "m" - ) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Alias", async () => { - const query = /* GraphQL */ ` - { - movies { - movieId: id - actors { - aliasActorsName: name - } - custom { - aliasCustomId: id - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - // NOTE: Order of these subqueries have been reversed after refactor - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - CALL { - WITH this - WITH this AS this - MATCH (m:Movie) - RETURN m - } - WITH m AS this0 - WITH this0 { aliasCustomId: this0.id } AS this0 - RETURN collect(this0) AS var1 - } - CALL { - WITH this - MATCH (this)<-[this2:ACTED_IN]-(this3:Actor) - WITH this3 { aliasActorsName: this3.name } AS this3 - RETURN collect(this3) AS var4 - } - RETURN this { movieId: this.id, actors: var4, custom: var1 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); -}); diff --git a/packages/graphql/tests/tck/connections/alias.test.ts b/packages/graphql/tests/tck/connections/alias.test.ts deleted file mode 100644 index e16e08766f..0000000000 --- a/packages/graphql/tests/tck/connections/alias.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; - -describe("Connections Alias", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type Actor { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - screenTime: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Alias Top Level Connection Field", async () => { - const query = /* GraphQL */ ` - { - movies { - actors: actorsConnection { - totalCount - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ node: { __id: id(this1), __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { actors: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Alias Top Level Connection Field Multiple Times", async () => { - const query = /* GraphQL */ ` - query { - movies(where: { title: "Forrest Gump" }) { - title - hanks: actorsConnection(where: { node: { name: "Tom Hanks" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - jenny: actorsConnection(where: { node: { name: "Robin Wright" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this1.name = $param1 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - CALL { - WITH this - MATCH (this)<-[this4:ACTED_IN]-(this5:Actor) - WHERE this5.name = $param2 - WITH collect({ node: this5, relationship: this4 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this5, edge.relationship AS this4 - RETURN collect({ properties: { screenTime: this4.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this5.name, __resolveType: \\"Actor\\" } }) AS var6 - } - RETURN { edges: var6, totalCount: totalCount } AS var7 - } - RETURN this { .title, hanks: var3, jenny: var7 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Forrest Gump\\", - \\"param1\\": \\"Tom Hanks\\", - \\"param2\\": \\"Robin Wright\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/connections/filtering/node/and.test.ts b/packages/graphql/tests/tck/connections/filtering/node/and.test.ts deleted file mode 100644 index f40c6e059b..0000000000 --- a/packages/graphql/tests/tck/connections/filtering/node/and.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../utils/tck-test-utils"; - -describe("Cypher -> Connections -> Filtering -> Node -> AND", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type Actor { - firstName: String! - lastName: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - screenTime: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("AND", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { AND: [{ firstName: "Tom" }, { lastName: "Hanks" }] } }) { - edges { - properties { - screenTime - } - node { - firstName - lastName - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE (this1.firstName = $param0 AND this1.lastName = $param1) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { firstName: this1.firstName, lastName: this1.lastName, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom\\", - \\"param1\\": \\"Hanks\\" - }" - `); - }); - - test("NOT", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { NOT: { firstName: "Tom" } } }) { - edges { - properties { - screenTime - } - node { - firstName - lastName - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this1.firstName = $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { firstName: this1.firstName, lastName: this1.lastName, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/connections/filtering/node/equality.test.ts b/packages/graphql/tests/tck/connections/filtering/node/equality.test.ts deleted file mode 100644 index 1a243ccea5..0000000000 --- a/packages/graphql/tests/tck/connections/filtering/node/equality.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../utils/tck-test-utils"; - -describe("Cypher -> Connections -> Filtering -> Node -> Equality", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type Actor { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - screenTime: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Equality", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { name: "Tom Hanks" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this1.name = $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom Hanks\\" - }" - `); - }); - - test("Inequality", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { name_NOT: "Tom Hanks" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this1.name = $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom Hanks\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/connections/filtering/node/or.test.ts b/packages/graphql/tests/tck/connections/filtering/node/or.test.ts deleted file mode 100644 index f4be7cb11f..0000000000 --- a/packages/graphql/tests/tck/connections/filtering/node/or.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../utils/tck-test-utils"; - -describe("Cypher -> Connections -> Filtering -> Node -> OR", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type Actor { - firstName: String! - lastName: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - screenTime: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("OR", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { OR: [{ firstName: "Tom" }, { lastName: "Hanks" }] } }) { - edges { - properties { - screenTime - } - node { - firstName - lastName - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE (this1.firstName = $param0 OR this1.lastName = $param1) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { firstName: this1.firstName, lastName: this1.lastName, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom\\", - \\"param1\\": \\"Hanks\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/connections/filtering/node/relationship.test.ts b/packages/graphql/tests/tck/connections/filtering/node/relationship.test.ts deleted file mode 100644 index 4f1323da0b..0000000000 --- a/packages/graphql/tests/tck/connections/filtering/node/relationship.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../utils/tck-test-utils"; - -describe("Cypher -> Connections -> Filtering -> Node -> Relationship", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type Actor { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Equality", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { movies: { title: "Forrest Gump" } } }) { - edges { - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE EXISTS { - MATCH (this1)-[:ACTED_IN]->(this2:Movie) - WHERE this2.title = $param0 - } - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var3 - } - RETURN { edges: var3, totalCount: totalCount } AS var4 - } - RETURN this { .title, actorsConnection: var4 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Forrest Gump\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/connections/filtering/node/string.test.ts b/packages/graphql/tests/tck/connections/filtering/node/string.test.ts deleted file mode 100644 index b76cc31e03..0000000000 --- a/packages/graphql/tests/tck/connections/filtering/node/string.test.ts +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../../../src"; -import { - formatCypher, - formatParams, - setTestEnvVars, - translateQuery, - unsetTestEnvVars, -} from "../../../utils/tck-test-utils"; - -describe("Cypher -> Connections -> Filtering -> Node -> String", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type Actor { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - screenTime: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - features: { - filters: { - String: { - MATCHES: true, - }, - }, - }, - }); - setTestEnvVars("NEO4J_GRAPHQL_ENABLE_REGEX=1"); - }); - - afterAll(() => { - unsetTestEnvVars(undefined); - }); - - test("CONTAINS", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { name_CONTAINS: "Tom" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this1.name CONTAINS $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom\\" - }" - `); - }); - - test("NOT_CONTAINS", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { name_NOT_CONTAINS: "Tom" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this1.name CONTAINS $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom\\" - }" - `); - }); - - test("STARTS_WITH", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { name_STARTS_WITH: "Tom" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this1.name STARTS WITH $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom\\" - }" - `); - }); - - test("NOT_STARTS_WITH", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { name_NOT_STARTS_WITH: "Tom" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this1.name STARTS WITH $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom\\" - }" - `); - }); - - test("ENDS_WITH", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { name_ENDS_WITH: "Hanks" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this1.name ENDS WITH $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Hanks\\" - }" - `); - }); - - test("NOT_ENDS_WITH", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { name_NOT_ENDS_WITH: "Hanks" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this1.name ENDS WITH $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Hanks\\" - }" - `); - }); - - test("MATCHES", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { node: { name_MATCHES: "Tom.+" } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this1.name =~ $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Tom.+\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/and.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/and.test.ts deleted file mode 100644 index 3b53b677f5..0000000000 --- a/packages/graphql/tests/tck/connections/filtering/relationship/and.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../utils/tck-test-utils"; - -describe("Cypher -> Connections -> Filtering -> Relationship -> AND", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type Actor { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - role: String! - screenTime: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("AND", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { AND: [{ role_ENDS_WITH: "Gump" }, { screenTime_LT: 60 }] } }) { - edges { - properties { - role - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE (this0.role ENDS WITH $param0 AND this0.screenTime < $param1) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Gump\\", - \\"param1\\": { - \\"low\\": 60, - \\"high\\": 0 - } - }" - `); - }); - - test("NOT", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { NOT: { role_ENDS_WITH: "Gump" } } }) { - edges { - properties { - role - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this0.role ENDS WITH $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Gump\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/equality.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/equality.test.ts deleted file mode 100644 index 531ffebf59..0000000000 --- a/packages/graphql/tests/tck/connections/filtering/relationship/equality.test.ts +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../utils/tck-test-utils"; - -describe("Cypher -> Connections -> Filtering -> Relationship -> Equality", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type Actor { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - screenTime: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Equality", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { screenTime: 60 } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this0.screenTime = $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 60, - \\"high\\": 0 - } - }" - `); - }); - - test("Inequality", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { screenTime_NOT: 60 } }) { - edges { - properties { - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this0.screenTime = $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 60, - \\"high\\": 0 - } - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/or.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/or.test.ts deleted file mode 100644 index 8b12f05669..0000000000 --- a/packages/graphql/tests/tck/connections/filtering/relationship/or.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../utils/tck-test-utils"; - -describe("Cypher -> Connections -> Filtering -> Relationship -> OR", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type Actor { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - role: String! - screenTime: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("OR", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { OR: [{ role_ENDS_WITH: "Gump" }, { screenTime_LT: 60 }] } }) { - edges { - properties { - role - screenTime - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE (this0.role ENDS WITH $param0 OR this0.screenTime < $param1) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, screenTime: this0.screenTime, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Gump\\", - \\"param1\\": { - \\"low\\": 60, - \\"high\\": 0 - } - }" - `); - }); - - test("OR between edge and node", async () => { - const query = /* GraphQL */ ` - { - movies(where: { actorsConnection: { OR: [{ node: { name: "Harry" } }, { edge: { role: "Tom" } }] } }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE EXISTS { - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE (this1.name = $param0 OR this0.role = $param1) - } - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Harry\\", - \\"param1\\": \\"Tom\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/string.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/string.test.ts deleted file mode 100644 index 90507ecae0..0000000000 --- a/packages/graphql/tests/tck/connections/filtering/relationship/string.test.ts +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../../../src"; -import { - formatCypher, - formatParams, - setTestEnvVars, - translateQuery, - unsetTestEnvVars, -} from "../../../utils/tck-test-utils"; - -describe("Cypher -> Connections -> Filtering -> Relationship -> String", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - title: String! - actors: [Actor!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: IN) - } - - type Actor { - name: String! - movies: [Movie!]! @relationship(type: "ACTED_IN", properties: "ActedIn", direction: OUT) - } - - type ActedIn @relationshipProperties { - role: String! - screenTime: Int! - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - features: { - filters: { - String: { - MATCHES: true, - }, - }, - }, - }); - setTestEnvVars("NEO4J_GRAPHQL_ENABLE_REGEX=1"); - }); - - afterAll(() => { - unsetTestEnvVars(undefined); - }); - test("CONTAINS", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { role_CONTAINS: "Forrest" } }) { - edges { - properties { - role - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this0.role CONTAINS $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Forrest\\" - }" - `); - }); - - test("NOT_CONTAINS", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { role_NOT_CONTAINS: "Forrest" } }) { - edges { - properties { - role - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this0.role CONTAINS $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Forrest\\" - }" - `); - }); - - test("STARTS_WITH", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { role_STARTS_WITH: "Forrest" } }) { - edges { - properties { - role - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this0.role STARTS WITH $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Forrest\\" - }" - `); - }); - - test("NOT_STARTS_WITH", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { role_NOT_STARTS_WITH: "Forrest" } }) { - edges { - properties { - role - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this0.role STARTS WITH $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Forrest\\" - }" - `); - }); - - test("ENDS_WITH", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { role_ENDS_WITH: "Gump" } }) { - edges { - properties { - role - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this0.role ENDS WITH $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Gump\\" - }" - `); - }); - - test("NOT_ENDS_WITH", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { role_NOT_ENDS_WITH: "Gump" } }) { - edges { - properties { - role - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE NOT (this0.role ENDS WITH $param0) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Gump\\" - }" - `); - }); - - test("MATCHES", async () => { - const query = /* GraphQL */ ` - query { - movies { - title - actorsConnection(where: { edge: { role_MATCHES: "Forrest.+" } }) { - edges { - properties { - role - } - node { - name - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WHERE this0.role =~ $param0 - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { role: this0.role, __resolveType: \\"ActedIn\\" }, node: { name: this1.name, __resolveType: \\"Actor\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .title, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Forrest.+\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/directives/alias.test.ts b/packages/graphql/tests/tck/directives/alias.test.ts index 9001e05e22..31f6925553 100644 --- a/packages/graphql/tests/tck/directives/alias.test.ts +++ b/packages/graphql/tests/tck/directives/alias.test.ts @@ -48,81 +48,6 @@ describe("Cypher alias directive", () => { }); }); - test("Simple relation", async () => { - const query = /* GraphQL */ ` - { - actors { - name - city - actedIn { - title - rating - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Actor) - CALL { - WITH this - MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - WITH this1 { .title, rating: this1.ratingPropInDb } AS this1 - RETURN collect(this1) AS var2 - } - RETURN this { .name, city: this.cityPropInDb, actedIn: var2 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("With relationship properties", async () => { - const query = /* GraphQL */ ` - { - actors { - name - city - actedInConnection { - edges { - properties { - character - screenTime - } - node { - title - rating - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Actor) - CALL { - WITH this - MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - RETURN collect({ properties: { character: this0.characterPropInDb, screenTime: this0.screenTime, __resolveType: \\"ActorActedInProps\\" }, node: { title: this1.title, rating: this1.ratingPropInDb, __resolveType: \\"Movie\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .name, city: this.cityPropInDb, actedInConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - test("Create mutation", async () => { const query = /* GraphQL */ ` mutation { diff --git a/packages/graphql/tests/tck/issues/190.test.ts b/packages/graphql/tests/tck/issues/190.test.ts deleted file mode 100644 index 61dd12ce73..0000000000 --- a/packages/graphql/tests/tck/issues/190.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; - -describe("#190", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type User { - client_id: String - uid: String - demographics: [UserDemographics!]! @relationship(type: "HAS_DEMOGRAPHIC", direction: OUT) - } - - type UserDemographics { - client_id: String - type: String - value: String - users: [User!]! @relationship(type: "HAS_DEMOGRAPHIC", direction: IN) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Example 1", async () => { - const query = /* GraphQL */ ` - query { - users(where: { demographics: { type: "Gender", value: "Female" } }) { - uid - demographics { - type - value - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE EXISTS { - MATCH (this)-[:HAS_DEMOGRAPHIC]->(this0:UserDemographics) - WHERE (this0.type = $param0 AND this0.value = $param1) - } - CALL { - WITH this - MATCH (this)-[this1:HAS_DEMOGRAPHIC]->(this2:UserDemographics) - WITH this2 { .type, .value } AS this2 - RETURN collect(this2) AS var3 - } - RETURN this { .uid, demographics: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Gender\\", - \\"param1\\": \\"Female\\" - }" - `); - }); - - test("Example 2", async () => { - const query = /* GraphQL */ ` - query { - users( - where: { - demographics: { OR: [{ type: "Gender", value: "Female" }, { type: "State" }, { type: "Age" }] } - } - ) { - uid - demographics { - type - value - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE EXISTS { - MATCH (this)-[:HAS_DEMOGRAPHIC]->(this0:UserDemographics) - WHERE ((this0.type = $param0 AND this0.value = $param1) OR this0.type = $param2 OR this0.type = $param3) - } - CALL { - WITH this - MATCH (this)-[this1:HAS_DEMOGRAPHIC]->(this2:UserDemographics) - WITH this2 { .type, .value } AS this2 - RETURN collect(this2) AS var3 - } - RETURN this { .uid, demographics: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Gender\\", - \\"param1\\": \\"Female\\", - \\"param2\\": \\"State\\", - \\"param3\\": \\"Age\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/pagination.test.ts b/packages/graphql/tests/tck/pagination.test.ts deleted file mode 100644 index 348952182e..0000000000 --- a/packages/graphql/tests/tck/pagination.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../src"; -import { formatCypher, formatParams, translateQuery } from "./utils/tck-test-utils"; - -describe("Cypher pagination tests", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - id: ID - title: String - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Skipping", async () => { - const query = /* GraphQL */ ` - { - movies(options: { offset: 1 }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH * - SKIP $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 1, - \\"high\\": 0 - } - }" - `); - }); - - test("Limit", async () => { - const query = /* GraphQL */ ` - { - movies(options: { limit: 1 }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH * - LIMIT $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 1, - \\"high\\": 0 - } - }" - `); - }); - - test("Skip + Limit", async () => { - const query = /* GraphQL */ ` - { - movies(options: { limit: 1, offset: 2 }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH * - SKIP $param0 - LIMIT $param1 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 2, - \\"high\\": 0 - }, - \\"param1\\": { - \\"low\\": 1, - \\"high\\": 0 - } - }" - `); - }); - - test("Skip + Limit as variables", async () => { - const query = /* GraphQL */ ` - query ($offset: Int, $limit: Int) { - movies(options: { limit: $limit, offset: $offset }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query, { - variableValues: { offset: 0, limit: 0 }, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH * - SKIP $param0 - LIMIT $param1 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 0, - \\"high\\": 0 - }, - \\"param1\\": { - \\"low\\": 0, - \\"high\\": 0 - } - }" - `); - }); - - test("Skip + Limit with other variables", async () => { - const query = /* GraphQL */ ` - query ($offset: Int, $limit: Int, $title: String) { - movies(options: { limit: $limit, offset: $offset }, where: { title: $title }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query, { - variableValues: { limit: 1, offset: 2, title: "some title" }, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - WITH * - SKIP $param1 - LIMIT $param2 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\", - \\"param1\\": { - \\"low\\": 2, - \\"high\\": 0 - }, - \\"param2\\": { - \\"low\\": 1, - \\"high\\": 0 - } - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/rfcs/query-limits.test.ts b/packages/graphql/tests/tck/rfcs/query-limits.test.ts deleted file mode 100644 index 72702f70a6..0000000000 --- a/packages/graphql/tests/tck/rfcs/query-limits.test.ts +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; - -describe("tck/rfcs/query-limits", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie @limit(default: 3, max: 5) { - id: ID! - actors: [Person!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type Person @limit(default: 2) { - id: ID! - } - - type Show @limit(max: 2) { - id: ID! - } - - type Festival { - name: String! - shows: [Show!]! @relationship(type: "PART_OF", direction: IN) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - describe("Top Level Query Limits", () => { - test("should limit the top level query with default value", async () => { - const query = /* GraphQL */ ` - query { - movies { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH * - LIMIT $param0 - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 3, - \\"high\\": 0 - } - }" - `); - }); - - test("should limit the top level query with max value if not default is available", async () => { - const query = /* GraphQL */ ` - query { - shows { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Show) - WITH * - LIMIT $param0 - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 2, - \\"high\\": 0 - } - }" - `); - }); - - test("should limit the top level query with max value the option given is higher", async () => { - const query = /* GraphQL */ ` - query { - shows(options: { limit: 5 }) { - id - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Show) - WITH * - LIMIT $param0 - RETURN this { .id } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 2, - \\"high\\": 0 - } - }" - `); - }); - }); - - describe("Field Level Query Limits", () => { - test("should limit the normal field level query", async () => { - const query = /* GraphQL */ ` - query { - movies { - id - actors { - id - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH * - LIMIT $param0 - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Person) - WITH this1 { .id } AS this1 - LIMIT $param1 - RETURN collect(this1) AS var2 - } - RETURN this { .id, actors: var2 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 3, - \\"high\\": 0 - }, - \\"param1\\": { - \\"low\\": 2, - \\"high\\": 0 - } - }" - `); - }); - - test("should limit the connection field level query", async () => { - const query = /* GraphQL */ ` - query { - movies { - id - actorsConnection { - edges { - node { - id - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH * - LIMIT $param0 - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Person) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - WITH * - LIMIT $param1 - RETURN collect({ node: { id: this1.id, __resolveType: \\"Person\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .id, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 3, - \\"high\\": 0 - }, - \\"param1\\": { - \\"low\\": 2, - \\"high\\": 0 - } - }" - `); - }); - - test("should extend the limit to the connection field if `first` provided", async () => { - const query = /* GraphQL */ ` - query { - movies { - id - actorsConnection(first: 4) { - edges { - node { - id - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH * - LIMIT $param0 - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Person) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - WITH * - LIMIT $param1 - RETURN collect({ node: { id: this1.id, __resolveType: \\"Person\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .id, actorsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 3, - \\"high\\": 0 - }, - \\"param1\\": { - \\"low\\": 4, - \\"high\\": 0 - } - }" - `); - }); - - test("should extend the limit to the connection field if `first` provided, honouring the `max` argument", async () => { - const query = /* GraphQL */ ` - query { - festivals { - name - showsConnection(first: 3) { - edges { - node { - id - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Festival) - CALL { - WITH this - MATCH (this)<-[this0:PART_OF]-(this1:Show) - WITH collect({ node: this1, relationship: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this1, edge.relationship AS this0 - WITH * - LIMIT $param0 - RETURN collect({ node: { id: this1.id, __resolveType: \\"Show\\" } }) AS var2 - } - RETURN { edges: var2, totalCount: totalCount } AS var3 - } - RETURN this { .name, showsConnection: var3 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 2, - \\"high\\": 0 - } - }" - `); - }); - - test("should limit the relationship field level query", async () => { - const query = /* GraphQL */ ` - query { - movies { - id - actors { - id - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH * - LIMIT $param0 - CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Person) - WITH this1 { .id } AS this1 - LIMIT $param1 - RETURN collect(this1) AS var2 - } - RETURN this { .id, actors: var2 } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 3, - \\"high\\": 0 - }, - \\"param1\\": { - \\"low\\": 2, - \\"high\\": 0 - } - }" - `); - }); - }); -}); diff --git a/packages/graphql/tests/tck/root-connection.test.ts b/packages/graphql/tests/tck/root-connection.test.ts deleted file mode 100644 index c4baa3cd68..0000000000 --- a/packages/graphql/tests/tck/root-connection.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../src"; -import { formatCypher, formatParams, translateQuery } from "./utils/tck-test-utils"; - -describe("Root Connection Query tests", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - id: ID - title: String - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) - } - - type Actor { - name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Simple selection, Movie by title", async () => { - const query = /* GraphQL */ ` - { - moviesConnection(where: { title: "River Runs Through It, A" }) { - totalCount - edges { - node { - title - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:Movie) - WHERE this0.title = $param0 - WITH collect({ node: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this0 - RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 - } - RETURN { edges: var1, totalCount: totalCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"River Runs Through It, A\\" - }" - `); - }); - - test("should apply limit and sort before return", async () => { - const query = /* GraphQL */ ` - { - moviesConnection(first: 20, sort: [{ title: ASC }]) { - edges { - node { - title - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:Movie) - WITH collect({ node: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this0 - WITH * - ORDER BY this0.title ASC - LIMIT $param0 - RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 - } - RETURN { edges: var1, totalCount: totalCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 20, - \\"high\\": 0 - } - }" - `); - }); - test("should apply limit, sort, and filter correctly when all three are used", async () => { - const query = /* GraphQL */ ` - { - moviesConnection(first: 20, where: { title_CONTAINS: "Matrix" }, sort: [{ title: ASC }]) { - edges { - node { - title - } - } - } - } - `; - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:Movie) - WHERE this0.title CONTAINS $param0 - WITH collect({ node: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this0 - WITH * - ORDER BY this0.title ASC - LIMIT $param1 - RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 - } - RETURN { edges: var1, totalCount: totalCount } AS this" - `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"Matrix\\", - \\"param1\\": { - \\"low\\": 20, - \\"high\\": 0 - } - }" - `); - }); - test("should correctly place any connection strings", async () => { - const query = /* GraphQL */ ` - { - moviesConnection(first: 20, sort: [{ title: ASC }]) { - edges { - node { - title - actorsConnection { - totalCount - edges { - node { - name - } - } - } - } - } - } - } - `; - const result = await translateQuery(neoSchema, query); - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:Movie) - WITH collect({ node: this0 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this0 - WITH * - ORDER BY this0.title ASC - LIMIT $param0 - CALL { - WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) - WITH collect({ node: this2, relationship: this1 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS this2, edge.relationship AS this1 - RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 - } - RETURN { edges: var3, totalCount: totalCount } AS var4 - } - RETURN collect({ node: { title: this0.title, actorsConnection: var4, __resolveType: \\"Movie\\" } }) AS var5 - } - RETURN { edges: var5, totalCount: totalCount } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 20, - \\"high\\": 0 - } - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/simple.test.ts b/packages/graphql/tests/tck/simple.test.ts deleted file mode 100644 index 22289f7a4b..0000000000 --- a/packages/graphql/tests/tck/simple.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQL } from "../../src"; -import { formatCypher, formatParams, translateQuery } from "./utils/tck-test-utils"; - -describe("Simple Cypher tests", () => { - let typeDefs: string; - let neoSchema: Neo4jGraphQL; - - beforeAll(() => { - typeDefs = /* GraphQL */ ` - type Movie { - id: ID - title: String - } - `; - - neoSchema = new Neo4jGraphQL({ - typeDefs, - }); - }); - - test("Single selection, Movie by title", async () => { - const query = /* GraphQL */ ` - { - movies(where: { title: "River Runs Through It, A" }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"River Runs Through It, A\\" - }" - `); - }); - - test("Multi selection, Movie by title", async () => { - const query = /* GraphQL */ ` - { - movies(where: { title: "River Runs Through It, A" }) { - id - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - RETURN this { .id, .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"River Runs Through It, A\\" - }" - `); - }); - - test("Multi selection, Movie by title via variable", async () => { - const query = /* GraphQL */ ` - query ($title: String) { - movies(where: { title: $title }) { - id - title - } - } - `; - - const result = await translateQuery(neoSchema, query, { - variableValues: { title: "some title" }, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - RETURN this { .id, .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\" - }" - `); - }); -}); diff --git a/packages/graphql/tests/tck/types/point.test.ts b/packages/graphql/tests/tck/types/point.test.ts index 924bde41bc..c8e7623b90 100644 --- a/packages/graphql/tests/tck/types/point.test.ts +++ b/packages/graphql/tests/tck/types/point.test.ts @@ -18,7 +18,7 @@ */ import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; +import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; describe("Cypher Points", () => { let typeDefs: string; @@ -37,329 +37,6 @@ describe("Cypher Points", () => { }); }); - test("Simple Point query", async () => { - const query = /* GraphQL */ ` - { - pointContainers(where: { point: { longitude: 1.0, latitude: 2.0 } }) { - point { - longitude - latitude - crs - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:PointContainer) - WHERE this.point = point($param0) - RETURN this { point: CASE - WHEN this.point IS NOT NULL THEN { point: this.point, crs: this.point.crs } - ELSE NULL - END } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"longitude\\": 1, - \\"latitude\\": 2 - } - }" - `); - }); - - test("Simple Point NOT query", async () => { - const query = /* GraphQL */ ` - { - pointContainers(where: { point_NOT: { longitude: 1.0, latitude: 2.0 } }) { - point { - longitude - latitude - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:PointContainer) - WHERE NOT (this.point = point($param0)) - RETURN this { point: CASE - WHEN this.point IS NOT NULL THEN { point: this.point } - ELSE NULL - END } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"longitude\\": 1, - \\"latitude\\": 2 - } - }" - `); - }); - - test("Simple Point IN query", async () => { - const query = /* GraphQL */ ` - { - pointContainers(where: { point_IN: [{ longitude: 1.0, latitude: 2.0 }] }) { - point { - longitude - latitude - crs - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:PointContainer) - WHERE this.point IN [var0 IN $param0 | point(var0)] - RETURN this { point: CASE - WHEN this.point IS NOT NULL THEN { point: this.point, crs: this.point.crs } - ELSE NULL - END } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": [ - { - \\"longitude\\": 1, - \\"latitude\\": 2 - } - ] - }" - `); - }); - - test("Simple Point NOT IN query", async () => { - const query = /* GraphQL */ ` - { - pointContainers(where: { point_NOT_IN: [{ longitude: 1.0, latitude: 2.0 }] }) { - point { - longitude - latitude - crs - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:PointContainer) - WHERE NOT (this.point IN [var0 IN $param0 | point(var0)]) - RETURN this { point: CASE - WHEN this.point IS NOT NULL THEN { point: this.point, crs: this.point.crs } - ELSE NULL - END } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": [ - { - \\"longitude\\": 1, - \\"latitude\\": 2 - } - ] - }" - `); - }); - - describe("tests using describe or point.describe", () => { - test("Simple Point LT query", async () => { - const query = /* GraphQL */ ` - { - pointContainers(where: { point_LT: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } }) { - point { - longitude - latitude - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:PointContainer) - WHERE point.distance(this.point, point($param0.point)) < $param0.distance - RETURN this { point: CASE - WHEN this.point IS NOT NULL THEN { point: this.point } - ELSE NULL - END } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"point\\": { - \\"longitude\\": 1.1, - \\"latitude\\": 2.2 - }, - \\"distance\\": 3.3 - } - }" - `); - }); - - test("Simple Point LTE query", async () => { - const query = /* GraphQL */ ` - { - pointContainers(where: { point_LTE: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } }) { - point { - longitude - latitude - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:PointContainer) - WHERE point.distance(this.point, point($param0.point)) <= $param0.distance - RETURN this { point: CASE - WHEN this.point IS NOT NULL THEN { point: this.point } - ELSE NULL - END } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"point\\": { - \\"longitude\\": 1.1, - \\"latitude\\": 2.2 - }, - \\"distance\\": 3.3 - } - }" - `); - }); - - test("Simple Point GT query", async () => { - const query = /* GraphQL */ ` - { - pointContainers(where: { point_GT: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } }) { - point { - longitude - latitude - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:PointContainer) - WHERE point.distance(this.point, point($param0.point)) > $param0.distance - RETURN this { point: CASE - WHEN this.point IS NOT NULL THEN { point: this.point } - ELSE NULL - END } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"point\\": { - \\"longitude\\": 1.1, - \\"latitude\\": 2.2 - }, - \\"distance\\": 3.3 - } - }" - `); - }); - - test("Simple Point GTE query", async () => { - const query = /* GraphQL */ ` - { - pointContainers(where: { point_GTE: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } }) { - point { - longitude - latitude - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:PointContainer) - WHERE point.distance(this.point, point($param0.point)) >= $param0.distance - RETURN this { point: CASE - WHEN this.point IS NOT NULL THEN { point: this.point } - ELSE NULL - END } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"point\\": { - \\"longitude\\": 1.1, - \\"latitude\\": 2.2 - }, - \\"distance\\": 3.3 - } - }" - `); - }); - - test("Simple Point DISTANCE query", async () => { - const query = /* GraphQL */ ` - { - pointContainers( - where: { point_DISTANCE: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } - ) { - point { - longitude - latitude - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:PointContainer) - WHERE point.distance(this.point, point($param0.point)) = $param0.distance - RETURN this { point: CASE - WHEN this.point IS NOT NULL THEN { point: this.point } - ELSE NULL - END } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"point\\": { - \\"longitude\\": 1.1, - \\"latitude\\": 2.2 - }, - \\"distance\\": 3.3 - } - }" - `); - }); - }); - test("Simple Point create mutation", async () => { const query = /* GraphQL */ ` mutation { diff --git a/packages/graphql/tests/tck/utils/tck-test-utils.ts b/packages/graphql/tests/tck/utils/tck-test-utils.ts index 5dc4db8dad..5ce66ea310 100644 --- a/packages/graphql/tests/tck/utils/tck-test-utils.ts +++ b/packages/graphql/tests/tck/utils/tck-test-utils.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { GraphQLArgs } from "graphql"; +import type { GraphQLArgs, GraphQLSchema } from "graphql"; import { graphql } from "graphql"; import { Neo4jError } from "neo4j-driver"; import type { Neo4jGraphQL } from "../../../src"; @@ -54,31 +54,32 @@ export function formatParams(params: Record): string { export async function translateQuery( neoSchema: Neo4jGraphQL, query: string, - options?: { + options: { token?: string; variableValues?: Record; neo4jVersion?: string; contextValues?: Record; subgraph?: boolean; - } + v6Api?: boolean; + } = {} ): Promise<{ cypher: string; params: Record }> { const driverBuilder = new DriverBuilder(); - const neo4jDatabaseInfo = new Neo4jDatabaseInfo(options?.neo4jVersion ?? "4.4"); + const neo4jDatabaseInfo = new Neo4jDatabaseInfo(options.neo4jVersion ?? "4.4"); let contextValue: Record = { executionContext: driverBuilder.instance(), neo4jDatabaseInfo, }; - if (options?.token) { + if (options.token) { contextValue.token = options.token; } - if (options?.contextValues) { + if (options.contextValues) { contextValue = { ...contextValue, ...options.contextValues }; } const graphqlArgs: GraphQLArgs = { - schema: await (options?.subgraph ? neoSchema.getSubgraphSchema() : neoSchema.getSchema()), + schema: await getSchema(neoSchema, options), source: query, contextValue, }; @@ -128,3 +129,19 @@ export async function translateQuery( params, }; } + +function getSchema( + neoSchema: Neo4jGraphQL, + options: { + subgraph?: boolean; + v6Api?: boolean; + } +): Promise { + if (options.subgraph) { + return neoSchema.getSubgraphSchema(); + } + if (options.v6Api) { + return neoSchema.getAuraSchema(); + } + return neoSchema.getSchema(); +} diff --git a/packages/graphql/tests/tck/where.test.ts b/packages/graphql/tests/tck/where.test.ts index d194c9190c..39a7aa90be 100644 --- a/packages/graphql/tests/tck/where.test.ts +++ b/packages/graphql/tests/tck/where.test.ts @@ -18,7 +18,7 @@ */ import { Neo4jGraphQL } from "../../src"; -import { formatCypher, translateQuery, formatParams } from "./utils/tck-test-utils"; +import { formatCypher, formatParams, translateQuery } from "./utils/tck-test-utils"; describe("Cypher WHERE", () => { let typeDefs: string; @@ -44,253 +44,6 @@ describe("Cypher WHERE", () => { }); }); - test("Simple", async () => { - const query = /* GraphQL */ ` - query ($title: String, $isFavorite: Boolean) { - movies(where: { title: $title, isFavorite: $isFavorite }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query, { - variableValues: { title: "some title", isFavorite: true }, - }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE (this.title = $param0 AND this.isFavorite = $param1) - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\", - \\"param1\\": true - }" - `); - }); - - test("Simple AND", async () => { - const query = /* GraphQL */ ` - { - movies(where: { AND: [{ title: "some title" }] }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\" - }" - `); - }); - - test("Simple AND with multiple parameters", async () => { - const query = /* GraphQL */ ` - { - movies(where: { AND: [{ title: "some title" }, { isFavorite: true }] }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE (this.title = $param0 AND this.isFavorite = $param1) - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\", - \\"param1\\": true - }" - `); - }); - - test("Nested AND", async () => { - const query = /* GraphQL */ ` - { - movies(where: { AND: [{ AND: [{ title: "some title" }] }] }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\" - }" - `); - }); - - test("Nested AND with multiple properties", async () => { - const query = /* GraphQL */ ` - { - movies(where: { AND: [{ AND: [{ title: "some title" }, { title: "another title" }] }] }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE (this.title = $param0 AND this.title = $param1) - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\", - \\"param1\\": \\"another title\\" - }" - `); - }); - - test("Nested AND and OR", async () => { - const query = /* GraphQL */ ` - { - movies(where: { AND: [{ OR: [{ title: "some title" }, { isFavorite: true }], id: 2 }] }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE (this.id = $param0 AND (this.title = $param1 OR this.isFavorite = $param2)) - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"2\\", - \\"param1\\": \\"some title\\", - \\"param2\\": true - }" - `); - }); - - test("Super Nested AND", async () => { - const query = /* GraphQL */ ` - { - movies(where: { AND: [{ AND: [{ AND: [{ title: "some title" }] }] }] }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\" - }" - `); - }); - - test("Simple OR", async () => { - const query = /* GraphQL */ ` - { - movies(where: { OR: [{ title: "some title" }] }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\" - }" - `); - }); - - test("Nested OR", async () => { - const query = /* GraphQL */ ` - { - movies(where: { OR: [{ OR: [{ title: "some title" }] }] }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\" - }" - `); - }); - - test("Super Nested OR", async () => { - const query = /* GraphQL */ ` - { - movies(where: { OR: [{ OR: [{ OR: [{ title: "some title" }] }] }] }) { - title - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - RETURN this { .title } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"some title\\" - }" - `); - }); - describe("Where with null", () => { test("Match with NULL in where", async () => { const query = /* GraphQL */ ` diff --git a/packages/graphql/tests/utils/raise-on-invalid-schema.ts b/packages/graphql/tests/utils/raise-on-invalid-schema.ts new file mode 100644 index 0000000000..b9504893fb --- /dev/null +++ b/packages/graphql/tests/utils/raise-on-invalid-schema.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { GraphQLSchema } from "graphql"; +import { validateSchema } from "graphql"; + +export function raiseOnInvalidSchema(schema: GraphQLSchema): void { + const errors = validateSchema(schema); + if (errors.length) { + throw new Error(`Invalid Schema: ${errors.map((err) => err.message).join("\n")}`); + } +} diff --git a/packages/graphql/tests/utils/tests-helper.ts b/packages/graphql/tests/utils/tests-helper.ts index 5e6208c106..faf60cccc8 100644 --- a/packages/graphql/tests/utils/tests-helper.ts +++ b/packages/graphql/tests/utils/tests-helper.ts @@ -36,14 +36,16 @@ export class TestHelper { private neo4jGraphQL: Neo4jGraphQL | undefined; private uniqueTypes: UniqueType[] = []; private driver: neo4j.Driver | undefined; + private isAuraApi: boolean; private lock: boolean = false; // Lock to avoid race condition between initNeo4jGraphQL private customDB: string | undefined; private cdc: boolean; - constructor({ cdc = false }: { cdc: boolean } = { cdc: false }) { + constructor({ cdc = false, v6Api = false }: { cdc?: boolean; v6Api?: boolean } = { cdc: false, v6Api: false }) { this.cdc = cdc; + this.isAuraApi = v6Api; } public get database(): string { @@ -89,7 +91,7 @@ export class TestHelper { if (args.contextValue instanceof Promise) { throw new Error("contextValue is a promise. Did you forget to use await with 'getContextValue'?"); } - const schema = await this.neo4jGraphQL.getSchema(); + const schema = this.isAuraApi ? await this.neo4jGraphQL.getAuraSchema() : await this.neo4jGraphQL.getSchema(); return graphqlRuntime({ schema,