From 34364b1ade5ac397ad017e10ebee605a7c6527f5 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 7 May 2024 11:26:32 +0100 Subject: [PATCH 001/177] Inital development of v6 API (#5073) * WIP schema builder * add QueryType and use AuraEntityOperations * add schema test for aura-api, add PageInfo and ConnectionType * Query operation * WIP read operation resolver * WIP Connection read operation * Small cleanup of new readResolver * EntitySchemaTypes * Simple tck test on aura API * ResolveTree parser * Move resolve tree parser to a separate class * Change types for ResolveTree * WIP Relationship schema * Factor and translation of relationships * Relationship integration test * Relationship in new API * Fix aliases in query * WIP add some interfaces to schema generation * adds interfaces to entitySchemaTypes * Improve schema types * Improve typenames * Remove comments in resolveTreeParser * Adds GraphQL type * WIP resolveTreParser * improve ResolveTreeParser * Read Operation Factory * add tests filtering for aura * implement node behaviour change * WIP refactor ResolveTreeParser * move ResoveTreeParser to use functions * Minor improvements on resolveTreeParser * Refactor of ResolveTreeParser * Remove comments in read operation factory * Rename aura API to v6 --------- Co-authored-by: MacondoExpress --- packages/graphql/package.json | 1 + .../api-v6/QueryIR/ConnectionReadOperation.ts | 405 ++++++++++++++++++ .../graphQLTypeNames/EntityTypeNames.ts | 58 +++ .../graphQLTypeNames/NestedEntityTypeNames.ts | 35 ++ .../TopLevelEntityTypeNames.ts | 37 ++ .../queryASTFactory/ReadOperationFactory.ts | 171 ++++++++ .../resolve-tree-parser/ResolveTreeParser.ts | 197 +++++++++ .../resolve-tree-parser/find-field-by-name.ts | 28 ++ .../resolve-tree-parser/graphql-tree.ts | 37 ++ .../mappers/connection-operation-mapper.ts | 7 + .../src/api-v6/resolvers/readResolver.ts | 33 ++ .../src/api-v6/schema/AuraSchemaGenerator.ts | 75 ++++ .../src/api-v6/schema/SchemaBuilder.ts | 70 +++ .../api-v6/schema/schema-types/EntityTypes.ts | 78 ++++ .../schema/schema-types/NestedEntityTypes.ts | 72 ++++ .../api-v6/schema/schema-types/StaticTypes.ts | 35 ++ .../schema-types/TopLevelEntityTypes.ts | 86 ++++ .../translators/translate-read-operation.ts | 41 ++ packages/graphql/src/classes/Neo4jGraphQL.ts | 16 +- .../src/schema-model/entity/ConcreteEntity.ts | 14 + .../src/schema-model/generate-model.ts | 68 +-- .../schema-model/relationship/Relationship.ts | 14 + .../src/translate/queryAST/ast/QueryAST.ts | 4 +- .../ast/operations/ConnectionReadOperation.ts | 9 +- .../integration/relationship.int.test.ts | 223 ++++++++++ .../api-v6/integration/simple.int.test.ts | 78 ++++ .../tests/api-v6/schema/relationship.test.ts | 228 ++++++++++ .../tests/api-v6/schema/simple.test.ts | 178 ++++++++ .../tests/api-v6/tck/relationship.test.ts | 99 +++++ .../graphql/tests/api-v6/tck/simple.test.ts | 72 ++++ .../graphql/tests/tck/utils/tck-test-utils.ts | 31 +- packages/graphql/tests/utils/tests-helper.ts | 6 +- 32 files changed, 2464 insertions(+), 42 deletions(-) create mode 100644 packages/graphql/src/api-v6/QueryIR/ConnectionReadOperation.ts create mode 100644 packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts create mode 100644 packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.ts create mode 100644 packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts create mode 100644 packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts create mode 100644 packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts create mode 100644 packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/find-field-by-name.ts create mode 100644 packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts create mode 100644 packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts create mode 100644 packages/graphql/src/api-v6/resolvers/readResolver.ts create mode 100644 packages/graphql/src/api-v6/schema/AuraSchemaGenerator.ts create mode 100644 packages/graphql/src/api-v6/schema/SchemaBuilder.ts create mode 100644 packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts create mode 100644 packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts create mode 100644 packages/graphql/src/api-v6/schema/schema-types/StaticTypes.ts create mode 100644 packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts create mode 100644 packages/graphql/src/api-v6/translators/translate-read-operation.ts create mode 100644 packages/graphql/tests/api-v6/integration/relationship.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/simple.int.test.ts create mode 100644 packages/graphql/tests/api-v6/schema/relationship.test.ts create mode 100644 packages/graphql/tests/api-v6/schema/simple.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/relationship.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/simple.test.ts diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 3a337ff186..c099ea2277 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -37,6 +37,7 @@ "test:e2e:watch": "jest e2e --watch", "test:schema": "jest tests/schema -c jest.minimal.config.js", "test:schema:watch": "jest tests/schema --watch -c jest.minimal.config.js", + "test:v6": "jest tests/api-v6", "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", "posttest:package-tests": "yarn run cleanup:package-tests", "test:package-tests": "yarn run setup:package-tests && cd ../package-tests/ && yarn run test:all", 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..3fdfd15bad --- /dev/null +++ b/packages/graphql/src/api-v6/QueryIR/ConnectionReadOperation.ts @@ -0,0 +1,405 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, + }: { + 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[]) { + 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 undefined; + } + 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/graphQLTypeNames/EntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts new file mode 100644 index 0000000000..b2c385a0ab --- /dev/null +++ b/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { plural } from "../../schema-model/utils/string-manipulation"; + +export abstract class EntityTypeNames { + private readonly prefix: string; + + constructor(prefix: string) { + this.prefix = prefix; + } + + public get connectionOperation(): string { + return `${this.prefix}Operation`; + } + + public get connectionType(): string { + return `${this.prefix}Connection`; + } + + public get edgeType(): string { + return `${this.prefix}Edge`; + } + + public get nodeType(): string { + return `${this.prefix}`; + } + + public get whereInputTypeName(): string { + return `${this.prefix}Where`; + } + + public get queryField(): string { + return this.plural; + } + + public get plural(): string { + return plural(this.prefix); + } + + public abstract get propertiesType(): string | undefined; +} diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.ts new file mode 100644 index 0000000000..282f0d35e4 --- /dev/null +++ b/packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.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 { Relationship } from "../../schema-model/relationship/Relationship"; +import { upperFirst } from "../../utils/upper-first"; +import { EntityTypeNames } from "./EntityTypeNames"; + +export class NestedEntityTypeNames extends EntityTypeNames { + private relationship: Relationship; + + constructor(relationship: Relationship) { + super(`${relationship.source.name}${upperFirst(relationship.name)}`); + this.relationship = relationship; + } + + public get propertiesType(): string | undefined { + return this.relationship.propertiesTypeName; + } +} diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts new file mode 100644 index 0000000000..25a68734ce --- /dev/null +++ b/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../schema-model/relationship/Relationship"; +import { EntityTypeNames } from "./EntityTypeNames"; +import { NestedEntityTypeNames } from "./NestedEntityTypeNames"; + +export class TopLevelEntityTypeNames extends EntityTypeNames { + constructor(concreteEntity: ConcreteEntity) { + super(concreteEntity.name); + } + + public relationship(relationship: Relationship): NestedEntityTypeNames { + return new NestedEntityTypeNames(relationship); + } + + public get propertiesType(): undefined { + return undefined; + } +} diff --git a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts new file mode 100644 index 0000000000..c2f88302d2 --- /dev/null +++ b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts @@ -0,0 +1,171 @@ +import type { ResolveTree } from "graphql-parse-resolve-info"; +import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import { 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 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 { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection"; +import { RelationshipSelection } from "../../translate/queryAST/ast/selection/RelationshipSelection"; +import { filterTruthy } from "../../utils/utils"; +import { V6ReadOperation } from "../QueryIR/ConnectionReadOperation"; +import { parseResolveInfoTree } from "./resolve-tree-parser/ResolveTreeParser"; +import type { + GraphQLTree, + GraphQLTreeEdgeProperties, + GraphQLTreeNode, + GraphQLTreeReadOperation, +} from "./resolve-tree-parser/graphql-tree"; + +export class ReadOperationFactory { + public schemaModel: Neo4jGraphQLSchemaModel; + + constructor(schemaModel: Neo4jGraphQLSchemaModel) { + this.schemaModel = schemaModel; + } + + public createAST({ resolveTree, entity }: { resolveTree: ResolveTree; entity: ConcreteEntity }): QueryAST { + const parsedTree = parseResolveInfoTree({ resolveTree, entity }); + + const operation = this.generateOperation({ + parsedTree: parsedTree, + entity, + }); + return new QueryAST(operation); + } + + private generateOperation({ + parsedTree, + entity, + }: { + parsedTree: GraphQLTree; + entity: ConcreteEntity; + }): V6ReadOperation { + const connectionTree = parsedTree.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 nodeFields = this.getNodeFields(entity, nodeResolveTree); + return new V6ReadOperation({ + target, + selection, + fields: { + edge: [], + node: nodeFields, + }, + }); + } + + 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 QueryParseError("Interfaces not supported"); + } + + // Selection + const selection = new RelationshipSelection({ + relationship: relationshipAdapter, + alias: parsedTree.alias, + }); + + // Fields + const nodeResolveTree = connectionTree.fields.edges?.fields.node; + const propertiesResolveTree = connectionTree.fields.edges?.fields.properties; + const nodeFields = this.getNodeFields(relationshipAdapter.target.entity, nodeResolveTree); + const edgeFields = this.getAttributeFields(relationship, propertiesResolveTree); + + return new V6ReadOperation({ + target: relationshipAdapter.target, + selection, + fields: { + edge: edgeFields, + node: nodeFields, + }, + }); + } + + 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.entries(propertiesTree.fields).map(([name, rawField]) => { + const attribute = target.findAttribute(name); + if (attribute) { + return new AttributeField({ + alias: rawField.alias, + attribute: new AttributeAdapter(attribute), + }); + } + return undefined; + }) + ); + } + + private getRelationshipFields(entity: ConcreteEntity, nodeResolveTree: GraphQLTreeNode | undefined): Field[] { + if (!nodeResolveTree) { + return []; + } + + return filterTruthy( + Object.entries(nodeResolveTree.fields).map(([name, rawField]) => { + const relationship = entity.findRelationship(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 generateRelationshipField( + resolveTree: GraphQLTreeReadOperation, + relationship: Relationship + ): OperationField { + const relationshipOperation = this.generateRelationshipOperation({ + relationship: relationship, + parsedTree: resolveTree, + }); + + return new OperationField({ + alias: resolveTree.alias, + operation: relationshipOperation, + }); + } +} + +export class QueryParseError extends Error {} diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts new file mode 100644 index 0000000000..f1fdfcae96 --- /dev/null +++ b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.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 { ResolveTree } from "graphql-parse-resolve-info"; +import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../schema-model/relationship/Relationship"; +import { findFieldByName } from "./find-field-by-name"; +import type { + GraphQLTree, + GraphQLTreeConnection, + GraphQLTreeEdge, + GraphQLTreeEdgeProperties, + GraphQLTreeLeafField, + GraphQLTreeNode, + GraphQLTreeReadOperation, +} from "./graphql-tree"; + +export function parseResolveInfoTree({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTree { + const parser = new TopLevelTreeParser({ entity }); + return parser.parseOperation(resolveTree); +} + +abstract class ResolveTreeParser { + protected entity: T; + + constructor({ entity }: { entity: T }) { + this.entity = entity; + } + + /** Parse a resolveTree into a Neo4j GraphQLTree */ + public parseOperation(resolveTree: ResolveTree): GraphQLTreeReadOperation { + const connectionResolveTree = findFieldByName(resolveTree, this.entity.types.connectionOperation, "connection"); + + const connection = connectionResolveTree ? this.parseConnection(connectionResolveTree) : undefined; + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: { + connection, + }, + }; + } + + protected abstract get targetNode(): ConcreteEntity; + + private parseConnection(resolveTree: ResolveTree): GraphQLTreeConnection { + const entityTypes = this.entity.types; + const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connectionType, "edges"); + const edgeResolveTree = edgesResolveTree ? this.parseEdges(edgesResolveTree) : undefined; + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: { + edges: edgeResolveTree, + }, + }; + } + + private parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { + const edgeType = this.entity.types.edgeType; + + const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); + const resolveTreeProperties = findFieldByName(resolveTree, edgeType, "properties"); + + const node = nodeResolveTree ? this.parseNode(nodeResolveTree) : undefined; + const properties = resolveTreeProperties ? this.parseEdgeProperties(resolveTreeProperties) : undefined; + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: { + node: node, + properties: properties, + }, + }; + } + + private parseNode(resolveTree: ResolveTree): GraphQLTreeNode { + const entityTypes = this.targetNode.types; + const fieldsResolveTree = resolveTree.fieldsByTypeName[entityTypes.nodeType] ?? {}; + + const fields = this.getNodeFields(fieldsResolveTree); + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: fields, + }; + } + + private getNodeFields( + fields: Record + ): Record { + const propertyFields: Record = {}; + for (const fieldResolveTree of Object.values(fields)) { + const fieldName = fieldResolveTree.name; + const field = + this.parseRelationshipField(fieldResolveTree, this.targetNode) ?? + this.parseAttributeField(fieldResolveTree, this.targetNode); + if (!field) { + throw new ResolveTreeParserError(`${fieldName} is not a field of node`); + } + propertyFields[fieldName] = field; + } + return propertyFields; + } + + private parseEdgeProperties(resolveTree: ResolveTree): GraphQLTreeEdgeProperties | undefined { + if (!this.entity.types.propertiesType) { + return undefined; + } + const fieldsResolveTree = resolveTree.fieldsByTypeName[this.entity.types.propertiesType] ?? {}; + + const fields = this.getEdgePropertyFields(fieldsResolveTree); + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: fields, + }; + } + + private getEdgePropertyFields(fields: Record): Record { + const propertyFields: Record = {}; + for (const fieldResolveTree of Object.values(fields)) { + const fieldName = fieldResolveTree.name; + const field = this.parseAttributeField(fieldResolveTree, this.entity); + if (!field) { + throw new ResolveTreeParserError(`${fieldName} is not an attribute of edge`); + } + propertyFields[fieldName] = field; + } + return propertyFields; + } + + private parseAttributeField( + resolveTree: ResolveTree, + entity: ConcreteEntity | Relationship + ): GraphQLTreeLeafField | undefined { + if (entity.hasAttribute(resolveTree.name)) { + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: undefined, + }; + } + } + + private parseRelationshipField( + resolveTree: ResolveTree, + entity: ConcreteEntity + ): GraphQLTreeReadOperation | undefined { + const relationship = entity.findRelationship(resolveTree.name); + if (!relationship) { + return; + } + const relationshipTreeParser = new RelationshipResolveTreeParser({ entity: relationship }); + return relationshipTreeParser.parseOperation(resolveTree); + } +} + +class TopLevelTreeParser extends ResolveTreeParser { + protected get targetNode(): ConcreteEntity { + return this.entity; + } +} + +class RelationshipResolveTreeParser extends ResolveTreeParser { + protected get targetNode(): ConcreteEntity { + return this.entity.target as ConcreteEntity; + } +} + +class ResolveTreeParserError extends Error {} diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/find-field-by-name.ts b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/find-field-by-name.ts new file mode 100644 index 0000000000..4cda5630e6 --- /dev/null +++ b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/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/queryASTFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts new file mode 100644 index 0000000000..be23821538 --- /dev/null +++ b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts @@ -0,0 +1,37 @@ +export type GraphQLTree = GraphQLTreeReadOperation; + +interface GraphQLTreeElement { + alias: string; + args: Record; +} + +export interface GraphQLTreeReadOperation extends GraphQLTreeElement { + fields: { + connection?: GraphQLTreeConnection; + }; +} + +export interface GraphQLTreeConnection extends GraphQLTreeElement { + fields: { + edges?: GraphQLTreeEdge; + }; +} + +export interface GraphQLTreeEdge extends GraphQLTreeElement { + fields: { + node?: GraphQLTreeNode; + properties?: GraphQLTreeEdgeProperties; + }; +} + +export interface GraphQLTreeNode extends GraphQLTreeElement { + fields: Record; +} + +export interface GraphQLTreeEdgeProperties extends GraphQLTreeElement { + fields: Record; +} + +export interface GraphQLTreeLeafField extends GraphQLTreeElement { + fields: undefined; +} diff --git a/packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts b/packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts new file mode 100644 index 0000000000..00b4930e7c --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts @@ -0,0 +1,7 @@ +import type { ExecuteResult } from "../../../utils/execute"; + +export function mapConnectionRecord(executionResult: ExecuteResult): any { + // Note: Connections only return a single record + const connections = executionResult.records.map((x) => x.this); + return connections[0]; +} diff --git a/packages/graphql/src/api-v6/resolvers/readResolver.ts b/packages/graphql/src/api-v6/resolvers/readResolver.ts new file mode 100644 index 0000000000..4635ea0321 --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/readResolver.ts @@ -0,0 +1,33 @@ +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 { translateReadOperation } from "../translators/translate-read-operation"; +import { mapConnectionRecord } from "./mappers/connection-operation-mapper"; + +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 { cypher, params } = translateReadOperation({ + context: context, + entity, + }); + const executeResult = await execute({ + cypher, + params, + defaultAccessMode: "READ", + context, + info, + }); + + return mapConnectionRecord(executeResult); + }; +} diff --git a/packages/graphql/src/api-v6/schema/AuraSchemaGenerator.ts b/packages/graphql/src/api-v6/schema/AuraSchemaGenerator.ts new file mode 100644 index 0000000000..619def22c2 --- /dev/null +++ b/packages/graphql/src/api-v6/schema/AuraSchemaGenerator.ts @@ -0,0 +1,75 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { generateReadResolver } from "../resolvers/readResolver"; +import { SchemaBuilder } from "./SchemaBuilder"; +import { StaticTypes } from "./schema-types/StaticTypes"; +import { TopLevelEntityTypes } from "./schema-types/TopLevelEntityTypes"; + +export class AuraSchemaGenerator { + private schemaBuilder: SchemaBuilder; + + constructor() { + this.schemaBuilder = new SchemaBuilder(); + } + + public generate(schemaModel: Neo4jGraphQLSchemaModel): GraphQLSchema { + const staticTypes = new StaticTypes({ schemaBuilder: this.schemaBuilder }); + const entityTypes = this.generateEntityTypes(schemaModel, staticTypes); + this.createQueryFields(entityTypes); + + return this.schemaBuilder.build(); + } + + private createQueryFields(entityTypes: Map): void { + entityTypes.forEach((entitySchemaTypes, entity) => { + const resolver = generateReadResolver({ + entity, + }); + this.schemaBuilder.addQueryField( + entitySchemaTypes.queryFieldName, + entitySchemaTypes.connectionOperation, + resolver + ); + }); + } + + private generateEntityTypes( + schemaModel: Neo4jGraphQLSchemaModel, + staticTypes: StaticTypes + ): Map { + const resultMap = new Map(); + for (const entity of schemaModel.entities.values()) { + if (entity.isConcreteEntity()) { + const entitySchemaTypes = new TopLevelEntityTypes({ + entity, + schemaBuilder: this.schemaBuilder, + staticTypes, + }); + + resultMap.set(entity, entitySchemaTypes); + } + } + + return resultMap; + } +} diff --git a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema/SchemaBuilder.ts new file mode 100644 index 0000000000..d3433a4ab8 --- /dev/null +++ b/packages/graphql/src/api-v6/schema/SchemaBuilder.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 GraphQLSchema } from "graphql"; +import type { ObjectTypeComposer } from "graphql-compose"; +import { SchemaComposer } from "graphql-compose"; + +export class SchemaBuilder { + private composer: SchemaComposer; + + constructor() { + this.composer = new SchemaComposer(); + } + + public createObjectType(name: string, fields?: Record, description?: string): ObjectTypeComposer { + return this.composer.createObjectTC({ + name, + description, + fields, + }); + } + + public getOrCreateObjectType(name: string, fields?: Record, description?: string): ObjectTypeComposer { + return this.composer.getOrCreateOTC(name, (tc) => { + if (fields) { + tc.addFields(fields); + } + if (description) { + tc.setDescription(description); + } + }); + } + + public addFieldToType(type: ObjectTypeComposer, fields: Record): void { + type.addFields(fields); + } + + public addQueryField(name: string, type: ObjectTypeComposer | string, resolver: (...args: any[]) => any): void { + this.composer.Query.addFields({ + [name]: { + type: type, + resolve: resolver, + }, + }); + } + + public getObjectType(typeName: string): ObjectTypeComposer { + return this.composer.getOTC(typeName); + } + + public build(): GraphQLSchema { + return this.composer.buildSchema(); + } +} diff --git a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts new file mode 100644 index 0000000000..ea68f4b95b --- /dev/null +++ b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.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 { ObjectTypeComposer } from "graphql-compose"; +import { Memoize } from "typescript-memoize"; +import type { EntityTypeNames } from "../../graphQLTypeNames/EntityTypeNames"; +import type { SchemaBuilder } from "../SchemaBuilder"; +import type { StaticTypes } from "./StaticTypes"; + +/** This class defines the GraphQL types for an entity */ +export abstract class EntityTypes { + protected schemaBuilder: SchemaBuilder; + protected entityTypes: T; + protected staticTypes: StaticTypes; + + constructor({ + schemaBuilder, + entityTypes, + staticTypes, + }: { + schemaBuilder: SchemaBuilder; + staticTypes: StaticTypes; + entityTypes: T; + }) { + this.schemaBuilder = schemaBuilder; + this.entityTypes = entityTypes; + this.staticTypes = staticTypes; + } + + @Memoize() + public get connectionOperation(): ObjectTypeComposer { + return this.schemaBuilder.createObjectType(this.entityTypes.connectionOperation, { + connection: this.connection, + }); + } + + @Memoize() + public get connection(): ObjectTypeComposer { + return this.schemaBuilder.createObjectType(this.entityTypes.connectionType, { + pageInfo: this.staticTypes.pageInfo, + edges: [this.edge], + }); + } + + @Memoize() + public get edge(): ObjectTypeComposer { + const fields = { + node: this.nodeType, + cursor: "String", + }; + + const properties = this.getEdgeProperties(); + if (properties) { + fields["properties"] = properties; + } + + return this.schemaBuilder.createObjectType(this.entityTypes.edgeType, fields); + } + + protected abstract getEdgeProperties(): ObjectTypeComposer | undefined; + public abstract get nodeType(): string; +} diff --git a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts new file mode 100644 index 0000000000..753c5fbc2a --- /dev/null +++ b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.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 { ObjectTypeComposer } from "graphql-compose"; +import { Memoize } from "typescript-memoize"; +import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../schema-model/relationship/Relationship"; +import type { NestedEntityTypeNames } from "../../graphQLTypeNames/NestedEntityTypeNames"; +import type { SchemaBuilder } from "../SchemaBuilder"; +import { EntityTypes } from "./EntityTypes"; +import type { StaticTypes } from "./StaticTypes"; + +export class NestedEntitySchemaTypes extends EntityTypes { + private relationship: Relationship; + + constructor({ + relationship, + schemaBuilder, + entityTypes, + staticTypes, + }: { + schemaBuilder: SchemaBuilder; + relationship: Relationship; + staticTypes: StaticTypes; + entityTypes: NestedEntityTypeNames; + }) { + super({ + schemaBuilder, + entityTypes, + staticTypes, + }); + this.relationship = relationship; + } + + protected getEdgeProperties(): ObjectTypeComposer | undefined { + if (this.entityTypes.propertiesType) { + const fields = this.getRelationshipFields(this.relationship); + return this.schemaBuilder.getOrCreateObjectType(this.entityTypes.propertiesType, fields); + } + } + + @Memoize() + public get nodeType(): string { + const target = this.relationship.target; + if (!(target instanceof ConcreteEntity)) { + throw new Error("Interfaces not supported yet"); + } + return target.types.nodeType; + } + + private getRelationshipFields(relationship: Relationship): Record { + return Object.fromEntries( + [...relationship.attributes.values()].map((attribute) => [attribute.name, attribute.type.name]) + ); + } +} diff --git a/packages/graphql/src/api-v6/schema/schema-types/StaticTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/StaticTypes.ts new file mode 100644 index 0000000000..5024c05fc4 --- /dev/null +++ b/packages/graphql/src/api-v6/schema/schema-types/StaticTypes.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 { ObjectTypeComposer } from "graphql-compose"; +import { Memoize } from "typescript-memoize"; +import type { SchemaBuilder } from "../SchemaBuilder"; + +export class StaticTypes { + private schemaBuilder: SchemaBuilder; + + constructor({ schemaBuilder }: { schemaBuilder: SchemaBuilder }) { + this.schemaBuilder = schemaBuilder; + } + + @Memoize() + public get pageInfo(): ObjectTypeComposer { + return this.schemaBuilder.createObjectType("PageInfo", { hasNextPage: "Boolean", hasPreviousPage: "Boolean" }); + } +} diff --git a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts new file mode 100644 index 0000000000..cd6193a121 --- /dev/null +++ b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ObjectTypeComposer } from "graphql-compose"; +import { Memoize } from "typescript-memoize"; +import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { EntityTypeNames } from "../../graphQLTypeNames/EntityTypeNames"; +import type { SchemaBuilder } from "../SchemaBuilder"; +import { EntityTypes } from "./EntityTypes"; +import { NestedEntitySchemaTypes } from "./NestedEntityTypes"; +import type { StaticTypes } from "./StaticTypes"; + +export class TopLevelEntityTypes extends EntityTypes { + private entity: ConcreteEntity; + + constructor({ + entity, + schemaBuilder, + staticTypes, + }: { + schemaBuilder: SchemaBuilder; + entity: ConcreteEntity; + staticTypes: StaticTypes; + }) { + super({ + schemaBuilder, + entityTypes: entity.types, + staticTypes, + }); + this.entity = entity; + } + + public get queryFieldName(): string { + return this.entity.types.queryField; + } + + @Memoize() + public get nodeType(): string { + const fields = this.getNodeFields(this.entity); + const relationships = this.getRelationshipFields(this.entity); + this.schemaBuilder.createObjectType(this.entity.types.nodeType, { ...fields, ...relationships }); + return this.entity.types.nodeType; + } + + protected getEdgeProperties(): ObjectTypeComposer | undefined { + return undefined; + } + + private getNodeFields(concreteEntity: ConcreteEntity): Record { + return Object.fromEntries( + [...concreteEntity.attributes.values()].map((attribute) => [attribute.name, attribute.type.name]) + ); + } + + private getRelationshipFields(concreteEntity: ConcreteEntity): Record { + return Object.fromEntries( + [...concreteEntity.relationships.values()].map((relationship) => { + const relationshipTypes = new NestedEntitySchemaTypes({ + schemaBuilder: this.schemaBuilder, + relationship, + entityTypes: this.entity.types.relationship(relationship), + staticTypes: this.staticTypes, + }); + const relationshipType = relationshipTypes.connectionOperation; + + return [relationship.name, relationshipType]; + }) + ); + } +} 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..c31d9c77ab --- /dev/null +++ b/packages/graphql/src/api-v6/translators/translate-read-operation.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 "../queryASTFactory/ReadOperationFactory"; + +const debug = Debug(DEBUG_TRANSLATE); + +export function translateReadOperation({ + context, + entity, +}: { + context: Neo4jGraphQLTranslationContext; + entity: ConcreteEntity; +}): Cypher.CypherResult { + const readFactory = new ReadOperationFactory(context.schemaModel); + const readOperation = readFactory.createAST({ resolveTree: context.resolveTree, entity }); + debug(readOperation.print()); + const results = readOperation.build(context); + return results.build(); +} diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index c08c0f0f93..2e1b43155b 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -25,6 +25,7 @@ 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 { AuraSchemaGenerator } from "../api-v6/schema/AuraSchemaGenerator"; import { DEBUG_ALL } from "../constants"; import { makeAugmentedSchema } from "../schema"; import type { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; @@ -115,6 +116,17 @@ class Neo4jGraphQL { return this.getExecutableSchema(); } + public getAuraSchema(): Promise { + const document = this.normalizeTypeDefinitions(this.typeDefs); + this.schemaModel = this.generateSchemaModel(document, true); + const auraSchemaGenerator = new AuraSchemaGenerator(); + + this._nodes = []; + this._relationships = []; + + return Promise.resolve(this.composeSchema(auraSchemaGenerator.generate(this.schemaModel))); + } + public async getExecutableSchema(): Promise { if (!this.executableSchema) { this.executableSchema = this.generateExecutableSchema(); @@ -346,9 +358,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/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index 18d67d5ab8..8b0578c441 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { TopLevelEntityTypeNames } from "../../api-v6/graphQLTypeNames/TopLevelEntityTypeNames"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import { setsAreEqual } from "../../utils/sets-are-equal"; import type { Annotations } from "../annotation/Annotation"; @@ -68,6 +69,11 @@ export class ConcreteEntity implements Entity { } } + /** Note: Types of the new API */ + public get types(): TopLevelEntityTypeNames { + return new TopLevelEntityTypeNames(this); + } + public isConcreteEntity(): this is ConcreteEntity { return true; } @@ -107,4 +113,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/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index e07f518040..c9bf1c80ef 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -17,12 +17,14 @@ * limitations under the License. */ +import type { NestedEntityTypeNames } from "../../api-v6/graphQLTypeNames/NestedEntityTypeNames"; 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 +141,14 @@ export class Relationship { }); } + /** Note: Types of the new API */ + public get types(): NestedEntityTypeNames { + if (!(this.source instanceof ConcreteEntity)) { + throw new Error("Interfaces not supported"); + } + return this.source.types.relationship(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 +160,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/translate/queryAST/ast/QueryAST.ts b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts index 85b9be11b8..a3d7f85fa3 100644 --- a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts +++ b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts @@ -18,15 +18,16 @@ */ import Cypher from "@neo4j/cypher-builder"; +import { V6ReadOperation } from "../../../api-v6/QueryIR/ConnectionReadOperation"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { createNodeFromEntity } 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 type { Operation, OperationTranspileResult } from "./operations/operations"; -import { DeleteOperation } from "./operations/DeleteOperation"; export class QueryAST { private operation: Operation; @@ -85,6 +86,7 @@ export class QueryAST { if ( this.operation instanceof ReadOperation || this.operation instanceof ConnectionReadOperation || + this.operation instanceof V6ReadOperation || this.operation instanceof DeleteOperation ) { return createNodeFromEntity(this.operation.target, neo4jGraphQLContext, varName); diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts index f1980b81d9..dc3a4bcd80 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/tests/api-v6/integration/relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/relationship.int.test.ts new file mode 100644 index 0000000000..63bddf500a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/relationship.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("Aura-api simple", () => { + const testHelper = new TestHelper({ cdc: false, 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 }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); + + 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 + 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: [ + { + n: { + name: "The Matrix", + a: { + nc: { + ne: [ + { + nn: { nodeName: "Keanu" }, + np: { y: 1999 }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/simple.int.test.ts b/packages/graphql/tests/api-v6/integration/simple.int.test.ts new file mode 100644 index 0000000000..79c370e03f --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/simple.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("Aura-api simple", () => { + const testHelper = new TestHelper({ cdc: false, 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/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts new file mode 100644 index 0000000000..4579d8efe5 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -0,0 +1,228 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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"; + +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 printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + } + + type Actor { + movies: ActorMoviesOperation + name: String + } + + type ActorConnection { + edges: [ActorEdge] + pageInfo: PageInfo + } + + type ActorEdge { + cursor: String + node: Actor + } + + type ActorMoviesConnection { + edges: [ActorMoviesEdge] + pageInfo: PageInfo + } + + type ActorMoviesEdge { + cursor: String + node: Movie + } + + type ActorMoviesOperation { + connection: ActorMoviesConnection + } + + type ActorOperation { + connection: ActorConnection + } + + type Movie { + actors: MovieActorsOperation + title: String + } + + type MovieActorsConnection { + edges: [MovieActorsEdge] + pageInfo: PageInfo + } + + type MovieActorsEdge { + cursor: String + node: Actor + } + + type MovieActorsOperation { + connection: MovieActorsConnection + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection: MovieConnection + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + type Query { + actors: ActorOperation + movies: MovieOperation + }" + `); + }); + + 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 printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + } + + type ActedIn { + year: Int + } + + type Actor { + movies: ActorMoviesOperation + name: String + } + + type ActorConnection { + edges: [ActorEdge] + pageInfo: PageInfo + } + + type ActorEdge { + cursor: String + node: Actor + } + + type ActorMoviesConnection { + edges: [ActorMoviesEdge] + pageInfo: PageInfo + } + + type ActorMoviesEdge { + cursor: String + node: Movie + properties: ActedIn + } + + type ActorMoviesOperation { + connection: ActorMoviesConnection + } + + type ActorOperation { + connection: ActorConnection + } + + type Movie { + actors: MovieActorsOperation + title: String + } + + type MovieActorsConnection { + edges: [MovieActorsEdge] + pageInfo: PageInfo + } + + type MovieActorsEdge { + cursor: String + node: Actor + properties: ActedIn + } + + type MovieActorsOperation { + connection: MovieActorsConnection + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection: MovieConnection + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + type Query { + actors: ActorOperation + movies: MovieOperation + }" + `); + }); +}); 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..3a2574c154 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -0,0 +1,178 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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"; + +describe("Simple Aura-API", () => { + test("single type", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + } + + type Movie { + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection: MovieConnection + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + type Query { + movies: MovieOperation + }" + `); + }); + + test("multiple types", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + title: String + } + type Actor @node { + name: String + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + } + + type Actor { + name: String + } + + type ActorConnection { + edges: [ActorEdge] + pageInfo: PageInfo + } + + type ActorEdge { + cursor: String + node: Actor + } + + type ActorOperation { + connection: ActorConnection + } + + type Movie { + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection: MovieConnection + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + type Query { + actors: ActorOperation + movies: MovieOperation + }" + `); + }); + + 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 printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + } + + type Movie { + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + type MovieEdge { + cursor: String + node: Movie + } + + type MovieOperation { + connection: MovieConnection + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + type Query { + movies: MovieOperation + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/relationship.test.ts b/packages/graphql/tests/api-v6/tck/relationship.test.ts new file mode 100644 index 0000000000..5d0eb2f07a --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/relationship.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("Relationship", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + 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) + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Simple relationship", async () => { + const query = /* GraphQL */ ` + query { + movies { + connection { + edges { + node { + title + actors { + connection { + edges { + node { + name + } + } + } + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/simple.test.ts b/packages/graphql/tests/api-v6/tck/simple.test.ts new file mode 100644 index 0000000000..e63b8bcd5d --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/simple.test.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 { Neo4jGraphQL } from "../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../tck/utils/tck-test-utils"; + +describe("Simple Aura API", () => { + 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 }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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/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/utils/tests-helper.ts b/packages/graphql/tests/utils/tests-helper.ts index 1a30bf1490..fd60c3ac81 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, From 96059a1ed870c0f1934722cb8b4a0269f2b7fdc0 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 7 May 2024 11:40:15 +0100 Subject: [PATCH 002/177] Add v6 tests to test commands --- packages/graphql/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/graphql/package.json b/packages/graphql/package.json index de2aea9b42..b3fad4e87d 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -29,14 +29,14 @@ "test": "jest", "test:unit": "jest src --coverage=true -c jest.minimal.config.js", "test:unit:watch": "jest src --watch -c jest.minimal.config.js", - "test:tck": "jest tests/tck -c jest.minimal.config.js", + "test:tck": "jest tests/tck tests/api-v6/tck -c jest.minimal.config.js", "test:tck:watch": "jest tck --watchAll -c jest.minimal.config.js", - "test:int": "jest tests/integration", + "test:int": "jest tests/integration tests/api-v6/integration", "test:int:watch": "jest int --watch", - "test:e2e": "jest tests/e2e", + "test:e2e": "jest tests/e2e tests/api-v6/e2e", "test:e2e:watch": "jest e2e --watch", "test:schema": "jest tests/schema -c jest.minimal.config.js", - "test:schema:watch": "jest tests/schema --watch -c jest.minimal.config.js", + "test:schema:watch": "jest tests/schema tests/api-v6/schema --watch -c jest.minimal.config.js", "test:v6": "jest tests/api-v6", "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", "posttest:package-tests": "yarn run cleanup:package-tests", From 23e313d2587d0d355e68b26ce8407f062fc88d70 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 7 May 2024 11:49:43 +0100 Subject: [PATCH 003/177] Fix types in test helper --- packages/graphql/tests/utils/tests-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/tests/utils/tests-helper.ts b/packages/graphql/tests/utils/tests-helper.ts index 991ba82685..faf60cccc8 100644 --- a/packages/graphql/tests/utils/tests-helper.ts +++ b/packages/graphql/tests/utils/tests-helper.ts @@ -43,7 +43,7 @@ export class TestHelper { private customDB: string | undefined; private cdc: boolean; - constructor({ cdc = false, v6Api = false }: { cdc: boolean; v6Api: boolean } = { cdc: false, v6Api: false }) { + constructor({ cdc = false, v6Api = false }: { cdc?: boolean; v6Api?: boolean } = { cdc: false, v6Api: false }) { this.cdc = cdc; this.isAuraApi = v6Api; } From 1cffbea10ab8e0d4b1d2ac353c94c603f59d1848 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 7 May 2024 13:41:25 +0100 Subject: [PATCH 004/177] Update license headers --- .../queryASTFactory/ReadOperationFactory.ts | 19 +++++++++++++++++++ .../resolve-tree-parser/graphql-tree.ts | 19 +++++++++++++++++++ .../mappers/connection-operation-mapper.ts | 19 +++++++++++++++++++ .../src/api-v6/resolvers/readResolver.ts | 19 +++++++++++++++++++ 4 files changed, 76 insertions(+) diff --git a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts index c2f88302d2..287e4fe92c 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts @@ -1,3 +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 { ResolveTree } from "graphql-parse-resolve-info"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; import { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts index be23821538..f6cfd76b32 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts @@ -1,3 +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. + */ + export type GraphQLTree = GraphQLTreeReadOperation; interface GraphQLTreeElement { diff --git a/packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts b/packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts index 00b4930e7c..77fba3aa92 100644 --- a/packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts +++ b/packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts @@ -1,3 +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 { ExecuteResult } from "../../../utils/execute"; export function mapConnectionRecord(executionResult: ExecuteResult): any { diff --git a/packages/graphql/src/api-v6/resolvers/readResolver.ts b/packages/graphql/src/api-v6/resolvers/readResolver.ts index 4635ea0321..3e3fcce080 100644 --- a/packages/graphql/src/api-v6/resolvers/readResolver.ts +++ b/packages/graphql/src/api-v6/resolvers/readResolver.ts @@ -1,3 +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 { GraphQLResolveInfo } from "graphql"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; From 67045a6c414040a520130c5c8c4777027a413066 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 8 May 2024 10:49:15 +0100 Subject: [PATCH 005/177] Add scalar fields resolvers --- .../src/api-v6/schema/SchemaBuilder.ts | 26 ++++++- .../schema-types/TopLevelEntityTypes.ts | 12 ++- .../integration/types/numeric.int.test.ts | 78 +++++++++++++++++++ 3 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts diff --git a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema/SchemaBuilder.ts index d3433a4ab8..6a0e645b5f 100644 --- a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema/SchemaBuilder.ts @@ -17,10 +17,22 @@ * limitations under the License. */ -import { type GraphQLSchema } from "graphql"; +import type { GraphQLSchema } from "graphql"; import type { ObjectTypeComposer } from "graphql-compose"; import { SchemaComposer } from "graphql-compose"; +export type TypeDefinition = string | ObjectTypeComposer[] | ObjectTypeComposer; + +export type GraphQLResolver = () => any; + +export type FieldDefinition = { + resolver?: GraphQLResolver; + type: TypeDefinition; + args?: Record; + deprecationReason?: string | null; + description?: string | null; +}; + export class SchemaBuilder { private composer: SchemaComposer; @@ -28,7 +40,11 @@ export class SchemaBuilder { this.composer = new SchemaComposer(); } - public createObjectType(name: string, fields?: Record, description?: string): ObjectTypeComposer { + public createObjectType( + name: string, + fields?: Record, + description?: string + ): ObjectTypeComposer { return this.composer.createObjectTC({ name, description, @@ -36,7 +52,11 @@ export class SchemaBuilder { }); } - public getOrCreateObjectType(name: string, fields?: Record, description?: string): ObjectTypeComposer { + public getOrCreateObjectType( + name: string, + fields?: Record, + description?: string + ): ObjectTypeComposer { return this.composer.getOrCreateOTC(name, (tc) => { if (fields) { tc.addFields(fields); diff --git a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts index cd6193a121..cf1a9e8971 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts @@ -19,9 +19,11 @@ import type { ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; +import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; import type { EntityTypeNames } from "../../graphQLTypeNames/EntityTypeNames"; -import type { SchemaBuilder } from "../SchemaBuilder"; +import type { FieldDefinition, SchemaBuilder } from "../SchemaBuilder"; import { EntityTypes } from "./EntityTypes"; import { NestedEntitySchemaTypes } from "./NestedEntityTypes"; import type { StaticTypes } from "./StaticTypes"; @@ -62,10 +64,12 @@ export class TopLevelEntityTypes extends EntityTypes { return undefined; } - private getNodeFields(concreteEntity: ConcreteEntity): Record { - return Object.fromEntries( - [...concreteEntity.attributes.values()].map((attribute) => [attribute.name, attribute.type.name]) + private getNodeFields(concreteEntity: ConcreteEntity): Record { + const entityAttributes = [...concreteEntity.attributes.values()].map( + (attribute) => new AttributeAdapter(attribute) ); + + return attributeAdapterToComposeFields(entityAttributes, new Map()) as Record; } private getRelationshipFields(concreteEntity: ConcreteEntity): Record { diff --git a/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts b/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts new file mode 100644 index 0000000000..cf7f5404d5 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/types/numeric.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("Aura-api simple", () => { + const testHelper = new TestHelper({ cdc: false, v6Api: true }); + + let Movie: UniqueType; + beforeAll(async () => { + Movie = testHelper.createUniqueType("Movie"); + + const typeDefs = /* GraphQL */ ` + type ${Movie} @node { + year: Int! + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (movie:${Movie} {year: 1999}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be able to get an integer field", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + edges { + node { + year + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + year: 1999, + }, + }, + ], + }, + }, + }); + }); +}); From ba4ac5545f6bf6e6739a95852281b35c402ac1b5 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 8 May 2024 10:58:08 +0100 Subject: [PATCH 006/177] Update tests names --- .../graphql/tests/api-v6/integration/relationship.int.test.ts | 2 +- .../graphql/tests/api-v6/integration/types/numeric.int.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graphql/tests/api-v6/integration/relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/relationship.int.test.ts index 63bddf500a..e04927b425 100644 --- a/packages/graphql/tests/api-v6/integration/relationship.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/relationship.int.test.ts @@ -20,7 +20,7 @@ import type { UniqueType } from "../../utils/graphql-types"; import { TestHelper } from "../../utils/tests-helper"; -describe("Aura-api simple", () => { +describe("Relationships simple", () => { const testHelper = new TestHelper({ cdc: false, v6Api: true }); let Movie: UniqueType; diff --git a/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts b/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts index cf7f5404d5..2c019d5727 100644 --- a/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts @@ -20,7 +20,7 @@ import type { UniqueType } from "../../../utils/graphql-types"; import { TestHelper } from "../../../utils/tests-helper"; -describe("Aura-api simple", () => { +describe("Numeric fields", () => { const testHelper = new TestHelper({ cdc: false, v6Api: true }); let Movie: UniqueType; From 51ab9f04bca0d8bb3f607afb928e0073ddeddfa3 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 7 May 2024 16:41:18 +0100 Subject: [PATCH 007/177] Test @alias with api v6 --- .../queryASTFactory/ReadOperationFactory.ts | 2 +- .../ConnectionReadOperation.ts | 0 .../src/translate/queryAST/ast/QueryAST.ts | 2 +- .../directives/alias/query.int.test.ts | 163 ++++++++++++++++++ .../integration/relationship.int.test.ts | 4 +- .../api-v6/integration/simple.int.test.ts | 2 +- 6 files changed, 168 insertions(+), 5 deletions(-) rename packages/graphql/src/api-v6/{QueryIR => queryIR}/ConnectionReadOperation.ts (100%) create mode 100644 packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts diff --git a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts index 287e4fe92c..24b2969684 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts @@ -31,7 +31,7 @@ import { AttributeField } from "../../translate/queryAST/ast/fields/attribute-fi import { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection"; import { RelationshipSelection } from "../../translate/queryAST/ast/selection/RelationshipSelection"; import { filterTruthy } from "../../utils/utils"; -import { V6ReadOperation } from "../QueryIR/ConnectionReadOperation"; +import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { parseResolveInfoTree } from "./resolve-tree-parser/ResolveTreeParser"; import type { GraphQLTree, diff --git a/packages/graphql/src/api-v6/QueryIR/ConnectionReadOperation.ts b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts similarity index 100% rename from packages/graphql/src/api-v6/QueryIR/ConnectionReadOperation.ts rename to packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts diff --git a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts index 17d357b2f2..7893655624 100644 --- a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts +++ b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts @@ -18,7 +18,7 @@ */ import Cypher from "@neo4j/cypher-builder"; -import { V6ReadOperation } from "../../../api-v6/QueryIR/ConnectionReadOperation"; +import { V6ReadOperation } from "../../../api-v6/queryIR/ConnectionReadOperation"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { createNode } from "../utils/create-node-from-entity"; import { QueryASTContext, QueryASTEnv } from "./QueryASTContext"; diff --git a/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts new file mode 100644 index 0000000000..8776099b70 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/alias/query.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 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 = ` + 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/relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/relationship.int.test.ts index e04927b425..80a52f0afe 100644 --- a/packages/graphql/tests/api-v6/integration/relationship.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/relationship.int.test.ts @@ -20,8 +20,8 @@ import type { UniqueType } from "../../utils/graphql-types"; import { TestHelper } from "../../utils/tests-helper"; -describe("Relationships simple", () => { - const testHelper = new TestHelper({ cdc: false, v6Api: true }); +describe("Relationship simple query", () => { + const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; let Actor: UniqueType; diff --git a/packages/graphql/tests/api-v6/integration/simple.int.test.ts b/packages/graphql/tests/api-v6/integration/simple.int.test.ts index 79c370e03f..1333e2e515 100644 --- a/packages/graphql/tests/api-v6/integration/simple.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/simple.int.test.ts @@ -21,7 +21,7 @@ import type { UniqueType } from "../../utils/graphql-types"; import { TestHelper } from "../../utils/tests-helper"; describe("Aura-api simple", () => { - const testHelper = new TestHelper({ cdc: false, v6Api: true }); + const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; beforeAll(async () => { From 75a9a2825c86a8f427297b3788543434897eea5a Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 8 May 2024 16:11:45 +0100 Subject: [PATCH 008/177] Add tck tests for alias directive --- .../api-v6/tck/directives/alias/query.test.ts | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 packages/graphql/tests/api-v6/tck/directives/alias/query.test.ts diff --git a/packages/graphql/tests/api-v6/tck/directives/alias/query.test.ts b/packages/graphql/tests/api-v6/tck/directives/alias/query.test.ts new file mode 100644 index 0000000000..34919e1e8a --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/directives/alias/query.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 { 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 }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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 }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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]-(directors:Director) + WITH collect({ node: directors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS directors, edge.relationship AS this1 + RETURN collect({ properties: { year: this1.year, movieYear: this1.year }, node: { name: directors.name, nameAgain: directors.name, __resolveType: \\"Director\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, titleAgain: this0.title, directors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); From 92c747d243aa6a96c77a315529e009d29d6fec52 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 9 May 2024 12:00:50 +0100 Subject: [PATCH 009/177] introduce top-level sort in the new API (#5102) * initial support for top-level sorting * skip failing test * apply suggestions --- .../graphQLTypeNames/EntityTypeNames.ts | 12 + .../queryASTFactory/ReadOperationFactory.ts | 31 ++- .../resolve-tree-parser/ResolveTreeParser.ts | 45 +++- .../resolve-tree-parser/graphql-tree.ts | 11 + .../api-v6/queryIR/ConnectionReadOperation.ts | 3 + .../src/api-v6/schema/SchemaBuilder.ts | 32 ++- .../api-v6/schema/schema-types/EntityTypes.ts | 41 +++- .../schema/schema-types/NestedEntityTypes.ts | 27 ++- .../api-v6/schema/schema-types/StaticTypes.ts | 7 +- .../schema-types/TopLevelEntityTypes.ts | 25 +- .../directives/alias/query.int.test.ts | 12 +- .../api-v6/integration/sort/sort.int.test.ts | 223 ++++++++++++++++++ .../tests/api-v6/schema/relationship.test.ts | 66 +++++- .../tests/api-v6/schema/simple.test.ts | 131 +++++++--- .../tests/api-v6/tck/sort/sort.test.ts | 109 +++++++++ 15 files changed, 704 insertions(+), 71 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/sort/sort.test.ts diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts index b2c385a0ab..aed22d0428 100644 --- a/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts @@ -34,6 +34,18 @@ export abstract class EntityTypeNames { return `${this.prefix}Connection`; } + public get connectionSortType(): string { + return `${this.prefix}ConnectionSort`; + } + + public get edgeSortType(): string { + return `${this.prefix}EdgeSort`; + } + + public get nodeSortType(): string { + return `${this.prefix}Sort`; + } + public get edgeType(): string { return `${this.prefix}Edge`; } diff --git a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts index 24b2969684..81a862833c 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts @@ -30,6 +30,7 @@ import { OperationField } from "../../translate/queryAST/ast/fields/OperationFie import { AttributeField } from "../../translate/queryAST/ast/fields/attribute-fields/AttributeField"; import { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection"; import { RelationshipSelection } from "../../translate/queryAST/ast/selection/RelationshipSelection"; +import { PropertySort } from "../../translate/queryAST/ast/sort/PropertySort"; import { filterTruthy } from "../../utils/utils"; import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { parseResolveInfoTree } from "./resolve-tree-parser/ResolveTreeParser"; @@ -49,9 +50,8 @@ export class ReadOperationFactory { public createAST({ resolveTree, entity }: { resolveTree: ResolveTree; entity: ConcreteEntity }): QueryAST { const parsedTree = parseResolveInfoTree({ resolveTree, entity }); - const operation = this.generateOperation({ - parsedTree: parsedTree, + parsedTree, entity, }); return new QueryAST(operation); @@ -75,7 +75,9 @@ export class ReadOperationFactory { }); const nodeResolveTree = connectionTree.fields.edges?.fields.node; + const nodeSortArgs = connectionTree.args.sort; const nodeFields = this.getNodeFields(entity, nodeResolveTree); + const sortInputFields = this.getSortInputFields(entity, nodeSortArgs); return new V6ReadOperation({ target, selection, @@ -83,6 +85,7 @@ export class ReadOperationFactory { edge: [], node: nodeFields, }, + sortFields: sortInputFields, }); } @@ -171,6 +174,30 @@ export class ReadOperationFactory { return [...attributeFields, ...relationshipFields]; } + private getSortInputFields( + entity: ConcreteEntity, + sortArguments: { edges: { node: Record } }[] | undefined + ): Array<{ edge: PropertySort[]; node: PropertySort[] }> { + if (!sortArguments) { + return []; + } + return sortArguments.map((sortArgument) => { + return { + edge: [], + node: Object.entries(sortArgument.edges.node).map(([fieldName, direction]) => { + const attribute = entity.findAttribute(fieldName); + if (!attribute) { + throw new Error(`no filter attribute ${fieldName}`); + } + return new PropertySort({ + direction, + attribute: new AttributeAdapter(attribute), + }); + }), + }; + }); + } + private generateRelationshipField( resolveTree: GraphQLTreeReadOperation, relationship: Relationship diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts index f1fdfcae96..ec7aa30727 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -22,6 +22,8 @@ import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity import type { Relationship } from "../../../schema-model/relationship/Relationship"; import { findFieldByName } from "./find-field-by-name"; import type { + GraphQLConnectionArgs, + GraphQLSortArgument, GraphQLTree, GraphQLTreeConnection, GraphQLTreeEdge, @@ -70,9 +72,10 @@ abstract class ResolveTreeParser { const entityTypes = this.entity.types; const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connectionType, "edges"); const edgeResolveTree = edgesResolveTree ? this.parseEdges(edgesResolveTree) : undefined; + const connectionArgs = this.parseConnectionArgs(resolveTree.args); return { alias: resolveTree.alias, - args: resolveTree.args, + args: connectionArgs, fields: { edges: edgeResolveTree, }, @@ -180,6 +183,46 @@ abstract class ResolveTreeParser { const relationshipTreeParser = new RelationshipResolveTreeParser({ entity: relationship }); return relationshipTreeParser.parseOperation(resolveTree); } + + private parseConnectionArgs(resolveTreeArgs: { [str: string]: any }): GraphQLConnectionArgs { + if (!resolveTreeArgs.sort) { + return {}; + } + + return { + sort: this.parseSort(resolveTreeArgs.sort), + }; + } + + private parseSort(sortArguments: { edges: { node: Record } }[]): GraphQLSortArgument[] { + return sortArguments.map((sortArgument) => { + const edges = sortArgument.edges; + const node = edges.node; + + const fields = Object.fromEntries( + Object.entries(node).map(([fieldName, resolveTreeDirection]) => { + if (this.entity.hasAttribute(fieldName)) { + const direction = this.parseDirection(resolveTreeDirection); + return [fieldName, direction]; + } + throw new ResolveTreeParserError(`Invalid sort field: ${fieldName}`); + }) + ); + + return { + edges: { + node: fields, + }, + }; + }); + } + + private parseDirection(direction: string): "ASC" | "DESC" { + if (direction === "ASC" || direction === "DESC") { + return direction; + } + throw new ResolveTreeParserError(`Invalid sort direction: ${direction}`); + } } class TopLevelTreeParser extends ResolveTreeParser { diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts index f6cfd76b32..f70d3f115d 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts @@ -34,6 +34,11 @@ export interface GraphQLTreeConnection extends GraphQLTreeElement { fields: { edges?: GraphQLTreeEdge; }; + args: GraphQLConnectionArgs; +} + +export interface GraphQLConnectionArgs { + sort?: GraphQLSortArgument[]; } export interface GraphQLTreeEdge extends GraphQLTreeElement { @@ -54,3 +59,9 @@ export interface GraphQLTreeEdgeProperties extends GraphQLTreeElement { export interface GraphQLTreeLeafField extends GraphQLTreeElement { fields: undefined; } + +export interface GraphQLSortArgument { + edges: { + node: Record; + }; +} diff --git a/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts index 3fdfd15bad..1201c0d458 100644 --- a/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts @@ -53,6 +53,7 @@ export class V6ReadOperation extends Operation { target, selection, fields, + sortFields, }: { relationship?: RelationshipAdapter; target: ConcreteEntityAdapter; @@ -61,6 +62,7 @@ export class V6ReadOperation extends Operation { node: Field[]; edge: Field[]; }; + sortFields?: Array<{ node: Sort[]; edge: Sort[] }>; }) { super(); this.relationship = relationship; @@ -68,6 +70,7 @@ export class V6ReadOperation extends Operation { this.selection = selection; this.nodeFields = fields?.node ?? []; this.edgeFields = fields?.edge ?? []; + this.sortFields = sortFields ?? []; } public setNodeFields(fields: Field[]) { diff --git a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema/SchemaBuilder.ts index 6a0e645b5f..af32ef959e 100644 --- a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema/SchemaBuilder.ts @@ -18,10 +18,10 @@ */ import type { GraphQLSchema } from "graphql"; -import type { ObjectTypeComposer } from "graphql-compose"; +import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose"; import { SchemaComposer } from "graphql-compose"; -export type TypeDefinition = string | ObjectTypeComposer[] | ObjectTypeComposer; +export type TypeDefinition = string | ListComposer | ObjectTypeComposer[] | ObjectTypeComposer; export type GraphQLResolver = () => any; @@ -42,7 +42,7 @@ export class SchemaBuilder { public createObjectType( name: string, - fields?: Record, + fields?: Record>, description?: string ): ObjectTypeComposer { return this.composer.createObjectTC({ @@ -54,7 +54,7 @@ export class SchemaBuilder { public getOrCreateObjectType( name: string, - fields?: Record, + fields?: Record>, description?: string ): ObjectTypeComposer { return this.composer.getOrCreateOTC(name, (tc) => { @@ -67,6 +67,30 @@ export class SchemaBuilder { }); } + public createInputObjectType( + name: string, + fields: Record>, + 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 addFieldToType(type: ObjectTypeComposer, fields: Record): void { type.addFields(fields); } diff --git a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts index ea68f4b95b..a906cc7e69 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts @@ -17,8 +17,9 @@ * limitations under the License. */ -import type { ObjectTypeComposer } from "graphql-compose"; +import type { InputTypeComposer, ListComposer, NonNullComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; +import type { Attribute } from "../../../schema-model/attribute/Attribute"; import type { EntityTypeNames } from "../../graphQLTypeNames/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import type { StaticTypes } from "./StaticTypes"; @@ -46,7 +47,10 @@ export abstract class EntityTypes { @Memoize() public get connectionOperation(): ObjectTypeComposer { return this.schemaBuilder.createObjectType(this.entityTypes.connectionOperation, { - connection: this.connection, + connection: { + type: this.connection, + args: this.getConnectionArgs(), + }, }); } @@ -54,10 +58,38 @@ export abstract class EntityTypes { public get connection(): ObjectTypeComposer { return this.schemaBuilder.createObjectType(this.entityTypes.connectionType, { pageInfo: this.staticTypes.pageInfo, - edges: [this.edge], + edges: this.edge.List, }); } + @Memoize() + public get connectionArgs(): InputTypeComposer { + return this.schemaBuilder.createInputObjectType(this.entityTypes.connectionSortType, { + edges: this.edgeSort, + }); + } + + @Memoize() + public get connectionSort(): InputTypeComposer { + return this.schemaBuilder.createInputObjectType(this.entityTypes.connectionSortType, { + edges: this.edgeSort, + }); + } + + @Memoize() + public get edgeSort(): InputTypeComposer { + return this.schemaBuilder.createInputObjectType(this.entityTypes.edgeSortType, { + node: this.nodeSort, + }); + } + + @Memoize() + public get nodeSort(): InputTypeComposer { + const fields = this.getFields(); + const sortFields = Object.fromEntries(fields.map((field) => [field.name, this.staticTypes.sortDirection])); + return this.schemaBuilder.createInputObjectType(this.entityTypes.nodeSortType, sortFields); + } + @Memoize() public get edge(): ObjectTypeComposer { const fields = { @@ -73,6 +105,9 @@ export abstract class EntityTypes { return this.schemaBuilder.createObjectType(this.entityTypes.edgeType, fields); } + // sort is optional because relationship sort is not yet implemented + protected abstract getConnectionArgs(): { sort?: ListComposer> }; protected abstract getEdgeProperties(): ObjectTypeComposer | undefined; + protected abstract getFields(): Attribute[]; public abstract get nodeType(): string; } diff --git a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts index 753c5fbc2a..d1114fdccd 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts @@ -17,8 +17,9 @@ * limitations under the License. */ -import type { ObjectTypeComposer } from "graphql-compose"; +import type { InputTypeComposer, ListComposer, NonNullComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; +import type { Attribute } from "../../../schema-model/attribute/Attribute"; import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; import type { NestedEntityTypeNames } from "../../graphQLTypeNames/NestedEntityTypeNames"; @@ -48,13 +49,6 @@ export class NestedEntitySchemaTypes extends EntityTypes this.relationship = relationship; } - protected getEdgeProperties(): ObjectTypeComposer | undefined { - if (this.entityTypes.propertiesType) { - const fields = this.getRelationshipFields(this.relationship); - return this.schemaBuilder.getOrCreateObjectType(this.entityTypes.propertiesType, fields); - } - } - @Memoize() public get nodeType(): string { const target = this.relationship.target; @@ -64,9 +58,22 @@ export class NestedEntitySchemaTypes extends EntityTypes return target.types.nodeType; } - private getRelationshipFields(relationship: Relationship): Record { + protected getEdgeProperties(): ObjectTypeComposer | undefined { + if (this.entityTypes.propertiesType) { + const fields = this.getRelationshipFields(); + return this.schemaBuilder.getOrCreateObjectType(this.entityTypes.propertiesType, fields); + } + } + + protected getFields(): Attribute[] { + return [...this.relationship.attributes.values()]; + } + protected getConnectionArgs(): { sort?: ListComposer> | undefined } { + return {}; + } + private getRelationshipFields(): Record { return Object.fromEntries( - [...relationship.attributes.values()].map((attribute) => [attribute.name, attribute.type.name]) + [...this.relationship.attributes.values()].map((attribute) => [attribute.name, attribute.type.name]) ); } } diff --git a/packages/graphql/src/api-v6/schema/schema-types/StaticTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/StaticTypes.ts index 5024c05fc4..dad127ac88 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/StaticTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/StaticTypes.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { ObjectTypeComposer } from "graphql-compose"; +import type { EnumTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { SchemaBuilder } from "../SchemaBuilder"; @@ -32,4 +32,9 @@ export class StaticTypes { public get pageInfo(): ObjectTypeComposer { return this.schemaBuilder.createObjectType("PageInfo", { hasNextPage: "Boolean", hasPreviousPage: "Boolean" }); } + + @Memoize() + public get sortDirection(): EnumTypeComposer { + return this.schemaBuilder.createEnumType("SortDirection", ["ASC", "DESC"]); + } } diff --git a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts index cf1a9e8971..0cbc75983d 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts @@ -17,8 +17,9 @@ * limitations under the License. */ -import type { ObjectTypeComposer } from "graphql-compose"; +import type { InputTypeComposer, ListComposer, NonNullComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; +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 { attributeAdapterToComposeFields } from "../../../schema/to-compose"; @@ -54,27 +55,37 @@ export class TopLevelEntityTypes extends EntityTypes { @Memoize() public get nodeType(): string { - const fields = this.getNodeFields(this.entity); - const relationships = this.getRelationshipFields(this.entity); + const fields = this.getNodeFieldsDefinitions(); + const relationships = this.getRelationshipFields(); this.schemaBuilder.createObjectType(this.entity.types.nodeType, { ...fields, ...relationships }); return this.entity.types.nodeType; } + protected getConnectionArgs(): { sort?: ListComposer> | undefined } { + return { + sort: this.connectionSort.NonNull.List, + }; + } + protected getEdgeProperties(): ObjectTypeComposer | undefined { return undefined; } - private getNodeFields(concreteEntity: ConcreteEntity): Record { - const entityAttributes = [...concreteEntity.attributes.values()].map( + protected getFields(): Attribute[] { + return [...this.entity.attributes.values()]; + } + + private getNodeFieldsDefinitions(): Record { + const entityAttributes = [...this.entity.attributes.values()].map( (attribute) => new AttributeAdapter(attribute) ); return attributeAdapterToComposeFields(entityAttributes, new Map()) as Record; } - private getRelationshipFields(concreteEntity: ConcreteEntity): Record { + private getRelationshipFields(): Record { return Object.fromEntries( - [...concreteEntity.relationships.values()].map((relationship) => { + [...this.entity.relationships.values()].map((relationship) => { const relationshipTypes = new NestedEntitySchemaTypes({ schemaBuilder: this.schemaBuilder, relationship, diff --git a/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts index 8776099b70..ddd7792106 100644 --- a/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts @@ -39,7 +39,7 @@ describe("@alias directive", () => { type Directed @relationshipProperties { year: Int! - movieYear: Int @alias(property: "year") + movieYear: Int! @alias(property: "year") } type ${Movie} @node { @@ -53,9 +53,11 @@ describe("@alias directive", () => { typeDefs, }); - await testHelper.executeCypher(` + await testHelper.executeCypher( + ` CREATE(m:${Movie} { title: "The Matrix" })<-[:DIRECTED {year: 1999}]-(d:${Director} {name: "Watchowsky"}) - `); + ` + ); }); afterEach(async () => { @@ -96,8 +98,8 @@ describe("@alias directive", () => { }, }); }); - - test("Query node and relationship with alias", async () => { + // this test fail, I believe is because the numeric resolver was not added on the relationship properties + test.skip("Query node and relationship with alias", async () => { const query = /* GraphQL */ ` query { ${Movie.plural} { 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..f53bfbf59d --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/sort/sort.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", () => { + 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: { edges: { 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: { edges: { 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: [{ 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({ + [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/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 4579d8efe5..2c52da0586 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -51,11 +51,19 @@ describe("Relationships", () => { pageInfo: PageInfo } + input ActorConnectionSort { + edges: ActorEdgeSort + } + type ActorEdge { cursor: String node: Actor } + input ActorEdgeSort { + node: ActorSort + } + type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo @@ -71,7 +79,11 @@ describe("Relationships", () => { } type ActorOperation { - connection: ActorConnection + connection(sort: [ActorConnectionSort!]): ActorConnection + } + + input ActorSort { + name: SortDirection } type Movie { @@ -98,13 +110,25 @@ describe("Relationships", () => { pageInfo: PageInfo } + input MovieConnectionSort { + edges: MovieEdgeSort + } + type MovieEdge { cursor: String node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { - connection: MovieConnection + connection(sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieSort { + title: SortDirection } type PageInfo { @@ -115,6 +139,11 @@ describe("Relationships", () => { type Query { actors: ActorOperation movies: MovieOperation + } + + enum SortDirection { + ASC + DESC }" `); }); @@ -156,11 +185,19 @@ describe("Relationships", () => { pageInfo: PageInfo } + input ActorConnectionSort { + edges: ActorEdgeSort + } + type ActorEdge { cursor: String node: Actor } + input ActorEdgeSort { + node: ActorSort + } + type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo @@ -177,7 +214,11 @@ describe("Relationships", () => { } type ActorOperation { - connection: ActorConnection + connection(sort: [ActorConnectionSort!]): ActorConnection + } + + input ActorSort { + name: SortDirection } type Movie { @@ -205,13 +246,25 @@ describe("Relationships", () => { pageInfo: PageInfo } + input MovieConnectionSort { + edges: MovieEdgeSort + } + type MovieEdge { cursor: String node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { - connection: MovieConnection + connection(sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieSort { + title: SortDirection } type PageInfo { @@ -222,6 +275,11 @@ describe("Relationships", () => { type Query { actors: ActorOperation movies: MovieOperation + } + + enum SortDirection { + ASC + DESC }" `); }); diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 3a2574c154..e1ec26b0e1 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -45,13 +45,25 @@ describe("Simple Aura-API", () => { pageInfo: PageInfo } + input MovieConnectionSort { + edges: MovieEdgeSort + } + type MovieEdge { cursor: String node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { - connection: MovieConnection + connection(sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieSort { + title: SortDirection } type PageInfo { @@ -61,6 +73,11 @@ describe("Simple Aura-API", () => { type Query { movies: MovieOperation + } + + enum SortDirection { + ASC + DESC }" `); }); @@ -91,13 +108,25 @@ describe("Simple Aura-API", () => { pageInfo: PageInfo } + input ActorConnectionSort { + edges: ActorEdgeSort + } + type ActorEdge { cursor: String node: Actor } + input ActorEdgeSort { + node: ActorSort + } + type ActorOperation { - connection: ActorConnection + connection(sort: [ActorConnectionSort!]): ActorConnection + } + + input ActorSort { + name: SortDirection } type Movie { @@ -109,13 +138,25 @@ describe("Simple Aura-API", () => { pageInfo: PageInfo } + input MovieConnectionSort { + edges: MovieEdgeSort + } + type MovieEdge { cursor: String node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { - connection: MovieConnection + connection(sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieSort { + title: SortDirection } type PageInfo { @@ -126,6 +167,11 @@ describe("Simple Aura-API", () => { type Query { actors: ActorOperation movies: MovieOperation + } + + enum SortDirection { + ASC + DESC }" `); }); @@ -143,36 +189,53 @@ describe("Simple Aura-API", () => { const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); expect(printedSchema).toMatchInlineSnapshot(` - "schema { - query: Query - } - - type Movie { - title: String - } - - type MovieConnection { - edges: [MovieEdge] - pageInfo: PageInfo - } - - type MovieEdge { - cursor: String - node: Movie - } - - type MovieOperation { - connection: MovieConnection - } - - type PageInfo { - hasNextPage: Boolean - hasPreviousPage: Boolean - } - - type Query { - movies: MovieOperation - }" - `); + "schema { + query: Query + } + + type Movie { + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + edges: MovieEdgeSort + } + + type MovieEdge { + cursor: String + node: Movie + } + + input MovieEdgeSort { + node: MovieSort + } + + type MovieOperation { + connection(sort: [MovieConnectionSort!]): MovieConnection + } + + input MovieSort { + title: SortDirection + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + type Query { + movies: MovieOperation + } + + enum SortDirection { + ASC + DESC + }" + `); }); }); 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..7bcdd42eee --- /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: { edges: { 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: [{ edges: { node: { title: DESC } } }, { edges: { 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(`"{}"`); + }); +}); From ad7f10ebcf9ca1704abe16a5a0ea83d4d7e31ada Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 10 May 2024 14:57:37 +0100 Subject: [PATCH 010/177] Fix scalar fields in relationships --- .../api-v6/schema/schema-types/NestedEntityTypes.ts | 12 ++++++++---- .../integration/directives/alias/query.int.test.ts | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts index d1114fdccd..27e7b5ab09 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts @@ -20,10 +20,12 @@ import type { InputTypeComposer, ListComposer, NonNullComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; 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 type { Relationship } from "../../../schema-model/relationship/Relationship"; +import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; import type { NestedEntityTypeNames } from "../../graphQLTypeNames/NestedEntityTypeNames"; -import type { SchemaBuilder } from "../SchemaBuilder"; +import type { FieldDefinition, SchemaBuilder } from "../SchemaBuilder"; import { EntityTypes } from "./EntityTypes"; import type { StaticTypes } from "./StaticTypes"; @@ -71,9 +73,11 @@ export class NestedEntitySchemaTypes extends EntityTypes protected getConnectionArgs(): { sort?: ListComposer> | undefined } { return {}; } - private getRelationshipFields(): Record { - return Object.fromEntries( - [...this.relationship.attributes.values()].map((attribute) => [attribute.name, attribute.type.name]) + private getRelationshipFields(): Record { + const entityAttributes = [...this.relationship.attributes.values()].map( + (attribute) => new AttributeAdapter(attribute) ); + + return attributeAdapterToComposeFields(entityAttributes, new Map()) as Record; } } diff --git a/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts index ddd7792106..5fd1d583c6 100644 --- a/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts @@ -98,8 +98,8 @@ describe("@alias directive", () => { }, }); }); - // this test fail, I believe is because the numeric resolver was not added on the relationship properties - test.skip("Query node and relationship with alias", async () => { + + test("Query node and relationship with alias", async () => { const query = /* GraphQL */ ` query { ${Movie.plural} { From 0d6fd5c86c557a6df67e750153b10072ad84934f Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 14 May 2024 10:31:43 +0100 Subject: [PATCH 011/177] Nested sorting (#5125) * implement nested-level sorting * add test for sort by relationship-properties --- .../graphQLTypeNames/EntityTypeNames.ts | 6 +- .../graphQLTypeNames/NestedEntityTypeNames.ts | 11 + .../TopLevelEntityTypeNames.ts | 13 +- .../queryASTFactory/ReadOperationFactory.ts | 73 ++- .../resolve-tree-parser/ResolveTreeParser.ts | 63 ++- .../resolve-tree-parser/graphql-tree.ts | 15 +- .../api-v6/queryIR/ConnectionReadOperation.ts | 2 +- .../src/api-v6/schema/SchemaBuilder.ts | 41 +- .../api-v6/schema/schema-types/EntityTypes.ts | 36 +- .../schema/schema-types/NestedEntityTypes.ts | 39 +- .../schema-types/TopLevelEntityTypes.ts | 26 +- .../sort/sort-relationship.int.test.ts | 478 ++++++++++++++++++ .../api-v6/integration/sort/sort.int.test.ts | 2 +- .../tests/api-v6/schema/relationship.test.ts | 62 ++- .../tests/api-v6/schema/simple.test.ts | 16 +- .../tests/api-v6/tck/relationship.test.ts | 65 ++- .../api-v6/tck/sort/sort-relationship.test.ts | 287 +++++++++++ .../tests/api-v6/tck/sort/sort.test.ts | 4 +- 18 files changed, 1119 insertions(+), 120 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/sort/sort-relationship.int.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/sort/sort-relationship.test.ts diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts index aed22d0428..a4a675f7fa 100644 --- a/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts @@ -42,10 +42,6 @@ export abstract class EntityTypeNames { return `${this.prefix}EdgeSort`; } - public get nodeSortType(): string { - return `${this.prefix}Sort`; - } - public get edgeType(): string { return `${this.prefix}Edge`; } @@ -67,4 +63,6 @@ export abstract class EntityTypeNames { } public abstract get propertiesType(): string | undefined; + public abstract get propertiesSortType(): string | undefined; + public abstract get nodeSortType(): string; } diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.ts index 282f0d35e4..1c253d8160 100644 --- a/packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.ts @@ -32,4 +32,15 @@ export class NestedEntityTypeNames extends EntityTypeNames { public get propertiesType(): string | undefined { return this.relationship.propertiesTypeName; } + + public get propertiesSortType(): string | undefined { + if (!this.relationship.propertiesTypeName) { + return; + } + return `${this.relationship.propertiesTypeName}Sort`; + } + + public get nodeSortType(): string { + return `${this.relationship.target.name}Sort`; + } } diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts index 25a68734ce..0288b7541d 100644 --- a/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts @@ -23,8 +23,11 @@ import { EntityTypeNames } from "./EntityTypeNames"; import { NestedEntityTypeNames } from "./NestedEntityTypeNames"; export class TopLevelEntityTypeNames extends EntityTypeNames { + private concreteEntity: ConcreteEntity; + constructor(concreteEntity: ConcreteEntity) { super(concreteEntity.name); + this.concreteEntity = concreteEntity; } public relationship(relationship: Relationship): NestedEntityTypeNames { @@ -32,6 +35,14 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { } public get propertiesType(): undefined { - return undefined; + return; + } + + public get propertiesSortType(): undefined { + return; + } + + public get nodeSortType(): string { + return `${this.concreteEntity.name}Sort`; } } diff --git a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts index 81a862833c..51955ca917 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts @@ -35,10 +35,12 @@ import { filterTruthy } from "../../utils/utils"; import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { parseResolveInfoTree } from "./resolve-tree-parser/ResolveTreeParser"; import type { + GraphQLSortArgument, GraphQLTree, GraphQLTreeEdgeProperties, GraphQLTreeNode, GraphQLTreeReadOperation, + GraphQLTreeSortElement, } from "./resolve-tree-parser/graphql-tree"; export class ReadOperationFactory { @@ -75,9 +77,12 @@ export class ReadOperationFactory { }); const nodeResolveTree = connectionTree.fields.edges?.fields.node; - const nodeSortArgs = connectionTree.args.sort; + const sortArgument = connectionTree.args.sort; const nodeFields = this.getNodeFields(entity, nodeResolveTree); - const sortInputFields = this.getSortInputFields(entity, nodeSortArgs); + const sortInputFields = this.getSortInputFields({ + entity, + sortArgument, + }); return new V6ReadOperation({ target, selection, @@ -115,9 +120,13 @@ export class ReadOperationFactory { // Fields const nodeResolveTree = connectionTree.fields.edges?.fields.node; const propertiesResolveTree = connectionTree.fields.edges?.fields.properties; - const nodeFields = this.getNodeFields(relationshipAdapter.target.entity, nodeResolveTree); + const relTarget = relationshipAdapter.target.entity; + const nodeFields = this.getNodeFields(relTarget, nodeResolveTree); const edgeFields = this.getAttributeFields(relationship, propertiesResolveTree); + const sortArgument = connectionTree.args.sort; + const sortInputFields = this.getSortInputFields({ entity: relTarget, relationship, sortArgument }); + return new V6ReadOperation({ target: relationshipAdapter.target, selection, @@ -125,6 +134,7 @@ export class ReadOperationFactory { edge: edgeFields, node: nodeFields, }, + sortFields: sortInputFields, }); } @@ -147,7 +157,7 @@ export class ReadOperationFactory { attribute: new AttributeAdapter(attribute), }); } - return undefined; + return; }) ); } @@ -174,30 +184,51 @@ export class ReadOperationFactory { return [...attributeFields, ...relationshipFields]; } - private getSortInputFields( - entity: ConcreteEntity, - sortArguments: { edges: { node: Record } }[] | undefined - ): Array<{ edge: PropertySort[]; node: PropertySort[] }> { - if (!sortArguments) { + private getSortInputFields({ + entity, + relationship, + sortArgument, + }: { + entity: ConcreteEntity; + relationship?: Relationship; + sortArgument: GraphQLSortArgument | undefined; + }): Array<{ edge: PropertySort[]; node: PropertySort[] }> { + if (!sortArgument) { return []; } - return sortArguments.map((sortArgument) => { + return sortArgument.edges.map((edge): { edge: PropertySort[]; node: PropertySort[] } => { + const nodeSortFields = edge.node ? this.getPropertiesSort({ target: entity, sortArgument: edge.node }) : []; + const edgeSortFields = + edge.properties && relationship + ? this.getPropertiesSort({ target: relationship, sortArgument: edge.properties }) + : []; return { - edge: [], - node: Object.entries(sortArgument.edges.node).map(([fieldName, direction]) => { - const attribute = entity.findAttribute(fieldName); - if (!attribute) { - throw new Error(`no filter attribute ${fieldName}`); - } - return new PropertySort({ - direction, - attribute: new AttributeAdapter(attribute), - }); - }), + 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 diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts index ec7aa30727..9e76121117 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -23,7 +23,7 @@ import type { Relationship } from "../../../schema-model/relationship/Relationsh import { findFieldByName } from "./find-field-by-name"; import type { GraphQLConnectionArgs, - GraphQLSortArgument, + GraphQLSortEdgeArgument, GraphQLTree, GraphQLTreeConnection, GraphQLTreeEdge, @@ -31,6 +31,7 @@ import type { GraphQLTreeLeafField, GraphQLTreeNode, GraphQLTreeReadOperation, + GraphQLTreeSortElement, } from "./graphql-tree"; export function parseResolveInfoTree({ @@ -133,7 +134,7 @@ abstract class ResolveTreeParser { private parseEdgeProperties(resolveTree: ResolveTree): GraphQLTreeEdgeProperties | undefined { if (!this.entity.types.propertiesType) { - return undefined; + return; } const fieldsResolveTree = resolveTree.fieldsByTypeName[this.entity.types.propertiesType] ?? {}; @@ -190,33 +191,51 @@ abstract class ResolveTreeParser { } return { - sort: this.parseSort(resolveTreeArgs.sort), + sort: { + edges: this.parseSortEdges(resolveTreeArgs.sort.edges), + }, }; } - private parseSort(sortArguments: { edges: { node: Record } }[]): GraphQLSortArgument[] { - return sortArguments.map((sortArgument) => { - const edges = sortArgument.edges; - const node = edges.node; - - const fields = Object.fromEntries( - Object.entries(node).map(([fieldName, resolveTreeDirection]) => { - if (this.entity.hasAttribute(fieldName)) { - const direction = this.parseDirection(resolveTreeDirection); - return [fieldName, direction]; - } - throw new ResolveTreeParserError(`Invalid sort field: ${fieldName}`); - }) - ); + private parseSortEdges( + sortEdges: { + node: Record | undefined; + properties: Record | undefined; + }[] + ): GraphQLSortEdgeArgument[] { + return sortEdges.map((edge) => { + const sortFields: GraphQLSortEdgeArgument = {}; + const nodeFields = edge.node; + + if (nodeFields) { + const fields = this.parseSort(this.targetNode, nodeFields); + sortFields.node = fields; + } + const edgeProperties = edge.properties; - return { - edges: { - node: fields, - }, - }; + if (edgeProperties) { + const fields = this.parseSort(this.entity, edgeProperties); + sortFields.properties = fields; + } + return sortFields; }); } + private parseSort( + target: Relationship | ConcreteEntity, + sortObject: Record + ): GraphQLTreeSortElement { + return Object.fromEntries( + Object.entries(sortObject).map(([fieldName, resolveTreeDirection]) => { + if (target.hasAttribute(fieldName)) { + const direction = this.parseDirection(resolveTreeDirection); + return [fieldName, direction]; + } + throw new ResolveTreeParserError(`Invalid sort field: ${fieldName}`); + }) + ); + } + private parseDirection(direction: string): "ASC" | "DESC" { if (direction === "ASC" || direction === "DESC") { return direction; diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts index f70d3f115d..e88b67d125 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts @@ -38,7 +38,7 @@ export interface GraphQLTreeConnection extends GraphQLTreeElement { } export interface GraphQLConnectionArgs { - sort?: GraphQLSortArgument[]; + sort?: GraphQLSortArgument; } export interface GraphQLTreeEdge extends GraphQLTreeElement { @@ -61,7 +61,14 @@ export interface GraphQLTreeLeafField extends GraphQLTreeElement { } export interface GraphQLSortArgument { - edges: { - node: Record; - }; + edges: GraphQLSortEdgeArgument[]; +} + +export interface GraphQLSortEdgeArgument { + node?: GraphQLTreeSortElement; + properties?: GraphQLTreeSortElement; +} + +export interface GraphQLTreeSortElement { + [key: string]: "ASC" | "DESC"; } diff --git a/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts index 1201c0d458..6b64dfa740 100644 --- a/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts @@ -290,7 +290,7 @@ export class V6ReadOperation extends Operation { private generateSortAndPaginationClause(context: QueryASTContext): Cypher.With | undefined { const shouldGenerateSortWith = this.pagination || this.sortFields.length > 0; if (!shouldGenerateSortWith) { - return undefined; + return; } const paginationWith = new Cypher.With("*"); this.addPaginationSubclauses(paginationWith); diff --git a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema/SchemaBuilder.ts index af32ef959e..ae94c38f63 100644 --- a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema/SchemaBuilder.ts @@ -18,10 +18,26 @@ */ import type { GraphQLSchema } from "graphql"; -import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose"; +import type { + EnumTypeComposer, + InputTypeComposer, + ListComposer, + NonNullComposer, + ObjectTypeComposer, +} from "graphql-compose"; import { SchemaComposer } from "graphql-compose"; -export type TypeDefinition = string | ListComposer | ObjectTypeComposer[] | ObjectTypeComposer; +export type TypeDefinition = string | ListComposer | ObjectTypeComposer; + +type ObjectOrInputTypeComposer = ObjectTypeComposer | InputTypeComposer; + +type ListOrNullComposer = + | ListComposer + | ListComposer> + | NonNullComposer + | NonNullComposer>; + +type WrappedComposer = T | ListOrNullComposer; export type GraphQLResolver = () => any; @@ -42,7 +58,7 @@ export class SchemaBuilder { public createObjectType( name: string, - fields?: Record>, + fields?: Record>, description?: string ): ObjectTypeComposer { return this.composer.createObjectTC({ @@ -54,7 +70,7 @@ export class SchemaBuilder { public getOrCreateObjectType( name: string, - fields?: Record>, + fields?: Record>, description?: string ): ObjectTypeComposer { return this.composer.getOrCreateOTC(name, (tc) => { @@ -69,7 +85,7 @@ export class SchemaBuilder { public createInputObjectType( name: string, - fields: Record>, + fields: Record>, description?: string ): InputTypeComposer { return this.composer.createInputTC({ @@ -79,6 +95,21 @@ export class SchemaBuilder { }); } + public getOrCreateInputObjectType( + name: string, + fields: Record>, + description?: string + ): InputTypeComposer { + return this.composer.getOrCreateITC(name, (itc) => { + if (fields) { + itc.addFields(fields); + } + if (description) { + itc.setDescription(description); + } + }); + } + public createEnumType(name: string, values: string[], description?: string): EnumTypeComposer { const enumValuesFormatted: Record = values.reduce((acc, value) => { acc[value] = { value }; diff --git a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts index a906cc7e69..dee34e42f8 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { InputTypeComposer, ListComposer, NonNullComposer, ObjectTypeComposer } from "graphql-compose"; +import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; import type { EntityTypeNames } from "../../graphQLTypeNames/EntityTypeNames"; @@ -49,7 +49,7 @@ export abstract class EntityTypes { return this.schemaBuilder.createObjectType(this.entityTypes.connectionOperation, { connection: { type: this.connection, - args: this.getConnectionArgs(), + args: this.connectionArgs, }, }); } @@ -63,31 +63,35 @@ export abstract class EntityTypes { } @Memoize() - public get connectionArgs(): InputTypeComposer { - return this.schemaBuilder.createInputObjectType(this.entityTypes.connectionSortType, { - edges: this.edgeSort, - }); + protected get connectionArgs(): { sort: InputTypeComposer } { + return { + sort: this.connectionSort, + }; } @Memoize() public get connectionSort(): InputTypeComposer { return this.schemaBuilder.createInputObjectType(this.entityTypes.connectionSortType, { - edges: this.edgeSort, + edges: this.edgeSort.NonNull.List, }); } @Memoize() public get edgeSort(): InputTypeComposer { - return this.schemaBuilder.createInputObjectType(this.entityTypes.edgeSortType, { - node: this.nodeSort, - }); + const edgeSortFields = { + node: this.nodeSortType, + }; + const properties = this.getEdgeSortProperties(); + if (properties) { + edgeSortFields["properties"] = properties; + } + + return this.schemaBuilder.createInputObjectType(this.entityTypes.edgeSortType, edgeSortFields); } @Memoize() - public get nodeSort(): InputTypeComposer { - const fields = this.getFields(); - const sortFields = Object.fromEntries(fields.map((field) => [field.name, this.staticTypes.sortDirection])); - return this.schemaBuilder.createInputObjectType(this.entityTypes.nodeSortType, sortFields); + public get sortFields(): Record { + return Object.fromEntries(this.getFields().map((field) => [field.name, this.staticTypes.sortDirection])); } @Memoize() @@ -105,9 +109,9 @@ export abstract class EntityTypes { return this.schemaBuilder.createObjectType(this.entityTypes.edgeType, fields); } - // sort is optional because relationship sort is not yet implemented - protected abstract getConnectionArgs(): { sort?: ListComposer> }; protected abstract getEdgeProperties(): ObjectTypeComposer | undefined; + protected abstract getEdgeSortProperties(): InputTypeComposer | undefined; protected abstract getFields(): Attribute[]; public abstract get nodeType(): string; + public abstract get nodeSortType(): string; } diff --git a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts index 27e7b5ab09..a2cbf2bc5a 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { InputTypeComposer, ListComposer, NonNullComposer, ObjectTypeComposer } from "graphql-compose"; +import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; @@ -25,7 +25,7 @@ import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; import type { NestedEntityTypeNames } from "../../graphQLTypeNames/NestedEntityTypeNames"; -import type { FieldDefinition, SchemaBuilder } from "../SchemaBuilder"; +import type { SchemaBuilder } from "../SchemaBuilder"; import { EntityTypes } from "./EntityTypes"; import type { StaticTypes } from "./StaticTypes"; @@ -59,7 +59,16 @@ export class NestedEntitySchemaTypes extends EntityTypes } return target.types.nodeType; } + @Memoize() + public get nodeSortType(): string { + const target = this.relationship.target; + if (!(target instanceof ConcreteEntity)) { + throw new Error("Interfaces not supported yet"); + } + return target.types.nodeSortType; + } + @Memoize() protected getEdgeProperties(): ObjectTypeComposer | undefined { if (this.entityTypes.propertiesType) { const fields = this.getRelationshipFields(); @@ -67,17 +76,29 @@ export class NestedEntitySchemaTypes extends EntityTypes } } + @Memoize() + protected getEdgeSortProperties(): InputTypeComposer | undefined { + if (this.entityTypes.propertiesSortType) { + const fields = this.getRelationshipSortFields(); + return this.schemaBuilder.getOrCreateInputObjectType(this.entityTypes.propertiesSortType, fields); + } + } + + @Memoize() protected getFields(): Attribute[] { return [...this.relationship.attributes.values()]; } - protected getConnectionArgs(): { sort?: ListComposer> | undefined } { - return {}; - } - private getRelationshipFields(): Record { - const entityAttributes = [...this.relationship.attributes.values()].map( - (attribute) => new AttributeAdapter(attribute) - ); + @Memoize() + private getRelationshipFields(): Record { + const entityAttributes = this.getFields().map((attribute) => new AttributeAdapter(attribute)); return attributeAdapterToComposeFields(entityAttributes, new Map()) as Record; } + + @Memoize() + private getRelationshipSortFields(): Record { + return Object.fromEntries( + this.getFields().map((attribute) => [attribute.name, this.staticTypes.sortDirection]) + ); + } } diff --git a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts index 0cbc75983d..6c4519e5c3 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { InputTypeComposer, ListComposer, NonNullComposer, ObjectTypeComposer } from "graphql-compose"; +import type { ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; @@ -61,28 +61,32 @@ export class TopLevelEntityTypes extends EntityTypes { return this.entity.types.nodeType; } - protected getConnectionArgs(): { sort?: ListComposer> | undefined } { - return { - sort: this.connectionSort.NonNull.List, - }; + protected getEdgeProperties(): undefined { + return; } - protected getEdgeProperties(): ObjectTypeComposer | undefined { - return undefined; + protected getEdgeSortProperties(): undefined { + return; } + @Memoize() protected getFields(): Attribute[] { return [...this.entity.attributes.values()]; } + @Memoize() private getNodeFieldsDefinitions(): Record { - const entityAttributes = [...this.entity.attributes.values()].map( - (attribute) => new AttributeAdapter(attribute) - ); - + const entityAttributes = this.getFields().map((attribute) => new AttributeAdapter(attribute)); return attributeAdapterToComposeFields(entityAttributes, new Map()) as Record; } + @Memoize() + public get nodeSortType(): string { + this.schemaBuilder.createInputObjectType(this.entity.types.nodeSortType, this.sortFields); + return this.entity.types.nodeSortType; + } + + @Memoize() private getRelationshipFields(): Record { return Object.fromEntries( [...this.entity.relationships.values()].map((relationship) => { 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..a4d34ee365 --- /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 } }, { 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 } }, { 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 } }, + { node: { title: ASC } }, + { properties: { year: DESC } }, + { 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 index f53bfbf59d..52c932b4b6 100644 --- a/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts @@ -159,7 +159,7 @@ describe("Sort", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(sort: [{ edges: { node: { title: ASC } } }, { edges: { node: { ratings: DESC } } }]) { + connection(sort: { edges: [{ node: { title: ASC } }, { node: { ratings: DESC } }] }) { edges { node { title diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 2c52da0586..4b30eae8a5 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -52,7 +52,7 @@ describe("Relationships", () => { } input ActorConnectionSort { - edges: ActorEdgeSort + edges: [ActorEdgeSort!] } type ActorEdge { @@ -69,17 +69,25 @@ describe("Relationships", () => { pageInfo: PageInfo } + input ActorMoviesConnectionSort { + edges: [ActorMoviesEdgeSort!] + } + type ActorMoviesEdge { cursor: String node: Movie } + input ActorMoviesEdgeSort { + node: MovieSort + } + type ActorMoviesOperation { - connection: ActorMoviesConnection + connection(sort: ActorMoviesConnectionSort): ActorMoviesConnection } type ActorOperation { - connection(sort: [ActorConnectionSort!]): ActorConnection + connection(sort: ActorConnectionSort): ActorConnection } input ActorSort { @@ -96,13 +104,21 @@ describe("Relationships", () => { pageInfo: PageInfo } + input MovieActorsConnectionSort { + edges: [MovieActorsEdgeSort!] + } + type MovieActorsEdge { cursor: String node: Actor } + input MovieActorsEdgeSort { + node: ActorSort + } + type MovieActorsOperation { - connection: MovieActorsConnection + connection(sort: MovieActorsConnectionSort): MovieActorsConnection } type MovieConnection { @@ -111,7 +127,7 @@ describe("Relationships", () => { } input MovieConnectionSort { - edges: MovieEdgeSort + edges: [MovieEdgeSort!] } type MovieEdge { @@ -124,7 +140,7 @@ describe("Relationships", () => { } type MovieOperation { - connection(sort: [MovieConnectionSort!]): MovieConnection + connection(sort: MovieConnectionSort): MovieConnection } input MovieSort { @@ -175,6 +191,10 @@ describe("Relationships", () => { year: Int } + input ActedInSort { + year: SortDirection + } + type Actor { movies: ActorMoviesOperation name: String @@ -186,7 +206,7 @@ describe("Relationships", () => { } input ActorConnectionSort { - edges: ActorEdgeSort + edges: [ActorEdgeSort!] } type ActorEdge { @@ -203,18 +223,27 @@ describe("Relationships", () => { pageInfo: PageInfo } + input ActorMoviesConnectionSort { + edges: [ActorMoviesEdgeSort!] + } + type ActorMoviesEdge { cursor: String node: Movie properties: ActedIn } + input ActorMoviesEdgeSort { + node: MovieSort + properties: ActedInSort + } + type ActorMoviesOperation { - connection: ActorMoviesConnection + connection(sort: ActorMoviesConnectionSort): ActorMoviesConnection } type ActorOperation { - connection(sort: [ActorConnectionSort!]): ActorConnection + connection(sort: ActorConnectionSort): ActorConnection } input ActorSort { @@ -231,14 +260,23 @@ describe("Relationships", () => { pageInfo: PageInfo } + input MovieActorsConnectionSort { + edges: [MovieActorsEdgeSort!] + } + type MovieActorsEdge { cursor: String node: Actor properties: ActedIn } + input MovieActorsEdgeSort { + node: ActorSort + properties: ActedInSort + } + type MovieActorsOperation { - connection: MovieActorsConnection + connection(sort: MovieActorsConnectionSort): MovieActorsConnection } type MovieConnection { @@ -247,7 +285,7 @@ describe("Relationships", () => { } input MovieConnectionSort { - edges: MovieEdgeSort + edges: [MovieEdgeSort!] } type MovieEdge { @@ -260,7 +298,7 @@ describe("Relationships", () => { } type MovieOperation { - connection(sort: [MovieConnectionSort!]): MovieConnection + connection(sort: MovieConnectionSort): MovieConnection } input MovieSort { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index e1ec26b0e1..9bc33c2913 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -46,7 +46,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - edges: MovieEdgeSort + edges: [MovieEdgeSort!] } type MovieEdge { @@ -59,7 +59,7 @@ describe("Simple Aura-API", () => { } type MovieOperation { - connection(sort: [MovieConnectionSort!]): MovieConnection + connection(sort: MovieConnectionSort): MovieConnection } input MovieSort { @@ -109,7 +109,7 @@ describe("Simple Aura-API", () => { } input ActorConnectionSort { - edges: ActorEdgeSort + edges: [ActorEdgeSort!] } type ActorEdge { @@ -122,7 +122,7 @@ describe("Simple Aura-API", () => { } type ActorOperation { - connection(sort: [ActorConnectionSort!]): ActorConnection + connection(sort: ActorConnectionSort): ActorConnection } input ActorSort { @@ -139,7 +139,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - edges: MovieEdgeSort + edges: [MovieEdgeSort!] } type MovieEdge { @@ -152,7 +152,7 @@ describe("Simple Aura-API", () => { } type MovieOperation { - connection(sort: [MovieConnectionSort!]): MovieConnection + connection(sort: MovieConnectionSort): MovieConnection } input MovieSort { @@ -203,7 +203,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - edges: MovieEdgeSort + edges: [MovieEdgeSort!] } type MovieEdge { @@ -216,7 +216,7 @@ describe("Simple Aura-API", () => { } type MovieOperation { - connection(sort: [MovieConnectionSort!]): MovieConnection + connection(sort: MovieConnectionSort): MovieConnection } input MovieSort { diff --git a/packages/graphql/tests/api-v6/tck/relationship.test.ts b/packages/graphql/tests/api-v6/tck/relationship.test.ts index 5d0eb2f07a..0c6b3e314b 100644 --- a/packages/graphql/tests/api-v6/tck/relationship.test.ts +++ b/packages/graphql/tests/api-v6/tck/relationship.test.ts @@ -28,11 +28,14 @@ describe("Relationship", () => { typeDefs = /* GraphQL */ ` type Movie @node { title: String - actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN) + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") } type Actor @node { name: String - movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + type ActedIn @relationshipProperties { + year: Int } `; @@ -41,7 +44,7 @@ describe("Relationship", () => { }); }); - test("Simple relationship", async () => { + test("should query a relationship", async () => { const query = /* GraphQL */ ` query { movies { @@ -96,4 +99,60 @@ describe("Relationship", () => { 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 }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + RETURN collect({ properties: { year: this1.year }, node: { __id: id(actors), __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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..a1bcc2a632 --- /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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + ORDER BY actors.name DESC + RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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 } }, { 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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + ORDER BY actors.name DESC, actors.age DESC + RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + ORDER BY this1.year DESC + RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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 } } + { node: { name: ASC } } + { 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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + ORDER BY this1.year DESC, actors.name ASC, this1.role ASC + RETURN collect({ node: { age: actors.age, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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 index 7bcdd42eee..b382d40caa 100644 --- a/packages/graphql/tests/api-v6/tck/sort/sort.test.ts +++ b/packages/graphql/tests/api-v6/tck/sort/sort.test.ts @@ -27,7 +27,7 @@ describe("Sort", () => { beforeAll(() => { typeDefs = /* GraphQL */ ` type Movie @node { - title: String! + title: String ratings: Int! } `; @@ -76,7 +76,7 @@ describe("Sort", () => { const query = /* GraphQL */ ` query { movies { - connection(sort: [{ edges: { node: { title: DESC } } }, { edges: { node: { ratings: DESC } } }]) { + connection(sort: { edges: [{ node: { title: DESC } }, { node: { ratings: DESC } }] }) { edges { node { title From 392548eb02ed8400dc5174c6556556b1d996d8dc Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 10:59:12 +0100 Subject: [PATCH 012/177] Improve naming of translation layer --- .../ReadOperationFactory.ts | 1 - .../resolve-tree-parser/ResolveTreeParser.ts | 16 ++++++++++------ .../resolve-tree-parser/find-field-by-name.ts | 0 .../resolve-tree-parser/graphql-tree.ts | 0 ...raSchemaGenerator.ts => SchemaGenerator.ts} | 2 +- .../api-v6/schema/schema-types/EntityTypes.ts | 18 +++++++++--------- .../schema/schema-types/NestedEntityTypes.ts | 18 +++++++++--------- .../schema/schema-types/TopLevelEntityTypes.ts | 14 +++++++------- .../translators/translate-read-operation.ts | 2 +- packages/graphql/src/classes/Neo4jGraphQL.ts | 6 +++--- .../src/schema-model/entity/ConcreteEntity.ts | 2 +- .../schema-model/relationship/Relationship.ts | 4 ++-- 12 files changed, 43 insertions(+), 40 deletions(-) rename packages/graphql/src/api-v6/{queryASTFactory => queryIRFactory}/ReadOperationFactory.ts (99%) rename packages/graphql/src/api-v6/{queryASTFactory => queryIRFactory}/resolve-tree-parser/ResolveTreeParser.ts (95%) rename packages/graphql/src/api-v6/{queryASTFactory => queryIRFactory}/resolve-tree-parser/find-field-by-name.ts (100%) rename packages/graphql/src/api-v6/{queryASTFactory => queryIRFactory}/resolve-tree-parser/graphql-tree.ts (100%) rename packages/graphql/src/api-v6/schema/{AuraSchemaGenerator.ts => SchemaGenerator.ts} (98%) diff --git a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts similarity index 99% rename from packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts rename to packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 51955ca917..d92cbb0ce2 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -228,7 +228,6 @@ export class ReadOperationFactory { }); } - private generateRelationshipField( resolveTree: GraphQLTreeReadOperation, relationship: Relationship diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts similarity index 95% rename from packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts rename to packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 9e76121117..23e7a8efe2 100644 --- a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -54,7 +54,11 @@ abstract class ResolveTreeParser { /** Parse a resolveTree into a Neo4j GraphQLTree */ public parseOperation(resolveTree: ResolveTree): GraphQLTreeReadOperation { - const connectionResolveTree = findFieldByName(resolveTree, this.entity.types.connectionOperation, "connection"); + const connectionResolveTree = findFieldByName( + resolveTree, + this.entity.typeNames.connectionOperation, + "connection" + ); const connection = connectionResolveTree ? this.parseConnection(connectionResolveTree) : undefined; @@ -70,7 +74,7 @@ abstract class ResolveTreeParser { protected abstract get targetNode(): ConcreteEntity; private parseConnection(resolveTree: ResolveTree): GraphQLTreeConnection { - const entityTypes = this.entity.types; + const entityTypes = this.entity.typeNames; const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connectionType, "edges"); const edgeResolveTree = edgesResolveTree ? this.parseEdges(edgesResolveTree) : undefined; const connectionArgs = this.parseConnectionArgs(resolveTree.args); @@ -84,7 +88,7 @@ abstract class ResolveTreeParser { } private parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { - const edgeType = this.entity.types.edgeType; + const edgeType = this.entity.typeNames.edgeType; const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); const resolveTreeProperties = findFieldByName(resolveTree, edgeType, "properties"); @@ -103,7 +107,7 @@ abstract class ResolveTreeParser { } private parseNode(resolveTree: ResolveTree): GraphQLTreeNode { - const entityTypes = this.targetNode.types; + const entityTypes = this.targetNode.typeNames; const fieldsResolveTree = resolveTree.fieldsByTypeName[entityTypes.nodeType] ?? {}; const fields = this.getNodeFields(fieldsResolveTree); @@ -133,10 +137,10 @@ abstract class ResolveTreeParser { } private parseEdgeProperties(resolveTree: ResolveTree): GraphQLTreeEdgeProperties | undefined { - if (!this.entity.types.propertiesType) { + if (!this.entity.typeNames.propertiesType) { return; } - const fieldsResolveTree = resolveTree.fieldsByTypeName[this.entity.types.propertiesType] ?? {}; + const fieldsResolveTree = resolveTree.fieldsByTypeName[this.entity.typeNames.propertiesType] ?? {}; const fields = this.getEdgePropertyFields(fieldsResolveTree); diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/find-field-by-name.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/find-field-by-name.ts similarity index 100% rename from packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/find-field-by-name.ts rename to packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/find-field-by-name.ts diff --git a/packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts similarity index 100% rename from packages/graphql/src/api-v6/queryASTFactory/resolve-tree-parser/graphql-tree.ts rename to packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts diff --git a/packages/graphql/src/api-v6/schema/AuraSchemaGenerator.ts b/packages/graphql/src/api-v6/schema/SchemaGenerator.ts similarity index 98% rename from packages/graphql/src/api-v6/schema/AuraSchemaGenerator.ts rename to packages/graphql/src/api-v6/schema/SchemaGenerator.ts index 619def22c2..f7fb0c67ac 100644 --- a/packages/graphql/src/api-v6/schema/AuraSchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema/SchemaGenerator.ts @@ -25,7 +25,7 @@ import { SchemaBuilder } from "./SchemaBuilder"; import { StaticTypes } from "./schema-types/StaticTypes"; import { TopLevelEntityTypes } from "./schema-types/TopLevelEntityTypes"; -export class AuraSchemaGenerator { +export class SchemaGenerator { private schemaBuilder: SchemaBuilder; constructor() { diff --git a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts index dee34e42f8..db976160cd 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts @@ -27,26 +27,26 @@ import type { StaticTypes } from "./StaticTypes"; /** This class defines the GraphQL types for an entity */ export abstract class EntityTypes { protected schemaBuilder: SchemaBuilder; - protected entityTypes: T; + protected entityTypeNames: T; protected staticTypes: StaticTypes; constructor({ schemaBuilder, - entityTypes, + entityTypeNames, staticTypes, }: { schemaBuilder: SchemaBuilder; staticTypes: StaticTypes; - entityTypes: T; + entityTypeNames: T; }) { this.schemaBuilder = schemaBuilder; - this.entityTypes = entityTypes; + this.entityTypeNames = entityTypeNames; this.staticTypes = staticTypes; } @Memoize() public get connectionOperation(): ObjectTypeComposer { - return this.schemaBuilder.createObjectType(this.entityTypes.connectionOperation, { + return this.schemaBuilder.createObjectType(this.entityTypeNames.connectionOperation, { connection: { type: this.connection, args: this.connectionArgs, @@ -56,7 +56,7 @@ export abstract class EntityTypes { @Memoize() public get connection(): ObjectTypeComposer { - return this.schemaBuilder.createObjectType(this.entityTypes.connectionType, { + return this.schemaBuilder.createObjectType(this.entityTypeNames.connectionType, { pageInfo: this.staticTypes.pageInfo, edges: this.edge.List, }); @@ -71,7 +71,7 @@ export abstract class EntityTypes { @Memoize() public get connectionSort(): InputTypeComposer { - return this.schemaBuilder.createInputObjectType(this.entityTypes.connectionSortType, { + return this.schemaBuilder.createInputObjectType(this.entityTypeNames.connectionSortType, { edges: this.edgeSort.NonNull.List, }); } @@ -86,7 +86,7 @@ export abstract class EntityTypes { edgeSortFields["properties"] = properties; } - return this.schemaBuilder.createInputObjectType(this.entityTypes.edgeSortType, edgeSortFields); + return this.schemaBuilder.createInputObjectType(this.entityTypeNames.edgeSortType, edgeSortFields); } @Memoize() @@ -106,7 +106,7 @@ export abstract class EntityTypes { fields["properties"] = properties; } - return this.schemaBuilder.createObjectType(this.entityTypes.edgeType, fields); + return this.schemaBuilder.createObjectType(this.entityTypeNames.edgeType, fields); } protected abstract getEdgeProperties(): ObjectTypeComposer | undefined; diff --git a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts index a2cbf2bc5a..70393e994a 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts @@ -35,17 +35,17 @@ export class NestedEntitySchemaTypes extends EntityTypes constructor({ relationship, schemaBuilder, - entityTypes, + entityTypeNames, staticTypes, }: { schemaBuilder: SchemaBuilder; relationship: Relationship; staticTypes: StaticTypes; - entityTypes: NestedEntityTypeNames; + entityTypeNames: NestedEntityTypeNames; }) { super({ schemaBuilder, - entityTypes, + entityTypeNames, staticTypes, }); this.relationship = relationship; @@ -57,7 +57,7 @@ export class NestedEntitySchemaTypes extends EntityTypes if (!(target instanceof ConcreteEntity)) { throw new Error("Interfaces not supported yet"); } - return target.types.nodeType; + return target.typeNames.nodeType; } @Memoize() public get nodeSortType(): string { @@ -65,22 +65,22 @@ export class NestedEntitySchemaTypes extends EntityTypes if (!(target instanceof ConcreteEntity)) { throw new Error("Interfaces not supported yet"); } - return target.types.nodeSortType; + return target.typeNames.nodeSortType; } @Memoize() protected getEdgeProperties(): ObjectTypeComposer | undefined { - if (this.entityTypes.propertiesType) { + if (this.entityTypeNames.propertiesType) { const fields = this.getRelationshipFields(); - return this.schemaBuilder.getOrCreateObjectType(this.entityTypes.propertiesType, fields); + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.propertiesType, fields); } } @Memoize() protected getEdgeSortProperties(): InputTypeComposer | undefined { - if (this.entityTypes.propertiesSortType) { + if (this.entityTypeNames.propertiesSortType) { const fields = this.getRelationshipSortFields(); - return this.schemaBuilder.getOrCreateInputObjectType(this.entityTypes.propertiesSortType, fields); + return this.schemaBuilder.getOrCreateInputObjectType(this.entityTypeNames.propertiesSortType, fields); } } diff --git a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts index 6c4519e5c3..c0c1752ecd 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts @@ -43,22 +43,22 @@ export class TopLevelEntityTypes extends EntityTypes { }) { super({ schemaBuilder, - entityTypes: entity.types, + entityTypeNames: entity.typeNames, staticTypes, }); this.entity = entity; } public get queryFieldName(): string { - return this.entity.types.queryField; + return this.entity.typeNames.queryField; } @Memoize() public get nodeType(): string { const fields = this.getNodeFieldsDefinitions(); const relationships = this.getRelationshipFields(); - this.schemaBuilder.createObjectType(this.entity.types.nodeType, { ...fields, ...relationships }); - return this.entity.types.nodeType; + this.schemaBuilder.createObjectType(this.entity.typeNames.nodeType, { ...fields, ...relationships }); + return this.entity.typeNames.nodeType; } protected getEdgeProperties(): undefined { @@ -82,8 +82,8 @@ export class TopLevelEntityTypes extends EntityTypes { @Memoize() public get nodeSortType(): string { - this.schemaBuilder.createInputObjectType(this.entity.types.nodeSortType, this.sortFields); - return this.entity.types.nodeSortType; + this.schemaBuilder.createInputObjectType(this.entity.typeNames.nodeSortType, this.sortFields); + return this.entity.typeNames.nodeSortType; } @Memoize() @@ -93,7 +93,7 @@ export class TopLevelEntityTypes extends EntityTypes { const relationshipTypes = new NestedEntitySchemaTypes({ schemaBuilder: this.schemaBuilder, relationship, - entityTypes: this.entity.types.relationship(relationship), + entityTypeNames: this.entity.typeNames.relationship(relationship), staticTypes: this.staticTypes, }); const relationshipType = relationshipTypes.connectionOperation; diff --git a/packages/graphql/src/api-v6/translators/translate-read-operation.ts b/packages/graphql/src/api-v6/translators/translate-read-operation.ts index c31d9c77ab..4c52043a05 100644 --- a/packages/graphql/src/api-v6/translators/translate-read-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-read-operation.ts @@ -22,7 +22,7 @@ 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 "../queryASTFactory/ReadOperationFactory"; +import { ReadOperationFactory } from "../queryIRFactory/ReadOperationFactory"; const debug = Debug(DEBUG_TRANSLATE); diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index 2e1b43155b..a792362234 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -25,7 +25,7 @@ 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 { AuraSchemaGenerator } from "../api-v6/schema/AuraSchemaGenerator"; +import { SchemaGenerator } from "../api-v6/schema/SchemaGenerator"; import { DEBUG_ALL } from "../constants"; import { makeAugmentedSchema } from "../schema"; import type { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; @@ -119,12 +119,12 @@ class Neo4jGraphQL { public getAuraSchema(): Promise { const document = this.normalizeTypeDefinitions(this.typeDefs); this.schemaModel = this.generateSchemaModel(document, true); - const auraSchemaGenerator = new AuraSchemaGenerator(); + const schemaGenerator = new SchemaGenerator(); this._nodes = []; this._relationships = []; - return Promise.resolve(this.composeSchema(auraSchemaGenerator.generate(this.schemaModel))); + return Promise.resolve(this.composeSchema(schemaGenerator.generate(this.schemaModel))); } public async getExecutableSchema(): Promise { diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index 8b0578c441..16a5e52e5c 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -70,7 +70,7 @@ export class ConcreteEntity implements Entity { } /** Note: Types of the new API */ - public get types(): TopLevelEntityTypeNames { + public get typeNames(): TopLevelEntityTypeNames { return new TopLevelEntityTypeNames(this); } diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index c9bf1c80ef..eb76d3d8fd 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -142,11 +142,11 @@ export class Relationship { } /** Note: Types of the new API */ - public get types(): NestedEntityTypeNames { + public get typeNames(): NestedEntityTypeNames { if (!(this.source instanceof ConcreteEntity)) { throw new Error("Interfaces not supported"); } - return this.source.types.relationship(this); + return this.source.typeNames.relationship(this); } private addAttribute(attribute: Attribute): void { From 2a9682dc32a61cf1514c6e563098bf17b28a9e7b Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 11:12:09 +0100 Subject: [PATCH 013/177] Change dependencies of resolve-tree-parser and factory --- .../queryIRFactory/ReadOperationFactory.ts | 19 +++++++++++-------- .../translators/translate-read-operation.ts | 6 +++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index d92cbb0ce2..8d4ae74da7 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import type { ResolveTree } from "graphql-parse-resolve-info"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; import { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; @@ -33,7 +32,6 @@ import { RelationshipSelection } from "../../translate/queryAST/ast/selection/Re import { PropertySort } from "../../translate/queryAST/ast/sort/PropertySort"; import { filterTruthy } from "../../utils/utils"; import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; -import { parseResolveInfoTree } from "./resolve-tree-parser/ResolveTreeParser"; import type { GraphQLSortArgument, GraphQLTree, @@ -50,23 +48,28 @@ export class ReadOperationFactory { this.schemaModel = schemaModel; } - public createAST({ resolveTree, entity }: { resolveTree: ResolveTree; entity: ConcreteEntity }): QueryAST { - const parsedTree = parseResolveInfoTree({ resolveTree, entity }); + public createAST({ + graphQLTree, + entity, + }: { + graphQLTree: GraphQLTreeReadOperation; + entity: ConcreteEntity; + }): QueryAST { const operation = this.generateOperation({ - parsedTree, + graphQLTree, entity, }); return new QueryAST(operation); } private generateOperation({ - parsedTree, + graphQLTree, entity, }: { - parsedTree: GraphQLTree; + graphQLTree: GraphQLTree; entity: ConcreteEntity; }): V6ReadOperation { - const connectionTree = parsedTree.fields.connection; + const connectionTree = graphQLTree.fields.connection; if (!connectionTree) { throw new Error("No Connection"); } diff --git a/packages/graphql/src/api-v6/translators/translate-read-operation.ts b/packages/graphql/src/api-v6/translators/translate-read-operation.ts index 4c52043a05..4a53dac4a7 100644 --- a/packages/graphql/src/api-v6/translators/translate-read-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-read-operation.ts @@ -23,6 +23,7 @@ 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 { parseResolveInfoTree } from "../queryIRFactory/resolve-tree-parser/ResolveTreeParser"; const debug = Debug(DEBUG_TRANSLATE); @@ -34,7 +35,10 @@ export function translateReadOperation({ entity: ConcreteEntity; }): Cypher.CypherResult { const readFactory = new ReadOperationFactory(context.schemaModel); - const readOperation = readFactory.createAST({ resolveTree: context.resolveTree, entity }); + + const parsedTree = parseResolveInfoTree({ resolveTree: context.resolveTree, entity }); + + const readOperation = readFactory.createAST({ graphQLTree: parsedTree, entity }); debug(readOperation.print()); const results = readOperation.build(context); return results.build(); From b928ebab5e8f5b41a77beef707a744ba1ee55498 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 11:28:12 +0100 Subject: [PATCH 014/177] Types improvements --- .../src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts | 6 ------ .../src/api-v6/schema/schema-types/TopLevelEntityTypes.ts | 2 +- packages/graphql/src/schema-model/entity/ConcreteEntity.ts | 2 ++ .../graphql/src/schema-model/relationship/Relationship.ts | 7 +++++-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts index 0288b7541d..74f0d8e237 100644 --- a/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts @@ -18,9 +18,7 @@ */ import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; -import type { Relationship } from "../../schema-model/relationship/Relationship"; import { EntityTypeNames } from "./EntityTypeNames"; -import { NestedEntityTypeNames } from "./NestedEntityTypeNames"; export class TopLevelEntityTypeNames extends EntityTypeNames { private concreteEntity: ConcreteEntity; @@ -30,10 +28,6 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { this.concreteEntity = concreteEntity; } - public relationship(relationship: Relationship): NestedEntityTypeNames { - return new NestedEntityTypeNames(relationship); - } - public get propertiesType(): undefined { return; } diff --git a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts index c0c1752ecd..339f76ffa0 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts @@ -93,7 +93,7 @@ export class TopLevelEntityTypes extends EntityTypes { const relationshipTypes = new NestedEntitySchemaTypes({ schemaBuilder: this.schemaBuilder, relationship, - entityTypeNames: this.entity.typeNames.relationship(relationship), + entityTypeNames: relationship.typeNames, staticTypes: this.staticTypes, }); const relationshipType = relationshipTypes.connectionOperation; diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index 16a5e52e5c..79cfd8bec0 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { Memoize } from "typescript-memoize"; import { TopLevelEntityTypeNames } from "../../api-v6/graphQLTypeNames/TopLevelEntityTypeNames"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import { setsAreEqual } from "../../utils/sets-are-equal"; @@ -70,6 +71,7 @@ export class ConcreteEntity implements Entity { } /** Note: Types of the new API */ + @Memoize() public get typeNames(): TopLevelEntityTypeNames { return new TopLevelEntityTypeNames(this); } diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index eb76d3d8fd..97966238af 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -17,7 +17,8 @@ * limitations under the License. */ -import type { NestedEntityTypeNames } from "../../api-v6/graphQLTypeNames/NestedEntityTypeNames"; +import { Memoize } from "typescript-memoize"; +import { NestedEntityTypeNames } from "../../api-v6/graphQLTypeNames/NestedEntityTypeNames"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import type { RelationshipNestedOperationsOption, RelationshipQueryDirectionOption } from "../../constants"; import { upperFirst } from "../../utils/upper-first"; @@ -142,11 +143,13 @@ export class Relationship { } /** Note: Types of the new API */ + @Memoize() public get typeNames(): NestedEntityTypeNames { if (!(this.source instanceof ConcreteEntity)) { throw new Error("Interfaces not supported"); } - return this.source.typeNames.relationship(this); + + return new NestedEntityTypeNames(this); } private addAttribute(attribute: Attribute): void { From f6c1fc8e2c32159cbca170af86ec77237f07eb39 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 11:40:17 +0100 Subject: [PATCH 015/177] ResolveTree parser improvements --- .../graphQLTypeNames/EntityTypeNames.ts | 2 - .../TopLevelEntityTypeNames.ts | 8 - .../resolve-tree-parser/ResolveTreeParser.ts | 147 ++++++++++-------- 3 files changed, 85 insertions(+), 72 deletions(-) diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts index a4a675f7fa..d72641dc7a 100644 --- a/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts @@ -62,7 +62,5 @@ export abstract class EntityTypeNames { return plural(this.prefix); } - public abstract get propertiesType(): string | undefined; - public abstract get propertiesSortType(): string | undefined; public abstract get nodeSortType(): string; } diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts index 74f0d8e237..b69b247824 100644 --- a/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts @@ -28,14 +28,6 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { this.concreteEntity = concreteEntity; } - public get propertiesType(): undefined { - return; - } - - public get propertiesSortType(): undefined { - return; - } - public get nodeSortType(): string { return `${this.concreteEntity.name}Sort`; } diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 23e7a8efe2..4b41a8a98a 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -19,7 +19,7 @@ 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 { Relationship } from "../../../schema-model/relationship/Relationship"; import { findFieldByName } from "./find-field-by-name"; import type { GraphQLConnectionArgs, @@ -73,6 +73,21 @@ abstract class ResolveTreeParser { protected abstract get targetNode(): ConcreteEntity; + protected abstract parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge; + + protected parseAttributeField( + resolveTree: ResolveTree, + entity: ConcreteEntity | Relationship + ): GraphQLTreeLeafField | undefined { + if (entity.hasAttribute(resolveTree.name)) { + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: undefined, + }; + } + } + private parseConnection(resolveTree: ResolveTree): GraphQLTreeConnection { const entityTypes = this.entity.typeNames; const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connectionType, "edges"); @@ -87,26 +102,7 @@ abstract class ResolveTreeParser { }; } - private parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { - const edgeType = this.entity.typeNames.edgeType; - - const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); - const resolveTreeProperties = findFieldByName(resolveTree, edgeType, "properties"); - - const node = nodeResolveTree ? this.parseNode(nodeResolveTree) : undefined; - const properties = resolveTreeProperties ? this.parseEdgeProperties(resolveTreeProperties) : undefined; - - return { - alias: resolveTree.alias, - args: resolveTree.args, - fields: { - node: node, - properties: properties, - }, - }; - } - - private parseNode(resolveTree: ResolveTree): GraphQLTreeNode { + protected parseNode(resolveTree: ResolveTree): GraphQLTreeNode { const entityTypes = this.targetNode.typeNames; const fieldsResolveTree = resolveTree.fieldsByTypeName[entityTypes.nodeType] ?? {}; @@ -136,47 +132,6 @@ abstract class ResolveTreeParser { return propertyFields; } - private parseEdgeProperties(resolveTree: ResolveTree): GraphQLTreeEdgeProperties | undefined { - if (!this.entity.typeNames.propertiesType) { - return; - } - const fieldsResolveTree = resolveTree.fieldsByTypeName[this.entity.typeNames.propertiesType] ?? {}; - - const fields = this.getEdgePropertyFields(fieldsResolveTree); - - return { - alias: resolveTree.alias, - args: resolveTree.args, - fields: fields, - }; - } - - private getEdgePropertyFields(fields: Record): Record { - const propertyFields: Record = {}; - for (const fieldResolveTree of Object.values(fields)) { - const fieldName = fieldResolveTree.name; - const field = this.parseAttributeField(fieldResolveTree, this.entity); - if (!field) { - throw new ResolveTreeParserError(`${fieldName} is not an attribute of edge`); - } - propertyFields[fieldName] = field; - } - return propertyFields; - } - - private parseAttributeField( - resolveTree: ResolveTree, - entity: ConcreteEntity | Relationship - ): GraphQLTreeLeafField | undefined { - if (entity.hasAttribute(resolveTree.name)) { - return { - alias: resolveTree.alias, - args: resolveTree.args, - fields: undefined, - }; - } - } - private parseRelationshipField( resolveTree: ResolveTree, entity: ConcreteEntity @@ -252,12 +207,80 @@ class TopLevelTreeParser extends ResolveTreeParser { protected get targetNode(): ConcreteEntity { return this.entity; } + + protected parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { + const edgeType = this.entity.typeNames.edgeType; + + const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); + + const node = nodeResolveTree ? this.parseNode(nodeResolveTree) : undefined; + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: { + node: node, + properties: undefined, + }, + }; + } } class RelationshipResolveTreeParser extends ResolveTreeParser { protected get targetNode(): ConcreteEntity { return this.entity.target as ConcreteEntity; } + + protected parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { + const edgeType = this.entity.typeNames.edgeType; + + const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); + const resolveTreeProperties = findFieldByName(resolveTree, edgeType, "properties"); + + const node = nodeResolveTree ? this.parseNode(nodeResolveTree) : undefined; + const properties = resolveTreeProperties ? this.parseEdgeProperties(resolveTreeProperties) : undefined; + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: { + node: node, + properties: properties, + }, + }; + } + + private parseEdgeProperties(resolveTree: ResolveTree): GraphQLTreeEdgeProperties | undefined { + if (!(this.entity instanceof Relationship)) { + return; + } + + if (!this.entity.typeNames.propertiesType) { + return; + } + const fieldsResolveTree = resolveTree.fieldsByTypeName[this.entity.typeNames.propertiesType] ?? {}; + + const fields = this.getEdgePropertyFields(fieldsResolveTree); + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: fields, + }; + } + + private getEdgePropertyFields(fields: Record): Record { + const propertyFields: Record = {}; + for (const fieldResolveTree of Object.values(fields)) { + const fieldName = fieldResolveTree.name; + const field = this.parseAttributeField(fieldResolveTree, this.entity); + if (!field) { + throw new ResolveTreeParserError(`${fieldName} is not an attribute of edge`); + } + propertyFields[fieldName] = field; + } + return propertyFields; + } } class ResolveTreeParserError extends Error {} From 3555ceb33385eda144c2d54d73bceef97429a1da Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 11:48:38 +0100 Subject: [PATCH 016/177] Minor improvements to SchemaBuilder --- .../resolve-tree-parser/ResolveTreeParser.ts | 4 ++-- packages/graphql/src/api-v6/schema/SchemaBuilder.ts | 12 ++---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 4b41a8a98a..86f6c4030f 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -157,10 +157,10 @@ abstract class ResolveTreeParser { } private parseSortEdges( - sortEdges: { + sortEdges: Array<{ node: Record | undefined; properties: Record | undefined; - }[] + }> ): GraphQLSortEdgeArgument[] { return sortEdges.map((edge) => { const sortFields: GraphQLSortEdgeArgument = {}; diff --git a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema/SchemaBuilder.ts index ae94c38f63..7930eab0b8 100644 --- a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema/SchemaBuilder.ts @@ -58,7 +58,7 @@ export class SchemaBuilder { public createObjectType( name: string, - fields?: Record>, + fields: Record>, description?: string ): ObjectTypeComposer { return this.composer.createObjectTC({ @@ -70,7 +70,7 @@ export class SchemaBuilder { public getOrCreateObjectType( name: string, - fields?: Record>, + fields: Record>, description?: string ): ObjectTypeComposer { return this.composer.getOrCreateOTC(name, (tc) => { @@ -122,10 +122,6 @@ export class SchemaBuilder { }); } - public addFieldToType(type: ObjectTypeComposer, fields: Record): void { - type.addFields(fields); - } - public addQueryField(name: string, type: ObjectTypeComposer | string, resolver: (...args: any[]) => any): void { this.composer.Query.addFields({ [name]: { @@ -135,10 +131,6 @@ export class SchemaBuilder { }); } - public getObjectType(typeName: string): ObjectTypeComposer { - return this.composer.getOTC(typeName); - } - public build(): GraphQLSchema { return this.composer.buildSchema(); } From 64e0e6c630737239dac464ec207b6c3a43f9f99a Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 13:41:43 +0100 Subject: [PATCH 017/177] Rename graphql-type-names --- .../{graphQLTypeNames => graphql-type-names}/EntityTypeNames.ts | 0 .../NestedEntityTypeNames.ts | 0 .../TopLevelEntityTypeNames.ts | 0 packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts | 2 +- .../graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts | 2 +- .../src/api-v6/schema/schema-types/TopLevelEntityTypes.ts | 2 +- packages/graphql/src/schema-model/entity/ConcreteEntity.ts | 2 +- packages/graphql/src/schema-model/relationship/Relationship.ts | 2 +- 8 files changed, 5 insertions(+), 5 deletions(-) rename packages/graphql/src/api-v6/{graphQLTypeNames => graphql-type-names}/EntityTypeNames.ts (100%) rename packages/graphql/src/api-v6/{graphQLTypeNames => graphql-type-names}/NestedEntityTypeNames.ts (100%) rename packages/graphql/src/api-v6/{graphQLTypeNames => graphql-type-names}/TopLevelEntityTypeNames.ts (100%) diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts b/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts similarity index 100% rename from packages/graphql/src/api-v6/graphQLTypeNames/EntityTypeNames.ts rename to packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.ts b/packages/graphql/src/api-v6/graphql-type-names/NestedEntityTypeNames.ts similarity index 100% rename from packages/graphql/src/api-v6/graphQLTypeNames/NestedEntityTypeNames.ts rename to packages/graphql/src/api-v6/graphql-type-names/NestedEntityTypeNames.ts diff --git a/packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts similarity index 100% rename from packages/graphql/src/api-v6/graphQLTypeNames/TopLevelEntityTypeNames.ts rename to packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts diff --git a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts index db976160cd..d369429ec2 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts @@ -20,7 +20,7 @@ import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; -import type { EntityTypeNames } from "../../graphQLTypeNames/EntityTypeNames"; +import type { EntityTypeNames } from "../../graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import type { StaticTypes } from "./StaticTypes"; diff --git a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts index 70393e994a..5ecbd5d324 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts @@ -24,7 +24,7 @@ import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; -import type { NestedEntityTypeNames } from "../../graphQLTypeNames/NestedEntityTypeNames"; +import type { NestedEntityTypeNames } from "../../graphql-type-names/NestedEntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import { EntityTypes } from "./EntityTypes"; import type { StaticTypes } from "./StaticTypes"; diff --git a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts index 339f76ffa0..98036c5ec3 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts @@ -23,7 +23,7 @@ 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 { attributeAdapterToComposeFields } from "../../../schema/to-compose"; -import type { EntityTypeNames } from "../../graphQLTypeNames/EntityTypeNames"; +import type { EntityTypeNames } from "../../graphql-type-names/EntityTypeNames"; import type { FieldDefinition, SchemaBuilder } from "../SchemaBuilder"; import { EntityTypes } from "./EntityTypes"; import { NestedEntitySchemaTypes } from "./NestedEntityTypes"; diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index 79cfd8bec0..df0be984e1 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -18,7 +18,7 @@ */ import { Memoize } from "typescript-memoize"; -import { TopLevelEntityTypeNames } from "../../api-v6/graphQLTypeNames/TopLevelEntityTypeNames"; +import { TopLevelEntityTypeNames } from "../../api-v6/graphql-type-names/TopLevelEntityTypeNames"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import { setsAreEqual } from "../../utils/sets-are-equal"; import type { Annotations } from "../annotation/Annotation"; diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index 97966238af..ab4eb66a65 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -18,7 +18,7 @@ */ import { Memoize } from "typescript-memoize"; -import { NestedEntityTypeNames } from "../../api-v6/graphQLTypeNames/NestedEntityTypeNames"; +import { NestedEntityTypeNames } from "../../api-v6/graphql-type-names/NestedEntityTypeNames"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import type { RelationshipNestedOperationsOption, RelationshipQueryDirectionOption } from "../../constants"; import { upperFirst } from "../../utils/upper-first"; From 21b2572087436e6400990ddfb83cf25e528ff231 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 13:47:43 +0100 Subject: [PATCH 018/177] Rename properties of typename classes --- .../graphql-type-names/EntityTypeNames.ts | 14 +++++++------- .../NestedEntityTypeNames.ts | 6 +++--- .../TopLevelEntityTypeNames.ts | 2 +- .../resolve-tree-parser/ResolveTreeParser.ts | 19 +++++++------------ .../api-v6/schema/schema-types/EntityTypes.ts | 8 ++++---- .../schema/schema-types/NestedEntityTypes.ts | 12 ++++++------ .../schema-types/TopLevelEntityTypes.ts | 8 ++++---- 7 files changed, 32 insertions(+), 37 deletions(-) diff --git a/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts b/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts index d72641dc7a..0f74dd6770 100644 --- a/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts @@ -30,27 +30,27 @@ export abstract class EntityTypeNames { return `${this.prefix}Operation`; } - public get connectionType(): string { + public get connection(): string { return `${this.prefix}Connection`; } - public get connectionSortType(): string { + public get connectionSort(): string { return `${this.prefix}ConnectionSort`; } - public get edgeSortType(): string { + public get edgeSort(): string { return `${this.prefix}EdgeSort`; } - public get edgeType(): string { + public get edge(): string { return `${this.prefix}Edge`; } - public get nodeType(): string { + public get node(): string { return `${this.prefix}`; } - public get whereInputTypeName(): string { + public get whereInput(): string { return `${this.prefix}Where`; } @@ -62,5 +62,5 @@ export abstract class EntityTypeNames { return plural(this.prefix); } - public abstract get nodeSortType(): string; + public abstract get nodeSort(): string; } diff --git a/packages/graphql/src/api-v6/graphql-type-names/NestedEntityTypeNames.ts b/packages/graphql/src/api-v6/graphql-type-names/NestedEntityTypeNames.ts index 1c253d8160..bf657ffae5 100644 --- a/packages/graphql/src/api-v6/graphql-type-names/NestedEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphql-type-names/NestedEntityTypeNames.ts @@ -29,18 +29,18 @@ export class NestedEntityTypeNames extends EntityTypeNames { this.relationship = relationship; } - public get propertiesType(): string | undefined { + public get properties(): string | undefined { return this.relationship.propertiesTypeName; } - public get propertiesSortType(): string | undefined { + public get propertiesSort(): string | undefined { if (!this.relationship.propertiesTypeName) { return; } return `${this.relationship.propertiesTypeName}Sort`; } - public get nodeSortType(): string { + public get nodeSort(): string { return `${this.relationship.target.name}Sort`; } } diff --git a/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts index b69b247824..1c32a53175 100644 --- a/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts @@ -28,7 +28,7 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { this.concreteEntity = concreteEntity; } - public get nodeSortType(): string { + public get nodeSort(): string { return `${this.concreteEntity.name}Sort`; } } diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 86f6c4030f..6acd5622aa 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -19,7 +19,7 @@ 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 { Relationship } from "../../../schema-model/relationship/Relationship"; import { findFieldByName } from "./find-field-by-name"; import type { GraphQLConnectionArgs, @@ -72,7 +72,6 @@ abstract class ResolveTreeParser { } protected abstract get targetNode(): ConcreteEntity; - protected abstract parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge; protected parseAttributeField( @@ -90,7 +89,7 @@ abstract class ResolveTreeParser { private parseConnection(resolveTree: ResolveTree): GraphQLTreeConnection { const entityTypes = this.entity.typeNames; - const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connectionType, "edges"); + const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges"); const edgeResolveTree = edgesResolveTree ? this.parseEdges(edgesResolveTree) : undefined; const connectionArgs = this.parseConnectionArgs(resolveTree.args); return { @@ -104,7 +103,7 @@ abstract class ResolveTreeParser { protected parseNode(resolveTree: ResolveTree): GraphQLTreeNode { const entityTypes = this.targetNode.typeNames; - const fieldsResolveTree = resolveTree.fieldsByTypeName[entityTypes.nodeType] ?? {}; + const fieldsResolveTree = resolveTree.fieldsByTypeName[entityTypes.node] ?? {}; const fields = this.getNodeFields(fieldsResolveTree); @@ -209,7 +208,7 @@ class TopLevelTreeParser extends ResolveTreeParser { } protected parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { - const edgeType = this.entity.typeNames.edgeType; + const edgeType = this.entity.typeNames.edge; const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); @@ -232,7 +231,7 @@ class RelationshipResolveTreeParser extends ResolveTreeParser { } protected parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { - const edgeType = this.entity.typeNames.edgeType; + const edgeType = this.entity.typeNames.edge; const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); const resolveTreeProperties = findFieldByName(resolveTree, edgeType, "properties"); @@ -251,14 +250,10 @@ class RelationshipResolveTreeParser extends ResolveTreeParser { } private parseEdgeProperties(resolveTree: ResolveTree): GraphQLTreeEdgeProperties | undefined { - if (!(this.entity instanceof Relationship)) { - return; - } - - if (!this.entity.typeNames.propertiesType) { + if (!this.entity.typeNames.properties) { return; } - const fieldsResolveTree = resolveTree.fieldsByTypeName[this.entity.typeNames.propertiesType] ?? {}; + const fieldsResolveTree = resolveTree.fieldsByTypeName[this.entity.typeNames.properties] ?? {}; const fields = this.getEdgePropertyFields(fieldsResolveTree); diff --git a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts index d369429ec2..00f9b59c76 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts @@ -56,7 +56,7 @@ export abstract class EntityTypes { @Memoize() public get connection(): ObjectTypeComposer { - return this.schemaBuilder.createObjectType(this.entityTypeNames.connectionType, { + return this.schemaBuilder.createObjectType(this.entityTypeNames.connection, { pageInfo: this.staticTypes.pageInfo, edges: this.edge.List, }); @@ -71,7 +71,7 @@ export abstract class EntityTypes { @Memoize() public get connectionSort(): InputTypeComposer { - return this.schemaBuilder.createInputObjectType(this.entityTypeNames.connectionSortType, { + return this.schemaBuilder.createInputObjectType(this.entityTypeNames.connectionSort, { edges: this.edgeSort.NonNull.List, }); } @@ -86,7 +86,7 @@ export abstract class EntityTypes { edgeSortFields["properties"] = properties; } - return this.schemaBuilder.createInputObjectType(this.entityTypeNames.edgeSortType, edgeSortFields); + return this.schemaBuilder.createInputObjectType(this.entityTypeNames.edgeSort, edgeSortFields); } @Memoize() @@ -106,7 +106,7 @@ export abstract class EntityTypes { fields["properties"] = properties; } - return this.schemaBuilder.createObjectType(this.entityTypeNames.edgeType, fields); + return this.schemaBuilder.createObjectType(this.entityTypeNames.edge, fields); } protected abstract getEdgeProperties(): ObjectTypeComposer | undefined; diff --git a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts index 5ecbd5d324..c632b860c7 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts @@ -57,7 +57,7 @@ export class NestedEntitySchemaTypes extends EntityTypes if (!(target instanceof ConcreteEntity)) { throw new Error("Interfaces not supported yet"); } - return target.typeNames.nodeType; + return target.typeNames.node; } @Memoize() public get nodeSortType(): string { @@ -65,22 +65,22 @@ export class NestedEntitySchemaTypes extends EntityTypes if (!(target instanceof ConcreteEntity)) { throw new Error("Interfaces not supported yet"); } - return target.typeNames.nodeSortType; + return target.typeNames.nodeSort; } @Memoize() protected getEdgeProperties(): ObjectTypeComposer | undefined { - if (this.entityTypeNames.propertiesType) { + if (this.entityTypeNames.properties) { const fields = this.getRelationshipFields(); - return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.propertiesType, fields); + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.properties, fields); } } @Memoize() protected getEdgeSortProperties(): InputTypeComposer | undefined { - if (this.entityTypeNames.propertiesSortType) { + if (this.entityTypeNames.propertiesSort) { const fields = this.getRelationshipSortFields(); - return this.schemaBuilder.getOrCreateInputObjectType(this.entityTypeNames.propertiesSortType, fields); + return this.schemaBuilder.getOrCreateInputObjectType(this.entityTypeNames.propertiesSort, fields); } } diff --git a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts index 98036c5ec3..bf92c98c07 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts @@ -57,8 +57,8 @@ export class TopLevelEntityTypes extends EntityTypes { public get nodeType(): string { const fields = this.getNodeFieldsDefinitions(); const relationships = this.getRelationshipFields(); - this.schemaBuilder.createObjectType(this.entity.typeNames.nodeType, { ...fields, ...relationships }); - return this.entity.typeNames.nodeType; + this.schemaBuilder.createObjectType(this.entity.typeNames.node, { ...fields, ...relationships }); + return this.entity.typeNames.node; } protected getEdgeProperties(): undefined { @@ -82,8 +82,8 @@ export class TopLevelEntityTypes extends EntityTypes { @Memoize() public get nodeSortType(): string { - this.schemaBuilder.createInputObjectType(this.entity.typeNames.nodeSortType, this.sortFields); - return this.entity.typeNames.nodeSortType; + this.schemaBuilder.createInputObjectType(this.entity.typeNames.nodeSort, this.sortFields); + return this.entity.typeNames.nodeSort; } @Memoize() From a59c8f41ea4bf6b830d16b781d9f8b92e165f06c Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 14:07:30 +0100 Subject: [PATCH 019/177] Improvements in typenames --- .../graphql-type-names/EntityTypeNames.ts | 50 ++++++------------- ...TypeNames.ts => RelatedEntityTypeNames.ts} | 36 ++++++++++--- .../TopLevelEntityTypeNames.ts | 33 +++++++++--- .../schema/schema-types/NestedEntityTypes.ts | 6 +-- .../schema-model/relationship/Relationship.ts | 6 +-- 5 files changed, 76 insertions(+), 55 deletions(-) rename packages/graphql/src/api-v6/graphql-type-names/{NestedEntityTypeNames.ts => RelatedEntityTypeNames.ts} (58%) diff --git a/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts b/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts index 0f74dd6770..eaa18fcc5b 100644 --- a/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts @@ -17,50 +17,28 @@ * limitations under the License. */ -import { plural } from "../../schema-model/utils/string-manipulation"; +import type { Entity } from "../../schema-model/entity/Entity"; +/** Abstract class to hold the typenames of a given entity */ export abstract class EntityTypeNames { - private readonly prefix: string; + protected readonly entityName: string; - constructor(prefix: string) { - this.prefix = prefix; - } - - public get connectionOperation(): string { - return `${this.prefix}Operation`; - } - - public get connection(): string { - return `${this.prefix}Connection`; - } - - public get connectionSort(): string { - return `${this.prefix}ConnectionSort`; - } - - public get edgeSort(): string { - return `${this.prefix}EdgeSort`; - } - - public get edge(): string { - return `${this.prefix}Edge`; + constructor(entity: Entity) { + this.entityName = entity.name; } public get node(): string { - return `${this.prefix}`; - } - - public get whereInput(): string { - return `${this.prefix}Where`; - } - - public get queryField(): string { - return this.plural; + return `${this.entityName}`; } - public get plural(): string { - return plural(this.prefix); + public get nodeSort(): string { + return `${this.entityName}Sort`; } - public abstract get nodeSort(): string; + public abstract get connectionOperation(): string; + public abstract get connection(): string; + public abstract get connectionSort(): string; + public abstract get edge(): string; + public abstract get edgeSort(): string; + public abstract get whereInput(): string; } diff --git a/packages/graphql/src/api-v6/graphql-type-names/NestedEntityTypeNames.ts b/packages/graphql/src/api-v6/graphql-type-names/RelatedEntityTypeNames.ts similarity index 58% rename from packages/graphql/src/api-v6/graphql-type-names/NestedEntityTypeNames.ts rename to packages/graphql/src/api-v6/graphql-type-names/RelatedEntityTypeNames.ts index bf657ffae5..f45047a5c9 100644 --- a/packages/graphql/src/api-v6/graphql-type-names/NestedEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphql-type-names/RelatedEntityTypeNames.ts @@ -21,14 +21,42 @@ import type { Relationship } from "../../schema-model/relationship/Relationship" import { upperFirst } from "../../utils/upper-first"; import { EntityTypeNames } from "./EntityTypeNames"; -export class NestedEntityTypeNames extends 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.source.name}${upperFirst(relationship.name)}`); + super(relationship.target); + + this.relatedEntityTypeName = `${relationship.source.name}${upperFirst(relationship.name)}`; this.relationship = relationship; } + public get connectionOperation(): string { + return `${this.relatedEntityTypeName}Operation`; + } + + 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 whereInput(): string { + return `${this.relatedEntityTypeName}Where`; + } + public get properties(): string | undefined { return this.relationship.propertiesTypeName; } @@ -39,8 +67,4 @@ export class NestedEntityTypeNames extends EntityTypeNames { } return `${this.relationship.propertiesTypeName}Sort`; } - - public get nodeSort(): string { - return `${this.relationship.target.name}Sort`; - } } diff --git a/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts index 1c32a53175..5b7a266764 100644 --- a/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts @@ -17,18 +17,37 @@ * limitations under the License. */ -import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import { plural } from "../../schema-model/utils/string-manipulation"; import { EntityTypeNames } from "./EntityTypeNames"; +/** Top level node typenames */ export class TopLevelEntityTypeNames extends EntityTypeNames { - private concreteEntity: ConcreteEntity; + /** Top Level Query field */ + public get queryField(): string { + return plural(this.entityName); + } + + public get connectionOperation(): string { + return `${this.entityName}Operation`; + } + + public get connection(): string { + return `${this.entityName}Connection`; + } + + public get connectionSort(): string { + return `${this.entityName}ConnectionSort`; + } + + public get edge(): string { + return `${this.entityName}Edge`; + } - constructor(concreteEntity: ConcreteEntity) { - super(concreteEntity.name); - this.concreteEntity = concreteEntity; + public get edgeSort(): string { + return `${this.entityName}EdgeSort`; } - public get nodeSort(): string { - return `${this.concreteEntity.name}Sort`; + public get whereInput(): string { + return `${this.entityName}Where`; } } diff --git a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts index c632b860c7..ca827fd935 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts @@ -24,12 +24,12 @@ import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; -import type { NestedEntityTypeNames } from "../../graphql-type-names/NestedEntityTypeNames"; +import type { RelatedEntityTypeNames } from "../../graphql-type-names/RelatedEntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import { EntityTypes } from "./EntityTypes"; import type { StaticTypes } from "./StaticTypes"; -export class NestedEntitySchemaTypes extends EntityTypes { +export class NestedEntitySchemaTypes extends EntityTypes { private relationship: Relationship; constructor({ @@ -41,7 +41,7 @@ export class NestedEntitySchemaTypes extends EntityTypes schemaBuilder: SchemaBuilder; relationship: Relationship; staticTypes: StaticTypes; - entityTypeNames: NestedEntityTypeNames; + entityTypeNames: RelatedEntityTypeNames; }) { super({ schemaBuilder, diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index ab4eb66a65..f4a689737b 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -18,7 +18,7 @@ */ import { Memoize } from "typescript-memoize"; -import { NestedEntityTypeNames } from "../../api-v6/graphql-type-names/NestedEntityTypeNames"; +import { RelatedEntityTypeNames } from "../../api-v6/graphql-type-names/RelatedEntityTypeNames"; import { Neo4jGraphQLSchemaValidationError } from "../../classes"; import type { RelationshipNestedOperationsOption, RelationshipQueryDirectionOption } from "../../constants"; import { upperFirst } from "../../utils/upper-first"; @@ -144,12 +144,12 @@ export class Relationship { /** Note: Types of the new API */ @Memoize() - public get typeNames(): NestedEntityTypeNames { + public get typeNames(): RelatedEntityTypeNames { if (!(this.source instanceof ConcreteEntity)) { throw new Error("Interfaces not supported"); } - return new NestedEntityTypeNames(this); + return new RelatedEntityTypeNames(this); } private addAttribute(attribute: Attribute): void { From b9eae7f02acfe4e8188b0f848ea465b5e8604c53 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 14:30:57 +0100 Subject: [PATCH 020/177] Rename folders for schema generation --- .../src/api-v6/{schema => schema-generation}/SchemaBuilder.ts | 0 .../api-v6/{schema => schema-generation}/SchemaGenerator.ts | 0 .../{schema => schema-generation}/schema-types/EntityTypes.ts | 2 +- .../schema-types/NestedEntityTypes.ts | 2 +- .../{schema => schema-generation}/schema-types/StaticTypes.ts | 0 .../schema-types/TopLevelEntityTypes.ts | 2 +- .../{ => schema-model}/graphql-type-names/EntityTypeNames.ts | 2 +- .../graphql-type-names/RelatedEntityTypeNames.ts | 4 ++-- .../graphql-type-names/TopLevelEntityTypeNames.ts | 2 +- packages/graphql/src/classes/Neo4jGraphQL.ts | 2 +- packages/graphql/src/schema-model/entity/ConcreteEntity.ts | 2 +- .../graphql/src/schema-model/relationship/Relationship.ts | 2 +- 12 files changed, 10 insertions(+), 10 deletions(-) rename packages/graphql/src/api-v6/{schema => schema-generation}/SchemaBuilder.ts (100%) rename packages/graphql/src/api-v6/{schema => schema-generation}/SchemaGenerator.ts (100%) rename packages/graphql/src/api-v6/{schema => schema-generation}/schema-types/EntityTypes.ts (97%) rename packages/graphql/src/api-v6/{schema => schema-generation}/schema-types/NestedEntityTypes.ts (97%) rename packages/graphql/src/api-v6/{schema => schema-generation}/schema-types/StaticTypes.ts (100%) rename packages/graphql/src/api-v6/{schema => schema-generation}/schema-types/TopLevelEntityTypes.ts (97%) rename packages/graphql/src/api-v6/{ => schema-model}/graphql-type-names/EntityTypeNames.ts (95%) rename packages/graphql/src/api-v6/{ => schema-model}/graphql-type-names/RelatedEntityTypeNames.ts (93%) rename packages/graphql/src/api-v6/{ => schema-model}/graphql-type-names/TopLevelEntityTypeNames.ts (95%) diff --git a/packages/graphql/src/api-v6/schema/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts similarity index 100% rename from packages/graphql/src/api-v6/schema/SchemaBuilder.ts rename to packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts diff --git a/packages/graphql/src/api-v6/schema/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts similarity index 100% rename from packages/graphql/src/api-v6/schema/SchemaGenerator.ts rename to packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts diff --git a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntityTypes.ts similarity index 97% rename from packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts rename to packages/graphql/src/api-v6/schema-generation/schema-types/EntityTypes.ts index 00f9b59c76..b5c2294bd4 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/EntityTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntityTypes.ts @@ -20,7 +20,7 @@ import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; -import type { EntityTypeNames } from "../../graphql-type-names/EntityTypeNames"; +import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import type { StaticTypes } from "./StaticTypes"; diff --git a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/NestedEntityTypes.ts similarity index 97% rename from packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts rename to packages/graphql/src/api-v6/schema-generation/schema-types/NestedEntityTypes.ts index ca827fd935..543335280d 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/NestedEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/NestedEntityTypes.ts @@ -24,7 +24,7 @@ import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; -import type { RelatedEntityTypeNames } from "../../graphql-type-names/RelatedEntityTypeNames"; +import type { RelatedEntityTypeNames } from "../../schema-model/graphql-type-names/RelatedEntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import { EntityTypes } from "./EntityTypes"; import type { StaticTypes } from "./StaticTypes"; diff --git a/packages/graphql/src/api-v6/schema/schema-types/StaticTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticTypes.ts similarity index 100% rename from packages/graphql/src/api-v6/schema/schema-types/StaticTypes.ts rename to packages/graphql/src/api-v6/schema-generation/schema-types/StaticTypes.ts diff --git a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntityTypes.ts similarity index 97% rename from packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts rename to packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntityTypes.ts index bf92c98c07..1c6e399beb 100644 --- a/packages/graphql/src/api-v6/schema/schema-types/TopLevelEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntityTypes.ts @@ -23,7 +23,7 @@ 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 { attributeAdapterToComposeFields } from "../../../schema/to-compose"; -import type { EntityTypeNames } from "../../graphql-type-names/EntityTypeNames"; +import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { FieldDefinition, SchemaBuilder } from "../SchemaBuilder"; import { EntityTypes } from "./EntityTypes"; import { NestedEntitySchemaTypes } from "./NestedEntityTypes"; diff --git a/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/EntityTypeNames.ts similarity index 95% rename from packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts rename to packages/graphql/src/api-v6/schema-model/graphql-type-names/EntityTypeNames.ts index eaa18fcc5b..2fa626c619 100644 --- a/packages/graphql/src/api-v6/graphql-type-names/EntityTypeNames.ts +++ b/packages/graphql/src/api-v6/schema-model/graphql-type-names/EntityTypeNames.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { Entity } from "../../schema-model/entity/Entity"; +import type { Entity } from "../../../schema-model/entity/Entity"; /** Abstract class to hold the typenames of a given entity */ export abstract class EntityTypeNames { diff --git a/packages/graphql/src/api-v6/graphql-type-names/RelatedEntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/RelatedEntityTypeNames.ts similarity index 93% rename from packages/graphql/src/api-v6/graphql-type-names/RelatedEntityTypeNames.ts rename to packages/graphql/src/api-v6/schema-model/graphql-type-names/RelatedEntityTypeNames.ts index f45047a5c9..d529deaade 100644 --- a/packages/graphql/src/api-v6/graphql-type-names/RelatedEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/schema-model/graphql-type-names/RelatedEntityTypeNames.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import type { Relationship } from "../../schema-model/relationship/Relationship"; -import { upperFirst } from "../../utils/upper-first"; +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 */ diff --git a/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/TopLevelEntityTypeNames.ts similarity index 95% rename from packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts rename to packages/graphql/src/api-v6/schema-model/graphql-type-names/TopLevelEntityTypeNames.ts index 5b7a266764..9056c8e4ce 100644 --- a/packages/graphql/src/api-v6/graphql-type-names/TopLevelEntityTypeNames.ts +++ b/packages/graphql/src/api-v6/schema-model/graphql-type-names/TopLevelEntityTypeNames.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import { plural } from "../../schema-model/utils/string-manipulation"; +import { plural } from "../../../schema-model/utils/string-manipulation"; import { EntityTypeNames } from "./EntityTypeNames"; /** Top level node typenames */ diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index a792362234..51cc47e41b 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -25,7 +25,7 @@ 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 { SchemaGenerator } from "../api-v6/schema/SchemaGenerator"; +import { SchemaGenerator } from "../api-v6/schema-generation/SchemaGenerator"; import { DEBUG_ALL } from "../constants"; import { makeAugmentedSchema } from "../schema"; import type { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index df0be984e1..59cd4b4882 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -18,7 +18,7 @@ */ import { Memoize } from "typescript-memoize"; -import { TopLevelEntityTypeNames } from "../../api-v6/graphql-type-names/TopLevelEntityTypeNames"; +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"; diff --git a/packages/graphql/src/schema-model/relationship/Relationship.ts b/packages/graphql/src/schema-model/relationship/Relationship.ts index f4a689737b..f93ce2f9e1 100644 --- a/packages/graphql/src/schema-model/relationship/Relationship.ts +++ b/packages/graphql/src/schema-model/relationship/Relationship.ts @@ -18,7 +18,7 @@ */ import { Memoize } from "typescript-memoize"; -import { RelatedEntityTypeNames } from "../../api-v6/graphql-type-names/RelatedEntityTypeNames"; +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"; From 13d509006143915b9a477a59779070a2dc32c4f0 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 15:03:13 +0100 Subject: [PATCH 021/177] Rename EntityTypes to EntitySchemaTypes --- .../schema-generation/SchemaGenerator.ts | 16 ++++++++-------- .../{EntityTypes.ts => EntitySchemaTypes.ts} | 19 +++++++------------ ...tyTypes.ts => RelatedEntitySchemaTypes.ts} | 8 ++++---- .../{StaticTypes.ts => StaticSchemaTypes.ts} | 2 +- ...yTypes.ts => TopLevelEntitySchemaTypes.ts} | 12 ++++++------ 5 files changed, 26 insertions(+), 31 deletions(-) rename packages/graphql/src/api-v6/schema-generation/schema-types/{EntityTypes.ts => EntitySchemaTypes.ts} (90%) rename packages/graphql/src/api-v6/schema-generation/schema-types/{NestedEntityTypes.ts => RelatedEntitySchemaTypes.ts} (93%) rename packages/graphql/src/api-v6/schema-generation/schema-types/{StaticTypes.ts => StaticSchemaTypes.ts} (97%) rename packages/graphql/src/api-v6/schema-generation/schema-types/{TopLevelEntityTypes.ts => TopLevelEntitySchemaTypes.ts} (89%) diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index f7fb0c67ac..0a30e2a847 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -22,8 +22,8 @@ import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSch import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; import { generateReadResolver } from "../resolvers/readResolver"; import { SchemaBuilder } from "./SchemaBuilder"; -import { StaticTypes } from "./schema-types/StaticTypes"; -import { TopLevelEntityTypes } from "./schema-types/TopLevelEntityTypes"; +import { StaticSchemaTypes } from "./schema-types/StaticSchemaTypes"; +import { TopLevelEntitySchemaTypes } from "./schema-types/TopLevelEntitySchemaTypes"; export class SchemaGenerator { private schemaBuilder: SchemaBuilder; @@ -33,14 +33,14 @@ export class SchemaGenerator { } public generate(schemaModel: Neo4jGraphQLSchemaModel): GraphQLSchema { - const staticTypes = new StaticTypes({ schemaBuilder: this.schemaBuilder }); + const staticTypes = new StaticSchemaTypes({ schemaBuilder: this.schemaBuilder }); const entityTypes = this.generateEntityTypes(schemaModel, staticTypes); this.createQueryFields(entityTypes); return this.schemaBuilder.build(); } - private createQueryFields(entityTypes: Map): void { + private createQueryFields(entityTypes: Map): void { entityTypes.forEach((entitySchemaTypes, entity) => { const resolver = generateReadResolver({ entity, @@ -55,12 +55,12 @@ export class SchemaGenerator { private generateEntityTypes( schemaModel: Neo4jGraphQLSchemaModel, - staticTypes: StaticTypes - ): Map { - const resultMap = new Map(); + staticTypes: StaticSchemaTypes + ): Map { + const resultMap = new Map(); for (const entity of schemaModel.entities.values()) { if (entity.isConcreteEntity()) { - const entitySchemaTypes = new TopLevelEntityTypes({ + const entitySchemaTypes = new TopLevelEntitySchemaTypes({ entity, schemaBuilder: this.schemaBuilder, staticTypes, diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntityTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts similarity index 90% rename from packages/graphql/src/api-v6/schema-generation/schema-types/EntityTypes.ts rename to packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index b5c2294bd4..301f5ca846 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntityTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -22,13 +22,13 @@ import { Memoize } from "typescript-memoize"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; -import type { StaticTypes } from "./StaticTypes"; +import type { StaticSchemaTypes } from "./StaticSchemaTypes"; /** This class defines the GraphQL types for an entity */ -export abstract class EntityTypes { +export abstract class EntitySchemaTypes { protected schemaBuilder: SchemaBuilder; protected entityTypeNames: T; - protected staticTypes: StaticTypes; + protected staticTypes: StaticSchemaTypes; constructor({ schemaBuilder, @@ -36,7 +36,7 @@ export abstract class EntityTypes { staticTypes, }: { schemaBuilder: SchemaBuilder; - staticTypes: StaticTypes; + staticTypes: StaticSchemaTypes; entityTypeNames: T; }) { this.schemaBuilder = schemaBuilder; @@ -49,7 +49,9 @@ export abstract class EntityTypes { return this.schemaBuilder.createObjectType(this.entityTypeNames.connectionOperation, { connection: { type: this.connection, - args: this.connectionArgs, + args: { + sort: this.connectionSort, + }, }, }); } @@ -62,13 +64,6 @@ export abstract class EntityTypes { }); } - @Memoize() - protected get connectionArgs(): { sort: InputTypeComposer } { - return { - sort: this.connectionSort, - }; - } - @Memoize() public get connectionSort(): InputTypeComposer { return this.schemaBuilder.createInputObjectType(this.entityTypeNames.connectionSort, { diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/NestedEntityTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts similarity index 93% rename from packages/graphql/src/api-v6/schema-generation/schema-types/NestedEntityTypes.ts rename to packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts index 543335280d..2d75c28071 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/NestedEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -26,10 +26,10 @@ import type { Relationship } from "../../../schema-model/relationship/Relationsh import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; import type { RelatedEntityTypeNames } from "../../schema-model/graphql-type-names/RelatedEntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; -import { EntityTypes } from "./EntityTypes"; -import type { StaticTypes } from "./StaticTypes"; +import { EntitySchemaTypes } from "./EntitySchemaTypes"; +import type { StaticSchemaTypes } from "./StaticSchemaTypes"; -export class NestedEntitySchemaTypes extends EntityTypes { +export class RelatedEntitySchemaTypes extends EntitySchemaTypes { private relationship: Relationship; constructor({ @@ -40,7 +40,7 @@ export class NestedEntitySchemaTypes extends EntityTypes }: { schemaBuilder: SchemaBuilder; relationship: Relationship; - staticTypes: StaticTypes; + staticTypes: StaticSchemaTypes; entityTypeNames: RelatedEntityTypeNames; }) { super({ diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts similarity index 97% rename from packages/graphql/src/api-v6/schema-generation/schema-types/StaticTypes.ts rename to packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts index dad127ac88..6cf23271eb 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -21,7 +21,7 @@ import type { EnumTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { SchemaBuilder } from "../SchemaBuilder"; -export class StaticTypes { +export class StaticSchemaTypes { private schemaBuilder: SchemaBuilder; constructor({ schemaBuilder }: { schemaBuilder: SchemaBuilder }) { diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntityTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts similarity index 89% rename from packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntityTypes.ts rename to packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts index 1c6e399beb..089ffee8ff 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntityTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -25,11 +25,11 @@ import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { FieldDefinition, SchemaBuilder } from "../SchemaBuilder"; -import { EntityTypes } from "./EntityTypes"; -import { NestedEntitySchemaTypes } from "./NestedEntityTypes"; -import type { StaticTypes } from "./StaticTypes"; +import { EntitySchemaTypes } from "./EntitySchemaTypes"; +import { RelatedEntitySchemaTypes } from "./RelatedEntitySchemaTypes"; +import type { StaticSchemaTypes } from "./StaticSchemaTypes"; -export class TopLevelEntityTypes extends EntityTypes { +export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { private entity: ConcreteEntity; constructor({ @@ -39,7 +39,7 @@ export class TopLevelEntityTypes extends EntityTypes { }: { schemaBuilder: SchemaBuilder; entity: ConcreteEntity; - staticTypes: StaticTypes; + staticTypes: StaticSchemaTypes; }) { super({ schemaBuilder, @@ -90,7 +90,7 @@ export class TopLevelEntityTypes extends EntityTypes { private getRelationshipFields(): Record { return Object.fromEntries( [...this.entity.relationships.values()].map((relationship) => { - const relationshipTypes = new NestedEntitySchemaTypes({ + const relationshipTypes = new RelatedEntitySchemaTypes({ schemaBuilder: this.schemaBuilder, relationship, entityTypeNames: relationship.typeNames, From 455c6628cddafd1a5b049342e18dac8f0c758bcd Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 14 May 2024 15:17:09 +0100 Subject: [PATCH 022/177] WIP improve entitySchemaTypes --- .../schema-generation/SchemaGenerator.ts | 2 +- .../schema-types/EntitySchemaTypes.ts | 48 ++------------- .../schema-types/RelatedEntitySchemaTypes.ts | 61 +++++++++++++------ .../schema-types/TopLevelEntitySchemaTypes.ts | 42 +++++++------ 4 files changed, 71 insertions(+), 82 deletions(-) diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index 0a30e2a847..97f163f373 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -46,7 +46,7 @@ export class SchemaGenerator { entity, }); this.schemaBuilder.addQueryField( - entitySchemaTypes.queryFieldName, + entity.typeNames.queryField, entitySchemaTypes.connectionOperation, resolver ); diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index 301f5ca846..fe455ad935 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -17,9 +17,8 @@ * limitations under the License. */ -import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; +import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; -import type { Attribute } from "../../../schema-model/attribute/Attribute"; import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import type { StaticSchemaTypes } from "./StaticSchemaTypes"; @@ -56,57 +55,22 @@ export abstract class EntitySchemaTypes { }); } - @Memoize() - public get connection(): ObjectTypeComposer { + protected get connection(): ObjectTypeComposer { return this.schemaBuilder.createObjectType(this.entityTypeNames.connection, { pageInfo: this.staticTypes.pageInfo, edges: this.edge.List, }); } - @Memoize() - public get connectionSort(): InputTypeComposer { + protected get connectionSort(): InputTypeComposer { return this.schemaBuilder.createInputObjectType(this.entityTypeNames.connectionSort, { edges: this.edgeSort.NonNull.List, }); } - @Memoize() - public get edgeSort(): InputTypeComposer { - const edgeSortFields = { - node: this.nodeSortType, - }; - const properties = this.getEdgeSortProperties(); - if (properties) { - edgeSortFields["properties"] = properties; - } - - return this.schemaBuilder.createInputObjectType(this.entityTypeNames.edgeSort, edgeSortFields); - } - - @Memoize() - public get sortFields(): Record { - return Object.fromEntries(this.getFields().map((field) => [field.name, this.staticTypes.sortDirection])); - } - - @Memoize() - public get edge(): ObjectTypeComposer { - const fields = { - node: this.nodeType, - cursor: "String", - }; - - const properties = this.getEdgeProperties(); - if (properties) { - fields["properties"] = properties; - } - - return this.schemaBuilder.createObjectType(this.entityTypeNames.edge, fields); - } + protected abstract get edgeSort(): InputTypeComposer; + protected abstract get edge(): ObjectTypeComposer; - protected abstract getEdgeProperties(): ObjectTypeComposer | undefined; - protected abstract getEdgeSortProperties(): InputTypeComposer | undefined; - protected abstract getFields(): Attribute[]; public abstract get nodeType(): string; - public abstract get nodeSortType(): string; + public abstract get nodeSort(): string; } 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 index 2d75c28071..8d49d17a53 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -51,7 +51,32 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { - const entityAttributes = this.getFields().map((attribute) => new AttributeAdapter(attribute)); + private getRelationshipFieldsDefinition(): Record { + const entityAttributes = this.getRelationshipFields().map((attribute) => new AttributeAdapter(attribute)); return attributeAdapterToComposeFields(entityAttributes, new Map()) as Record; } - @Memoize() private getRelationshipSortFields(): Record { return Object.fromEntries( - this.getFields().map((attribute) => [attribute.name, this.staticTypes.sortDirection]) + this.getRelationshipFields().map((attribute) => [attribute.name, this.staticTypes.sortDirection]) ); } + + private getEdgeProperties(): ObjectTypeComposer | undefined { + if (this.entityTypeNames.properties) { + const fields = this.getRelationshipFieldsDefinition(); + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.properties, fields); + } + } } 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 index 089ffee8ff..de8fcc7d80 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { ObjectTypeComposer } from "graphql-compose"; +import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; @@ -49,11 +49,23 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes [field.name, this.staticTypes.sortDirection]) + ); + this.schemaBuilder.createInputObjectType(this.entity.typeNames.nodeSort, sortFields); + return this.entity.typeNames.nodeSort; } @Memoize() - protected getFields(): Attribute[] { + private getFields(): Attribute[] { return [...this.entity.attributes.values()]; } - @Memoize() private getNodeFieldsDefinitions(): Record { const entityAttributes = this.getFields().map((attribute) => new AttributeAdapter(attribute)); return attributeAdapterToComposeFields(entityAttributes, new Map()) as Record; } - @Memoize() - public get nodeSortType(): string { - this.schemaBuilder.createInputObjectType(this.entity.typeNames.nodeSort, this.sortFields); - return this.entity.typeNames.nodeSort; - } - - @Memoize() private getRelationshipFields(): Record { return Object.fromEntries( [...this.entity.relationships.values()].map((relationship) => { From 2a5e5447418677e07392cb7366b4d9f453f2f507 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 15 May 2024 16:20:24 +0100 Subject: [PATCH 023/177] Update schema builder --- .../api-v6/schema-generation/SchemaBuilder.ts | 52 ++++++------ .../schema-generation/SchemaGenerator.ts | 7 +- .../schema-types/EntitySchemaTypes.ts | 50 ++++++----- .../schema-types/RelatedEntitySchemaTypes.ts | 82 ++++++++++++------- .../schema-types/SchemaTypes.ts | 28 +++++++ .../schema-types/StaticSchemaTypes.ts | 5 +- .../schema-types/TopLevelEntitySchemaTypes.ts | 66 +++++++++------ 7 files changed, 180 insertions(+), 110 deletions(-) create mode 100644 packages/graphql/src/api-v6/schema-generation/schema-types/SchemaTypes.ts diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index 7930eab0b8..acd219d40a 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -56,24 +56,15 @@ export class SchemaBuilder { this.composer = new SchemaComposer(); } - public createObjectType( - name: string, - fields: Record>, - description?: string - ): ObjectTypeComposer { - return this.composer.createObjectTC({ - name, - description, - fields, - }); - } - public getOrCreateObjectType( name: string, - fields: Record>, - description?: string + onCreate: () => { + fields: Record>; + description?: string; + } ): ObjectTypeComposer { return this.composer.getOrCreateOTC(name, (tc) => { + const { fields, description } = onCreate(); if (fields) { tc.addFields(fields); } @@ -83,24 +74,15 @@ export class SchemaBuilder { }); } - public createInputObjectType( - name: string, - fields: Record>, - description?: string - ): InputTypeComposer { - return this.composer.createInputTC({ - name, - description, - fields, - }); - } - - public getOrCreateInputObjectType( + public getOrCreateInputType( name: string, - fields: Record>, - description?: string + onCreate: () => { + fields: Record>; + description?: string; + } ): InputTypeComposer { return this.composer.getOrCreateITC(name, (itc) => { + const { fields, description } = onCreate(); if (fields) { itc.addFields(fields); } @@ -110,6 +92,18 @@ export class SchemaBuilder { }); } + public createInputObjectType( + name: string, + fields: Record>, + 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 }; diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index 97f163f373..199e844701 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -22,6 +22,7 @@ import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSch import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; import { generateReadResolver } from "../resolvers/readResolver"; import { SchemaBuilder } from "./SchemaBuilder"; +import { SchemaTypes } from "./schema-types/SchemaTypes"; import { StaticSchemaTypes } from "./schema-types/StaticSchemaTypes"; import { TopLevelEntitySchemaTypes } from "./schema-types/TopLevelEntitySchemaTypes"; @@ -58,12 +59,16 @@ export class SchemaGenerator { staticTypes: StaticSchemaTypes ): Map { const resultMap = new Map(); + const schemaTypes = new SchemaTypes({ + staticTypes, + entitySchemas: resultMap, + }); for (const entity of schemaModel.entities.values()) { if (entity.isConcreteEntity()) { const entitySchemaTypes = new TopLevelEntitySchemaTypes({ entity, schemaBuilder: this.schemaBuilder, - staticTypes, + schemaTypes, }); resultMap.set(entity, entitySchemaTypes); diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index fe455ad935..9ce64320ce 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -18,59 +18,69 @@ */ import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; -import { Memoize } from "typescript-memoize"; import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; -import type { StaticSchemaTypes } from "./StaticSchemaTypes"; +import type { SchemaTypes } from "./SchemaTypes"; /** This class defines the GraphQL types for an entity */ export abstract class EntitySchemaTypes { protected schemaBuilder: SchemaBuilder; protected entityTypeNames: T; - protected staticTypes: StaticSchemaTypes; + protected schemaTypes: SchemaTypes; constructor({ schemaBuilder, entityTypeNames, - staticTypes, + schemaTypes, }: { schemaBuilder: SchemaBuilder; - staticTypes: StaticSchemaTypes; + schemaTypes: SchemaTypes; entityTypeNames: T; }) { this.schemaBuilder = schemaBuilder; this.entityTypeNames = entityTypeNames; - this.staticTypes = staticTypes; + this.schemaTypes = schemaTypes; } - @Memoize() public get connectionOperation(): ObjectTypeComposer { - return this.schemaBuilder.createObjectType(this.entityTypeNames.connectionOperation, { - connection: { - type: this.connection, - args: { - sort: this.connectionSort, + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connectionOperation, () => { + return { + fields: { + connection: { + type: this.connection, + args: { + sort: this.connectionSort, + }, + }, }, - }, + }; }); } protected get connection(): ObjectTypeComposer { - return this.schemaBuilder.createObjectType(this.entityTypeNames.connection, { - pageInfo: this.staticTypes.pageInfo, - edges: this.edge.List, + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connection, () => { + return { + fields: { + pageInfo: this.schemaTypes.staticTypes.pageInfo, + edges: this.edge.List, + }, + }; }); } protected get connectionSort(): InputTypeComposer { - return this.schemaBuilder.createInputObjectType(this.entityTypeNames.connectionSort, { - edges: this.edgeSort.NonNull.List, + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { + return { + fields: { + edges: this.edgeSort.NonNull.List, + }, + }; }); } protected abstract get edgeSort(): InputTypeComposer; protected abstract get edge(): ObjectTypeComposer; - public abstract get nodeType(): string; - public abstract get nodeSort(): string; + public abstract get nodeType(): ObjectTypeComposer; + public abstract get nodeSort(): InputTypeComposer; } 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 index 8d49d17a53..a5d8517141 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -27,7 +27,7 @@ import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; import type { RelatedEntityTypeNames } from "../../schema-model/graphql-type-names/RelatedEntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import { EntitySchemaTypes } from "./EntitySchemaTypes"; -import type { StaticSchemaTypes } from "./StaticSchemaTypes"; +import type { SchemaTypes } from "./SchemaTypes"; export class RelatedEntitySchemaTypes extends EntitySchemaTypes { private relationship: Relationship; @@ -36,61 +36,70 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { + const properties = this.getEdgeProperties(); + const fields = { + node: this.nodeType, + cursor: "String", + }; - return this.schemaBuilder.createObjectType(this.entityTypeNames.edge, fields); + if (properties) { + fields["properties"] = properties; + } + return { + fields, + }; + }); } protected get edgeSort(): InputTypeComposer { - const edgeSortFields = { - node: this.nodeSort, - }; - const properties = this.getEdgeSortProperties(); - if (properties) { - edgeSortFields["properties"] = properties; - } + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.edgeSort, () => { + const edgeSortFields = { + node: this.nodeSort, + }; + const properties = this.getEdgeSortProperties(); + if (properties) { + edgeSortFields["properties"] = properties; + } - return this.schemaBuilder.createInputObjectType(this.entityTypeNames.edgeSort, edgeSortFields); + return { fields: edgeSortFields }; + }); } - public get nodeType(): string { + public get nodeType(): ObjectTypeComposer { const target = this.relationship.target; if (!(target instanceof ConcreteEntity)) { throw new Error("Interfaces not supported yet"); } - return target.typeNames.node; + const targetSchemaTypes = this.schemaTypes.getEntitySchemaTypes(target); + + return targetSchemaTypes.nodeType; } - public get nodeSort(): string { + public get nodeSort(): InputTypeComposer { const target = this.relationship.target; if (!(target instanceof ConcreteEntity)) { throw new Error("Interfaces not supported yet"); } - return target.typeNames.nodeSort; + const targetSchemaTypes = this.schemaTypes.getEntitySchemaTypes(target); + + return targetSchemaTypes.nodeSort; } @Memoize() @@ -100,8 +109,12 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { + const fields = this.getRelationshipSortFields(); + return { + fields, + }; + }); } } @@ -112,14 +125,21 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { return Object.fromEntries( - this.getRelationshipFields().map((attribute) => [attribute.name, this.staticTypes.sortDirection]) + this.getRelationshipFields().map((attribute) => [ + attribute.name, + this.schemaTypes.staticTypes.sortDirection, + ]) ); } private getEdgeProperties(): ObjectTypeComposer | undefined { if (this.entityTypeNames.properties) { - const fields = this.getRelationshipFieldsDefinition(); - return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.properties, fields); + 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..05dbc20bf9 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/SchemaTypes.ts @@ -0,0 +1,28 @@ +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 index 6cf23271eb..ac3912983c 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -28,9 +28,10 @@ export class StaticSchemaTypes { this.schemaBuilder = schemaBuilder; } - @Memoize() public get pageInfo(): ObjectTypeComposer { - return this.schemaBuilder.createObjectType("PageInfo", { hasNextPage: "Boolean", hasPreviousPage: "Boolean" }); + return this.schemaBuilder.getOrCreateObjectType("PageInfo", () => { + return { fields: { hasNextPage: "Boolean", hasPreviousPage: "Boolean" } }; + }); } @Memoize() 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 index de8fcc7d80..901d5b6c2f 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -27,7 +27,7 @@ import type { EntityTypeNames } from "../../schema-model/graphql-type-names/Enti import type { FieldDefinition, SchemaBuilder } from "../SchemaBuilder"; import { EntitySchemaTypes } from "./EntitySchemaTypes"; import { RelatedEntitySchemaTypes } from "./RelatedEntitySchemaTypes"; -import type { StaticSchemaTypes } from "./StaticSchemaTypes"; +import type { SchemaTypes } from "./SchemaTypes"; export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { private entity: ConcreteEntity; @@ -35,50 +35,62 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { + return { + fields: { + node: this.nodeType, + cursor: "String", + }, + }; + }); } protected get edgeSort(): InputTypeComposer { - const edgeSortFields = { - node: this.nodeSort, - }; - - return this.schemaBuilder.createInputObjectType(this.entityTypeNames.edgeSort, edgeSortFields); + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.edgeSort, () => { + return { + fields: { + node: this.nodeSort, + }, + }; + }); } - public get nodeType(): string { - const fields = this.getNodeFieldsDefinitions(); - const relationships = this.getRelationshipFields(); - this.schemaBuilder.createObjectType(this.entity.typeNames.node, { ...fields, ...relationships }); - return this.entity.typeNames.node; + public get nodeType(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.node, () => { + const fields = this.getNodeFieldsDefinitions(); + const relationships = this.getRelationshipFields(); + + return { + fields: { ...fields, ...relationships }, + }; + }); } - public get nodeSort(): string { - const sortFields = Object.fromEntries( - this.getFields().map((field) => [field.name, this.staticTypes.sortDirection]) - ); - this.schemaBuilder.createInputObjectType(this.entity.typeNames.nodeSort, sortFields); - return this.entity.typeNames.nodeSort; + public get nodeSort(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.nodeSort, () => { + const sortFields = Object.fromEntries( + this.getFields().map((field) => [field.name, this.schemaTypes.staticTypes.sortDirection]) + ); + + return { + fields: sortFields, + }; + }); } @Memoize() @@ -98,7 +110,7 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes Date: Wed, 15 May 2024 16:22:41 +0100 Subject: [PATCH 024/177] add tests for sorting + alias (#5131) --- .../integration/sort/sort-alias.int.test.ts | 223 ++++++++ .../sort/sort-relationship-alias.int.test.ts | 478 ++++++++++++++++++ .../tests/api-v6/tck/sort/sort-alias.test.ts | 109 ++++ .../tck/sort/sort-relationship-alias.test.ts | 287 +++++++++++ 4 files changed, 1097 insertions(+) create mode 100644 packages/graphql/tests/api-v6/integration/sort/sort-alias.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/sort/sort-relationship-alias.int.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/sort/sort-alias.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/sort/sort-relationship-alias.test.ts diff --git a/packages/graphql/tests/api-v6/integration/sort/sort-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/sort/sort-alias.int.test.ts new file mode 100644 index 0000000000..a14df6103e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/sort/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: { edges: { 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: { edges: { 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: { edges: [{ 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/sort/sort-relationship-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/sort/sort-relationship-alias.int.test.ts new file mode 100644 index 0000000000..49b529c3f1 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/sort/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 } }, { 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 } }, { 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 } }, + { node: { title: ASC } }, + { properties: { year: DESC } }, + { 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/tck/sort/sort-alias.test.ts b/packages/graphql/tests/api-v6/tck/sort/sort-alias.test.ts new file mode 100644 index 0000000000..b8e9905e22 --- /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: { edges: { 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: { edges: [{ 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..8bd3ddabda --- /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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + ORDER BY actors.actorName DESC + RETURN collect({ node: { name: actors.actorName, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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 } }, { 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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + ORDER BY actors.actorName DESC, actors.actorAge DESC + RETURN collect({ node: { name: actors.actorName, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + ORDER BY this1.actedInYear DESC + RETURN collect({ node: { name: actors.actorName, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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 } } + { node: { name: ASC } } + { 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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + ORDER BY this1.actedInYear DESC, actors.actorName ASC, this1.role ASC + RETURN collect({ node: { age: actors.actorAge, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); From aa76cac7afa29f08a339d778841fa10f1bb8295a Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 23 May 2024 16:38:51 +0100 Subject: [PATCH 025/177] New api filters (#5141) * add schema generation for top-level filters * add nested fields filters * add support for all GraphQLBuiltInScalars * createListWhere types * Simple filters translation (#5140) * WIP basic string filters * Number filtering * Add logical filtering tests for edges * Improve filter factory * Move topLevelResolveParser to a different file * Add edge properties filters * Logical filters * Skips logical tests in nodes * Update schema on array filters * WIP fix aliasing * Fix error with using same fields with different aliases * add nested filters in the generated schema * update tcks * refactor namings on filter classes * Rename tests file * Add basic filtering test * Test filtering * Add tests for AND * Some cases for and or filters in tests * Support for all none single some filters * Add tests on nested operations --------- Co-authored-by: MacondoExpress Co-authored-by: MacondoExpress --- .../api-v6/queryIR/ConnectionReadOperation.ts | 3 + .../api-v6/queryIRFactory/FilterFactory.ts | 239 ++++++++++ .../api-v6/queryIRFactory/FilterOperators.ts | 51 ++ .../queryIRFactory/ReadOperationFactory.ts | 18 +- .../resolve-tree-parser/ResolveTreeParser.ts | 63 +-- .../TopLevelResolveTreeParser.ts | 47 ++ .../resolve-tree-parser/graphql-tree.ts | 51 ++ .../parse-resolve-info-tree.ts | 34 ++ .../api-v6/schema-generation/SchemaBuilder.ts | 24 +- .../schema-generation/SchemaGenerator.ts | 31 +- .../schema-types/RelatedEntitySchemaTypes.ts | 8 +- .../schema-types/StaticSchemaTypes.ts | 175 ++++++- .../schema-types/TopLevelEntitySchemaTypes.ts | 51 +- .../filter-schema-types/FilterSchemaTypes.ts | 132 ++++++ .../RelatedEntityFilterSchemaTypes.ts | 116 +++++ .../TopLevelFilterSchemaTypes.ts | 86 ++++ .../graphql-type-names/EntityTypeNames.ts | 10 +- .../RelatedEntityTypeNames.ts | 20 +- .../TopLevelEntityTypeNames.ts | 18 +- .../translators/translate-read-operation.ts | 3 +- packages/graphql/src/classes/Neo4jGraphQL.ts | 3 +- .../queryAST/ast/filters/ConnectionFilter.ts | 7 +- .../property-filters/PropertyFilter.ts | 10 +- .../src/translate/utils/logical-operators.ts | 2 +- .../api-v6/integration/aliasing.int.test.ts | 260 ++++++++++ ...ry.int.test.ts => query-alias.int.test.ts} | 0 .../alias}/sort-alias.int.test.ts | 4 +- .../sort-relationship-alias.int.test.ts | 4 +- .../filters/logical/and-filter.test.ts | 194 ++++++++ .../filters/logical/or-filter.test.ts | 193 ++++++++ .../filters/nested/all.int.test.ts | 170 +++++++ .../filters/nested/none.int.test.ts | 175 +++++++ .../filters/nested/single.int.test.ts | 172 +++++++ .../filters/nested/some.int.test.ts | 185 ++++++++ .../filters/relationship.int.test.ts | 158 +++++++ .../filters/top-level-filters.int.test.ts | 86 ++++ .../integration/relationship.int.test.ts | 55 --- ...e.int.test.ts => simple-query.int.test.ts} | 2 +- .../integration/types/numeric.int.test.ts | 9 +- .../graphql/tests/api-v6/schema/array.test.ts | 352 ++++++++++++++ .../tests/api-v6/schema/relationship.test.ts | 273 ++++++++++- .../tests/api-v6/schema/scalars.test.ts | 336 +++++++++++++ .../tests/api-v6/schema/simple.test.ts | 128 ++++- .../tck/filters/array/array-filters.test.ts | 80 ++++ .../filters/filters-on-relationships.test.ts | 242 ++++++++++ .../logical-filters/and-filter.test.ts | 139 ++++++ .../logical-filters/not-filter.test.ts | 124 +++++ .../filters/logical-filters/or-filter.test.ts | 184 ++++++++ .../api-v6/tck/filters/nested/all.test.ts | 208 ++++++++ .../api-v6/tck/filters/nested/none.test.ts | 199 ++++++++ .../api-v6/tck/filters/nested/single.test.ts | 192 ++++++++ .../api-v6/tck/filters/nested/some.test.ts | 199 ++++++++ .../tck/filters/top-level-filters.test.ts | 90 ++++ .../{simple.test.ts => simple-query.test.ts} | 2 +- .../tests/integration/aliasing.int.test.ts | 135 ------ .../integration/connections/alias.int.test.ts | 443 ------------------ .../connections/filtering.int.test.ts | 243 ---------- packages/graphql/tests/tck/where.test.ts | 249 +--------- 58 files changed, 5435 insertions(+), 1252 deletions(-) create mode 100644 packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-resolve-info-tree.ts create mode 100644 packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/FilterSchemaTypes.ts create mode 100644 packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/RelatedEntityFilterSchemaTypes.ts create mode 100644 packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/TopLevelFilterSchemaTypes.ts create mode 100644 packages/graphql/tests/api-v6/integration/aliasing.int.test.ts rename packages/graphql/tests/api-v6/integration/directives/alias/{query.int.test.ts => query-alias.int.test.ts} (100%) rename packages/graphql/tests/api-v6/integration/{sort => directives/alias}/sort-alias.int.test.ts (98%) rename packages/graphql/tests/api-v6/integration/{sort => directives/alias}/sort-relationship-alias.int.test.ts (99%) create mode 100644 packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/nested/all.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/nested/none.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/nested/single.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/nested/some.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/relationship.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/top-level-filters.int.test.ts rename packages/graphql/tests/api-v6/integration/{simple.int.test.ts => simple-query.int.test.ts} (98%) create mode 100644 packages/graphql/tests/api-v6/schema/array.test.ts create mode 100644 packages/graphql/tests/api-v6/schema/scalars.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/array/array-filters.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/logical-filters/and-filter.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/logical-filters/not-filter.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/logical-filters/or-filter.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/top-level-filters.test.ts rename packages/graphql/tests/api-v6/tck/{simple.test.ts => simple-query.test.ts} (98%) delete mode 100644 packages/graphql/tests/integration/aliasing.int.test.ts delete mode 100644 packages/graphql/tests/integration/connections/filtering.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts index 6b64dfa740..78b84f1b55 100644 --- a/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts @@ -54,6 +54,7 @@ export class V6ReadOperation extends Operation { selection, fields, sortFields, + filters, }: { relationship?: RelationshipAdapter; target: ConcreteEntityAdapter; @@ -63,6 +64,7 @@ export class V6ReadOperation extends Operation { edge: Field[]; }; sortFields?: Array<{ node: Sort[]; edge: Sort[] }>; + filters: Filter[]; }) { super(); this.relationship = relationship; @@ -71,6 +73,7 @@ export class V6ReadOperation extends Operation { this.nodeFields = fields?.node ?? []; this.edgeFields = fields?.edge ?? []; this.sortFields = sortFields ?? []; + this.filters = filters ?? []; } public setNodeFields(fields: Field[]) { 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..37240ce376 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.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 type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +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 { PropertyFilter } from "../../translate/queryAST/ast/filters/property-filters/PropertyFilter"; +import { getFilterOperator, getRelationshipOperator } from "./FilterOperators"; +import type { + GraphQLAttributeFilters, + GraphQLEdgeWhereArgs, + GraphQLNodeFilters, + GraphQLWhereArgs, + RelationshipFilters, +} from "./resolve-tree-parser/graphql-tree"; + +export class FilterFactory { + public schemaModel: Neo4jGraphQLSchemaModel; + + constructor(schemaModel: Neo4jGraphQLSchemaModel) { + this.schemaModel = schemaModel; + } + + public createFilters({ + where = {}, + relationship, + entity, + }: { + entity: ConcreteEntity; + relationship?: Relationship; + where?: GraphQLWhereArgs; + }): Filter[] { + const andFilters = this.createLogicalFilters({ operation: "AND", entity, relationship, where: where.AND }); + const orFilters = this.createLogicalFilters({ operation: "OR", entity, relationship, where: where.OR }); + const notFilters = this.createLogicalFilters({ + operation: "NOT", + entity, + relationship, + where: where.NOT ? [where.NOT] : undefined, + }); + + const edgeFilters = this.createEdgeFilters({ entity, relationship, edgeWhere: where.edges }); + + return [...edgeFilters, ...andFilters, ...orFilters, ...notFilters]; + } + + private createLogicalFilters({ + where = [], + relationship, + operation, + entity, + }: { + entity: ConcreteEntity; + relationship?: Relationship; + operation: LogicalOperators; + where?: GraphQLEdgeWhereArgs[]; + }): [] | [Filter] { + if (where.length === 0) { + return []; + } + const nestedFilters = where.flatMap((orWhere: GraphQLEdgeWhereArgs) => { + return this.createFilters({ entity, relationship, where: orWhere }); + }); + + if (nestedFilters.length > 0) { + return [ + new LogicalFilter({ + operation, + filters: nestedFilters, + }), + ]; + } + + return []; + } + + private createEdgeFilters({ + edgeWhere = {}, + relationship, + entity, + }: { + entity: ConcreteEntity; + relationship?: Relationship; + edgeWhere?: GraphQLEdgeWhereArgs; + }): Filter[] { + const andFilters = this.createLogicalEdgeFilters(entity, relationship, "AND", edgeWhere.AND); + const orFilters = this.createLogicalEdgeFilters(entity, relationship, "OR", edgeWhere.OR); + const notFilters = this.createLogicalEdgeFilters( + entity, + relationship, + "NOT", + edgeWhere.NOT ? [edgeWhere.NOT] : undefined + ); + + const nodeFilters = this.createNodeFilter({ where: edgeWhere.node, entity }); + + let edgePropertiesFilters: Filter[] = []; + if (relationship) { + edgePropertiesFilters = this.createEdgePropertiesFilters({ + where: edgeWhere.properties, + relationship, + }); + } + return [...nodeFilters, ...edgePropertiesFilters, ...andFilters, ...orFilters, ...notFilters]; + } + + private createLogicalEdgeFilters( + entity: ConcreteEntity, + relationship: Relationship | undefined, + operation: LogicalOperators, + where: GraphQLEdgeWhereArgs[] = [] + ): [] | [Filter] { + if (where.length === 0) { + return []; + } + const nestedFilters = where.flatMap((orWhere: GraphQLEdgeWhereArgs) => { + return this.createEdgeFilters({ entity, relationship, edgeWhere: orWhere }); + }); + + if (nestedFilters.length > 0) { + return [ + new LogicalFilter({ + operation, + filters: nestedFilters, + }), + ]; + } + + return []; + } + + private createNodeFilter({ + where = {}, + entity, + }: { + entity: ConcreteEntity; + where?: Record; + }): Filter[] { + return Object.entries(where).flatMap(([fieldName, filters]) => { + // TODO: Logical filters here + const 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 []; + }); + } + + private createRelationshipFilters(relationship: Relationship, filters: RelationshipFilters): Filter[] { + const relationshipAdapter = new RelationshipAdapter(relationship); + + const target = relationshipAdapter.target as ConcreteEntityAdapter; + const edgeFilters = filters.edges ?? {}; + + return Object.entries(edgeFilters).map(([rawOperator, filter]) => { + const relatedNodeFilters = this.createEdgeFilters({ + edgeWhere: filter, + relationship: relationship, + entity: relationship.target as ConcreteEntity, + }); + const operator = getRelationshipOperator(rawOperator); + const relationshipFilter = new ConnectionFilter({ + relationship: relationshipAdapter, + target, + operator, + }); + relationshipFilter.addFilters(relatedNodeFilters); + return relationshipFilter; + }); + } + + private createEdgePropertiesFilters({ + where, + relationship, + }: { + where: GraphQLEdgeWhereArgs["properties"]; + relationship: Relationship; + }): Filter[] { + if (!where) { + return []; + } + return Object.entries(where).flatMap(([fieldName, filters]) => { + // TODO: Logical filters here + const attribute = relationship.findAttribute(fieldName); + if (!attribute) return []; + const attributeAdapter = new AttributeAdapter(attribute); + return this.createPropertyFilters(attributeAdapter, filters, "relationship"); + }); + } + + private createPropertyFilters( + attribute: AttributeAdapter, + filters: GraphQLAttributeFilters, + attachedTo: "node" | "relationship" = "node" + ): PropertyFilter[] { + return Object.entries(filters).map(([key, value]) => { + const operator = getFilterOperator(attribute, key); + if (!operator) throw new Error(`Invalid operator: ${key}`); + + return new PropertyFilter({ + attribute, + relationship: undefined, + comparisonValue: value, + operator, + attachedTo, + }); + }); + } +} 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..6b5c64cfb4 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts @@ -0,0 +1,51 @@ +import type { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; +import type { FilterOperator, RelationshipWhereOperator } from "../../translate/queryAST/ast/filters/Filter"; + +export function getFilterOperator(attribute: AttributeAdapter, operator: string): FilterOperator | undefined { + if (attribute.typeHelper.isString() || attribute.typeHelper.isID()) { + return getStringOperator(operator); + } + + if (attribute.typeHelper.isNumeric()) { + return getNumberOperator(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]; +} + +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 index 8d4ae74da7..b0a39046e9 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -32,6 +32,7 @@ import { RelationshipSelection } from "../../translate/queryAST/ast/selection/Re import { PropertySort } from "../../translate/queryAST/ast/sort/PropertySort"; import { filterTruthy } from "../../utils/utils"; import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; +import { FilterFactory } from "./FilterFactory"; import type { GraphQLSortArgument, GraphQLTree, @@ -43,9 +44,11 @@ import type { export class ReadOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; + private filterFactory: FilterFactory; constructor(schemaModel: Neo4jGraphQLSchemaModel) { this.schemaModel = schemaModel; + this.filterFactory = new FilterFactory(schemaModel); } public createAST({ @@ -70,6 +73,7 @@ export class ReadOperationFactory { entity: ConcreteEntity; }): V6ReadOperation { const connectionTree = graphQLTree.fields.connection; + if (!connectionTree) { throw new Error("No Connection"); } @@ -94,6 +98,7 @@ export class ReadOperationFactory { node: nodeFields, }, sortFields: sortInputFields, + filters: this.filterFactory.createFilters({ entity, where: graphQLTree.args.where }), }); } @@ -138,6 +143,11 @@ export class ReadOperationFactory { node: nodeFields, }, sortFields: sortInputFields, + filters: this.filterFactory.createFilters({ + entity: relationshipAdapter.target.entity, + relationship, + where: parsedTree.args.where, + }), }); } @@ -152,8 +162,8 @@ export class ReadOperationFactory { } return filterTruthy( - Object.entries(propertiesTree.fields).map(([name, rawField]) => { - const attribute = target.findAttribute(name); + Object.values(propertiesTree.fields).map((rawField) => { + const attribute = target.findAttribute(rawField.name); if (attribute) { return new AttributeField({ alias: rawField.alias, @@ -171,8 +181,8 @@ export class ReadOperationFactory { } return filterTruthy( - Object.entries(nodeResolveTree.fields).map(([name, rawField]) => { - const relationship = entity.findRelationship(name); + Object.values(nodeResolveTree.fields).map((rawField) => { + const relationship = entity.findRelationship(rawField.name); if (relationship) { // FIX casting here return this.generateRelationshipField(rawField as GraphQLTreeReadOperation, relationship); diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 6acd5622aa..ec7e0dce5b 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -23,8 +23,8 @@ import type { Relationship } from "../../../schema-model/relationship/Relationsh import { findFieldByName } from "./find-field-by-name"; import type { GraphQLConnectionArgs, + GraphQLReadOperationArgs, GraphQLSortEdgeArgument, - GraphQLTree, GraphQLTreeConnection, GraphQLTreeEdge, GraphQLTreeEdgeProperties, @@ -34,18 +34,7 @@ import type { GraphQLTreeSortElement, } from "./graphql-tree"; -export function parseResolveInfoTree({ - resolveTree, - entity, -}: { - resolveTree: ResolveTree; - entity: ConcreteEntity; -}): GraphQLTree { - const parser = new TopLevelTreeParser({ entity }); - return parser.parseOperation(resolveTree); -} - -abstract class ResolveTreeParser { +export abstract class ResolveTreeParser { protected entity: T; constructor({ entity }: { entity: T }) { @@ -61,16 +50,24 @@ abstract class ResolveTreeParser { ); const connection = connectionResolveTree ? this.parseConnection(connectionResolveTree) : undefined; - + const connectionOperationArgs = this.parseOperationArgs(resolveTree.args); return { alias: resolveTree.alias, - args: resolveTree.args, + args: connectionOperationArgs, + name: resolveTree.name, fields: { connection, }, }; } + private parseOperationArgs(resolveTreeArgs: Record): GraphQLReadOperationArgs { + // Not properly parsed, assuming the type is the same + return { + where: resolveTreeArgs.where, + }; + } + protected abstract get targetNode(): ConcreteEntity; protected abstract parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge; @@ -82,6 +79,7 @@ abstract class ResolveTreeParser { return { alias: resolveTree.alias, args: resolveTree.args, + name: resolveTree.name, fields: undefined, }; } @@ -118,7 +116,7 @@ abstract class ResolveTreeParser { fields: Record ): Record { const propertyFields: Record = {}; - for (const fieldResolveTree of Object.values(fields)) { + for (const [key, fieldResolveTree] of Object.entries(fields)) { const fieldName = fieldResolveTree.name; const field = this.parseRelationshipField(fieldResolveTree, this.targetNode) ?? @@ -126,7 +124,7 @@ abstract class ResolveTreeParser { if (!field) { throw new ResolveTreeParserError(`${fieldName} is not a field of node`); } - propertyFields[fieldName] = field; + propertyFields[key] = field; } return propertyFields; } @@ -202,30 +200,9 @@ abstract class ResolveTreeParser { } } -class TopLevelTreeParser extends ResolveTreeParser { - protected get targetNode(): ConcreteEntity { - return this.entity; - } - - protected parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { - const edgeType = this.entity.typeNames.edge; - - const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); - - const node = nodeResolveTree ? this.parseNode(nodeResolveTree) : undefined; - - return { - alias: resolveTree.alias, - args: resolveTree.args, - fields: { - node: node, - properties: undefined, - }, - }; - } -} +export class ResolveTreeParserError extends Error {} -class RelationshipResolveTreeParser extends ResolveTreeParser { +export class RelationshipResolveTreeParser extends ResolveTreeParser { protected get targetNode(): ConcreteEntity { return this.entity.target as ConcreteEntity; } @@ -266,16 +243,14 @@ class RelationshipResolveTreeParser extends ResolveTreeParser { private getEdgePropertyFields(fields: Record): Record { const propertyFields: Record = {}; - for (const fieldResolveTree of Object.values(fields)) { + for (const [key, fieldResolveTree] of Object.entries(fields)) { const fieldName = fieldResolveTree.name; const field = this.parseAttributeField(fieldResolveTree, this.entity); if (!field) { throw new ResolveTreeParserError(`${fieldName} is not an attribute of edge`); } - propertyFields[fieldName] = field; + propertyFields[key] = field; } return propertyFields; } } - -class ResolveTreeParserError extends Error {} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts new file mode 100644 index 0000000000..9e8b4c573c --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.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 { ResolveTree } from "graphql-parse-resolve-info"; +import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import { ResolveTreeParser } from "./ResolveTreeParser"; +import { findFieldByName } from "./find-field-by-name"; +import type { GraphQLTreeEdge } from "./graphql-tree"; + +export class TopLevelResolveTreeParser extends ResolveTreeParser { + protected get targetNode(): ConcreteEntity { + return this.entity; + } + + protected parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { + const edgeType = this.entity.typeNames.edge; + + const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); + + const node = nodeResolveTree ? this.parseNode(nodeResolveTree) : undefined; + + return { + alias: resolveTree.alias, + args: resolveTree.args, + fields: { + node: node, + properties: undefined, + }, + }; + } +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index e88b67d125..cddc8fe1be 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -24,12 +24,62 @@ interface GraphQLTreeElement { args: Record; } +type LogicalOperation = { + AND?: Array>; + OR?: Array>; + NOT?: LogicalOperation; +} & T; + +export type StringFilters = { + equals?: string; + in?: string[]; + matches?: string; + contains?: string; + startsWith?: string; + endsWith?: string; +}; +export type NumberFilters = { + equals?: string; + in?: string[]; + lt?: string; + lte?: string; + gt?: string; + gte?: string; +}; + +export type RelationshipFilters = { + edges?: { + some?: GraphQLEdgeWhereArgs; + single?: GraphQLEdgeWhereArgs; + all?: GraphQLEdgeWhereArgs; + none?: GraphQLEdgeWhereArgs; + }; +}; + export interface GraphQLTreeReadOperation extends GraphQLTreeElement { fields: { connection?: GraphQLTreeConnection; }; + args: GraphQLReadOperationArgs; + name: string; } +export interface GraphQLReadOperationArgs { + where?: GraphQLWhereArgs; +} + +export type GraphQLWhereArgs = LogicalOperation<{ + edges?: GraphQLEdgeWhereArgs; +}>; + +export type GraphQLEdgeWhereArgs = LogicalOperation<{ + properties?: Record; + node?: Record; +}>; + +export type GraphQLAttributeFilters = StringFilters | NumberFilters; +export type GraphQLNodeFilters = GraphQLAttributeFilters | RelationshipFilters; + export interface GraphQLTreeConnection extends GraphQLTreeElement { fields: { edges?: GraphQLTreeEdge; @@ -58,6 +108,7 @@ export interface GraphQLTreeEdgeProperties extends GraphQLTreeElement { export interface GraphQLTreeLeafField extends GraphQLTreeElement { fields: undefined; + name: string; } export interface GraphQLSortArgument { 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..9edac75c0d --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-resolve-info-tree.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ResolveTree } from "graphql-parse-resolve-info"; +import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; +import type { GraphQLTree } from "./graphql-tree"; + +export function parseResolveInfoTree({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTree { + const parser = new TopLevelResolveTreeParser({ entity }); + return parser.parseOperation(resolveTree); +} diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index acd219d40a..f9793a046f 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { GraphQLSchema } from "graphql"; +import { type GraphQLNamedInputType, type GraphQLSchema } from "graphql"; import type { EnumTypeComposer, InputTypeComposer, @@ -76,13 +76,16 @@ export class SchemaBuilder { public getOrCreateInputType( name: string, - onCreate: () => { - fields: Record>; + onCreate: (itc: InputTypeComposer) => { + fields: Record< + string, + EnumTypeComposer | string | GraphQLNamedInputType | WrappedComposer + >; description?: string; } ): InputTypeComposer { return this.composer.getOrCreateITC(name, (itc) => { - const { fields, description } = onCreate(); + const { fields, description } = onCreate(itc); if (fields) { itc.addFields(fields); } @@ -116,10 +119,21 @@ export class SchemaBuilder { }); } - public addQueryField(name: string, type: ObjectTypeComposer | string, resolver: (...args: any[]) => any): void { + public addQueryField({ + name, + type, + args, + resolver, + }: { + name: string; + type: ObjectTypeComposer; + args: Record; + resolver: (...args: any[]) => any; + }): void { this.composer.Query.addFields({ [name]: { type: type, + args, resolve: resolver, }, }); diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index 199e844701..4bb3117325 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -35,29 +35,11 @@ export class SchemaGenerator { public generate(schemaModel: Neo4jGraphQLSchemaModel): GraphQLSchema { const staticTypes = new StaticSchemaTypes({ schemaBuilder: this.schemaBuilder }); - const entityTypes = this.generateEntityTypes(schemaModel, staticTypes); - this.createQueryFields(entityTypes); - + this.generateEntityTypes(schemaModel, staticTypes); return this.schemaBuilder.build(); } - private createQueryFields(entityTypes: Map): void { - entityTypes.forEach((entitySchemaTypes, entity) => { - const resolver = generateReadResolver({ - entity, - }); - this.schemaBuilder.addQueryField( - entity.typeNames.queryField, - entitySchemaTypes.connectionOperation, - resolver - ); - }); - } - - private generateEntityTypes( - schemaModel: Neo4jGraphQLSchemaModel, - staticTypes: StaticSchemaTypes - ): Map { + private generateEntityTypes(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { const resultMap = new Map(); const schemaTypes = new SchemaTypes({ staticTypes, @@ -70,11 +52,14 @@ export class SchemaGenerator { schemaBuilder: this.schemaBuilder, schemaTypes, }); - resultMap.set(entity, entitySchemaTypes); } } - - return resultMap; + for (const [entity, entitySchemaTypes] of resultMap.entries()) { + const resolver = generateReadResolver({ + entity, + }); + entitySchemaTypes.addTopLevelQueryField(resolver); + } } } 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 index a5d8517141..e49e46524d 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -28,9 +28,10 @@ import type { RelatedEntityTypeNames } from "../../schema-model/graphql-type-nam import type { SchemaBuilder } from "../SchemaBuilder"; import { EntitySchemaTypes } from "./EntitySchemaTypes"; import type { SchemaTypes } from "./SchemaTypes"; - +import { RelatedEntityFilterSchemaTypes } from "./filter-schema-types/RelatedEntityFilterSchemaTypes"; export class RelatedEntitySchemaTypes extends EntitySchemaTypes { private relationship: Relationship; + public filterSchemaTypes: RelatedEntityFilterSchemaTypes; constructor({ relationship, @@ -49,6 +50,11 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { + return { + fields: { + equals: "[String]", + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { + return { + fields: { + equals: "[String!]", + }, + }; + }); + } + + public get stringWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("StringWhere", (itc) => { + return { + fields: { + OR: itc.NonNull.List, + AND: itc.NonNull.List, + NOT: itc, + equals: GraphQLString, + in: list(nonNull(GraphQLString.name)), + matches: GraphQLString, + contains: GraphQLString, + startsWith: GraphQLString, + endsWith: GraphQLString, + }, + }; + }); + } + + public getIdListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("IDListWhereNullable", (itc) => { + return { + fields: { + equals: "[String]", + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("IDListWhere", () => { + return { + fields: { + equals: "[String!]", + }, + }; + }); + } + + public get idWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("IDWhere", (itc) => { + return { + fields: { + OR: itc.NonNull.List, + AND: itc.NonNull.List, + NOT: itc, + equals: GraphQLID, + in: list(nonNull(GraphQLID.name)), + matches: GraphQLID, + contains: GraphQLID, + startsWith: GraphQLID, + endsWith: GraphQLID, + }, + }; + }); + } + + public getIntListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("IntListWhereNullable", (itc) => { + return { + fields: { + equals: "[Int]", + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("IntListWhere", () => { + return { + fields: { + equals: "[Int!]", + }, + }; + }); + } + + public get intWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("IntWhere", (itc) => { + return { + fields: { + OR: itc.NonNull.List, + AND: itc.NonNull.List, + NOT: itc, + equals: GraphQLInt, + in: list(nonNull(GraphQLInt.name)), + lt: GraphQLInt, + lte: GraphQLInt, + gt: GraphQLInt, + gte: GraphQLInt, + }, + }; + }); + } + + public getFloatListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("FloatListWhereNullable", (itc) => { + return { + fields: { + equals: "[Float]", + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("FloatListWhere", () => { + return { + fields: { + equals: "[Float!]", + }, + }; + }); + } + + public get floatWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("FloatWhere", (itc) => { + return { + fields: { + OR: itc.NonNull.List, + AND: itc.NonNull.List, + NOT: itc, + equals: GraphQLFloat, + in: list(nonNull(GraphQLFloat.name)), + lt: GraphQLFloat, + lte: GraphQLFloat, + gt: GraphQLFloat, + gte: GraphQLFloat, + }, + }; + }); + } + + // private getListWhereFields(itc: InputTypeComposer, targetType: InputTypeComposer): Record { + // return { + // OR: itc.NonNull.List, + // AND: itc.NonNull.List, + // NOT: itc, + // all: targetType, + // none: targetType, + // single: targetType, + // some: targetType, + // }; + // } } 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 index 901d5b6c2f..133a1e139f 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -17,20 +17,26 @@ * limitations under the License. */ +import type { GraphQLResolveInfo } from "graphql"; import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; +import { GraphQLBuiltInScalarType } from "../../../schema-model/attribute/AttributeType"; import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; -import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; +import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; +import { filterTruthy } from "../../../utils/utils"; +import type { TopLevelEntityTypeNames } from "../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; import type { FieldDefinition, SchemaBuilder } from "../SchemaBuilder"; import { EntitySchemaTypes } from "./EntitySchemaTypes"; import { RelatedEntitySchemaTypes } from "./RelatedEntitySchemaTypes"; import type { SchemaTypes } from "./SchemaTypes"; +import { TopLevelFilterSchemaTypes } from "./filter-schema-types/TopLevelFilterSchemaTypes"; -export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { +export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { private entity: ConcreteEntity; + private filterSchemaTypes: TopLevelFilterSchemaTypes; constructor({ entity, @@ -47,6 +53,25 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes Promise + ): void { + this.schemaBuilder.addQueryField({ + name: this.entity.typeNames.queryField, + type: this.connectionOperation, + args: { + where: this.filterSchemaTypes.operationWhere, + }, + resolver, + }); } protected get edge(): ObjectTypeComposer { @@ -84,7 +109,17 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { const sortFields = Object.fromEntries( - this.getFields().map((field) => [field.name, this.schemaTypes.staticTypes.sortDirection]) + filterTruthy( + this.getFields().map((field) => { + if ( + Object.values(GraphQLBuiltInScalarType).includes( + field.type.name as GraphQLBuiltInScalarType + ) + ) { + return [field.name, this.schemaTypes.staticTypes.sortDirection]; + } + }) + ) ); return { @@ -93,6 +128,10 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes; } - private getRelationshipFields(): Record { + private getRelationshipFields(): Record }> { return Object.fromEntries( [...this.entity.relationships.values()].map((relationship) => { const relationshipTypes = new RelatedEntitySchemaTypes({ @@ -113,8 +152,8 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { + 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 operationWhere(): 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: ([string, InputTypeComposer | GraphQLScalarType] | [])[] = filterTruthy( + attributes.map((attribute) => { + const propertyFilter = this.attributeToPropertyFilter(attribute); + if (propertyFilter) { + return [attribute.name, propertyFilter]; + } + }) + ); + return Object.fromEntries(fields); + } + + private attributeToPropertyFilter(attribute: Attribute): GraphQLScalarType | InputTypeComposer | undefined { + const isList = attribute.type instanceof ListType; + const wrappedType = isList ? attribute.type.ofType : attribute.type; + + switch (wrappedType.name as GraphQLBuiltInScalarType) { + case GraphQLBuiltInScalarType.Boolean: { + if (isList) { + return; + } + return GraphQLBoolean; + } + + case GraphQLBuiltInScalarType.String: { + if (isList) { + const isNullable = !wrappedType.isRequired; + return this.schemaTypes.staticTypes.getStringListWhere(isNullable); + } + return this.schemaTypes.staticTypes.stringWhere; + } + + case GraphQLBuiltInScalarType.ID: { + if (isList) { + const isNullable = !wrappedType.isRequired; + return this.schemaTypes.staticTypes.getIdListWhere(isNullable); + } + return this.schemaTypes.staticTypes.idWhere; + } + + case GraphQLBuiltInScalarType.Int: { + if (isList) { + const isNullable = !wrappedType.isRequired; + + return this.schemaTypes.staticTypes.getIntListWhere(isNullable); + } + return this.schemaTypes.staticTypes.intWhere; + } + + case GraphQLBuiltInScalarType.Float: { + if (isList) { + const isNullable = !wrappedType.isRequired; + + return this.schemaTypes.staticTypes.getFloatListWhere(isNullable); + } + return this.schemaTypes.staticTypes.floatWhere; + } + + default: { + return; + } + } + } + + 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..cec5897b17 --- /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, + edges: 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, + all: this.edgeWhere, + none: this.edgeWhere, + single: this.edgeWhere, + some: 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..72e69a3b59 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/filter-schema-types/TopLevelFilterSchemaTypes.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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()]), + }, + }; + }); + } + + 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-model/graphql-type-names/EntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/EntityTypeNames.ts index 2fa626c619..1549e4ab25 100644 --- 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 @@ -27,18 +27,10 @@ export abstract class EntityTypeNames { this.entityName = entity.name; } - public get node(): string { - return `${this.entityName}`; - } - - public get nodeSort(): string { - return `${this.entityName}Sort`; - } - 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; - public abstract get whereInput(): 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 index d529deaade..94fdb2896f 100644 --- 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 @@ -37,6 +37,14 @@ export class RelatedEntityTypeNames extends EntityTypeNames { 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`; } @@ -53,14 +61,22 @@ export class RelatedEntityTypeNames extends EntityTypeNames { return `${this.relatedEntityTypeName}EdgeSort`; } - public get whereInput(): string { - return `${this.relatedEntityTypeName}Where`; + 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; 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 index 9056c8e4ce..a7dad76b2d 100644 --- 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 @@ -31,6 +31,10 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { return `${this.entityName}Operation`; } + public get operationWhere(): string { + return `${this.entityName}OperationWhere`; + } + public get connection(): string { return `${this.entityName}Connection`; } @@ -47,7 +51,19 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { return `${this.entityName}EdgeSort`; } - public get whereInput(): string { + 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; + } } diff --git a/packages/graphql/src/api-v6/translators/translate-read-operation.ts b/packages/graphql/src/api-v6/translators/translate-read-operation.ts index 4a53dac4a7..7e92396c27 100644 --- a/packages/graphql/src/api-v6/translators/translate-read-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-read-operation.ts @@ -23,7 +23,7 @@ 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 { parseResolveInfoTree } from "../queryIRFactory/resolve-tree-parser/ResolveTreeParser"; +import { parseResolveInfoTree } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; const debug = Debug(DEBUG_TRANSLATE); @@ -37,7 +37,6 @@ export function translateReadOperation({ const readFactory = new ReadOperationFactory(context.schemaModel); const parsedTree = parseResolveInfoTree({ resolveTree: context.resolveTree, entity }); - const readOperation = readFactory.createAST({ graphQLTree: parsedTree, entity }); debug(readOperation.print()); const results = readOperation.build(context); diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index 51cc47e41b..d846010e71 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -25,6 +25,7 @@ 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 { DEBUG_ALL } from "../constants"; import { makeAugmentedSchema } from "../schema"; @@ -115,7 +116,7 @@ class Neo4jGraphQL { public async getSchema(): Promise { return this.getExecutableSchema(); } - + @Memoize() public getAuraSchema(): Promise { const document = this.normalizeTypeDefinitions(this.typeDefs); this.schemaModel = this.generateSchemaModel(document, true); 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/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/tests/api-v6/integration/aliasing.int.test.ts b/packages/graphql/tests/api-v6/integration/aliasing.int.test.ts new file mode 100644 index 0000000000..17e5fbd863 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/aliasing.int.test.ts @@ -0,0 +1,260 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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("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, + }, + }, + ]), + }, + }, + }, + }, + ], + }, + }, + }); + }); + + // Check Original tests: connection/alias.int.test.ts + test.todo("should alias pageInfo"); + test.todo("should alias cursor"); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/query-alias.int.test.ts similarity index 100% rename from packages/graphql/tests/api-v6/integration/directives/alias/query.int.test.ts rename to packages/graphql/tests/api-v6/integration/directives/alias/query-alias.int.test.ts diff --git a/packages/graphql/tests/api-v6/integration/sort/sort-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/sort-alias.int.test.ts similarity index 98% rename from packages/graphql/tests/api-v6/integration/sort/sort-alias.int.test.ts rename to packages/graphql/tests/api-v6/integration/directives/alias/sort-alias.int.test.ts index a14df6103e..e46a01bc7b 100644 --- a/packages/graphql/tests/api-v6/integration/sort/sort-alias.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/directives/alias/sort-alias.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Sort with alias", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/sort/sort-relationship-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/alias/sort-relationship-alias.int.test.ts similarity index 99% rename from packages/graphql/tests/api-v6/integration/sort/sort-relationship-alias.int.test.ts rename to packages/graphql/tests/api-v6/integration/directives/alias/sort-relationship-alias.int.test.ts index 49b529c3f1..ff8add1000 100644 --- a/packages/graphql/tests/api-v6/integration/sort/sort-relationship-alias.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/directives/alias/sort-relationship-alias.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Sort relationship with alias", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts new file mode 100644 index 0000000000..141eb3b5ae --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts @@ -0,0 +1,194 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: [ + { edges: { node: { runtime: { equals: 90.5 } } } } + { edges: { 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: [ + { edges: { node: { runtime: { equals: 90.5 } } } } + { edges: { 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("AND filter in edges by node", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + edges: { + 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/or-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts new file mode 100644 index 0000000000..09efe6f979 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts @@ -0,0 +1,193 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 + 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: [ + { edges: { node: { title: { equals: "The Matrix" } } } } + { edges: { 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: [ + { edges: { node: { title: { equals: "The Matrix" } } } } + { edges: { 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("OR filter in edges by node", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + edges: { + 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", + }, + }, + ]), + }, + }, + }); + }); +}); 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..72cfb57fac --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/nested/all.int.test.ts @@ -0,0 +1,170 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { edges: { node: { actors: { edges: { all: { 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: { edges: { node: { actors: { edges: { all: { 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: { edges: { node: { actors: { edges: { all: { 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", + }, + }, + ]), + }, + }, + }); + }); +}); 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..0edfbc9dc4 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/nested/none.int.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { edges: { node: { actors: { edges: { none: { 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: { edges: { node: { actors: { edges: { none: { 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: { edges: { node: { actors: { edges: { none: { 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", + }, + }, + ]), + }, + }, + }); + }); +}); 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..ac93f19454 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/nested/single.int.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { edges: { node: { actors: { edges: { single: { 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: { edges: { node: { actors: { edges: { single: { 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: { edges: { node: { actors: { edges: { single: { 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", + }, + }, + ]), + }, + }, + }); + }); +}); 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..43c64d53ec --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/nested/some.int.test.ts @@ -0,0 +1,185 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { edges: { node: { actors: { edges: { some: { 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: { edges: { node: { actors: { edges: { some: { 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: { edges: { node: { actors: { edges: { some: { 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", + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/filters/relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/relationship.int.test.ts new file mode 100644 index 0000000000..ffc519da34 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/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("filter by 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..5d90d04abd --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/top-level-filters.int.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { + edges: { + 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/relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/relationship.int.test.ts index 80a52f0afe..b38bdcdec8 100644 --- a/packages/graphql/tests/api-v6/integration/relationship.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/relationship.int.test.ts @@ -165,59 +165,4 @@ describe("Relationship simple query", () => { }, }); }); - - 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 - 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: [ - { - n: { - name: "The Matrix", - a: { - nc: { - ne: [ - { - nn: { nodeName: "Keanu" }, - np: { y: 1999 }, - }, - ], - }, - }, - }, - }, - ], - }, - }, - }); - }); }); diff --git a/packages/graphql/tests/api-v6/integration/simple.int.test.ts b/packages/graphql/tests/api-v6/integration/simple-query.int.test.ts similarity index 98% rename from packages/graphql/tests/api-v6/integration/simple.int.test.ts rename to packages/graphql/tests/api-v6/integration/simple-query.int.test.ts index 1333e2e515..badbe7332a 100644 --- a/packages/graphql/tests/api-v6/integration/simple.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/simple-query.int.test.ts @@ -20,7 +20,7 @@ import type { UniqueType } from "../../utils/graphql-types"; import { TestHelper } from "../../utils/tests-helper"; -describe("Aura-api simple", () => { +describe("Simple Query", () => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; diff --git a/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts b/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts index 2c019d5727..db6b42e497 100644 --- a/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts @@ -21,7 +21,7 @@ import type { UniqueType } from "../../../utils/graphql-types"; import { TestHelper } from "../../../utils/tests-helper"; describe("Numeric fields", () => { - const testHelper = new TestHelper({ cdc: false, v6Api: true }); + const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; beforeAll(async () => { @@ -30,12 +30,13 @@ describe("Numeric fields", () => { const typeDefs = /* GraphQL */ ` type ${Movie} @node { year: Int! + rating: Float! } `; await testHelper.initNeo4jGraphQL({ typeDefs }); await testHelper.executeCypher(` - CREATE (movie:${Movie} {year: 1999}) + CREATE (movie:${Movie} {year: 1999, rating: 4.0}) `); }); @@ -43,7 +44,7 @@ describe("Numeric fields", () => { await testHelper.close(); }); - test("should be able to get an integer field", async () => { + test("should be able to get int and float fields", async () => { const query = /* GraphQL */ ` query { ${Movie.plural} { @@ -51,6 +52,7 @@ describe("Numeric fields", () => { edges { node { year + rating } } @@ -68,6 +70,7 @@ describe("Numeric fields", () => { { node: { year: 1999, + rating: 4.0, }, }, ], diff --git a/packages/graphql/tests/api-v6/schema/array.test.ts b/packages/graphql/tests/api-v6/schema/array.test.ts new file mode 100644 index 0000000000..a887aa8d47 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/array.test.ts @@ -0,0 +1,352 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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"; + +describe("Scalars", () => { + test("should generate the right types for all the scalars", async () => { + const typeDefs = /* GraphQL */ ` + type NodeType @node { + stringList: [String!] + stringListNullable: [String] + intList: [Int!] + intListNullable: [Int] + floatList: [Float!] + floatListNullable: [Float] + idList: [ID!] + idListNullable: [ID] + booleanList: [Boolean!] + booleanListNullable: [Boolean] + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNode @node { + stringList: [String!] + stringListNullable: [String] + intList: [Int!] + intListNullable: [Int] + floatList: [Float!] + floatListNullable: [Float] + idList: [ID!] + idListNullable: [ID] + booleanList: [Boolean!] + booleanListNullable: [Boolean] + } + + type RelatedNodeProperties @relationshipProperties { + stringList: [String!] + stringListNullable: [String] + intList: [Int!] + intListNullable: [Int] + floatList: [Float!] + floatListNullable: [Float] + idList: [ID!] + idListNullable: [ID] + booleanList: [Boolean!] + booleanListNullable: [Boolean] + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + } + + input FloatListWhere { + equals: [Float!] + } + + input FloatListWhereNullable { + equals: [Float] + } + + input IDListWhere { + equals: [String!] + } + + input IDListWhereNullable { + equals: [String] + } + + input IntListWhere { + equals: [Int!] + } + + input IntListWhereNullable { + equals: [Int] + } + + type NodeType { + booleanList: [Boolean!] + booleanListNullable: [Boolean] + floatList: [Float!] + floatListNullable: [Float] + idList: [ID!] + idListNullable: [ID] + intList: [Int!] + intListNullable: [Int] + relatedNode(where: NodeTypeRelatedNodeOperationWhere): NodeTypeRelatedNodeOperation + stringList: [String!] + stringListNullable: [String] + } + + type NodeTypeConnection { + edges: [NodeTypeEdge] + pageInfo: PageInfo + } + + input NodeTypeConnectionSort { + edges: [NodeTypeEdgeSort!] + } + + type NodeTypeEdge { + cursor: String + node: NodeType + } + + input NodeTypeEdgeSort { + node: NodeTypeSort + } + + input NodeTypeEdgeWhere { + AND: [NodeTypeEdgeWhere!] + NOT: NodeTypeEdgeWhere + OR: [NodeTypeEdgeWhere!] + node: NodeTypeWhere + } + + type NodeTypeOperation { + connection(sort: NodeTypeConnectionSort): NodeTypeConnection + } + + input NodeTypeOperationWhere { + AND: [NodeTypeOperationWhere!] + NOT: NodeTypeOperationWhere + OR: [NodeTypeOperationWhere!] + edges: NodeTypeEdgeWhere + } + + type NodeTypeRelatedNodeConnection { + edges: [NodeTypeRelatedNodeEdge] + pageInfo: PageInfo + } + + input NodeTypeRelatedNodeConnectionSort { + edges: [NodeTypeRelatedNodeEdgeSort!] + } + + type NodeTypeRelatedNodeEdge { + cursor: String + node: RelatedNode + properties: RelatedNodeProperties + } + + input NodeTypeRelatedNodeEdgeListWhere { + AND: [NodeTypeRelatedNodeEdgeListWhere!] + NOT: NodeTypeRelatedNodeEdgeListWhere + OR: [NodeTypeRelatedNodeEdgeListWhere!] + all: NodeTypeRelatedNodeEdgeWhere + none: NodeTypeRelatedNodeEdgeWhere + single: NodeTypeRelatedNodeEdgeWhere + some: 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!] + edges: NodeTypeRelatedNodeEdgeListWhere + } + + type NodeTypeRelatedNodeOperation { + connection(sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection + } + + input NodeTypeRelatedNodeOperationWhere { + AND: [NodeTypeRelatedNodeOperationWhere!] + NOT: NodeTypeRelatedNodeOperationWhere + OR: [NodeTypeRelatedNodeOperationWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeSort + + input NodeTypeWhere { + AND: [NodeTypeWhere!] + NOT: NodeTypeWhere + OR: [NodeTypeWhere!] + floatList: FloatListWhere + floatListNullable: FloatListWhereNullable + idList: IDListWhere + idListNullable: IDListWhereNullable + intList: IntListWhere + intListNullable: IntListWhereNullable + relatedNode: NodeTypeRelatedNodeNestedOperationWhere + stringList: StringListWhere + stringListNullable: StringListWhereNullable + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + type Query { + nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation + relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation + } + + type RelatedNode { + booleanList: [Boolean!] + booleanListNullable: [Boolean] + floatList: [Float!] + floatListNullable: [Float] + idList: [ID!] + idListNullable: [ID] + intList: [Int!] + intListNullable: [Int] + stringList: [String!] + stringListNullable: [String] + } + + type RelatedNodeConnection { + edges: [RelatedNodeEdge] + pageInfo: PageInfo + } + + input RelatedNodeConnectionSort { + edges: [RelatedNodeEdgeSort!] + } + + type RelatedNodeEdge { + cursor: String + node: RelatedNode + } + + input RelatedNodeEdgeSort { + node: RelatedNodeSort + } + + input RelatedNodeEdgeWhere { + AND: [RelatedNodeEdgeWhere!] + NOT: RelatedNodeEdgeWhere + OR: [RelatedNodeEdgeWhere!] + node: RelatedNodeWhere + } + + type RelatedNodeOperation { + connection(sort: RelatedNodeConnectionSort): RelatedNodeConnection + } + + input RelatedNodeOperationWhere { + AND: [RelatedNodeOperationWhere!] + NOT: RelatedNodeOperationWhere + OR: [RelatedNodeOperationWhere!] + edges: RelatedNodeEdgeWhere + } + + type RelatedNodeProperties { + booleanList: [Boolean!] + booleanListNullable: [Boolean] + floatList: [Float!] + floatListNullable: [Float] + idList: [ID!] + idListNullable: [ID] + intList: [Int!] + intListNullable: [Int] + stringList: [String!] + stringListNullable: [String] + } + + input RelatedNodePropertiesSort { + booleanList: SortDirection + booleanListNullable: SortDirection + floatList: SortDirection + floatListNullable: SortDirection + idList: SortDirection + idListNullable: SortDirection + intList: SortDirection + intListNullable: SortDirection + stringList: SortDirection + stringListNullable: SortDirection + } + + input RelatedNodePropertiesWhere { + AND: [RelatedNodePropertiesWhere!] + NOT: RelatedNodePropertiesWhere + OR: [RelatedNodePropertiesWhere!] + floatList: FloatListWhere + floatListNullable: FloatListWhereNullable + idList: IDListWhere + idListNullable: IDListWhereNullable + intList: IntListWhere + intListNullable: IntListWhereNullable + stringList: StringListWhere + stringListNullable: StringListWhereNullable + } + + input RelatedNodeSort + + input RelatedNodeWhere { + AND: [RelatedNodeWhere!] + NOT: RelatedNodeWhere + OR: [RelatedNodeWhere!] + floatList: FloatListWhere + floatListNullable: FloatListWhereNullable + idList: IDListWhere + idListNullable: IDListWhereNullable + intList: IntListWhere + intListNullable: IntListWhereNullable + stringList: StringListWhere + stringListNullable: StringListWhereNullable + } + + enum SortDirection { + ASC + DESC + } + + input StringListWhere { + equals: [String!] + } + + input StringListWhereNullable { + equals: [String] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 4b30eae8a5..da641ec915 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -42,7 +42,7 @@ describe("Relationships", () => { } type Actor { - movies: ActorMoviesOperation + movies(where: ActorMoviesOperationWhere): ActorMoviesOperation name: String } @@ -64,6 +64,13 @@ describe("Relationships", () => { node: ActorSort } + input ActorEdgeWhere { + AND: [ActorEdgeWhere!] + NOT: ActorEdgeWhere + OR: [ActorEdgeWhere!] + node: ActorWhere + } + type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo @@ -78,24 +85,70 @@ describe("Relationships", () => { node: Movie } + input ActorMoviesEdgeListWhere { + AND: [ActorMoviesEdgeListWhere!] + NOT: ActorMoviesEdgeListWhere + OR: [ActorMoviesEdgeListWhere!] + all: ActorMoviesEdgeWhere + none: ActorMoviesEdgeWhere + single: ActorMoviesEdgeWhere + some: ActorMoviesEdgeWhere + } + input ActorMoviesEdgeSort { node: MovieSort } + input ActorMoviesEdgeWhere { + AND: [ActorMoviesEdgeWhere!] + NOT: ActorMoviesEdgeWhere + OR: [ActorMoviesEdgeWhere!] + node: MovieWhere + } + + input ActorMoviesNestedOperationWhere { + AND: [ActorMoviesNestedOperationWhere!] + NOT: ActorMoviesNestedOperationWhere + OR: [ActorMoviesNestedOperationWhere!] + edges: ActorMoviesEdgeListWhere + } + type ActorMoviesOperation { connection(sort: ActorMoviesConnectionSort): ActorMoviesConnection } + input ActorMoviesOperationWhere { + AND: [ActorMoviesOperationWhere!] + NOT: ActorMoviesOperationWhere + OR: [ActorMoviesOperationWhere!] + edges: ActorMoviesEdgeWhere + } + type ActorOperation { connection(sort: ActorConnectionSort): ActorConnection } + input ActorOperationWhere { + AND: [ActorOperationWhere!] + NOT: ActorOperationWhere + OR: [ActorOperationWhere!] + edges: ActorEdgeWhere + } + input ActorSort { name: SortDirection } + input ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + movies: ActorMoviesNestedOperationWhere + name: StringWhere + } + type Movie { - actors: MovieActorsOperation + actors(where: MovieActorsOperationWhere): MovieActorsOperation title: String } @@ -113,14 +166,45 @@ describe("Relationships", () => { node: Actor } + input MovieActorsEdgeListWhere { + AND: [MovieActorsEdgeListWhere!] + NOT: MovieActorsEdgeListWhere + OR: [MovieActorsEdgeListWhere!] + all: MovieActorsEdgeWhere + none: MovieActorsEdgeWhere + single: MovieActorsEdgeWhere + some: MovieActorsEdgeWhere + } + input MovieActorsEdgeSort { node: ActorSort } + input MovieActorsEdgeWhere { + AND: [MovieActorsEdgeWhere!] + NOT: MovieActorsEdgeWhere + OR: [MovieActorsEdgeWhere!] + node: ActorWhere + } + + input MovieActorsNestedOperationWhere { + AND: [MovieActorsNestedOperationWhere!] + NOT: MovieActorsNestedOperationWhere + OR: [MovieActorsNestedOperationWhere!] + edges: MovieActorsEdgeListWhere + } + type MovieActorsOperation { connection(sort: MovieActorsConnectionSort): MovieActorsConnection } + input MovieActorsOperationWhere { + AND: [MovieActorsOperationWhere!] + NOT: MovieActorsOperationWhere + OR: [MovieActorsOperationWhere!] + edges: MovieActorsEdgeWhere + } + type MovieConnection { edges: [MovieEdge] pageInfo: PageInfo @@ -139,27 +223,61 @@ describe("Relationships", () => { node: MovieSort } + input MovieEdgeWhere { + AND: [MovieEdgeWhere!] + NOT: MovieEdgeWhere + OR: [MovieEdgeWhere!] + node: MovieWhere + } + type MovieOperation { connection(sort: MovieConnectionSort): MovieConnection } + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + edges: MovieEdgeWhere + } + input MovieSort { title: SortDirection } + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + actors: MovieActorsNestedOperationWhere + title: StringWhere + } + type PageInfo { hasNextPage: Boolean hasPreviousPage: Boolean } type Query { - actors: ActorOperation - movies: MovieOperation + actors(where: ActorOperationWhere): ActorOperation + movies(where: MovieOperationWhere): MovieOperation } enum SortDirection { ASC DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + matches: String + startsWith: String }" `); }); @@ -195,8 +313,15 @@ describe("Relationships", () => { year: SortDirection } + input ActedInWhere { + AND: [ActedInWhere!] + NOT: ActedInWhere + OR: [ActedInWhere!] + year: IntWhere + } + type Actor { - movies: ActorMoviesOperation + movies(where: ActorMoviesOperationWhere): ActorMoviesOperation name: String } @@ -218,6 +343,13 @@ describe("Relationships", () => { node: ActorSort } + input ActorEdgeWhere { + AND: [ActorEdgeWhere!] + NOT: ActorEdgeWhere + OR: [ActorEdgeWhere!] + node: ActorWhere + } + type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo @@ -233,25 +365,84 @@ describe("Relationships", () => { properties: ActedIn } + input ActorMoviesEdgeListWhere { + AND: [ActorMoviesEdgeListWhere!] + NOT: ActorMoviesEdgeListWhere + OR: [ActorMoviesEdgeListWhere!] + all: ActorMoviesEdgeWhere + none: ActorMoviesEdgeWhere + single: ActorMoviesEdgeWhere + some: 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!] + edges: ActorMoviesEdgeListWhere + } + type ActorMoviesOperation { connection(sort: ActorMoviesConnectionSort): ActorMoviesConnection } + input ActorMoviesOperationWhere { + AND: [ActorMoviesOperationWhere!] + NOT: ActorMoviesOperationWhere + OR: [ActorMoviesOperationWhere!] + edges: ActorMoviesEdgeWhere + } + type ActorOperation { connection(sort: ActorConnectionSort): ActorConnection } + input ActorOperationWhere { + AND: [ActorOperationWhere!] + NOT: ActorOperationWhere + OR: [ActorOperationWhere!] + edges: ActorEdgeWhere + } + input ActorSort { name: SortDirection } + input ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + movies: ActorMoviesNestedOperationWhere + name: StringWhere + } + + input IntWhere { + AND: [IntWhere!] + NOT: IntWhere + OR: [IntWhere!] + equals: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + } + type Movie { - actors: MovieActorsOperation + actors(where: MovieActorsOperationWhere): MovieActorsOperation title: String } @@ -270,15 +461,47 @@ describe("Relationships", () => { properties: ActedIn } + input MovieActorsEdgeListWhere { + AND: [MovieActorsEdgeListWhere!] + NOT: MovieActorsEdgeListWhere + OR: [MovieActorsEdgeListWhere!] + all: MovieActorsEdgeWhere + none: MovieActorsEdgeWhere + single: MovieActorsEdgeWhere + some: 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!] + edges: MovieActorsEdgeListWhere + } + type MovieActorsOperation { connection(sort: MovieActorsConnectionSort): MovieActorsConnection } + input MovieActorsOperationWhere { + AND: [MovieActorsOperationWhere!] + NOT: MovieActorsOperationWhere + OR: [MovieActorsOperationWhere!] + edges: MovieActorsEdgeWhere + } + type MovieConnection { edges: [MovieEdge] pageInfo: PageInfo @@ -297,27 +520,61 @@ describe("Relationships", () => { node: MovieSort } + input MovieEdgeWhere { + AND: [MovieEdgeWhere!] + NOT: MovieEdgeWhere + OR: [MovieEdgeWhere!] + node: MovieWhere + } + type MovieOperation { connection(sort: MovieConnectionSort): MovieConnection } + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + edges: MovieEdgeWhere + } + input MovieSort { title: SortDirection } + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + actors: MovieActorsNestedOperationWhere + title: StringWhere + } + type PageInfo { hasNextPage: Boolean hasPreviousPage: Boolean } type Query { - actors: ActorOperation - movies: MovieOperation + actors(where: ActorOperationWhere): ActorOperation + movies(where: MovieOperationWhere): MovieOperation } enum SortDirection { ASC DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + matches: String + startsWith: String }" `); }); diff --git a/packages/graphql/tests/api-v6/schema/scalars.test.ts b/packages/graphql/tests/api-v6/schema/scalars.test.ts new file mode 100644 index 0000000000..7538291d47 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/scalars.test.ts @@ -0,0 +1,336 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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"; + +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 + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNode @node { + string: String + int: Int + float: Float + id: ID + boolean: Boolean + } + + type RelatedNodeProperties @relationshipProperties { + string: String + int: Int + float: Float + id: ID + boolean: Boolean + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + } + + input FloatWhere { + AND: [FloatWhere!] + NOT: FloatWhere + OR: [FloatWhere!] + equals: Float + gt: Float + gte: Float + in: [Float!] + lt: Float + lte: Float + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + matches: ID + startsWith: ID + } + + input IntWhere { + AND: [IntWhere!] + NOT: IntWhere + OR: [IntWhere!] + equals: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + } + + type NodeType { + boolean: Boolean + float: Float + id: ID + int: Int + relatedNode(where: NodeTypeRelatedNodeOperationWhere): NodeTypeRelatedNodeOperation + string: String + } + + type NodeTypeConnection { + edges: [NodeTypeEdge] + pageInfo: PageInfo + } + + input NodeTypeConnectionSort { + edges: [NodeTypeEdgeSort!] + } + + type NodeTypeEdge { + cursor: String + node: NodeType + } + + input NodeTypeEdgeSort { + node: NodeTypeSort + } + + input NodeTypeEdgeWhere { + AND: [NodeTypeEdgeWhere!] + NOT: NodeTypeEdgeWhere + OR: [NodeTypeEdgeWhere!] + node: NodeTypeWhere + } + + type NodeTypeOperation { + connection(sort: NodeTypeConnectionSort): NodeTypeConnection + } + + input NodeTypeOperationWhere { + AND: [NodeTypeOperationWhere!] + NOT: NodeTypeOperationWhere + OR: [NodeTypeOperationWhere!] + edges: NodeTypeEdgeWhere + } + + type NodeTypeRelatedNodeConnection { + edges: [NodeTypeRelatedNodeEdge] + pageInfo: PageInfo + } + + input NodeTypeRelatedNodeConnectionSort { + edges: [NodeTypeRelatedNodeEdgeSort!] + } + + type NodeTypeRelatedNodeEdge { + cursor: String + node: RelatedNode + properties: RelatedNodeProperties + } + + input NodeTypeRelatedNodeEdgeListWhere { + AND: [NodeTypeRelatedNodeEdgeListWhere!] + NOT: NodeTypeRelatedNodeEdgeListWhere + OR: [NodeTypeRelatedNodeEdgeListWhere!] + all: NodeTypeRelatedNodeEdgeWhere + none: NodeTypeRelatedNodeEdgeWhere + single: NodeTypeRelatedNodeEdgeWhere + some: 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!] + edges: NodeTypeRelatedNodeEdgeListWhere + } + + type NodeTypeRelatedNodeOperation { + connection(sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection + } + + input NodeTypeRelatedNodeOperationWhere { + AND: [NodeTypeRelatedNodeOperationWhere!] + NOT: NodeTypeRelatedNodeOperationWhere + OR: [NodeTypeRelatedNodeOperationWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeSort { + boolean: SortDirection + float: SortDirection + id: SortDirection + int: SortDirection + string: SortDirection + } + + input NodeTypeWhere { + AND: [NodeTypeWhere!] + NOT: NodeTypeWhere + OR: [NodeTypeWhere!] + boolean: Boolean + float: FloatWhere + id: IDWhere + int: IntWhere + relatedNode: NodeTypeRelatedNodeNestedOperationWhere + string: StringWhere + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + type Query { + nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation + relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation + } + + type RelatedNode { + boolean: Boolean + float: Float + id: ID + int: Int + string: String + } + + type RelatedNodeConnection { + edges: [RelatedNodeEdge] + pageInfo: PageInfo + } + + input RelatedNodeConnectionSort { + edges: [RelatedNodeEdgeSort!] + } + + type RelatedNodeEdge { + cursor: String + node: RelatedNode + } + + input RelatedNodeEdgeSort { + node: RelatedNodeSort + } + + input RelatedNodeEdgeWhere { + AND: [RelatedNodeEdgeWhere!] + NOT: RelatedNodeEdgeWhere + OR: [RelatedNodeEdgeWhere!] + node: RelatedNodeWhere + } + + type RelatedNodeOperation { + connection(sort: RelatedNodeConnectionSort): RelatedNodeConnection + } + + input RelatedNodeOperationWhere { + AND: [RelatedNodeOperationWhere!] + NOT: RelatedNodeOperationWhere + OR: [RelatedNodeOperationWhere!] + edges: RelatedNodeEdgeWhere + } + + type RelatedNodeProperties { + boolean: Boolean + float: Float + id: ID + int: Int + string: String + } + + input RelatedNodePropertiesSort { + boolean: SortDirection + float: SortDirection + id: SortDirection + int: SortDirection + string: SortDirection + } + + input RelatedNodePropertiesWhere { + AND: [RelatedNodePropertiesWhere!] + NOT: RelatedNodePropertiesWhere + OR: [RelatedNodePropertiesWhere!] + boolean: Boolean + float: FloatWhere + id: IDWhere + int: IntWhere + string: StringWhere + } + + input RelatedNodeSort { + boolean: SortDirection + float: SortDirection + id: SortDirection + int: SortDirection + string: SortDirection + } + + input RelatedNodeWhere { + AND: [RelatedNodeWhere!] + NOT: RelatedNodeWhere + OR: [RelatedNodeWhere!] + boolean: Boolean + float: FloatWhere + id: IDWhere + int: IntWhere + string: StringWhere + } + + enum SortDirection { + ASC + DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + matches: 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 index 9bc33c2913..2a9ce67051 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -58,26 +58,59 @@ describe("Simple Aura-API", () => { node: MovieSort } + input MovieEdgeWhere { + AND: [MovieEdgeWhere!] + NOT: MovieEdgeWhere + OR: [MovieEdgeWhere!] + node: MovieWhere + } + type MovieOperation { connection(sort: MovieConnectionSort): MovieConnection } + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + edges: MovieEdgeWhere + } + input MovieSort { title: SortDirection } + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + title: StringWhere + } + type PageInfo { hasNextPage: Boolean hasPreviousPage: Boolean } type Query { - movies: MovieOperation + movies(where: MovieOperationWhere): MovieOperation } enum SortDirection { ASC DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + matches: String + startsWith: String }" `); }); @@ -121,14 +154,35 @@ describe("Simple Aura-API", () => { node: ActorSort } + input ActorEdgeWhere { + AND: [ActorEdgeWhere!] + NOT: ActorEdgeWhere + OR: [ActorEdgeWhere!] + node: ActorWhere + } + type ActorOperation { connection(sort: ActorConnectionSort): ActorConnection } + input ActorOperationWhere { + AND: [ActorOperationWhere!] + NOT: ActorOperationWhere + OR: [ActorOperationWhere!] + edges: ActorEdgeWhere + } + input ActorSort { name: SortDirection } + input ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + name: StringWhere + } + type Movie { title: String } @@ -151,27 +205,60 @@ describe("Simple Aura-API", () => { node: MovieSort } + input MovieEdgeWhere { + AND: [MovieEdgeWhere!] + NOT: MovieEdgeWhere + OR: [MovieEdgeWhere!] + node: MovieWhere + } + type MovieOperation { connection(sort: MovieConnectionSort): MovieConnection } + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + edges: MovieEdgeWhere + } + input MovieSort { title: SortDirection } + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + title: StringWhere + } + type PageInfo { hasNextPage: Boolean hasPreviousPage: Boolean } type Query { - actors: ActorOperation - movies: MovieOperation + actors(where: ActorOperationWhere): ActorOperation + movies(where: MovieOperationWhere): MovieOperation } enum SortDirection { ASC DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + matches: String + startsWith: String }" `); }); @@ -215,26 +302,59 @@ describe("Simple Aura-API", () => { node: MovieSort } + input MovieEdgeWhere { + AND: [MovieEdgeWhere!] + NOT: MovieEdgeWhere + OR: [MovieEdgeWhere!] + node: MovieWhere + } + type MovieOperation { connection(sort: MovieConnectionSort): MovieConnection } + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + edges: MovieEdgeWhere + } + input MovieSort { title: SortDirection } + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + title: StringWhere + } + type PageInfo { hasNextPage: Boolean hasPreviousPage: Boolean } type Query { - movies: MovieOperation + movies(where: MovieOperationWhere): MovieOperation } enum SortDirection { ASC DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + matches: String + startsWith: String }" `); }); 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..a684aa3853 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/array/array-filters.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("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: { edges: { node: { alternativeTitles: { equals: ["potato"] } } } }) { + connection { + edges { + node { + alternativeTitles + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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/filters-on-relationships.test.ts b/packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts new file mode 100644 index 0000000000..b7edd6b49a --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts @@ -0,0 +1,242 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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]-(actors:Actor) + WHERE actors.name = $param0 + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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 }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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]-(actors:Actor) + WHERE this1.year = $param0 + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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 }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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]-(actors:Actor) + WHERE (actors.name = $param0 AND this1.year = $param1) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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/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..f100d3845b --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/logical-filters/and-filter.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: [ + { edges: { node: { title: { equals: "The Matrix" } } } } + { edges: { node: { year: { equals: 100 } } } } + ] + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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 on edges", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { + edges: { + AND: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 100 } } }] + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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..4767743776 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/logical-filters/not-filter.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 { 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: { edges: { node: { title: { equals: "The Matrix" } } } } }) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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 edges", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { edges: { NOT: { node: { title: { equals: "The Matrix" }, year: { equals: 100 } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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..def04a44b1 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/logical-filters/or-filter.test.ts @@ -0,0 +1,184 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: [ + { edges: { node: { title: { equals: "The Matrix" } } } } + { edges: { node: { year: { equals: 100 } } } } + ] + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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 edges", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { + edges: { + OR: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 100 } } }] + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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.skip("OR logical filter in nodes", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { edges: { node: { OR: [{ title: { equals: "The Matrix" } }, { year: { equals: 100 } }] } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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 + } + }" + `); + }); +}); 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..55a1cf6046 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/nested/all.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 { 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: { edges: { node: { actors: { edges: { all: { node: { name: { equals: "Keanu" } } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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: { + edges: { node: { actors: { edges: { all: { properties: { year: { equals: 1999 } } } } } } } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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: { + edges: { + node: { + actors: { + edges: { + all: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "eeves" } } } + ] + } + } + } + } + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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..748107a12f --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts @@ -0,0 +1,199 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { edges: { node: { actors: { edges: { none: { node: { name: { equals: "Keanu" } } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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: { + edges: { node: { actors: { edges: { none: { properties: { year: { equals: 1999 } } } } } } } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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: { + edges: { + node: { + actors: { + edges: { + some: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "eeves" } } } + ] + } + } + } + } + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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..d4ec37e8b4 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts @@ -0,0 +1,192 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { + edges: { node: { actors: { edges: { single: { node: { name: { equals: "Keanu" } } } } } } } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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: { + edges: { node: { actors: { edges: { single: { properties: { year: { equals: 1999 } } } } } } } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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: { + edges: { + node: { + actors: { + edges: { + single: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "eeves" } } } + ] + } + } + } + } + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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..b877d46304 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts @@ -0,0 +1,199 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { edges: { node: { actors: { edges: { some: { node: { name: { equals: "Keanu" } } } } } } } } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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: { + edges: { node: { actors: { edges: { some: { properties: { year: { equals: 1999 } } } } } } } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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: { + edges: { + node: { + actors: { + edges: { + some: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "eeves" } } } + ] + } + } + } + } + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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/top-level-filters.test.ts b/packages/graphql/tests/api-v6/tck/filters/top-level-filters.test.ts new file mode 100644 index 0000000000..3d784bc086 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/top-level-filters.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 { 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: { + edges: { + node: { title: { equals: "The Matrix" }, year: { equals: 100 }, runtime: { equals: 90.5 } } + } + } + ) { + connection { + edges { + node { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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/simple.test.ts b/packages/graphql/tests/api-v6/tck/simple-query.test.ts similarity index 98% rename from packages/graphql/tests/api-v6/tck/simple.test.ts rename to packages/graphql/tests/api-v6/tck/simple-query.test.ts index e63b8bcd5d..154d058157 100644 --- a/packages/graphql/tests/api-v6/tck/simple.test.ts +++ b/packages/graphql/tests/api-v6/tck/simple-query.test.ts @@ -20,7 +20,7 @@ import { Neo4jGraphQL } from "../../../src"; import { formatCypher, formatParams, translateQuery } from "../../tck/utils/tck-test-utils"; -describe("Simple Aura API", () => { +describe("Simple Query", () => { let typeDefs: string; let neoSchema: Neo4jGraphQL; 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 index 8642be8c3d..f0dc3a451a 100644 --- a/packages/graphql/tests/integration/connections/alias.int.test.ts +++ b/packages/graphql/tests/integration/connections/alias.int.test.ts @@ -391,56 +391,6 @@ describe("Connections Alias", () => { 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} { @@ -490,397 +440,4 @@ describe("Connections Alias", () => { 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/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 */ ` From b15844f1fb28961059ff7a699e5756e02fc473e4 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 23 May 2024 14:50:46 +0100 Subject: [PATCH 026/177] Introduce temporal types in the v6 API --- .../api-v6/queryIRFactory/FilterOperators.ts | 4 +- .../schema-types/StaticSchemaTypes.ts | 182 ++++++--- .../schema-types/TopLevelEntitySchemaTypes.ts | 17 +- .../filter-schema-types/FilterSchemaTypes.ts | 70 +++- .../filters/types/temporals.int.test.ts | 118 ++++++ .../api-v6/schema/{ => types}/array.test.ts | 6 +- .../api-v6/schema/{ => types}/scalars.test.ts | 2 +- .../api-v6/schema/types/temporals.test.ts | 362 ++++++++++++++++++ .../tck/filters/types/temporals.test.ts | 140 +++++++ 9 files changed, 834 insertions(+), 67 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/temporals.int.test.ts rename packages/graphql/tests/api-v6/schema/{ => types}/array.test.ts (99%) rename packages/graphql/tests/api-v6/schema/{ => types}/scalars.test.ts (99%) create mode 100644 packages/graphql/tests/api-v6/schema/types/temporals.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts index 6b5c64cfb4..cdd513aaf1 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts @@ -5,7 +5,9 @@ export function getFilterOperator(attribute: AttributeAdapter, operator: string) if (attribute.typeHelper.isString() || attribute.typeHelper.isID()) { return getStringOperator(operator); } - + if (attribute.typeHelper.isTemporal()) { + return getNumberOperator(operator); + } if (attribute.typeHelper.isNumeric()) { return getNumberOperator(operator); } 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 index 72aec0bc76..bad83adb7c 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -17,9 +17,18 @@ * limitations under the License. */ +import type { GraphQLScalarType } from "graphql"; import { GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; -import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; +import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; +import { + GraphQLDate, + GraphQLDateTime, + GraphQLDuration, + GraphQLLocalDateTime, + GraphQLLocalTime, + GraphQLTime, +} from "../../../graphql/scalars"; import type { SchemaBuilder } from "../SchemaBuilder"; function nonNull(type: string): string { @@ -32,9 +41,11 @@ function list(type: string): string { export class StaticSchemaTypes { private schemaBuilder: SchemaBuilder; + public staticFilterTypes: StaticFilterTypes; constructor({ schemaBuilder }: { schemaBuilder: SchemaBuilder }) { this.schemaBuilder = schemaBuilder; + this.staticFilterTypes = new StaticFilterTypes({ schemaBuilder }); } public get pageInfo(): ObjectTypeComposer { @@ -47,13 +58,21 @@ export class StaticSchemaTypes { public get sortDirection(): EnumTypeComposer { return this.schemaBuilder.createEnumType("SortDirection", ["ASC", "DESC"]); } +} + +class StaticFilterTypes { + private schemaBuilder: SchemaBuilder; + + constructor({ schemaBuilder }: { schemaBuilder: SchemaBuilder }) { + this.schemaBuilder = schemaBuilder; + } public getStringListWhere(nullable: boolean): InputTypeComposer { if (nullable) { return this.schemaBuilder.getOrCreateInputType("StringListWhereNullable", () => { return { fields: { - equals: "[String]", + equals: list(GraphQLString.name), }, }; }); @@ -62,7 +81,7 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { return { fields: { - equals: "[String!]", + equals: list(nonNull(GraphQLString.name)), }, }; }); @@ -72,15 +91,81 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateInputType("StringWhere", (itc) => { return { fields: { - OR: itc.NonNull.List, - AND: itc.NonNull.List, - NOT: itc, - equals: GraphQLString, + ...this.createBooleanOperators(itc), + ...this.createStringOperators(GraphQLString), in: list(nonNull(GraphQLString.name)), - matches: GraphQLString, - contains: GraphQLString, - startsWith: GraphQLString, - endsWith: GraphQLString, + }, + }; + }); + } + + public get dateWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DateWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + in: list(nonNull(GraphQLDate.name)), + ...this.createRelationalOperators(GraphQLDate), + }, + }; + }); + } + + public get dateTimeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DateTimeWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + in: list(nonNull(GraphQLDateTime.name)), + ...this.createRelationalOperators(GraphQLDateTime), + }, + }; + }); + } + + public get localDateTimeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("LocalDateTimeWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createRelationalOperators(GraphQLLocalDateTime), + in: list(nonNull(GraphQLLocalDateTime.name)), + }, + }; + }); + } + + public get durationWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("DurationWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createRelationalOperators(GraphQLDuration), + in: list(nonNull(GraphQLDuration.name)), + }, + }; + }); + } + + public get timeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("TimeWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createRelationalOperators(GraphQLTime), + in: list(nonNull(GraphQLTime.name)), + }, + }; + }); + } + + public get localTimeWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("LocalTimeWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createRelationalOperators(GraphQLLocalTime), + in: list(nonNull(GraphQLLocalTime.name)), }, }; }); @@ -88,10 +173,10 @@ export class StaticSchemaTypes { public getIdListWhere(nullable: boolean): InputTypeComposer { if (nullable) { - return this.schemaBuilder.getOrCreateInputType("IDListWhereNullable", (itc) => { + return this.schemaBuilder.getOrCreateInputType("IDListWhereNullable", () => { return { fields: { - equals: "[String]", + equals: list(GraphQLID.name), }, }; }); @@ -100,7 +185,7 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateInputType("IDListWhere", () => { return { fields: { - equals: "[String!]", + equals: list(nonNull(GraphQLID.name)), }, }; }); @@ -110,15 +195,9 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateInputType("IDWhere", (itc) => { return { fields: { - OR: itc.NonNull.List, - AND: itc.NonNull.List, - NOT: itc, - equals: GraphQLID, + ...this.createBooleanOperators(itc), + ...this.createStringOperators(GraphQLID), in: list(nonNull(GraphQLID.name)), - matches: GraphQLID, - contains: GraphQLID, - startsWith: GraphQLID, - endsWith: GraphQLID, }, }; }); @@ -126,10 +205,10 @@ export class StaticSchemaTypes { public getIntListWhere(nullable: boolean): InputTypeComposer { if (nullable) { - return this.schemaBuilder.getOrCreateInputType("IntListWhereNullable", (itc) => { + return this.schemaBuilder.getOrCreateInputType("IntListWhereNullable", () => { return { fields: { - equals: "[Int]", + equals: list(GraphQLInt.name), }, }; }); @@ -138,7 +217,7 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateInputType("IntListWhere", () => { return { fields: { - equals: "[Int!]", + equals: list(nonNull(GraphQLInt.name)), }, }; }); @@ -148,15 +227,9 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateInputType("IntWhere", (itc) => { return { fields: { - OR: itc.NonNull.List, - AND: itc.NonNull.List, - NOT: itc, - equals: GraphQLInt, + ...this.createBooleanOperators(itc), + ...this.createRelationalOperators(GraphQLInt), in: list(nonNull(GraphQLInt.name)), - lt: GraphQLInt, - lte: GraphQLInt, - gt: GraphQLInt, - gte: GraphQLInt, }, }; }); @@ -164,10 +237,10 @@ export class StaticSchemaTypes { public getFloatListWhere(nullable: boolean): InputTypeComposer { if (nullable) { - return this.schemaBuilder.getOrCreateInputType("FloatListWhereNullable", (itc) => { + return this.schemaBuilder.getOrCreateInputType("FloatListWhereNullable", () => { return { fields: { - equals: "[Float]", + equals: list(GraphQLFloat.name), }, }; }); @@ -176,7 +249,7 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateInputType("FloatListWhere", () => { return { fields: { - equals: "[Float!]", + equals: list(nonNull(GraphQLFloat.name)), }, }; }); @@ -186,20 +259,41 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateInputType("FloatWhere", (itc) => { return { fields: { - OR: itc.NonNull.List, - AND: itc.NonNull.List, - NOT: itc, - equals: GraphQLFloat, + ...this.createBooleanOperators(itc), + ...this.createRelationalOperators(GraphQLFloat), in: list(nonNull(GraphQLFloat.name)), - lt: GraphQLFloat, - lte: GraphQLFloat, - gt: GraphQLFloat, - gte: GraphQLFloat, }, }; }); } + private createStringOperators(type: GraphQLScalarType): Record { + return { + equals: type, + matches: type, + contains: type, + startsWith: type, + endsWith: type, + }; + } + + private createRelationalOperators(type: GraphQLScalarType): 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, + }; + } // private getListWhereFields(itc: InputTypeComposer, targetType: InputTypeComposer): Record { // return { // OR: itc.NonNull.List, 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 index 133a1e139f..7aeb20c821 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -21,7 +21,11 @@ import type { GraphQLResolveInfo } from "graphql"; import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; -import { GraphQLBuiltInScalarType } from "../../../schema-model/attribute/AttributeType"; +import { + GraphQLBuiltInScalarType, + Neo4jGraphQLNumberType, + Neo4jGraphQLTemporalType, +} from "../../../schema-model/attribute/AttributeType"; import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; @@ -112,8 +116,15 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { if ( - Object.values(GraphQLBuiltInScalarType).includes( - field.type.name as GraphQLBuiltInScalarType + [ + ...Object.values(GraphQLBuiltInScalarType), + ...Object.values(Neo4jGraphQLNumberType), + ...Object.values(Neo4jGraphQLTemporalType), + ].includes( + field.type.name as + | GraphQLBuiltInScalarType + | Neo4jGraphQLNumberType + | Neo4jGraphQLTemporalType ) ) { return [field.name, this.schemaTypes.staticTypes.sortDirection]; 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 index 4f6eb13ab1..202ce1f626 100644 --- 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 @@ -21,7 +21,12 @@ import type { GraphQLScalarType } from "graphql"; import { GraphQLBoolean } from "graphql"; import type { InputTypeComposer } from "graphql-compose"; import type { Attribute } from "../../../../schema-model/attribute/Attribute"; -import { GraphQLBuiltInScalarType, ListType } from "../../../../schema-model/attribute/AttributeType"; +import { + GraphQLBuiltInScalarType, + ListType, + Neo4jGraphQLTemporalType, + ScalarType, +} from "../../../../schema-model/attribute/AttributeType"; 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"; @@ -64,7 +69,7 @@ export abstract class FilterSchemaTypes { - const fields: ([string, InputTypeComposer | GraphQLScalarType] | [])[] = filterTruthy( + const fields: Array<[string, InputTypeComposer | GraphQLScalarType] | []> = filterTruthy( attributes.map((attribute) => { const propertyFilter = this.attributeToPropertyFilter(attribute); if (propertyFilter) { @@ -78,8 +83,13 @@ export abstract class FilterSchemaTypes { + const testHelper = new TestHelper({ v6Api: true }); + + let TypeNode: UniqueType; + beforeAll(async () => { + TypeNode = testHelper.createUniqueType("TypeNode"); + + const typeDefs = /* GraphQL */ ` + type ${TypeNode} @node { + id: ID! + dateTime: DateTime + localDateTime: LocalDateTime + duration: Duration + time: Time + localTime: LocalTime + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + + await testHelper.executeCypher(` + CREATE (:${TypeNode} { + id: "1", + dateTime: datetime('2015-06-24T12:50:35.556+0100'), + localDateTime: localDateTime('2003-09-14T12:00:00'), + duration: duration('P1Y'), + time: time('22:00:15.555'), + localTime: localTime('12:50:35.556') + }) + + CREATE (:${TypeNode} { + id: "2", + dateTime: datetime('2011-06-24T12:50:35.556+0100'), + localDateTime: localDateTime('2003-09-14T12:00:00'), + duration: duration('P1Y'), + time: time('22:00:15.555'), + localTime: localTime('12:50:35.556') + }) + + CREATE (:${TypeNode} { + id: "3", + dateTime: datetime('2015-06-24T12:50:35.556+0100'), + localDateTime: localDateTime('2003-09-14T12:00:00'), + duration: duration('P2Y'), + time: time('22:00:15.555'), + localTime: localTime('12:50:35.556') + }) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test("should be possible to filter temporal types", async () => { + const query = /* GraphQL */ ` + query { + ${TypeNode.plural}( + where: { + edges: { + node: { + dateTime: { gt: "2012-06-24T12:50:35.556+0100" } + localDateTime: { lte: "2004-09-14T12:00:00" } + duration: { equals: "P1Y" } + time: { gte: "22:00:15.555" } + localTime: { lt: "12:55:35.556" } + } + } + } + ) { + connection { + edges { + node { + id + } + } + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [TypeNode.plural]: { + connection: { + edges: [ + { + node: { id: "1" }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts similarity index 99% rename from packages/graphql/tests/api-v6/schema/array.test.ts rename to packages/graphql/tests/api-v6/schema/types/array.test.ts index a887aa8d47..4853e9d62f 100644 --- a/packages/graphql/tests/api-v6/schema/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -19,7 +19,7 @@ import { printSchemaWithDirectives } from "@graphql-tools/utils"; import { lexicographicSortSchema } from "graphql/utilities"; -import { Neo4jGraphQL } from "../../../src"; +import { Neo4jGraphQL } from "../../../../src"; describe("Scalars", () => { test("should generate the right types for all the scalars", async () => { @@ -82,11 +82,11 @@ describe("Scalars", () => { } input IDListWhere { - equals: [String!] + equals: [ID!] } input IDListWhereNullable { - equals: [String] + equals: [ID] } input IntListWhere { diff --git a/packages/graphql/tests/api-v6/schema/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts similarity index 99% rename from packages/graphql/tests/api-v6/schema/scalars.test.ts rename to packages/graphql/tests/api-v6/schema/types/scalars.test.ts index 7538291d47..f629aedcb8 100644 --- a/packages/graphql/tests/api-v6/schema/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -19,7 +19,7 @@ import { printSchemaWithDirectives } from "@graphql-tools/utils"; import { lexicographicSortSchema } from "graphql/utilities"; -import { Neo4jGraphQL } from "../../../src"; +import { Neo4jGraphQL } from "../../../../src"; describe("Scalars", () => { test("should generate the right types for all the scalars", async () => { 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..8ca300e518 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -0,0 +1,362 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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"; + +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 printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + } + + scalar Date + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeWhere { + AND: [DateTimeWhere!] + NOT: DateTimeWhere + OR: [DateTimeWhere!] + equals: DateTime + gt: DateTime + gte: DateTime + in: [DateTime!] + lt: DateTime + lte: DateTime + } + + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" + scalar 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 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 LocalTimeWhere { + AND: [LocalTimeWhere!] + NOT: LocalTimeWhere + OR: [LocalTimeWhere!] + equals: LocalTime + gt: LocalTime + gte: LocalTime + in: [LocalTime!] + lt: LocalTime + lte: LocalTime + } + + 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 { + edges: [NodeTypeEdgeSort!] + } + + type NodeTypeEdge { + cursor: String + node: NodeType + } + + input NodeTypeEdgeSort { + node: NodeTypeSort + } + + input NodeTypeEdgeWhere { + AND: [NodeTypeEdgeWhere!] + NOT: NodeTypeEdgeWhere + OR: [NodeTypeEdgeWhere!] + node: NodeTypeWhere + } + + type NodeTypeOperation { + connection(sort: NodeTypeConnectionSort): NodeTypeConnection + } + + input NodeTypeOperationWhere { + AND: [NodeTypeOperationWhere!] + NOT: NodeTypeOperationWhere + OR: [NodeTypeOperationWhere!] + edges: NodeTypeEdgeWhere + } + + type NodeTypeRelatedNodeConnection { + edges: [NodeTypeRelatedNodeEdge] + pageInfo: PageInfo + } + + input NodeTypeRelatedNodeConnectionSort { + edges: [NodeTypeRelatedNodeEdgeSort!] + } + + type NodeTypeRelatedNodeEdge { + cursor: String + node: RelatedNode + properties: RelatedNodeProperties + } + + input NodeTypeRelatedNodeEdgeListWhere { + AND: [NodeTypeRelatedNodeEdgeListWhere!] + NOT: NodeTypeRelatedNodeEdgeListWhere + OR: [NodeTypeRelatedNodeEdgeListWhere!] + all: NodeTypeRelatedNodeEdgeWhere + none: NodeTypeRelatedNodeEdgeWhere + single: NodeTypeRelatedNodeEdgeWhere + some: 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!] + edges: NodeTypeRelatedNodeEdgeListWhere + } + + type NodeTypeRelatedNodeOperation { + connection(sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection + } + + input NodeTypeRelatedNodeOperationWhere { + AND: [NodeTypeRelatedNodeOperationWhere!] + NOT: NodeTypeRelatedNodeOperationWhere + OR: [NodeTypeRelatedNodeOperationWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeSort + + input NodeTypeWhere { + AND: [NodeTypeWhere!] + NOT: NodeTypeWhere + OR: [NodeTypeWhere!] + dateTime: DateTimeWhere + duration: DurationWhere + localDateTime: LocalDateTimeWhere + localTime: LocalTimeWhere + relatedNode: NodeTypeRelatedNodeNestedOperationWhere + time: TimeWhere + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + 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 { + edges: [RelatedNodeEdgeSort!] + } + + type RelatedNodeEdge { + cursor: String + node: RelatedNode + } + + input RelatedNodeEdgeSort { + node: RelatedNodeSort + } + + input RelatedNodeEdgeWhere { + AND: [RelatedNodeEdgeWhere!] + NOT: RelatedNodeEdgeWhere + OR: [RelatedNodeEdgeWhere!] + node: RelatedNodeWhere + } + + type RelatedNodeOperation { + connection(sort: RelatedNodeConnectionSort): RelatedNodeConnection + } + + input RelatedNodeOperationWhere { + AND: [RelatedNodeOperationWhere!] + NOT: RelatedNodeOperationWhere + OR: [RelatedNodeOperationWhere!] + edges: RelatedNodeEdgeWhere + } + + 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!] + dateTime: DateTimeWhere + duration: DurationWhere + localDateTime: LocalDateTimeWhere + localTime: LocalTimeWhere + time: TimeWhere + } + + input RelatedNodeSort + + input RelatedNodeWhere { + AND: [RelatedNodeWhere!] + NOT: RelatedNodeWhere + OR: [RelatedNodeWhere!] + 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 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/filters/types/temporals.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts new file mode 100644 index 0000000000..cba8d07127 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Filter temporals types", async () => { + const query = /* GraphQL */ ` + query { + typeNodes( + 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 }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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: this0.dateTime, 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 + } + }" + `); + }); +}); From bad4d09ea7fdf31af03878e9f98344f7f9c1396a Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 23 May 2024 15:43:09 +0100 Subject: [PATCH 027/177] move dateTime tests --- .../queryIRFactory/ReadOperationFactory.ts | 11 +- .../filters/types/temporals.int.test.ts | 118 --------------- .../integration/types/datetime.int.test.ts | 139 ++++++++++++++++++ .../api-v6/schema/types/temporals.test.ts | 18 ++- .../tck/{filters => }/types/temporals.test.ts | 4 +- 5 files changed, 167 insertions(+), 123 deletions(-) delete mode 100644 packages/graphql/tests/api-v6/integration/filters/types/temporals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts rename packages/graphql/tests/api-v6/tck/{filters => }/types/temporals.test.ts (98%) diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index b0a39046e9..91521d082c 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -27,6 +27,7 @@ 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 { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection"; import { RelationshipSelection } from "../../translate/queryAST/ast/selection/RelationshipSelection"; import { PropertySort } from "../../translate/queryAST/ast/sort/PropertySort"; @@ -165,9 +166,17 @@ export class ReadOperationFactory { Object.values(propertiesTree.fields).map((rawField) => { const attribute = target.findAttribute(rawField.name); if (attribute) { + const attributeAdapter = new AttributeAdapter(attribute); + if (attributeAdapter.typeHelper.isDateTime()) { + return new DateTimeField({ + alias: rawField.alias, + attribute: attributeAdapter, + }); + } + return new AttributeField({ alias: rawField.alias, - attribute: new AttributeAdapter(attribute), + attribute: attributeAdapter, }); } return; diff --git a/packages/graphql/tests/api-v6/integration/filters/types/temporals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/temporals.int.test.ts deleted file mode 100644 index dcb6a767e6..0000000000 --- a/packages/graphql/tests/api-v6/integration/filters/types/temporals.int.test.ts +++ /dev/null @@ -1,118 +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("Temporal types", () => { - const testHelper = new TestHelper({ v6Api: true }); - - let TypeNode: UniqueType; - beforeAll(async () => { - TypeNode = testHelper.createUniqueType("TypeNode"); - - const typeDefs = /* GraphQL */ ` - type ${TypeNode} @node { - id: ID! - dateTime: DateTime - localDateTime: LocalDateTime - duration: Duration - time: Time - localTime: LocalTime - } - `; - await testHelper.initNeo4jGraphQL({ typeDefs }); - - - await testHelper.executeCypher(` - CREATE (:${TypeNode} { - id: "1", - dateTime: datetime('2015-06-24T12:50:35.556+0100'), - localDateTime: localDateTime('2003-09-14T12:00:00'), - duration: duration('P1Y'), - time: time('22:00:15.555'), - localTime: localTime('12:50:35.556') - }) - - CREATE (:${TypeNode} { - id: "2", - dateTime: datetime('2011-06-24T12:50:35.556+0100'), - localDateTime: localDateTime('2003-09-14T12:00:00'), - duration: duration('P1Y'), - time: time('22:00:15.555'), - localTime: localTime('12:50:35.556') - }) - - CREATE (:${TypeNode} { - id: "3", - dateTime: datetime('2015-06-24T12:50:35.556+0100'), - localDateTime: localDateTime('2003-09-14T12:00:00'), - duration: duration('P2Y'), - time: time('22:00:15.555'), - localTime: localTime('12:50:35.556') - }) - `); - }); - - afterAll(async () => { - await testHelper.close(); - }); - - test("should be possible to filter temporal types", async () => { - const query = /* GraphQL */ ` - query { - ${TypeNode.plural}( - where: { - edges: { - node: { - dateTime: { gt: "2012-06-24T12:50:35.556+0100" } - localDateTime: { lte: "2004-09-14T12:00:00" } - duration: { equals: "P1Y" } - time: { gte: "22:00:15.555" } - localTime: { lt: "12:55:35.556" } - } - } - } - ) { - connection { - edges { - node { - id - } - } - } - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - expect(gqlResult.errors).toBeFalsy(); - expect(gqlResult.data).toEqual({ - [TypeNode.plural]: { - connection: { - edges: [ - { - node: { id: "1" }, - }, - ], - }, - }, - }); - }); -}); diff --git a/packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts b/packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts new file mode 100644 index 0000000000..e643151143 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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(); + }); + + describe("find", () => { + test("should find a movie (with a DateTime)", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + datetime: DateTime + } + `; + + const date = new Date(); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { edges: { node: { datetime: { equals: "${date.toISOString()}" }} }}) { + connection{ + edges { + node { + datetime + } + } + } + } + } + `; + + const nDateTime = neo4jDriver.types.DateTime.fromStandardDate(date); + + await testHelper.executeCypher( + ` + CREATE (m:${Movie.name}) + SET m.datetime = $nDateTime + `, + { nDateTime } + ); + + 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 find a movie (with a DateTime created with a timezone)", async () => { + const typeDefs = /* GraphQL */ ` + type ${Movie.name} @node { + name: String + datetime: DateTime + } + `; + + const date = new Date(); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { edges: { node: { name: { equals: "${Movie.name}" } } } }) { + connection { + edges { + node { + datetime + } + } + } + } + } + `; + + await testHelper.executeCypher(` + CREATE (m:${Movie.name}) + SET m.name = "${Movie.name}" + SET m.datetime = datetime("${date.toISOString().replace("Z", "[Etc/UTC]")}") + `); + + 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.todo("packages/graphql/tests/integration/types/datetime.int.test.ts (create)"); + test.todo("packages/graphql/tests/integration/types/datetime.int.test.ts (update)"); +}); diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 8ca300e518..9381fbc5aa 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -227,7 +227,14 @@ describe("Temporals", () => { edges: NodeTypeRelatedNodeEdgeWhere } - input NodeTypeSort + input NodeTypeSort { + date: SortDirection + dateTime: SortDirection + duration: SortDirection + localDateTime: SortDirection + localTime: SortDirection + time: SortDirection + } input NodeTypeWhere { AND: [NodeTypeWhere!] @@ -325,7 +332,14 @@ describe("Temporals", () => { time: TimeWhere } - input RelatedNodeSort + input RelatedNodeSort { + date: SortDirection + dateTime: SortDirection + duration: SortDirection + localDateTime: SortDirection + localTime: SortDirection + time: SortDirection + } input RelatedNodeWhere { AND: [RelatedNodeWhere!] diff --git a/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts b/packages/graphql/tests/api-v6/tck/types/temporals.test.ts similarity index 98% rename from packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts rename to packages/graphql/tests/api-v6/tck/types/temporals.test.ts index cba8d07127..9783768f93 100644 --- a/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/tck/types/temporals.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import { Neo4jGraphQL } from "../../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; describe("Temporal types", () => { let typeDefs: string; From 6d2c2e5cfe9ec92d373a367d474343ff54dfd48a Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 23 May 2024 15:49:08 +0100 Subject: [PATCH 028/177] update tcks --- packages/graphql/tests/api-v6/tck/types/temporals.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/tests/api-v6/tck/types/temporals.test.ts b/packages/graphql/tests/api-v6/tck/types/temporals.test.ts index 9783768f93..5e7e0bda6b 100644 --- a/packages/graphql/tests/api-v6/tck/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/tck/types/temporals.test.ts @@ -83,7 +83,7 @@ describe("Temporal types", () => { WITH edges UNWIND edges AS edge WITH edge.node AS this0 - RETURN collect({ node: { dateTime: this0.dateTime, localDateTime: this0.localDateTime, duration: this0.duration, time: this0.time, localTime: this0.localTime, __resolveType: \\"TypeNode\\" } }) AS var1 + 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" `); From 88df510159668ac4fc2f136ae03077a924a2d643 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 23 May 2024 17:21:32 +0100 Subject: [PATCH 029/177] add scalar types at the very start --- .../api-v6/queryIRFactory/FilterOperators.ts | 6 ++--- .../api-v6/schema-generation/SchemaBuilder.ts | 6 ++++- .../schema-types/StaticSchemaTypes.ts | 27 ++++++++++++------- .../filter-schema-types/FilterSchemaTypes.ts | 12 +++------ .../api-v6/schema/types/temporals.test.ts | 16 +++++++++++ 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts index cdd513aaf1..2bd31e60f6 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts @@ -5,10 +5,8 @@ export function getFilterOperator(attribute: AttributeAdapter, operator: string) if (attribute.typeHelper.isString() || attribute.typeHelper.isID()) { return getStringOperator(operator); } - if (attribute.typeHelper.isTemporal()) { - return getNumberOperator(operator); - } - if (attribute.typeHelper.isNumeric()) { + + if (attribute.typeHelper.isNumeric() || attribute.typeHelper.isTemporal()) { return getNumberOperator(operator); } } diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index f9793a046f..6b01ed9a14 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import { type GraphQLNamedInputType, type GraphQLSchema } from "graphql"; +import type { GraphQLNamedInputType, GraphQLScalarType, GraphQLSchema } from "graphql"; import type { EnumTypeComposer, InputTypeComposer, @@ -56,6 +56,10 @@ export class SchemaBuilder { this.composer = new SchemaComposer(); } + public createScalar(scalar: GraphQLScalarType): void { + this.composer.createScalarTC(scalar); + } + public getOrCreateObjectType( name: string, onCreate: () => { 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 index bad83adb7c..9154795bd9 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -31,6 +31,8 @@ import { } from "../../../graphql/scalars"; import type { SchemaBuilder } from "../SchemaBuilder"; +import * as Scalars from "../../../graphql/scalars"; + function nonNull(type: string): string { return `${type}!`; } @@ -46,6 +48,13 @@ export class StaticSchemaTypes { constructor({ schemaBuilder }: { schemaBuilder: SchemaBuilder }) { this.schemaBuilder = schemaBuilder; this.staticFilterTypes = new StaticFilterTypes({ schemaBuilder }); + this.addScalars(); + } + + private addScalars(): void { + Object.values(Scalars).forEach((scalar) => { + this.schemaBuilder.createScalar(scalar); + }); } public get pageInfo(): ObjectTypeComposer { @@ -105,7 +114,7 @@ class StaticFilterTypes { fields: { ...this.createBooleanOperators(itc), in: list(nonNull(GraphQLDate.name)), - ...this.createRelationalOperators(GraphQLDate), + ...this.createNumericOperators(GraphQLDate), }, }; }); @@ -117,7 +126,7 @@ class StaticFilterTypes { fields: { ...this.createBooleanOperators(itc), in: list(nonNull(GraphQLDateTime.name)), - ...this.createRelationalOperators(GraphQLDateTime), + ...this.createNumericOperators(GraphQLDateTime), }, }; }); @@ -128,7 +137,7 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createRelationalOperators(GraphQLLocalDateTime), + ...this.createNumericOperators(GraphQLLocalDateTime), in: list(nonNull(GraphQLLocalDateTime.name)), }, }; @@ -140,7 +149,7 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createRelationalOperators(GraphQLDuration), + ...this.createNumericOperators(GraphQLDuration), in: list(nonNull(GraphQLDuration.name)), }, }; @@ -152,7 +161,7 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createRelationalOperators(GraphQLTime), + ...this.createNumericOperators(GraphQLTime), in: list(nonNull(GraphQLTime.name)), }, }; @@ -164,7 +173,7 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createRelationalOperators(GraphQLLocalTime), + ...this.createNumericOperators(GraphQLLocalTime), in: list(nonNull(GraphQLLocalTime.name)), }, }; @@ -228,7 +237,7 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createRelationalOperators(GraphQLInt), + ...this.createNumericOperators(GraphQLInt), in: list(nonNull(GraphQLInt.name)), }, }; @@ -260,7 +269,7 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createRelationalOperators(GraphQLFloat), + ...this.createNumericOperators(GraphQLFloat), in: list(nonNull(GraphQLFloat.name)), }, }; @@ -277,7 +286,7 @@ class StaticFilterTypes { }; } - private createRelationalOperators(type: GraphQLScalarType): Record { + private createNumericOperators(type: GraphQLScalarType): Record { return { equals: type, lt: type, 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 index 202ce1f626..5c577b34df 100644 --- 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 @@ -130,17 +130,11 @@ export abstract class FilterSchemaTypes { query: Query } + \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" scalar Date \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" @@ -78,6 +79,18 @@ describe("Temporals", () => { lte: DateTime } + input DateWhere { + AND: [DateWhere!] + NOT: DateWhere + OR: [DateWhere!] + equals: Date + gt: Date + gte: Date + in: [Date!] + lt: Date + lte: Date + } + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" scalar Duration @@ -240,6 +253,7 @@ describe("Temporals", () => { AND: [NodeTypeWhere!] NOT: NodeTypeWhere OR: [NodeTypeWhere!] + date: DateWhere dateTime: DateTimeWhere duration: DurationWhere localDateTime: LocalDateTimeWhere @@ -325,6 +339,7 @@ describe("Temporals", () => { AND: [RelatedNodePropertiesWhere!] NOT: RelatedNodePropertiesWhere OR: [RelatedNodePropertiesWhere!] + date: DateWhere dateTime: DateTimeWhere duration: DurationWhere localDateTime: LocalDateTimeWhere @@ -345,6 +360,7 @@ describe("Temporals", () => { AND: [RelatedNodeWhere!] NOT: RelatedNodeWhere OR: [RelatedNodeWhere!] + date: DateWhere dateTime: DateTimeWhere duration: DurationWhere localDateTime: LocalDateTimeWhere From a2b0736e27dc845b8d53902148bf0f9f88c8a0ec Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 28 May 2024 14:24:13 +0100 Subject: [PATCH 030/177] remove nested description from datetime test --- .../integration/types/datetime.int.test.ts | 87 +++++++++---------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts b/packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts index e643151143..893c812ed6 100644 --- a/packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts @@ -33,19 +33,18 @@ describe("DateTime", () => { await testHelper.close(); }); - describe("find", () => { - test("should find a movie (with a DateTime)", async () => { - const typeDefs = /* GraphQL */ ` + test("should find a movie (with a DateTime)", async () => { + const typeDefs = /* GraphQL */ ` type ${Movie.name} @node { datetime: DateTime } `; - const date = new Date(); + const date = new Date(); - await testHelper.initNeo4jGraphQL({ typeDefs }); + await testHelper.initNeo4jGraphQL({ typeDefs }); - const query = /* GraphQL */ ` + const query = /* GraphQL */ ` query { ${Movie.plural}(where: { edges: { node: { datetime: { equals: "${date.toISOString()}" }} }}) { connection{ @@ -59,45 +58,45 @@ describe("DateTime", () => { } `; - const nDateTime = neo4jDriver.types.DateTime.fromStandardDate(date); + const nDateTime = neo4jDriver.types.DateTime.fromStandardDate(date); - await testHelper.executeCypher( - ` + await testHelper.executeCypher( + ` CREATE (m:${Movie.name}) SET m.datetime = $nDateTime `, - { nDateTime } - ); - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data as any)[Movie.plural]).toEqual({ - connection: { - edges: [ - { - node: { - datetime: date.toISOString(), - }, + { nDateTime } + ); + + 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 find a movie (with a DateTime created with a timezone)", async () => { - const typeDefs = /* GraphQL */ ` + test("should find a movie (with a DateTime created with a timezone)", async () => { + const typeDefs = /* GraphQL */ ` type ${Movie.name} @node { name: String datetime: DateTime } `; - const date = new Date(); + const date = new Date(); - await testHelper.initNeo4jGraphQL({ typeDefs }); + await testHelper.initNeo4jGraphQL({ typeDefs }); - const query = /* GraphQL */ ` + const query = /* GraphQL */ ` query { ${Movie.plural}(where: { edges: { node: { name: { equals: "${Movie.name}" } } } }) { connection { @@ -111,29 +110,25 @@ describe("DateTime", () => { } `; - await testHelper.executeCypher(` + await testHelper.executeCypher(` CREATE (m:${Movie.name}) SET m.name = "${Movie.name}" SET m.datetime = datetime("${date.toISOString().replace("Z", "[Etc/UTC]")}") `); - const gqlResult = await testHelper.executeGraphQL(query); + const gqlResult = await testHelper.executeGraphQL(query); - expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data as any)[Movie.plural]).toEqual({ - connection: { - edges: [ - { - node: { - datetime: date.toISOString(), - }, + expect(gqlResult.errors).toBeFalsy(); + expect((gqlResult.data as any)[Movie.plural]).toEqual({ + connection: { + edges: [ + { + node: { + datetime: date.toISOString(), }, - ], - }, - }); + }, + ], + }, }); }); - - test.todo("packages/graphql/tests/integration/types/datetime.int.test.ts (create)"); - test.todo("packages/graphql/tests/integration/types/datetime.int.test.ts (update)"); }); From 9548237044547a107edde6d0bad1ae3b78628e3f Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 23 May 2024 14:16:31 +0100 Subject: [PATCH 031/177] Add string filtering tests --- .../schema-types/StaticSchemaTypes.ts | 2 +- .../types/string/string-contains.int.test.ts | 118 +++++ .../types/string/string-ends-with-int.test.ts | 118 +++++ .../types/string/string-equals.int.test.ts | 118 +++++ .../types/string/string-in.int.test.ts | 118 +++++ .../string/string-starts-with.int.test.ts | 118 +++++ .../tests/api-v6/schema/relationship.test.ts | 2 - .../tests/api-v6/schema/simple.test.ts | 3 - .../tests/api-v6/schema/types/scalars.test.ts | 1 - .../filtering/advanced-filtering.int.test.ts | 423 ------------------ 10 files changed, 591 insertions(+), 430 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/string/string-contains.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/string/string-ends-with-int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/string/string-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/string/string-in.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/string/string-starts-with.int.test.ts 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 index 9154795bd9..7f371df846 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -279,7 +279,7 @@ class StaticFilterTypes { private createStringOperators(type: GraphQLScalarType): Record { return { equals: type, - matches: type, + // matches: type, contains: type, startsWith: type, endsWith: type, 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..af59c41da1 --- /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: { edges: { 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: { edges: { 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..02d7a30662 --- /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: { edges: { 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: { edges: { 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..7d3fd5a052 --- /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: { edges: { 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: { edges: { 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..5a0d3bbc6f --- /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: { edges: { 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: { edges: { 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..fce8cfdff0 --- /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: { edges: { 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: { edges: { 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/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index da641ec915..e2a8da0faa 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -276,7 +276,6 @@ describe("Relationships", () => { endsWith: String equals: String in: [String!] - matches: String startsWith: String }" `); @@ -573,7 +572,6 @@ describe("Relationships", () => { endsWith: String equals: String in: [String!] - matches: 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 index 2a9ce67051..898f07a498 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -109,7 +109,6 @@ describe("Simple Aura-API", () => { endsWith: String equals: String in: [String!] - matches: String startsWith: String }" `); @@ -257,7 +256,6 @@ describe("Simple Aura-API", () => { endsWith: String equals: String in: [String!] - matches: String startsWith: String }" `); @@ -353,7 +351,6 @@ describe("Simple Aura-API", () => { endsWith: String equals: String in: [String!] - matches: String startsWith: String }" `); diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index f629aedcb8..10211cb1fb 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -328,7 +328,6 @@ describe("Scalars", () => { endsWith: String equals: String in: [String!] - matches: String startsWith: String }" `); 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..d39d14e873 100644 --- a/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts +++ b/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts @@ -34,54 +34,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,381 +80,6 @@ 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"); - - 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", - }); - - await testHelper.executeCypher( - ` - CREATE (:${randomType.name} {property: $value}) - CREATE (:${randomType.name} {property: $randomValue1}) - `, - { value, randomValue1 } - ); - - const query = ` - { - ${randomType.plural}(where: { property_NOT: "${randomValue1}" }) { - 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 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}) - 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 CONTAINS 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 superValue = `${value}${value}`; - - 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_CONTAINS: "${value}" }) { - property - } - } - `; - - const gqlResult = await testHelper.executeGraphQL(query); - - expect(gqlResult.errors).toBeUndefined(); - - expect((gqlResult.data as any)[randomType.plural]).toHaveLength(3); - - expect((gqlResult.data as any)[randomType.plural][0].property).toEqual(superValue); - }); - - test("should find Movies NOT_CONTAINS 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_CONTAINS: "${notValue}" }) { - 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 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 superValue = `${value}${value}`; - - 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", () => { From 2c65f7a469b4f640416f3d8e8d3279271a9b4d8d Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 23 May 2024 14:43:47 +0100 Subject: [PATCH 032/177] number filters --- .../types/number/number-equals.int.test.ts | 119 ++++++ .../types/number/number-gt.int.test.ts | 188 ++++++++++ .../types/number/number-in.int.test.ts | 119 ++++++ .../types/number/number-lt.int.test.ts | 188 ++++++++++ ...numeric.int.test.ts => number.int.test.ts} | 0 .../filtering/advanced-filtering.int.test.ts | 349 ------------------ 6 files changed, 614 insertions(+), 349 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/number/number-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/number/number-gt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/number/number-in.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/number/number-lt.int.test.ts rename packages/graphql/tests/api-v6/integration/types/{numeric.int.test.ts => number.int.test.ts} (100%) 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..cbe19ec3a7 --- /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"] 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: { edges: { 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: { edges: { 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..8ed774c39f --- /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"] 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: { edges: { 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: { edges: { 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: { edges: { 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: { edges: { 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..eac3067715 --- /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"] 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: { edges: { 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: { edges: { 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..f57694980b --- /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"] as const)("%s Filtering - 'lt' and 'lte'", (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: { edges: { 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 'gt'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { edges: { 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 'gte'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { edges: { 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 'gte'", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { edges: { 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/types/numeric.int.test.ts b/packages/graphql/tests/api-v6/integration/types/number.int.test.ts similarity index 100% rename from packages/graphql/tests/api-v6/integration/types/numeric.int.test.ts rename to packages/graphql/tests/api-v6/integration/types/number.int.test.ts 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 d39d14e873..39f08e87d4 100644 --- a/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts +++ b/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts @@ -330,355 +330,6 @@ describe("Advanced Filtering", () => { }); }); - 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"); From a4020875e7ee2aa1030bf0625054c5711d1c6210 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 28 May 2024 14:14:44 +0100 Subject: [PATCH 033/177] Fix boolean filtering --- .../api-v6/queryIRFactory/FilterOperators.ts | 13 ++ .../schema-types/StaticSchemaTypes.ts | 35 ++++- .../filter-schema-types/FilterSchemaTypes.ts | 6 +- .../types/boolean/boolean-equals.int.test.ts | 145 ++++++++++++++++++ .../tests/api-v6/schema/types/array.test.ts | 6 + .../tests/api-v6/schema/types/scalars.test.ts | 14 +- .../filtering/advanced-filtering.int.test.ts | 72 --------- 7 files changed, 211 insertions(+), 80 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/boolean/boolean-equals.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts index 2bd31e60f6..20d046857a 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts @@ -6,6 +6,10 @@ export function getFilterOperator(attribute: AttributeAdapter, operator: string) return getStringOperator(operator); } + if (attribute.typeHelper.isBoolean()) { + return getBooleanOperator(operator); + } + if (attribute.typeHelper.isNumeric() || attribute.typeHelper.isTemporal()) { return getNumberOperator(operator); } @@ -39,6 +43,15 @@ function getNumberOperator(operator: string): FilterOperator | undefined { 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", 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 index 7f371df846..6a8aa10d6e 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -18,7 +18,7 @@ */ import type { GraphQLScalarType } from "graphql"; -import { GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; +import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import { @@ -180,6 +180,39 @@ class StaticFilterTypes { }); } + public getBooleanListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("StringListWhereNullable", () => { + return { + fields: { + equals: "[Boolean]", + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { + return { + fields: { + equals: "[Boolean!]", + }, + }; + }); + } + + 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(nullable: boolean): InputTypeComposer { if (nullable) { return this.schemaBuilder.getOrCreateInputType("IDListWhereNullable", () => { 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 index 5c577b34df..74bddacf0b 100644 --- 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 @@ -18,7 +18,6 @@ */ import type { GraphQLScalarType } from "graphql"; -import { GraphQLBoolean } from "graphql"; import type { InputTypeComposer } from "graphql-compose"; import type { Attribute } from "../../../../schema-model/attribute/Attribute"; import { @@ -92,9 +91,10 @@ export abstract class FilterSchemaTypes { + 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: { edges: { 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: { edges: { 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: { edges: { 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/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 4853e9d62f..6dbd1dbd5b 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -209,6 +209,8 @@ describe("Scalars", () => { AND: [NodeTypeWhere!] NOT: NodeTypeWhere OR: [NodeTypeWhere!] + booleanList: StringListWhere + booleanListNullable: StringListWhereNullable floatList: FloatListWhere floatListNullable: FloatListWhereNullable idList: IDListWhere @@ -309,6 +311,8 @@ describe("Scalars", () => { AND: [RelatedNodePropertiesWhere!] NOT: RelatedNodePropertiesWhere OR: [RelatedNodePropertiesWhere!] + booleanList: StringListWhere + booleanListNullable: StringListWhereNullable floatList: FloatListWhere floatListNullable: FloatListWhereNullable idList: IDListWhere @@ -325,6 +329,8 @@ describe("Scalars", () => { AND: [RelatedNodeWhere!] NOT: RelatedNodeWhere OR: [RelatedNodeWhere!] + booleanList: StringListWhere + booleanListNullable: StringListWhereNullable floatList: FloatListWhere floatListNullable: FloatListWhereNullable idList: IDListWhere diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index 10211cb1fb..38bf5cbcc7 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -58,6 +58,13 @@ describe("Scalars", () => { query: Query } + input BooleanWhere { + AND: [BooleanWhere!] + NOT: BooleanWhere + OR: [BooleanWhere!] + equals: Boolean + } + input FloatWhere { AND: [FloatWhere!] NOT: FloatWhere @@ -78,7 +85,6 @@ describe("Scalars", () => { endsWith: ID equals: ID in: [ID!] - matches: ID startsWith: ID } @@ -207,7 +213,7 @@ describe("Scalars", () => { AND: [NodeTypeWhere!] NOT: NodeTypeWhere OR: [NodeTypeWhere!] - boolean: Boolean + boolean: BooleanWhere float: FloatWhere id: IDWhere int: IntWhere @@ -289,7 +295,7 @@ describe("Scalars", () => { AND: [RelatedNodePropertiesWhere!] NOT: RelatedNodePropertiesWhere OR: [RelatedNodePropertiesWhere!] - boolean: Boolean + boolean: BooleanWhere float: FloatWhere id: IDWhere int: IntWhere @@ -308,7 +314,7 @@ describe("Scalars", () => { AND: [RelatedNodeWhere!] NOT: RelatedNodeWhere OR: [RelatedNodeWhere!] - boolean: Boolean + boolean: BooleanWhere float: FloatWhere id: IDWhere int: IntWhere 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 39f08e87d4..0a5d4ad3a1 100644 --- a/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts +++ b/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts @@ -330,78 +330,6 @@ describe("Advanced Filtering", () => { }); }); - 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 () => { From 045c6e198d28130430e84d729a1d7e68b47fc27d Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 28 May 2024 14:36:15 +0100 Subject: [PATCH 034/177] Add v6 tests to normal tests commands --- packages/graphql/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/graphql/package.json b/packages/graphql/package.json index c0ab1fc741..126e87805d 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -33,10 +33,10 @@ "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" }, From 02e40b9b48a6eec1af450da16c29b8cb0efe0fd1 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 28 May 2024 15:01:26 +0100 Subject: [PATCH 035/177] Update tests on datetime --- .../datetime/datetime-equals.int.test.ts | 90 +++++++++++++++++++ .../datetime.int.test.ts | 45 +++++----- .../number.int.test.ts | 0 3 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/datetime/datetime-equals.int.test.ts rename packages/graphql/tests/api-v6/integration/{types => return-types}/datetime.int.test.ts (86%) rename packages/graphql/tests/api-v6/integration/{types => return-types}/number.int.test.ts (100%) 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..45a845e2f6 --- /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 equasl 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: { edges: { 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/types/datetime.int.test.ts b/packages/graphql/tests/api-v6/integration/return-types/datetime.int.test.ts similarity index 86% rename from packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts rename to packages/graphql/tests/api-v6/integration/return-types/datetime.int.test.ts index 893c812ed6..001edc8a8a 100644 --- a/packages/graphql/tests/api-v6/integration/types/datetime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/return-types/datetime.int.test.ts @@ -33,21 +33,31 @@ describe("DateTime", () => { await testHelper.close(); }); - test("should find a movie (with a DateTime)", async () => { + test("should return a movie created with a datetime parameter", async () => { const typeDefs = /* GraphQL */ ` type ${Movie.name} @node { datetime: DateTime } `; - const date = new Date(); + 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}(where: { edges: { node: { datetime: { equals: "${date.toISOString()}" }} }}) { - connection{ + ${Movie.plural} { + connection { edges { node { datetime @@ -58,16 +68,6 @@ describe("DateTime", () => { } `; - const nDateTime = neo4jDriver.types.DateTime.fromStandardDate(date); - - await testHelper.executeCypher( - ` - CREATE (m:${Movie.name}) - SET m.datetime = $nDateTime - `, - { nDateTime } - ); - const gqlResult = await testHelper.executeGraphQL(query); expect(gqlResult.errors).toBeFalsy(); @@ -84,21 +84,24 @@ describe("DateTime", () => { }); }); - test("should find a movie (with a DateTime created with a timezone)", async () => { + test("should return a movie created with a datetime with timezone", async () => { const typeDefs = /* GraphQL */ ` type ${Movie.name} @node { - name: String datetime: DateTime } `; - const date = new Date(); + 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}(where: { edges: { node: { name: { equals: "${Movie.name}" } } } }) { + ${Movie.plural} { connection { edges { node { @@ -110,12 +113,6 @@ describe("DateTime", () => { } `; - await testHelper.executeCypher(` - CREATE (m:${Movie.name}) - SET m.name = "${Movie.name}" - SET m.datetime = datetime("${date.toISOString().replace("Z", "[Etc/UTC]")}") - `); - const gqlResult = await testHelper.executeGraphQL(query); expect(gqlResult.errors).toBeFalsy(); diff --git a/packages/graphql/tests/api-v6/integration/types/number.int.test.ts b/packages/graphql/tests/api-v6/integration/return-types/number.int.test.ts similarity index 100% rename from packages/graphql/tests/api-v6/integration/types/number.int.test.ts rename to packages/graphql/tests/api-v6/integration/return-types/number.int.test.ts From 503f4225a17a9b2bd4c55343d6f5a96b77dee754 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 28 May 2024 15:23:35 +0100 Subject: [PATCH 036/177] Move projection tests to a folder --- .../api-v6/integration/{ => projection}/aliasing.int.test.ts | 4 ++-- .../integration/{ => projection}/relationship.int.test.ts | 4 ++-- .../integration/{ => projection}/simple-query.int.test.ts | 4 ++-- .../{return-types => projection/types}/datetime.int.test.ts | 4 ++-- .../{return-types => projection/types}/number.int.test.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) rename packages/graphql/tests/api-v6/integration/{ => projection}/aliasing.int.test.ts (98%) rename packages/graphql/tests/api-v6/integration/{ => projection}/relationship.int.test.ts (97%) rename packages/graphql/tests/api-v6/integration/{ => projection}/simple-query.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/{return-types => projection/types}/datetime.int.test.ts (96%) rename packages/graphql/tests/api-v6/integration/{return-types => projection/types}/number.int.test.ts (94%) diff --git a/packages/graphql/tests/api-v6/integration/aliasing.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts similarity index 98% rename from packages/graphql/tests/api-v6/integration/aliasing.int.test.ts rename to packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts index 17e5fbd863..128efa97d4 100644 --- a/packages/graphql/tests/api-v6/integration/aliasing.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Query aliasing", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/relationship.int.test.ts similarity index 97% rename from packages/graphql/tests/api-v6/integration/relationship.int.test.ts rename to packages/graphql/tests/api-v6/integration/projection/relationship.int.test.ts index b38bdcdec8..d9c73ea786 100644 --- a/packages/graphql/tests/api-v6/integration/relationship.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/projection/relationship.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Relationship simple query", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/simple-query.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/simple-query.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/simple-query.int.test.ts rename to packages/graphql/tests/api-v6/integration/projection/simple-query.int.test.ts index badbe7332a..454875e972 100644 --- a/packages/graphql/tests/api-v6/integration/simple-query.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/projection/simple-query.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Simple Query", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/return-types/datetime.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/types/datetime.int.test.ts similarity index 96% rename from packages/graphql/tests/api-v6/integration/return-types/datetime.int.test.ts rename to packages/graphql/tests/api-v6/integration/projection/types/datetime.int.test.ts index 001edc8a8a..590ff22e25 100644 --- a/packages/graphql/tests/api-v6/integration/return-types/datetime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/projection/types/datetime.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("DateTime", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/return-types/number.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/types/number.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/return-types/number.int.test.ts rename to packages/graphql/tests/api-v6/integration/projection/types/number.int.test.ts index db6b42e497..3e85099bc4 100644 --- a/packages/graphql/tests/api-v6/integration/return-types/number.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/projection/types/number.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Numeric fields", () => { const testHelper = new TestHelper({ v6Api: true }); From e6c03136609515774e78598a6fd5ca1507dbe2b8 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 28 May 2024 15:41:22 +0100 Subject: [PATCH 037/177] Move tck tests to projection folder --- .../tests/api-v6/tck/{ => projection}/relationship.test.ts | 4 ++-- .../tests/api-v6/tck/{ => projection}/simple-query.test.ts | 4 ++-- .../tests/api-v6/tck/{ => projection}/types/temporals.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename packages/graphql/tests/api-v6/tck/{ => projection}/relationship.test.ts (98%) rename packages/graphql/tests/api-v6/tck/{ => projection}/simple-query.test.ts (96%) rename packages/graphql/tests/api-v6/tck/{ => projection}/types/temporals.test.ts (98%) diff --git a/packages/graphql/tests/api-v6/tck/relationship.test.ts b/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts similarity index 98% rename from packages/graphql/tests/api-v6/tck/relationship.test.ts rename to packages/graphql/tests/api-v6/tck/projection/relationship.test.ts index 0c6b3e314b..2a1288f69a 100644 --- a/packages/graphql/tests/api-v6/tck/relationship.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../tck/utils/tck-test-utils"; +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; describe("Relationship", () => { let typeDefs: string; diff --git a/packages/graphql/tests/api-v6/tck/simple-query.test.ts b/packages/graphql/tests/api-v6/tck/projection/simple-query.test.ts similarity index 96% rename from packages/graphql/tests/api-v6/tck/simple-query.test.ts rename to packages/graphql/tests/api-v6/tck/projection/simple-query.test.ts index 154d058157..eae20da21b 100644 --- a/packages/graphql/tests/api-v6/tck/simple-query.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/simple-query.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../tck/utils/tck-test-utils"; +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; describe("Simple Query", () => { let typeDefs: string; diff --git a/packages/graphql/tests/api-v6/tck/types/temporals.test.ts b/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts similarity index 98% rename from packages/graphql/tests/api-v6/tck/types/temporals.test.ts rename to packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts index 5e7e0bda6b..3179e766eb 100644 --- a/packages/graphql/tests/api-v6/tck/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import { Neo4jGraphQL } from "../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; +import { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; describe("Temporal types", () => { let typeDefs: string; From e3fa44ee2a81ac549b94a91a29ce79cde856acba Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 28 May 2024 16:11:27 +0100 Subject: [PATCH 038/177] Add pagination arguments --- .../schema-types/EntitySchemaTypes.ts | 3 ++ .../schema-types/RelatedEntitySchemaTypes.ts | 1 + .../schema-types/StaticSchemaTypes.ts | 4 +-- .../filter-schema-types/FilterSchemaTypes.ts | 32 +++++++++---------- .../tests/api-v6/schema/relationship.test.ts | 16 +++++----- .../tests/api-v6/schema/simple.test.ts | 8 ++--- .../tests/api-v6/schema/types/array.test.ts | 6 ++-- .../tests/api-v6/schema/types/scalars.test.ts | 6 ++-- .../api-v6/schema/types/temporals.test.ts | 6 ++-- 9 files changed, 43 insertions(+), 39 deletions(-) diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index 9ce64320ce..df269fe6ea 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { GraphQLInt, GraphQLString } from "graphql"; import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; @@ -50,6 +51,8 @@ export abstract class EntitySchemaTypes { type: this.connection, args: { sort: this.connectionSort, + first: GraphQLInt.name, + after: GraphQLString.name, }, }, }, 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 index e49e46524d..297d2c7f71 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -29,6 +29,7 @@ import type { SchemaBuilder } from "../SchemaBuilder"; import { EntitySchemaTypes } from "./EntitySchemaTypes"; import type { SchemaTypes } from "./SchemaTypes"; import { RelatedEntityFilterSchemaTypes } from "./filter-schema-types/RelatedEntityFilterSchemaTypes"; + export class RelatedEntitySchemaTypes extends EntitySchemaTypes { private relationship: Relationship; public filterSchemaTypes: RelatedEntityFilterSchemaTypes; 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 index 6a8aa10d6e..0ae752b500 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -43,11 +43,11 @@ function list(type: string): string { export class StaticSchemaTypes { private schemaBuilder: SchemaBuilder; - public staticFilterTypes: StaticFilterTypes; + public readonly filters: StaticFilterTypes; constructor({ schemaBuilder }: { schemaBuilder: SchemaBuilder }) { this.schemaBuilder = schemaBuilder; - this.staticFilterTypes = new StaticFilterTypes({ schemaBuilder }); + this.filters = new StaticFilterTypes({ schemaBuilder }); this.addScalars(); } 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 index 74bddacf0b..0bde5b3979 100644 --- 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 @@ -92,67 +92,67 @@ export abstract class FilterSchemaTypes { } type ActorMoviesOperation { - connection(sort: ActorMoviesConnectionSort): ActorMoviesConnection + connection(after: String, first: Int, sort: ActorMoviesConnectionSort): ActorMoviesConnection } input ActorMoviesOperationWhere { @@ -125,7 +125,7 @@ describe("Relationships", () => { } type ActorOperation { - connection(sort: ActorConnectionSort): ActorConnection + connection(after: String, first: Int, sort: ActorConnectionSort): ActorConnection } input ActorOperationWhere { @@ -195,7 +195,7 @@ describe("Relationships", () => { } type MovieActorsOperation { - connection(sort: MovieActorsConnectionSort): MovieActorsConnection + connection(after: String, first: Int, sort: MovieActorsConnectionSort): MovieActorsConnection } input MovieActorsOperationWhere { @@ -231,7 +231,7 @@ describe("Relationships", () => { } type MovieOperation { - connection(sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } input MovieOperationWhere { @@ -395,7 +395,7 @@ describe("Relationships", () => { } type ActorMoviesOperation { - connection(sort: ActorMoviesConnectionSort): ActorMoviesConnection + connection(after: String, first: Int, sort: ActorMoviesConnectionSort): ActorMoviesConnection } input ActorMoviesOperationWhere { @@ -406,7 +406,7 @@ describe("Relationships", () => { } type ActorOperation { - connection(sort: ActorConnectionSort): ActorConnection + connection(after: String, first: Int, sort: ActorConnectionSort): ActorConnection } input ActorOperationWhere { @@ -491,7 +491,7 @@ describe("Relationships", () => { } type MovieActorsOperation { - connection(sort: MovieActorsConnectionSort): MovieActorsConnection + connection(after: String, first: Int, sort: MovieActorsConnectionSort): MovieActorsConnection } input MovieActorsOperationWhere { @@ -527,7 +527,7 @@ describe("Relationships", () => { } type MovieOperation { - connection(sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } input MovieOperationWhere { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 898f07a498..c48fcfbb87 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -66,7 +66,7 @@ describe("Simple Aura-API", () => { } type MovieOperation { - connection(sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } input MovieOperationWhere { @@ -161,7 +161,7 @@ describe("Simple Aura-API", () => { } type ActorOperation { - connection(sort: ActorConnectionSort): ActorConnection + connection(after: String, first: Int, sort: ActorConnectionSort): ActorConnection } input ActorOperationWhere { @@ -212,7 +212,7 @@ describe("Simple Aura-API", () => { } type MovieOperation { - connection(sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } input MovieOperationWhere { @@ -308,7 +308,7 @@ describe("Simple Aura-API", () => { } type MovieOperation { - connection(sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } input MovieOperationWhere { diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 6dbd1dbd5b..2e855abe3b 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -137,7 +137,7 @@ describe("Scalars", () => { } type NodeTypeOperation { - connection(sort: NodeTypeConnectionSort): NodeTypeConnection + connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection } input NodeTypeOperationWhere { @@ -193,7 +193,7 @@ describe("Scalars", () => { } type NodeTypeRelatedNodeOperation { - connection(sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection + connection(after: String, first: Int, sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection } input NodeTypeRelatedNodeOperationWhere { @@ -271,7 +271,7 @@ describe("Scalars", () => { } type RelatedNodeOperation { - connection(sort: RelatedNodeConnectionSort): RelatedNodeConnection + connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection } input RelatedNodeOperationWhere { diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index 38bf5cbcc7..9cfaa975e5 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -135,7 +135,7 @@ describe("Scalars", () => { } type NodeTypeOperation { - connection(sort: NodeTypeConnectionSort): NodeTypeConnection + connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection } input NodeTypeOperationWhere { @@ -191,7 +191,7 @@ describe("Scalars", () => { } type NodeTypeRelatedNodeOperation { - connection(sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection + connection(after: String, first: Int, sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection } input NodeTypeRelatedNodeOperationWhere { @@ -265,7 +265,7 @@ describe("Scalars", () => { } type RelatedNodeOperation { - connection(sort: RelatedNodeConnectionSort): RelatedNodeConnection + connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection } input RelatedNodeOperationWhere { diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 4a045a4114..a4b64efd99 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -174,7 +174,7 @@ describe("Temporals", () => { } type NodeTypeOperation { - connection(sort: NodeTypeConnectionSort): NodeTypeConnection + connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection } input NodeTypeOperationWhere { @@ -230,7 +230,7 @@ describe("Temporals", () => { } type NodeTypeRelatedNodeOperation { - connection(sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection + connection(after: String, first: Int, sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection } input NodeTypeRelatedNodeOperationWhere { @@ -307,7 +307,7 @@ describe("Temporals", () => { } type RelatedNodeOperation { - connection(sort: RelatedNodeConnectionSort): RelatedNodeConnection + connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection } input RelatedNodeOperationWhere { From 153e97507f60039a22d3792670a86e3761b85f4c Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 28 May 2024 17:03:34 +0100 Subject: [PATCH 039/177] First argument for pagination --- .../api-v6/queryIR/ConnectionReadOperation.ts | 3 + .../queryIRFactory/ReadOperationFactory.ts | 6 ++ .../resolve-tree-parser/ResolveTreeParser.ts | 14 ++- .../resolve-tree-parser/graphql-tree.ts | 6 ++ .../integration/pagination/first.int.test.ts | 76 +++++++++++++++ .../tests/api-v6/tck/pagination/first.test.ts | 92 +++++++++++++++++++ 6 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/pagination/first.int.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/pagination/first.test.ts diff --git a/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts index 78b84f1b55..f6d6d76d6a 100644 --- a/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/ConnectionReadOperation.ts @@ -54,6 +54,7 @@ export class V6ReadOperation extends Operation { selection, fields, sortFields, + pagination, filters, }: { relationship?: RelationshipAdapter; @@ -63,6 +64,7 @@ export class V6ReadOperation extends Operation { node: Field[]; edge: Field[]; }; + pagination?: Pagination; sortFields?: Array<{ node: Sort[]; edge: Sort[] }>; filters: Filter[]; }) { @@ -73,6 +75,7 @@ export class V6ReadOperation extends Operation { this.nodeFields = fields?.node ?? []; this.edgeFields = fields?.edge ?? []; this.sortFields = sortFields ?? []; + this.pagination = pagination; this.filters = filters ?? []; } diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 91521d082c..8689de6219 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -28,6 +28,7 @@ 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 { Pagination } from "../../translate/queryAST/ast/pagination/Pagination"; import { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection"; import { RelationshipSelection } from "../../translate/queryAST/ast/selection/RelationshipSelection"; import { PropertySort } from "../../translate/queryAST/ast/sort/PropertySort"; @@ -86,6 +87,10 @@ export class ReadOperationFactory { const nodeResolveTree = connectionTree.fields.edges?.fields.node; const sortArgument = connectionTree.args.sort; + const firstArgument = connectionTree.args.first; + + const pagination = firstArgument ? new Pagination({ limit: firstArgument }) : undefined; + const nodeFields = this.getNodeFields(entity, nodeResolveTree); const sortInputFields = this.getSortInputFields({ entity, @@ -98,6 +103,7 @@ export class ReadOperationFactory { edge: [], node: nodeFields, }, + pagination, sortFields: sortInputFields, filters: this.filterFactory.createFilters({ entity, where: graphQLTree.args.where }), }); diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index ec7e0dce5b..4839107686 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -24,6 +24,7 @@ import { findFieldByName } from "./find-field-by-name"; import type { GraphQLConnectionArgs, GraphQLReadOperationArgs, + GraphQLSortArgument, GraphQLSortEdgeArgument, GraphQLTreeConnection, GraphQLTreeEdge, @@ -142,14 +143,17 @@ export abstract class ResolveTreeParser } private parseConnectionArgs(resolveTreeArgs: { [str: string]: any }): GraphQLConnectionArgs { - if (!resolveTreeArgs.sort) { - return {}; + let sortArg: GraphQLSortArgument | undefined = undefined; + if (resolveTreeArgs.sort) { + sortArg = { + edges: this.parseSortEdges(resolveTreeArgs.sort.edges), + }; } return { - sort: { - edges: this.parseSortEdges(resolveTreeArgs.sort.edges), - }, + sort: sortArg, + first: resolveTreeArgs.first, + after: resolveTreeArgs.after, }; } diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index cddc8fe1be..9d954881a5 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -17,6 +17,8 @@ * limitations under the License. */ +import type { Integer } from "neo4j-driver"; + export type GraphQLTree = GraphQLTreeReadOperation; interface GraphQLTreeElement { @@ -38,6 +40,8 @@ export type StringFilters = { startsWith?: string; endsWith?: string; }; + +// TODO: this is incorrect? export type NumberFilters = { equals?: string; in?: string[]; @@ -89,6 +93,8 @@ export interface GraphQLTreeConnection extends GraphQLTreeElement { export interface GraphQLConnectionArgs { sort?: GraphQLSortArgument; + first?: Integer; + after?: string; } export interface GraphQLTreeEdge extends GraphQLTreeElement { 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..1754ac1e3d --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/pagination/first.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 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; + 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 argument", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(first: 3) { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }); + }); +}); 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..f3049c9c75 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/pagination/first.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 { 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 }); + + // NOTE: Order of these subqueries have been reversed after refactor + 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 + } + }" + `); + }); +}); From 8f1614be7fe90210e1c715d767f64a207304fca8 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 29 May 2024 12:55:31 +0100 Subject: [PATCH 040/177] sort-pagination tests --- .../sort-pagination.int.test.ts | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 packages/graphql/tests/api-v6/integration/combinations/sort-pagination/sort-pagination.int.test.ts 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..f5340c85fb --- /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: { edges: { 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: { edges: { 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", + }, + }, + ], + }, + }, + }); + }); +}); From b7058aa6a7a97a6aeb455a78a3d64b9c18e90c24 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 29 May 2024 14:05:13 +0100 Subject: [PATCH 041/177] Improve types on static schema types --- .../schema-generation/schema-types/StaticSchemaTypes.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 6a8aa10d6e..0148ce9f30 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -19,7 +19,7 @@ import type { GraphQLScalarType } from "graphql"; import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; -import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose"; +import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer, ListComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import { GraphQLDate, @@ -185,7 +185,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("StringListWhereNullable", () => { return { fields: { - equals: "[Boolean]", + equals: list(nonNull(GraphQLBoolean.name)), }, }; }); @@ -194,7 +194,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { return { fields: { - equals: "[Boolean!]", + equals: list(nonNull(GraphQLBoolean.name)), }, }; }); From 684a569191636f7f5c96f91b2b9b9b042e6ddb82 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 29 May 2024 15:56:24 +0100 Subject: [PATCH 042/177] Add nested pagination --- .../queryIRFactory/ReadOperationFactory.ts | 5 + .../resolve-tree-parser/graphql-tree.ts | 1 - .../sort-pagination-relationship.int.test.ts | 197 ++++++++++++++++++ .../pagination/first-relationship.int.test.ts | 116 +++++++++++ 4 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 packages/graphql/tests/api-v6/integration/combinations/sort-pagination/sort-pagination-relationship.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/pagination/first-relationship.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 8689de6219..3afbc28430 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -142,6 +142,10 @@ export class ReadOperationFactory { const sortArgument = connectionTree.args.sort; const sortInputFields = this.getSortInputFields({ entity: relTarget, relationship, sortArgument }); + const firstArgument = connectionTree.args.first; + + const pagination = firstArgument ? new Pagination({ limit: firstArgument }) : undefined; + return new V6ReadOperation({ target: relationshipAdapter.target, selection, @@ -155,6 +159,7 @@ export class ReadOperationFactory { relationship, where: parsedTree.args.where, }), + pagination, }); } diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index 9d954881a5..ec681be59d 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -41,7 +41,6 @@ export type StringFilters = { endsWith?: string; }; -// TODO: this is incorrect? export type NumberFilters = { equals?: string; in?: string[]; 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/pagination/first-relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/pagination/first-relationship.int.test.ts new file mode 100644 index 0000000000..78e51b1142 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/pagination/first-relationship.int.test.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 { UniqueType } from "../../../utils/graphql-types"; +import { TestHelper } from "../../../utils/tests-helper"; + +describe("Nested 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 { + ${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), + }, + }, + }, + }, + ], + }, + }, + }); + }); +}); From 15e1859e58f676f0f19a6c80aaf7e5f845ebfc32 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 30 May 2024 09:31:14 +0100 Subject: [PATCH 043/177] Fix PR comments --- .../queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts | 2 +- .../schema-generation/schema-types/EntitySchemaTypes.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 4839107686..6489e6ab8d 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -143,7 +143,7 @@ export abstract class ResolveTreeParser } private parseConnectionArgs(resolveTreeArgs: { [str: string]: any }): GraphQLConnectionArgs { - let sortArg: GraphQLSortArgument | undefined = undefined; + let sortArg: GraphQLSortArgument | undefined; if (resolveTreeArgs.sort) { sortArg = { edges: this.parseSortEdges(resolveTreeArgs.sort.edges), diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index df269fe6ea..aa3ca4196e 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -51,8 +51,8 @@ export abstract class EntitySchemaTypes { type: this.connection, args: { sort: this.connectionSort, - first: GraphQLInt.name, - after: GraphQLString.name, + first: GraphQLInt, + after: GraphQLString, }, }, }, From 6cf385f2d2b5de28d28893c5dca8965959868ae2 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 30 May 2024 17:41:57 +0100 Subject: [PATCH 044/177] Add pageInfo and cursors to connections --- .../resolvers/connectionOperationResolver.ts | 26 +++ .../mappers/connection-operation-mapper.ts | 26 --- .../src/api-v6/resolvers/readResolver.ts | 7 +- .../schema-types/EntitySchemaTypes.ts | 2 + .../schema-types/StaticSchemaTypes.ts | 9 +- .../translators/translate-read-operation.ts | 7 +- .../integration/pagination/after.int.test.ts | 76 ++++++++ .../integration/pagination/first.int.test.ts | 14 +- .../pagination/page-info.int.test.ts | 177 ++++++++++++++++++ .../tests/api-v6/schema/relationship.test.ts | 4 + .../tests/api-v6/schema/simple.test.ts | 6 + .../tests/api-v6/schema/types/array.test.ts | 2 + .../tests/api-v6/schema/types/scalars.test.ts | 2 + .../api-v6/schema/types/temporals.test.ts | 2 + 14 files changed, 327 insertions(+), 33 deletions(-) create mode 100644 packages/graphql/src/api-v6/resolvers/connectionOperationResolver.ts delete mode 100644 packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts create mode 100644 packages/graphql/tests/api-v6/integration/pagination/after.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/pagination/page-info.int.test.ts diff --git a/packages/graphql/src/api-v6/resolvers/connectionOperationResolver.ts b/packages/graphql/src/api-v6/resolvers/connectionOperationResolver.ts new file mode 100644 index 0000000000..a3b7220c5b --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/connectionOperationResolver.ts @@ -0,0 +1,26 @@ +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/mappers/connection-operation-mapper.ts b/packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts deleted file mode 100644 index 77fba3aa92..0000000000 --- a/packages/graphql/src/api-v6/resolvers/mappers/connection-operation-mapper.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ExecuteResult } from "../../../utils/execute"; - -export function mapConnectionRecord(executionResult: ExecuteResult): any { - // Note: Connections only return a single record - const connections = executionResult.records.map((x) => x.this); - return connections[0]; -} diff --git a/packages/graphql/src/api-v6/resolvers/readResolver.ts b/packages/graphql/src/api-v6/resolvers/readResolver.ts index 3e3fcce080..53e505ebd7 100644 --- a/packages/graphql/src/api-v6/resolvers/readResolver.ts +++ b/packages/graphql/src/api-v6/resolvers/readResolver.ts @@ -22,8 +22,8 @@ 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"; -import { mapConnectionRecord } from "./mappers/connection-operation-mapper"; export function generateReadResolver({ entity }: { entity: ConcreteEntity }) { return async function resolve( @@ -35,8 +35,11 @@ export function generateReadResolver({ entity }: { entity: ConcreteEntity }) { 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({ @@ -47,6 +50,6 @@ export function generateReadResolver({ entity }: { entity: ConcreteEntity }) { info, }); - return mapConnectionRecord(executeResult); + return executeResult.records[0]?.this; }; } diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index df269fe6ea..689b451b68 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -19,6 +19,7 @@ import { GraphQLInt, GraphQLString } from "graphql"; import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; +import { connectionOperationResolver } from "../../resolvers/connectionOperationResolver"; import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import type { SchemaTypes } from "./SchemaTypes"; @@ -54,6 +55,7 @@ export abstract class EntitySchemaTypes { first: GraphQLInt.name, after: GraphQLString.name, }, + resolve: connectionOperationResolver, }, }, }; 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 index 0ae752b500..5cb6b9e012 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -59,7 +59,14 @@ export class StaticSchemaTypes { public get pageInfo(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType("PageInfo", () => { - return { fields: { hasNextPage: "Boolean", hasPreviousPage: "Boolean" } }; + return { + fields: { + hasNextPage: "Boolean", + hasPreviousPage: "Boolean", + startCursor: "String", + endCursor: "String", + }, + }; }); } diff --git a/packages/graphql/src/api-v6/translators/translate-read-operation.ts b/packages/graphql/src/api-v6/translators/translate-read-operation.ts index 7e92396c27..235aab916a 100644 --- a/packages/graphql/src/api-v6/translators/translate-read-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-read-operation.ts @@ -23,21 +23,22 @@ 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 { parseResolveInfoTree } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; +import type { GraphQLTreeReadOperation } from "../queryIRFactory/resolve-tree-parser/graphql-tree"; const debug = Debug(DEBUG_TRANSLATE); export function translateReadOperation({ context, entity, + graphQLTree, }: { context: Neo4jGraphQLTranslationContext; + graphQLTree: GraphQLTreeReadOperation; entity: ConcreteEntity; }): Cypher.CypherResult { const readFactory = new ReadOperationFactory(context.schemaModel); - const parsedTree = parseResolveInfoTree({ resolveTree: context.resolveTree, entity }); - const readOperation = readFactory.createAST({ graphQLTree: parsedTree, entity }); + const readOperation = readFactory.createAST({ graphQLTree, entity }); debug(readOperation.print()); const results = readOperation.build(context); return results.build(); 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..db16d4dc3a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/pagination/after.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 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; + 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 argument", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(first: 3, after: "") { + edges { + node { + title + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: expect.toBeArrayOfSize(3), + }, + }, + }); + }); +}); 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 index 1754ac1e3d..e15b80b54a 100644 --- a/packages/graphql/tests/api-v6/integration/pagination/first.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/pagination/first.int.test.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { offsetToCursor } from "graphql-relay"; import type { UniqueType } from "../../../utils/graphql-types"; import { TestHelper } from "../../../utils/tests-helper"; @@ -52,12 +53,17 @@ describe("Pagination with first", () => { query { ${Movie.plural} { connection(first: 3) { + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } edges { node { title } } - } } } @@ -69,6 +75,12 @@ describe("Pagination with first", () => { [Movie.plural]: { connection: { edges: expect.toBeArrayOfSize(3), + pageInfo: { + endCursor: offsetToCursor(2), + hasNextPage: true, + hasPreviousPage: false, + startCursor: offsetToCursor(0), + }, }, }, }); 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/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index cc807bd511..dd144dbf54 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -254,8 +254,10 @@ describe("Relationships", () => { } type PageInfo { + endCursor: String hasNextPage: Boolean hasPreviousPage: Boolean + startCursor: String } type Query { @@ -550,8 +552,10 @@ describe("Relationships", () => { } type PageInfo { + endCursor: String hasNextPage: Boolean hasPreviousPage: Boolean + startCursor: String } type Query { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index c48fcfbb87..ddac7fe328 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -88,8 +88,10 @@ describe("Simple Aura-API", () => { } type PageInfo { + endCursor: String hasNextPage: Boolean hasPreviousPage: Boolean + startCursor: String } type Query { @@ -234,8 +236,10 @@ describe("Simple Aura-API", () => { } type PageInfo { + endCursor: String hasNextPage: Boolean hasPreviousPage: Boolean + startCursor: String } type Query { @@ -330,8 +334,10 @@ describe("Simple Aura-API", () => { } type PageInfo { + endCursor: String hasNextPage: Boolean hasPreviousPage: Boolean + startCursor: String } type Query { diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 2e855abe3b..044bcb8c83 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -223,8 +223,10 @@ describe("Scalars", () => { } type PageInfo { + endCursor: String hasNextPage: Boolean hasPreviousPage: Boolean + startCursor: String } type Query { diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index 9cfaa975e5..516327bf0d 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -222,8 +222,10 @@ describe("Scalars", () => { } type PageInfo { + endCursor: String hasNextPage: Boolean hasPreviousPage: Boolean + startCursor: String } type Query { diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index a4b64efd99..b2774c9269 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -263,8 +263,10 @@ describe("Temporals", () => { } type PageInfo { + endCursor: String hasNextPage: Boolean hasPreviousPage: Boolean + startCursor: String } type Query { From 516a2182cca4cdfee39a1be3f3986780f87909f1 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 31 May 2024 10:21:58 +0100 Subject: [PATCH 045/177] Add after argument --- .../queryIRFactory/ReadOperationFactory.ts | 9 ++- .../integration/pagination/after.int.test.ts | 8 +- .../pagination/first-after.int.test.ts | 78 +++++++++++++++++++ 3 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/pagination/first-after.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 3afbc28430..d07a629c91 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { cursorToOffset } from "graphql-relay"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; import { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; @@ -88,8 +89,10 @@ export class ReadOperationFactory { const nodeResolveTree = connectionTree.fields.edges?.fields.node; const sortArgument = connectionTree.args.sort; const firstArgument = connectionTree.args.first; + const afterArgument = connectionTree.args.after ? cursorToOffset(connectionTree.args.after) : undefined; - const pagination = firstArgument ? new Pagination({ limit: firstArgument }) : undefined; + const hasPagination = firstArgument || afterArgument; + const pagination = hasPagination ? new Pagination({ limit: firstArgument, skip: afterArgument }) : undefined; const nodeFields = this.getNodeFields(entity, nodeResolveTree); const sortInputFields = this.getSortInputFields({ @@ -143,8 +146,10 @@ export class ReadOperationFactory { const sortInputFields = this.getSortInputFields({ entity: relTarget, relationship, sortArgument }); const firstArgument = connectionTree.args.first; + const afterArgument = connectionTree.args.after ? cursorToOffset(connectionTree.args.after) : undefined; - const pagination = firstArgument ? new Pagination({ limit: firstArgument }) : undefined; + const hasPagination = firstArgument || afterArgument; + const pagination = hasPagination ? new Pagination({ limit: firstArgument, skip: afterArgument }) : undefined; return new V6ReadOperation({ target: relationshipAdapter.target, 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 index db16d4dc3a..c8000169cc 100644 --- a/packages/graphql/tests/api-v6/integration/pagination/after.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/pagination/after.int.test.ts @@ -17,10 +17,11 @@ * 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", () => { +describe("Pagination with after", () => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; @@ -47,11 +48,12 @@ describe("Pagination with first", () => { await testHelper.close(); }); - test("Get movies with first argument", async () => { + test("Get movies with after argument", async () => { + const afterCursor = offsetToCursor(2); const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(first: 3, after: "") { + connection(after: "${afterCursor}") { edges { node { title 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..f19b6ceb0f --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/pagination/first-after.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 { 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 after argument", async () => { + const afterCursor = offsetToCursor(2); + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection(first: 2, 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(2), + }, + }, + }); + }); +}); From 4e04a47517df61df88c0abb6f4db01379ff0be2f Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 31 May 2024 13:55:54 +0100 Subject: [PATCH 046/177] Add after argument --- ...ionOperationResolver.ts => connection-operation-resolver.ts} | 0 .../src/api-v6/resolvers/{readResolver.ts => read-resolver.ts} | 0 .../graphql/src/api-v6/schema-generation/SchemaGenerator.ts | 2 +- .../api-v6/schema-generation/schema-types/EntitySchemaTypes.ts | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename packages/graphql/src/api-v6/resolvers/{connectionOperationResolver.ts => connection-operation-resolver.ts} (100%) rename packages/graphql/src/api-v6/resolvers/{readResolver.ts => read-resolver.ts} (100%) diff --git a/packages/graphql/src/api-v6/resolvers/connectionOperationResolver.ts b/packages/graphql/src/api-v6/resolvers/connection-operation-resolver.ts similarity index 100% rename from packages/graphql/src/api-v6/resolvers/connectionOperationResolver.ts rename to packages/graphql/src/api-v6/resolvers/connection-operation-resolver.ts diff --git a/packages/graphql/src/api-v6/resolvers/readResolver.ts b/packages/graphql/src/api-v6/resolvers/read-resolver.ts similarity index 100% rename from packages/graphql/src/api-v6/resolvers/readResolver.ts rename to packages/graphql/src/api-v6/resolvers/read-resolver.ts diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index 4bb3117325..c3e9ef9282 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -20,7 +20,7 @@ import type { GraphQLSchema } from "graphql"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; -import { generateReadResolver } from "../resolvers/readResolver"; +import { generateReadResolver } from "../resolvers/read-resolver"; import { SchemaBuilder } from "./SchemaBuilder"; import { SchemaTypes } from "./schema-types/SchemaTypes"; import { StaticSchemaTypes } from "./schema-types/StaticSchemaTypes"; diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index 689b451b68..327d0434ab 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -19,7 +19,7 @@ import { GraphQLInt, GraphQLString } from "graphql"; import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; -import { connectionOperationResolver } from "../../resolvers/connectionOperationResolver"; +import { connectionOperationResolver } from "../../resolvers/connection-operation-resolver"; import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; import type { SchemaTypes } from "./SchemaTypes"; From bf70e20b46642355c54569dfa15384b6530c05be Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 31 May 2024 14:47:13 +0100 Subject: [PATCH 047/177] Improve tests on pagination --- .../integration/pagination/after.int.test.ts | 78 +++++++++++- .../pagination/first-after.int.test.ts | 4 +- .../pagination/first-relationship.int.test.ts | 116 ------------------ .../integration/pagination/first.int.test.ts | 77 +++++++++++- .../projection/aliasing.int.test.ts | 61 ++++++++- 5 files changed, 206 insertions(+), 130 deletions(-) delete mode 100644 packages/graphql/tests/api-v6/integration/pagination/first-relationship.int.test.ts 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 index c8000169cc..dcbb3c0002 100644 --- a/packages/graphql/tests/api-v6/integration/pagination/after.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/pagination/after.int.test.ts @@ -25,22 +25,44 @@ 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 (:${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"}) + 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) + `); }); @@ -75,4 +97,50 @@ describe("Pagination with after", () => { }, }); }); + + 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 index f19b6ceb0f..50503fb052 100644 --- 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 @@ -48,7 +48,7 @@ describe("Pagination with first and after", () => { await testHelper.close(); }); - test("Get movies with after argument", async () => { + test("Get movies with first and after argument", async () => { const afterCursor = offsetToCursor(2); const query = /* GraphQL */ ` query { @@ -75,4 +75,6 @@ describe("Pagination with first and after", () => { }, }); }); + + test.todo("Get movies and nested actors with first and after"); }); diff --git a/packages/graphql/tests/api-v6/integration/pagination/first-relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/pagination/first-relationship.int.test.ts deleted file mode 100644 index 78e51b1142..0000000000 --- a/packages/graphql/tests/api-v6/integration/pagination/first-relationship.int.test.ts +++ /dev/null @@ -1,116 +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("Nested 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 { - ${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/first.int.test.ts b/packages/graphql/tests/api-v6/integration/pagination/first.int.test.ts index e15b80b54a..c349d970fd 100644 --- a/packages/graphql/tests/api-v6/integration/pagination/first.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/pagination/first.int.test.ts @@ -25,22 +25,44 @@ 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 (:${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"}) + 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) + `); }); @@ -85,4 +107,49 @@ describe("Pagination with first", () => { }, }); }); + + 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/projection/aliasing.int.test.ts b/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts index 128efa97d4..9ece9b24fb 100644 --- a/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { offsetToCursor } from "graphql-relay"; import type { UniqueType } from "../../../utils/graphql-types"; import { TestHelper } from "../../../utils/tests-helper"; @@ -254,7 +255,61 @@ describe("Query aliasing", () => { }); }); - // Check Original tests: connection/alias.int.test.ts - test.todo("should alias pageInfo"); - test.todo("should alias cursor"); + test("Should alias pageInfo and cursor", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural} { + connection { + info: pageInfo { + hasPreviousPage + hasNextPage + startCursor + 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: { + hasPreviousPage: false, + hasNextPage: false, + startCursor: offsetToCursor(0), + endCursor: 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), + }, + ], + }, + }, + }); + }); }); From ab0389fb812c2c1747aea6587ddb6c5dbdff8786 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 31 May 2024 15:02:49 +0100 Subject: [PATCH 048/177] Add aliasing checks --- .../projection/aliasing.int.test.ts | 16 +- .../integration/connections/alias.int.test.ts | 443 ------------------ 2 files changed, 8 insertions(+), 451 deletions(-) delete mode 100644 packages/graphql/tests/integration/connections/alias.int.test.ts 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 index 9ece9b24fb..fd3189df91 100644 --- a/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/projection/aliasing.int.test.ts @@ -261,10 +261,10 @@ describe("Query aliasing", () => { ${Movie.plural} { connection { info: pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor + previous: hasPreviousPage + next: hasNextPage + start: startCursor + end: endCursor } info2: pageInfo { hasPreviousPage @@ -287,10 +287,10 @@ describe("Query aliasing", () => { [Movie.plural]: { connection: { info: { - hasPreviousPage: false, - hasNextPage: false, - startCursor: offsetToCursor(0), - endCursor: offsetToCursor(1), + previous: false, + next: false, + start: offsetToCursor(0), + end: offsetToCursor(1), }, info2: { hasPreviousPage: false, 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 f0dc3a451a..0000000000 --- a/packages/graphql/tests/integration/connections/alias.int.test.ts +++ /dev/null @@ -1,443 +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 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(); - }); -}); From c9486c479fb5c9e1d30ecf911884c6b12cb6e960 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 31 May 2024 16:30:29 +0100 Subject: [PATCH 049/177] Move some connection tests to v6 --- .../integration/projection/empty.int.test.ts | 80 +++ .../integration/field-filtering.int.test.ts | 102 ---- .../tests/integration/find.int.test.ts | 503 ------------------ .../integration/root-connections.int.test.ts | 238 --------- 4 files changed, 80 insertions(+), 843 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/projection/empty.int.test.ts delete mode 100644 packages/graphql/tests/integration/field-filtering.int.test.ts delete mode 100644 packages/graphql/tests/integration/find.int.test.ts delete mode 100644 packages/graphql/tests/integration/root-connections.int.test.ts 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/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/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/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=", - }, - }); - }); -}); From fc688a523b1749ac75048d064545158da0c56037 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 31 May 2024 16:43:54 +0100 Subject: [PATCH 050/177] Add tests on NOT filters --- .../filters/logical/not-filter.test.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 packages/graphql/tests/api-v6/integration/filters/logical/not-filter.test.ts diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.test.ts new file mode 100644 index 0000000000..8a65e60f5f --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.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("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: + { edges: { 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", + }, + }, + ]), + }, + }, + }); + }); + + test("NOT filter in edges by node", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + edges: { + 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", + }, + }, + ]), + }, + }, + }); + }); +}); From 93132b80f5e9915387ad96e99003eca8677c3a47 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 4 Jun 2024 10:45:34 +0100 Subject: [PATCH 051/177] refactor scalar support, add testing, BigInt Support, Spatial type generation, List of Temporals --- .../api-v6/schema-generation/SchemaBuilder.ts | 17 +- .../schema-types/EntitySchemaTypes.ts | 15 +- .../schema-types/RelatedEntitySchemaTypes.ts | 50 +- .../schema-types/StaticSchemaTypes.ts | 244 +++++-- .../schema-types/TopLevelEntitySchemaTypes.ts | 109 ++- .../filter-schema-types/FilterSchemaTypes.ts | 62 +- .../utils/to-graphql-list.ts | 25 + .../utils/to-graphql-non-null.ts | 25 + .../model-adapters/ArgumentAdapter.ts | 202 +----- .../schema-model/attribute/AttributeType.ts | 17 +- .../attribute/AttributeTypeHelper.ts | 9 +- .../model-adapters/AttributeAdapter.test.ts | 12 +- .../model-adapters/AttributeAdapter.ts | 1 - .../schema-model/parser/parse-attribute.ts | 15 +- .../src/schema/resolvers/field/numerical.ts | 2 +- .../ast/operations/FulltextOperation.ts | 1 + .../array/datetime-equals.int.test.ts | 92 +++ .../datetime/datetime-equals.int.test.ts | 2 +- .../number/array/number-equals.int.test.ts | 120 ++++ .../types/number/number-equals.int.test.ts | 2 +- .../types/number/number-gt.int.test.ts | 2 +- .../types/number/number-in.int.test.ts | 2 +- .../types/number/number-lt.int.test.ts | 8 +- .../types/array/number-array.int.test.ts | 102 +++ .../projection/types/number.int.test.ts | 5 +- .../tests/api-v6/schema/relationship.test.ts | 9 +- .../tests/api-v6/schema/simple.test.ts | 13 +- .../tests/api-v6/schema/types/array.test.ts | 271 ++++++-- .../tests/api-v6/schema/types/scalars.test.ts | 166 ++++- .../tests/api-v6/schema/types/spatial.test.ts | 267 ++++++++ .../api-v6/schema/types/temporals.test.ts | 5 +- .../types/array/temporals-array.test.ts | 629 ++++++++++++++++++ .../tck/filters/types/temporals.test.ts | 392 +++++++++++ .../types/array/temporals-array.test.ts | 243 +++++++ .../tck/projection/types/temporals.test.ts | 195 ++++-- .../tests/utils/raise-on-invalid-schema.ts | 28 + 36 files changed, 2883 insertions(+), 476 deletions(-) create mode 100644 packages/graphql/src/api-v6/schema-generation/utils/to-graphql-list.ts create mode 100644 packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/datetime/array/datetime-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/number/array/number-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/projection/types/array/number-array.int.test.ts create mode 100644 packages/graphql/tests/api-v6/schema/types/spatial.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/array/temporals-array.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/projection/types/array/temporals-array.test.ts create mode 100644 packages/graphql/tests/utils/raise-on-invalid-schema.ts diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index 6b01ed9a14..909298b628 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { GraphQLNamedInputType, GraphQLScalarType, GraphQLSchema } from "graphql"; +import type { GraphQLList, GraphQLNamedInputType, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema } from "graphql"; import type { EnumTypeComposer, InputTypeComposer, @@ -39,10 +39,10 @@ type ListOrNullComposer = type WrappedComposer = T | ListOrNullComposer; -export type GraphQLResolver = () => any; +export type GraphQLResolver = (...args) => any; export type FieldDefinition = { - resolver?: GraphQLResolver; + resolve?: GraphQLResolver; type: TypeDefinition; args?: Record; deprecationReason?: string | null; @@ -60,6 +60,10 @@ export class SchemaBuilder { this.composer.createScalarTC(scalar); } + public createObject(object: GraphQLObjectType): void { + this.composer.createObjectTC(object); + } + public getOrCreateObjectType( name: string, onCreate: () => { @@ -83,7 +87,12 @@ export class SchemaBuilder { onCreate: (itc: InputTypeComposer) => { fields: Record< string, - EnumTypeComposer | string | GraphQLNamedInputType | WrappedComposer + | EnumTypeComposer + | string + | GraphQLNamedInputType + | GraphQLList + | GraphQLNonNull + | WrappedComposer >; description?: string; } diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index aa3ca4196e..2503503cd2 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -45,15 +45,18 @@ export abstract class EntitySchemaTypes { public get connectionOperation(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connectionOperation, () => { + const args = { + first: GraphQLInt, + after: GraphQLString, + }; + if (this.isSortable()) { + args["sort"] = this.connectionSort; + } return { fields: { connection: { type: this.connection, - args: { - sort: this.connectionSort, - first: GraphQLInt, - after: GraphQLString, - }, + args, }, }, }; @@ -86,4 +89,6 @@ export abstract class EntitySchemaTypes { public abstract get nodeType(): ObjectTypeComposer; public abstract get nodeSort(): InputTypeComposer; + + public abstract isSortable(): boolean; } 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 index 297d2c7f71..3e1ea63ad1 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -20,6 +20,11 @@ import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer } 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"; @@ -28,6 +33,7 @@ import type { RelatedEntityTypeNames } from "../../schema-model/graphql-type-nam import type { SchemaBuilder } from "../SchemaBuilder"; import { EntitySchemaTypes } from "./EntitySchemaTypes"; import type { SchemaTypes } from "./SchemaTypes"; +import type { TopLevelEntitySchemaTypes } from "./TopLevelEntitySchemaTypes"; import { RelatedEntityFilterSchemaTypes } from "./filter-schema-types/RelatedEntityFilterSchemaTypes"; export class RelatedEntitySchemaTypes extends EntitySchemaTypes { @@ -77,36 +83,39 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { - const edgeSortFields = { - node: this.nodeSort, - }; + 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 { - const target = this.relationship.target; - if (!(target instanceof ConcreteEntity)) { - throw new Error("Interfaces not supported yet"); - } - const targetSchemaTypes = this.schemaTypes.getEntitySchemaTypes(target); - - return targetSchemaTypes.nodeType; + 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"); } - const targetSchemaTypes = this.schemaTypes.getEntitySchemaTypes(target); + return this.schemaTypes.getEntitySchemaTypes(target); + } - return targetSchemaTypes.nodeSort; + public isSortable(): boolean { + const isTargetSortable = this.getTargetEntitySchemaTypes().isSortable(); + return this.getRelationshipSortableFields().length > 0 || isTargetSortable; } @Memoize() @@ -115,11 +124,10 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes 0) { return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.propertiesSort, () => { - const fields = this.getRelationshipSortFields(); return { - fields, + fields: this.getRelationshipSortFields(), }; }); } @@ -132,13 +140,23 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { return Object.fromEntries( - this.getRelationshipFields().map((attribute) => [ + this.getRelationshipSortableFields().map((attribute) => [ attribute.name, this.schemaTypes.staticTypes.sortDirection, ]) ); } + @Memoize() + private getRelationshipSortableFields(): Attribute[] { + return this.getRelationshipFields().filter( + (field) => + field.type.name === GraphQLBuiltInScalarType[GraphQLBuiltInScalarType[field.type.name]] || + field.type.name === Neo4jGraphQLNumberType[Neo4jGraphQLNumberType[field.type.name]] || + field.type.name === Neo4jGraphQLTemporalType[Neo4jGraphQLTemporalType[field.type.name]] + ); + } + private getEdgeProperties(): ObjectTypeComposer | undefined { if (this.entityTypeNames.properties) { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.properties, () => { 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 index 90240695ee..7331f0c030 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -19,9 +19,10 @@ import type { GraphQLScalarType } from "graphql"; import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; -import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer, ListComposer } from "graphql-compose"; +import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; import { + GraphQLBigInt, GraphQLDate, GraphQLDateTime, GraphQLDuration, @@ -31,15 +32,11 @@ import { } from "../../../graphql/scalars"; import type { SchemaBuilder } from "../SchemaBuilder"; +import { CartesianPoint } from "../../../graphql/objects/CartesianPoint"; +import { Point } from "../../../graphql/objects/Point"; import * as Scalars from "../../../graphql/scalars"; - -function nonNull(type: string): string { - return `${type}!`; -} - -function list(type: string): string { - return `[${type}]`; -} +import { toGraphQLList } from "../utils/to-graphql-list"; +import { toGraphQLNonNull } from "../utils/to-graphql-non-null"; export class StaticSchemaTypes { private schemaBuilder: SchemaBuilder; @@ -48,13 +45,15 @@ export class StaticSchemaTypes { constructor({ schemaBuilder }: { schemaBuilder: SchemaBuilder }) { this.schemaBuilder = schemaBuilder; this.filters = new StaticFilterTypes({ schemaBuilder }); - this.addScalars(); + this.addBuiltInTypes(); } - private addScalars(): void { + private addBuiltInTypes(): void { Object.values(Scalars).forEach((scalar) => { this.schemaBuilder.createScalar(scalar); }); + this.schemaBuilder.createObject(CartesianPoint); + this.schemaBuilder.createObject(Point); } public get pageInfo(): ObjectTypeComposer { @@ -81,7 +80,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("StringListWhereNullable", () => { return { fields: { - equals: list(GraphQLString.name), + equals: toGraphQLList(GraphQLString), }, }; }); @@ -90,7 +89,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { return { fields: { - equals: list(nonNull(GraphQLString.name)), + equals: toGraphQLList(toGraphQLNonNull(GraphQLString)), }, }; }); @@ -102,7 +101,7 @@ class StaticFilterTypes { fields: { ...this.createBooleanOperators(itc), ...this.createStringOperators(GraphQLString), - in: list(nonNull(GraphQLString.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLString)), }, }; }); @@ -113,32 +112,91 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - in: list(nonNull(GraphQLDate.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLDate)), ...this.createNumericOperators(GraphQLDate), }, }; }); } + public getDateListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("DateListWhereNullable", () => { + return { + fields: { + equals: toGraphQLList(GraphQLDate), + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("DateListWhere", () => { + return { + fields: { + equals: toGraphQLList(toGraphQLNonNull(GraphQLDate)), + }, + }; + }); + } public get dateTimeWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("DateTimeWhere", (itc) => { return { fields: { ...this.createBooleanOperators(itc), - in: list(nonNull(GraphQLDateTime.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLDateTime)), ...this.createNumericOperators(GraphQLDateTime), }, }; }); } + public getDateTimeListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("DateTimeListWhereNullable", () => { + return { + fields: { + equals: toGraphQLList(GraphQLDateTime), + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("DateTimeListWhere", () => { + return { + fields: { + equals: toGraphQLList(toGraphQLNonNull(GraphQLDateTime)), + }, + }; + }); + } + public get localDateTimeWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("LocalDateTimeWhere", (itc) => { return { fields: { ...this.createBooleanOperators(itc), ...this.createNumericOperators(GraphQLLocalDateTime), - in: list(nonNull(GraphQLLocalDateTime.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLLocalDateTime)), + }, + }; + }); + } + + public getLocalDateTimeListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("LocalDateTimeListWhereNullable", () => { + return { + fields: { + equals: toGraphQLList(GraphQLLocalDateTime), + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("LocalDateTimeListWhere", () => { + return { + fields: { + equals: toGraphQLList(toGraphQLNonNull(GraphQLLocalDateTime)), }, }; }); @@ -150,7 +208,27 @@ class StaticFilterTypes { fields: { ...this.createBooleanOperators(itc), ...this.createNumericOperators(GraphQLDuration), - in: list(nonNull(GraphQLDuration.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLDuration)), + }, + }; + }); + } + + public getDurationListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("DurationListWhereNullable", () => { + return { + fields: { + equals: toGraphQLList(GraphQLDuration), + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("DurationListWhere", () => { + return { + fields: { + equals: toGraphQLList(toGraphQLNonNull(GraphQLDuration)), }, }; }); @@ -162,7 +240,27 @@ class StaticFilterTypes { fields: { ...this.createBooleanOperators(itc), ...this.createNumericOperators(GraphQLTime), - in: list(nonNull(GraphQLTime.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLTime)), + }, + }; + }); + } + + public getTimeListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("TimeListWhereNullable", () => { + return { + fields: { + equals: toGraphQLList(GraphQLTime), + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("TimeListWhere", () => { + return { + fields: { + equals: toGraphQLList(toGraphQLNonNull(GraphQLTime)), }, }; }); @@ -174,7 +272,27 @@ class StaticFilterTypes { fields: { ...this.createBooleanOperators(itc), ...this.createNumericOperators(GraphQLLocalTime), - in: list(nonNull(GraphQLLocalTime.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLLocalTime)), + }, + }; + }); + } + + public getLocalTimeListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("LocalTimeListWhereNullable", () => { + return { + fields: { + equals: toGraphQLList(GraphQLLocalTime), + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("LocalTimeListWhere", () => { + return { + fields: { + equals: toGraphQLList(toGraphQLNonNull(GraphQLLocalTime)), }, }; }); @@ -185,7 +303,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("StringListWhereNullable", () => { return { fields: { - equals: list(nonNull(GraphQLBoolean.name)), + equals: toGraphQLList(toGraphQLNonNull(GraphQLBoolean)), }, }; }); @@ -194,7 +312,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { return { fields: { - equals: list(nonNull(GraphQLBoolean.name)), + equals: toGraphQLList(toGraphQLNonNull(GraphQLBoolean)), }, }; }); @@ -218,7 +336,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("IDListWhereNullable", () => { return { fields: { - equals: list(GraphQLID.name), + equals: toGraphQLList(GraphQLID), }, }; }); @@ -227,7 +345,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("IDListWhere", () => { return { fields: { - equals: list(nonNull(GraphQLID.name)), + equals: toGraphQLList(toGraphQLNonNull(GraphQLID)), }, }; }); @@ -239,7 +357,7 @@ class StaticFilterTypes { fields: { ...this.createBooleanOperators(itc), ...this.createStringOperators(GraphQLID), - in: list(nonNull(GraphQLID.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLID)), }, }; }); @@ -250,7 +368,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("IntListWhereNullable", () => { return { fields: { - equals: list(GraphQLInt.name), + equals: toGraphQLList(GraphQLInt), }, }; }); @@ -259,19 +377,50 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("IntListWhere", () => { return { fields: { - equals: list(nonNull(GraphQLInt.name)), + equals: toGraphQLList(toGraphQLNonNull(GraphQLInt)), }, }; }); } - public get intWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("IntWhere", (itc) => { return { fields: { ...this.createBooleanOperators(itc), ...this.createNumericOperators(GraphQLInt), - in: list(nonNull(GraphQLInt.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLInt)), + }, + }; + }); + } + + public getBigIntListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("BigIntListWhereNullable", () => { + return { + fields: { + equals: toGraphQLList(GraphQLBigInt), + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("BigIntListWhere", () => { + return { + fields: { + equals: toGraphQLList(toGraphQLNonNull(GraphQLBigInt)), + }, + }; + }); + } + + public get bigIntWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("BigIntWhere", (itc) => { + return { + fields: { + ...this.createBooleanOperators(itc), + ...this.createNumericOperators(GraphQLBigInt), + in: toGraphQLList(toGraphQLNonNull(GraphQLBigInt)), }, }; }); @@ -282,7 +431,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("FloatListWhereNullable", () => { return { fields: { - equals: list(GraphQLFloat.name), + equals: toGraphQLList(GraphQLFloat), }, }; }); @@ -291,7 +440,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("FloatListWhere", () => { return { fields: { - equals: list(nonNull(GraphQLFloat.name)), + equals: toGraphQLList(toGraphQLNonNull(GraphQLFloat)), }, }; }); @@ -303,7 +452,27 @@ class StaticFilterTypes { fields: { ...this.createBooleanOperators(itc), ...this.createNumericOperators(GraphQLFloat), - in: list(nonNull(GraphQLFloat.name)), + in: toGraphQLList(toGraphQLNonNull(GraphQLFloat)), + }, + }; + }); + } + + public get cartesianPointWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("CartesianPointWhere", (_itc) => { + return { + fields: { + ...this.createNumericOperators(GraphQLFloat), + }, + }; + }); + } + + public get pointWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("PointWhere", (_itc) => { + return { + fields: { + ...this.createNumericOperators(GraphQLFloat), }, }; }); @@ -336,15 +505,4 @@ class StaticFilterTypes { NOT: itc, }; } - // private getListWhereFields(itc: InputTypeComposer, targetType: InputTypeComposer): Record { - // return { - // OR: itc.NonNull.List, - // AND: itc.NonNull.List, - // NOT: itc, - // all: targetType, - // none: targetType, - // single: targetType, - // some: targetType, - // }; - // } } 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 index 7aeb20c821..42044de803 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -17,22 +17,25 @@ * limitations under the License. */ -import type { GraphQLResolveInfo } from "graphql"; +import { type GraphQLResolveInfo } from "graphql"; import type { InputTypeComposer, ObjectTypeComposer } 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 { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; -import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; +//import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; +import { idResolver } from "../../../schema/resolvers/field/id"; +import { numericalResolver } from "../../../schema/resolvers/field/numerical"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; -import { filterTruthy } from "../../../utils/utils"; import type { TopLevelEntityTypeNames } from "../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; -import type { FieldDefinition, SchemaBuilder } from "../SchemaBuilder"; +import type { FieldDefinition, GraphQLResolver, SchemaBuilder } from "../SchemaBuilder"; import { EntitySchemaTypes } from "./EntitySchemaTypes"; import { RelatedEntitySchemaTypes } from "./RelatedEntitySchemaTypes"; import type { SchemaTypes } from "./SchemaTypes"; @@ -113,24 +116,9 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { const sortFields = Object.fromEntries( - filterTruthy( - this.getFields().map((field) => { - if ( - [ - ...Object.values(GraphQLBuiltInScalarType), - ...Object.values(Neo4jGraphQLNumberType), - ...Object.values(Neo4jGraphQLTemporalType), - ].includes( - field.type.name as - | GraphQLBuiltInScalarType - | Neo4jGraphQLNumberType - | Neo4jGraphQLTemporalType - ) - ) { - return [field.name, this.schemaTypes.staticTypes.sortDirection]; - } - }) - ) + this.getSortableFields().map((field) => { + return [field.name, this.schemaTypes.staticTypes.sortDirection]; + }) ); return { @@ -143,14 +131,59 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes 0; + } + @Memoize() private getFields(): Attribute[] { return [...this.entity.attributes.values()]; } + @Memoize() + private getSortableFields(): Attribute[] { + return this.getFields().filter( + (field) => + field.type.name === GraphQLBuiltInScalarType[GraphQLBuiltInScalarType[field.type.name]] || + field.type.name === Neo4jGraphQLNumberType[Neo4jGraphQLNumberType[field.type.name]] || + field.type.name === Neo4jGraphQLTemporalType[Neo4jGraphQLTemporalType[field.type.name]] + ); + } + private getNodeFieldsDefinitions(): Record { - const entityAttributes = this.getFields().map((attribute) => new AttributeAdapter(attribute)); - return attributeAdapterToComposeFields(entityAttributes, new Map()) as 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, + }, + ]; + }); + return Object.fromEntries(entries); } private getRelationshipFields(): Record }> { @@ -169,3 +202,29 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes(type: T): GraphQLList { + return new GraphQLList(type); +} diff --git a/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts b/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts new file mode 100644 index 0000000000..4590cecec5 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { GraphQLType } from "graphql"; +import { GraphQLNonNull } from "graphql"; + +export function toGraphQLNonNull(type: T): GraphQLNonNull { + return new GraphQLNonNull(type); +} 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..a8e2882c57 100644 --- a/packages/graphql/src/schema-model/attribute/AttributeType.ts +++ b/packages/graphql/src/schema-model/attribute/AttributeType.ts @@ -55,20 +55,11 @@ export class ScalarType { } } -export class Neo4jCartesianPointType { - public readonly name: string; - public readonly isRequired: boolean; - constructor(isRequired: boolean) { - this.name = Neo4jGraphQLSpatialType.CartesianPoint; - 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; } } diff --git a/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts b/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts index 8b56681082..e3da7867eb 100644 --- a/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts +++ b/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts @@ -23,11 +23,12 @@ import { InputType, InterfaceType, ListType, - Neo4jCartesianPointType, + Neo4jSpatialType, + // Neo4jCartesianPointType, Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - Neo4jPointType, + //Neo4jPointType, ObjectType, ScalarType, UnionType, @@ -84,12 +85,12 @@ export class AttributeTypeHelper { public isCartesianPoint(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof Neo4jCartesianPointType; + return type instanceof Neo4jSpatialType && type.name === Neo4jGraphQLSpatialType.CartesianPoint; } public isPoint(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof Neo4jPointType; + return type instanceof Neo4jSpatialType && type.name === Neo4jGraphQLSpatialType.Point; } public isBigInt(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..95405e9ca7 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,12 @@ import { GraphQLBuiltInScalarType, InterfaceType, ListType, - Neo4jCartesianPointType, + // Neo4jCartesianPointType, Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - Neo4jPointType, + Neo4jSpatialType, + // Neo4jPointType, ObjectType, ScalarType, UnionType, @@ -154,7 +156,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new Neo4jCartesianPointType(true), + type: new Neo4jSpatialType(Neo4jGraphQLSpatialType.CartesianPoint, true), args: [], }) ); @@ -167,7 +169,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new Neo4jPointType(true), + type: new Neo4jSpatialType(Neo4jGraphQLSpatialType.Point, true), args: [], }) ); @@ -421,7 +423,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new Neo4jCartesianPointType(true), + type: new Neo4jSpatialType(Neo4jGraphQLSpatialType.CartesianPoint, 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/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 930f993c92..838b81d110 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -29,11 +29,12 @@ import { InputType, InterfaceType, ListType, - Neo4jCartesianPointType, + //Neo4jCartesianPointType, Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - Neo4jPointType, + Neo4jSpatialType, + // Neo4jPointType, ObjectType, ScalarType, UnionType, @@ -100,9 +101,11 @@ function parseTypeNode( if (isScalarType(typeNode.name.value)) { return new ScalarType(typeNode.name.value, isRequired); } else if (isPoint(typeNode.name.value)) { - return new Neo4jPointType(isRequired); + return new Neo4jSpatialType(typeNode.name.value, isRequired); + // return new Neo4jPointType(isRequired); } else if (isCartesianPoint(typeNode.name.value)) { - return new Neo4jCartesianPointType(isRequired); + return new Neo4jSpatialType(typeNode.name.value, isRequired); + // return new Neo4jCartesianPointType(isRequired); } else if (isEnum(definitionCollection, typeNode.name.value)) { return new EnumType(typeNode.name.value, isRequired); } else if (isUserScalar(definitionCollection, typeNode.name.value)) { @@ -153,11 +156,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; } 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/translate/queryAST/ast/operations/FulltextOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts index 24c5024304..523c113b06 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: FulltextScoreField | undefined; 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..4ed3cee923 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/datetime/array/datetime-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("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: { edges: { 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 index 45a845e2f6..8b095c2b0d 100644 --- 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 @@ -33,7 +33,7 @@ describe("DateTime - Equals", () => { await testHelper.close(); }); - test("datetime equasl to ISO string", async () => { + test("datetime equals to ISO string", async () => { const typeDefs = /* GraphQL */ ` type ${Movie.name} @node { title: String! 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..bf50d7d8b3 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/number/array/number-equals.int.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 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}!]! + listNullable: [${type}]! + title: String! + } + + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (:${Movie} {list: [1999, 2000], listNullable: [1999, 2000], title: "The Matrix"}) + CREATE (:${Movie} {list: [2001, 2000], listNullable: [2001, 2000], title: "The Matrix 2"}) + CREATE (:${Movie} {list: [1999, 2000], listNullable: [1999, 2000], title: "Bill And Ted"}) + `); + }); + + afterAll(async () => { + await testHelper.close(); + }); + + test.each(["list", "listNullable"])("%s filter by 'equals'", async (field) => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { edges: { node: { ${field}: { 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.each(["list", "listNullable"])("%s filter by NOT 'equals'", async (field) => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { edges: { NOT: { node: { ${field}: { 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 index cbe19ec3a7..48cabe886d 100644 --- 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 @@ -20,7 +20,7 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe.each(["Float", "Int"] as const)("%s Filtering - 'equals'", (type) => { +describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'equals'", (type) => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; 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 index 8ed774c39f..b089e53a31 100644 --- 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 @@ -20,7 +20,7 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe.each(["Float", "Int"] as const)("%s Filtering - 'gt' and 'gte'", (type) => { +describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'gt' and 'gte'", (type) => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; 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 index eac3067715..315996ce32 100644 --- 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 @@ -20,7 +20,7 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe.each(["Float", "Int"] as const)("%s Filtering - 'in'", (type) => { +describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'in'", (type) => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; 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 index f57694980b..fb8b0051dc 100644 --- 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 @@ -20,7 +20,7 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe.each(["Float", "Int"] as const)("%s Filtering - 'lt' and 'lte'", (type) => { +describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering", (type) => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; @@ -80,7 +80,7 @@ describe.each(["Float", "Int"] as const)("%s Filtering - 'lt' and 'lte'", (type) }); }); - test("filter by NOT 'gt'", async () => { + test("filter by NOT 'lt'", async () => { const query = /* GraphQL */ ` query { ${Movie.plural}(where: { edges: { NOT: { node: { value: { lt: 1999 } } } } }) { @@ -117,7 +117,7 @@ describe.each(["Float", "Int"] as const)("%s Filtering - 'lt' and 'lte'", (type) }); }); - test("filter by 'gte'", async () => { + test("filter by 'lte'", async () => { const query = /* GraphQL */ ` query { ${Movie.plural}(where: { edges: { node: { value: { lte: 1999 } } } }) { @@ -154,7 +154,7 @@ describe.each(["Float", "Int"] as const)("%s Filtering - 'lt' and 'lte'", (type) }); }); - test("filter by NOT 'gte'", async () => { + test("filter by NOT 'lte'", async () => { const query = /* GraphQL */ ` query { ${Movie.plural}(where: { edges: { NOT: { node: { value: { lte: 1999 } } } } }) { 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..433495b34b --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/types/array/number-array.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 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!]! + yearNullable: [Int]! + ratingNullable: [Float]! + viewingsNullable: [BigInt]! + + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (movie:${Movie} { + yearNullable: [1999], + ratingNullable: [4.0], + viewingsNullable: [4294967297], + 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 + yearNullable + viewings + viewingsNullable + rating + ratingNullable + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + year: [1999], + yearNullable: [1999], + rating: [4.0], + ratingNullable: [4.0], + viewings: ["4294967297"], + viewingsNullable: ["4294967297"], + }, + }, + ], + }, + }, + }); + }); +}); 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 index 3e85099bc4..9ffea3739c 100644 --- 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 @@ -31,12 +31,13 @@ describe("Numeric fields", () => { type ${Movie} @node { year: Int! rating: Float! + viewings: BigInt! } `; await testHelper.initNeo4jGraphQL({ typeDefs }); await testHelper.executeCypher(` - CREATE (movie:${Movie} {year: 1999, rating: 4.0}) + CREATE (movie:${Movie} {year: 1999, rating: 4.0, viewings: 4294967297 }) `); }); @@ -53,6 +54,7 @@ describe("Numeric fields", () => { node { year rating + viewings } } @@ -71,6 +73,7 @@ describe("Numeric fields", () => { node: { year: 1999, rating: 4.0, + viewings: "4294967297", }, }, ], diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index cc807bd511..2c723de2f4 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -20,6 +20,7 @@ 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 () => { @@ -34,7 +35,9 @@ describe("Relationships", () => { } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); - const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); expect(printedSchema).toMatchInlineSnapshot(` "schema { @@ -297,7 +300,9 @@ describe("Relationships", () => { } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); - const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); expect(printedSchema).toMatchInlineSnapshot(` "schema { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index c48fcfbb87..afe362e6cb 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -20,6 +20,7 @@ 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 () => { @@ -29,7 +30,9 @@ describe("Simple Aura-API", () => { } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); - const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); expect(printedSchema).toMatchInlineSnapshot(` "schema { @@ -124,7 +127,9 @@ describe("Simple Aura-API", () => { } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); - const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); expect(printedSchema).toMatchInlineSnapshot(` "schema { @@ -271,7 +276,9 @@ describe("Simple Aura-API", () => { } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); - const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); expect(printedSchema).toMatchInlineSnapshot(` "schema { diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 2e855abe3b..e1739a6077 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -20,6 +20,7 @@ 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 () => { @@ -35,11 +36,25 @@ describe("Scalars", () => { idListNullable: [ID] booleanList: [Boolean!] booleanListNullable: [Boolean] + dateList: [Date!] + dateListNullable: [Date] + dateTimeList: [DateTime!] + dateTimeListNullable: [DateTime] + localDateTimeList: [LocalDateTime!] + localDateTimeListNullable: [LocalDateTime] + durationList: [Duration!] + durationListNullable: [Duration] + timeList: [Time!] + timeListNullable: [Time] + localTimeList: [LocalTime!] + localTimeListNullable: [LocalTime] + bigIntList: [BigInt!] + bigIntListNullable: [BigInt] relatedNode: [RelatedNode!]! @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") } - type RelatedNode @node { + type RelatedNodeProperties @relationshipProperties { stringList: [String!] stringListNullable: [String] intList: [Int!] @@ -50,9 +65,23 @@ describe("Scalars", () => { idListNullable: [ID] booleanList: [Boolean!] booleanListNullable: [Boolean] + dateList: [Date!] + dateListNullable: [Date] + dateTimeList: [DateTime!] + dateTimeListNullable: [DateTime] + localDateTimeList: [LocalDateTime!] + localDateTimeListNullable: [LocalDateTime] + durationList: [Duration!] + durationListNullable: [Duration] + timeList: [Time!] + timeListNullable: [Time] + localTimeList: [LocalTime!] + localTimeListNullable: [LocalTime] + bigIntList: [BigInt!] + bigIntListNullable: [BigInt] } - type RelatedNodeProperties @relationshipProperties { + type RelatedNode @node { stringList: [String!] stringListNullable: [String] intList: [Int!] @@ -63,16 +92,78 @@ describe("Scalars", () => { idListNullable: [ID] booleanList: [Boolean!] booleanListNullable: [Boolean] + dateList: [Date!] + dateListNullable: [Date] + dateTimeList: [DateTime!] + dateTimeListNullable: [DateTime] + localDateTimeList: [LocalDateTime!] + localDateTimeListNullable: [LocalDateTime] + durationList: [Duration!] + durationListNullable: [Duration] + timeList: [Time!] + timeListNullable: [Time] + localTimeList: [LocalTime!] + localTimeListNullable: [LocalTime] + bigIntList: [BigInt!] + bigIntListNullable: [BigInt] } `; - const neoSchema = new Neo4jGraphQL({ typeDefs }); - const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query } + \\"\\"\\" + 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 BigIntListWhereNullable { + equals: [BigInt] + } + + \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" + scalar Date + + input DateListWhere { + equals: [Date!] + } + + input DateListWhereNullable { + equals: [Date] + } + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeListWhere { + equals: [DateTime!] + } + + input DateTimeListWhereNullable { + equals: [DateTime] + } + + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" + scalar Duration + + input DurationListWhere { + equals: [Duration!] + } + + input DurationListWhereNullable { + equals: [Duration] + } + input FloatListWhere { equals: [Float!] } @@ -97,18 +188,56 @@ describe("Scalars", () => { equals: [Int] } + \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" + scalar LocalDateTime + + input LocalDateTimeListWhere { + equals: [LocalDateTime!] + } + + input LocalDateTimeListWhereNullable { + equals: [LocalDateTime] + } + + \\"\\"\\" + A local time, represented as a time string without timezone information + \\"\\"\\" + scalar LocalTime + + input LocalTimeListWhere { + equals: [LocalTime!] + } + + input LocalTimeListWhereNullable { + equals: [LocalTime] + } + type NodeType { + bigIntList: [BigInt!] + bigIntListNullable: [BigInt] booleanList: [Boolean!] booleanListNullable: [Boolean] + dateList: [Date!] + dateListNullable: [Date] + dateTimeList: [DateTime!] + dateTimeListNullable: [DateTime] + durationList: [Duration!] + durationListNullable: [Duration] floatList: [Float!] floatListNullable: [Float] idList: [ID!] idListNullable: [ID] intList: [Int!] intListNullable: [Int] + localDateTimeList: [LocalDateTime!] + localDateTimeListNullable: [LocalDateTime] + localTimeList: [LocalTime!] + localTimeListNullable: [LocalTime] relatedNode(where: NodeTypeRelatedNodeOperationWhere): NodeTypeRelatedNodeOperation stringList: [String!] stringListNullable: [String] + timeList: [Time!] + timeListNullable: [Time] } type NodeTypeConnection { @@ -116,19 +245,11 @@ describe("Scalars", () => { pageInfo: PageInfo } - input NodeTypeConnectionSort { - edges: [NodeTypeEdgeSort!] - } - type NodeTypeEdge { cursor: String node: NodeType } - input NodeTypeEdgeSort { - node: NodeTypeSort - } - input NodeTypeEdgeWhere { AND: [NodeTypeEdgeWhere!] NOT: NodeTypeEdgeWhere @@ -137,7 +258,7 @@ describe("Scalars", () => { } type NodeTypeOperation { - connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection + connection(after: String, first: Int): NodeTypeConnection } input NodeTypeOperationWhere { @@ -152,10 +273,6 @@ describe("Scalars", () => { pageInfo: PageInfo } - input NodeTypeRelatedNodeConnectionSort { - edges: [NodeTypeRelatedNodeEdgeSort!] - } - type NodeTypeRelatedNodeEdge { cursor: String node: RelatedNode @@ -172,11 +289,6 @@ describe("Scalars", () => { some: NodeTypeRelatedNodeEdgeWhere } - input NodeTypeRelatedNodeEdgeSort { - node: RelatedNodeSort - properties: RelatedNodePropertiesSort - } - input NodeTypeRelatedNodeEdgeWhere { AND: [NodeTypeRelatedNodeEdgeWhere!] NOT: NodeTypeRelatedNodeEdgeWhere @@ -193,7 +305,7 @@ describe("Scalars", () => { } type NodeTypeRelatedNodeOperation { - connection(after: String, first: Int, sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection + connection(after: String, first: Int): NodeTypeRelatedNodeConnection } input NodeTypeRelatedNodeOperationWhere { @@ -203,23 +315,35 @@ describe("Scalars", () => { edges: NodeTypeRelatedNodeEdgeWhere } - input NodeTypeSort - input NodeTypeWhere { AND: [NodeTypeWhere!] NOT: NodeTypeWhere OR: [NodeTypeWhere!] + bigIntList: BigIntListWhere + bigIntListNullable: BigIntListWhereNullable booleanList: StringListWhere booleanListNullable: StringListWhereNullable + dateList: DateListWhere + dateListNullable: DateListWhereNullable + dateTimeList: DateTimeListWhere + dateTimeListNullable: DateTimeListWhereNullable + durationList: DurationListWhere + durationListNullable: DurationListWhereNullable floatList: FloatListWhere floatListNullable: FloatListWhereNullable idList: IDListWhere idListNullable: IDListWhereNullable intList: IntListWhere intListNullable: IntListWhereNullable + localDateTimeList: LocalDateTimeListWhere + localDateTimeListNullable: LocalDateTimeListWhereNullable + localTimeList: LocalTimeListWhere + localTimeListNullable: LocalTimeListWhereNullable relatedNode: NodeTypeRelatedNodeNestedOperationWhere stringList: StringListWhere stringListNullable: StringListWhereNullable + timeList: TimeListWhere + timeListNullable: TimeListWhereNullable } type PageInfo { @@ -233,16 +357,30 @@ describe("Scalars", () => { } type RelatedNode { + bigIntList: [BigInt!] + bigIntListNullable: [BigInt] booleanList: [Boolean!] booleanListNullable: [Boolean] + dateList: [Date!] + dateListNullable: [Date] + dateTimeList: [DateTime!] + dateTimeListNullable: [DateTime] + durationList: [Duration!] + durationListNullable: [Duration] floatList: [Float!] floatListNullable: [Float] idList: [ID!] idListNullable: [ID] intList: [Int!] intListNullable: [Int] + localDateTimeList: [LocalDateTime!] + localDateTimeListNullable: [LocalDateTime] + localTimeList: [LocalTime!] + localTimeListNullable: [LocalTime] stringList: [String!] stringListNullable: [String] + timeList: [Time!] + timeListNullable: [Time] } type RelatedNodeConnection { @@ -250,19 +388,11 @@ describe("Scalars", () => { pageInfo: PageInfo } - input RelatedNodeConnectionSort { - edges: [RelatedNodeEdgeSort!] - } - type RelatedNodeEdge { cursor: String node: RelatedNode } - input RelatedNodeEdgeSort { - node: RelatedNodeSort - } - input RelatedNodeEdgeWhere { AND: [RelatedNodeEdgeWhere!] NOT: RelatedNodeEdgeWhere @@ -271,7 +401,7 @@ describe("Scalars", () => { } type RelatedNodeOperation { - connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection + connection(after: String, first: Int): RelatedNodeConnection } input RelatedNodeOperationWhere { @@ -282,68 +412,90 @@ describe("Scalars", () => { } type RelatedNodeProperties { + bigIntList: [BigInt!] + bigIntListNullable: [BigInt] booleanList: [Boolean!] booleanListNullable: [Boolean] + dateList: [Date!] + dateListNullable: [Date] + dateTimeList: [DateTime!] + dateTimeListNullable: [DateTime] + durationList: [Duration!] + durationListNullable: [Duration] floatList: [Float!] floatListNullable: [Float] idList: [ID!] idListNullable: [ID] intList: [Int!] intListNullable: [Int] + localDateTimeList: [LocalDateTime!] + localDateTimeListNullable: [LocalDateTime] + localTimeList: [LocalTime!] + localTimeListNullable: [LocalTime] stringList: [String!] stringListNullable: [String] - } - - input RelatedNodePropertiesSort { - booleanList: SortDirection - booleanListNullable: SortDirection - floatList: SortDirection - floatListNullable: SortDirection - idList: SortDirection - idListNullable: SortDirection - intList: SortDirection - intListNullable: SortDirection - stringList: SortDirection - stringListNullable: SortDirection + timeList: [Time!] + timeListNullable: [Time] } input RelatedNodePropertiesWhere { AND: [RelatedNodePropertiesWhere!] NOT: RelatedNodePropertiesWhere OR: [RelatedNodePropertiesWhere!] + bigIntList: BigIntListWhere + bigIntListNullable: BigIntListWhereNullable booleanList: StringListWhere booleanListNullable: StringListWhereNullable + dateList: DateListWhere + dateListNullable: DateListWhereNullable + dateTimeList: DateTimeListWhere + dateTimeListNullable: DateTimeListWhereNullable + durationList: DurationListWhere + durationListNullable: DurationListWhereNullable floatList: FloatListWhere floatListNullable: FloatListWhereNullable idList: IDListWhere idListNullable: IDListWhereNullable intList: IntListWhere intListNullable: IntListWhereNullable + localDateTimeList: LocalDateTimeListWhere + localDateTimeListNullable: LocalDateTimeListWhereNullable + localTimeList: LocalTimeListWhere + localTimeListNullable: LocalTimeListWhereNullable stringList: StringListWhere stringListNullable: StringListWhereNullable + timeList: TimeListWhere + timeListNullable: TimeListWhereNullable } - input RelatedNodeSort - input RelatedNodeWhere { AND: [RelatedNodeWhere!] NOT: RelatedNodeWhere OR: [RelatedNodeWhere!] + bigIntList: BigIntListWhere + bigIntListNullable: BigIntListWhereNullable booleanList: StringListWhere booleanListNullable: StringListWhereNullable + dateList: DateListWhere + dateListNullable: DateListWhereNullable + dateTimeList: DateTimeListWhere + dateTimeListNullable: DateTimeListWhereNullable + durationList: DurationListWhere + durationListNullable: DurationListWhereNullable floatList: FloatListWhere floatListNullable: FloatListWhereNullable idList: IDListWhere idListNullable: IDListWhereNullable intList: IntListWhere intListNullable: IntListWhereNullable + localDateTimeList: LocalDateTimeListWhere + localDateTimeListNullable: LocalDateTimeListWhereNullable + localTimeList: LocalTimeListWhere + localTimeListNullable: LocalTimeListWhereNullable stringList: StringListWhere stringListNullable: StringListWhereNullable - } - - enum SortDirection { - ASC - DESC + timeList: TimeListWhere + timeListNullable: TimeListWhereNullable } input StringListWhere { @@ -352,6 +504,17 @@ describe("Scalars", () => { input StringListWhereNullable { equals: [String] + } + + \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" + scalar Time + + input TimeListWhere { + equals: [Time!] + } + + input TimeListWhereNullable { + equals: [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 index 9cfaa975e5..b11a565f25 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -20,44 +20,85 @@ 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 + 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 + 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 + 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 printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query } + \\"\\"\\" + 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 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 @@ -101,12 +142,19 @@ describe("Scalars", () => { } type NodeType { - boolean: Boolean - float: Float - id: ID - int: Int + 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 + string: String! + stringNullable: String } type NodeTypeConnection { @@ -202,23 +250,37 @@ describe("Scalars", () => { } 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 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 { @@ -232,11 +294,18 @@ describe("Scalars", () => { } type RelatedNode { - boolean: Boolean - float: Float - id: ID - int: Int - string: String + 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 { @@ -276,49 +345,84 @@ describe("Scalars", () => { } type RelatedNodeProperties { - boolean: Boolean - float: Float - id: ID - int: Int - string: String + 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 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 { 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..11ec40ecce --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -0,0 +1,267 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 + } + + \\"\\"\\" + 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 CartesianPointWhere { + equals: Float + gt: Float + gte: Float + lt: Float + lte: Float + } + + type NodeType { + cartesianPoint: CartesianPoint! + cartesianPointNullable: CartesianPoint + point: Point! + pointNullable: Point + relatedNode(where: NodeTypeRelatedNodeOperationWhere): NodeTypeRelatedNodeOperation + } + + type NodeTypeConnection { + edges: [NodeTypeEdge] + pageInfo: PageInfo + } + + type NodeTypeEdge { + cursor: String + node: NodeType + } + + input NodeTypeEdgeWhere { + AND: [NodeTypeEdgeWhere!] + NOT: NodeTypeEdgeWhere + OR: [NodeTypeEdgeWhere!] + node: NodeTypeWhere + } + + type NodeTypeOperation { + connection(after: String, first: Int): NodeTypeConnection + } + + input NodeTypeOperationWhere { + AND: [NodeTypeOperationWhere!] + NOT: NodeTypeOperationWhere + OR: [NodeTypeOperationWhere!] + edges: NodeTypeEdgeWhere + } + + type NodeTypeRelatedNodeConnection { + edges: [NodeTypeRelatedNodeEdge] + pageInfo: PageInfo + } + + type NodeTypeRelatedNodeEdge { + cursor: String + node: RelatedNode + properties: RelatedNodeProperties + } + + input NodeTypeRelatedNodeEdgeListWhere { + AND: [NodeTypeRelatedNodeEdgeListWhere!] + NOT: NodeTypeRelatedNodeEdgeListWhere + OR: [NodeTypeRelatedNodeEdgeListWhere!] + all: NodeTypeRelatedNodeEdgeWhere + none: NodeTypeRelatedNodeEdgeWhere + single: NodeTypeRelatedNodeEdgeWhere + some: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeRelatedNodeEdgeWhere { + AND: [NodeTypeRelatedNodeEdgeWhere!] + NOT: NodeTypeRelatedNodeEdgeWhere + OR: [NodeTypeRelatedNodeEdgeWhere!] + node: RelatedNodeWhere + properties: RelatedNodePropertiesWhere + } + + input NodeTypeRelatedNodeNestedOperationWhere { + AND: [NodeTypeRelatedNodeNestedOperationWhere!] + NOT: NodeTypeRelatedNodeNestedOperationWhere + OR: [NodeTypeRelatedNodeNestedOperationWhere!] + edges: NodeTypeRelatedNodeEdgeListWhere + } + + type NodeTypeRelatedNodeOperation { + connection(after: String, first: Int): NodeTypeRelatedNodeConnection + } + + input NodeTypeRelatedNodeOperationWhere { + AND: [NodeTypeRelatedNodeOperationWhere!] + NOT: NodeTypeRelatedNodeOperationWhere + OR: [NodeTypeRelatedNodeOperationWhere!] + edges: NodeTypeRelatedNodeEdgeWhere + } + + input NodeTypeWhere { + AND: [NodeTypeWhere!] + NOT: NodeTypeWhere + OR: [NodeTypeWhere!] + cartesianPoint: CartesianPointWhere + cartesianPointNullable: CartesianPointWhere + point: PointWhere + pointNullable: PointWhere + relatedNode: NodeTypeRelatedNodeNestedOperationWhere + } + + type PageInfo { + hasNextPage: Boolean + hasPreviousPage: Boolean + } + + \\"\\"\\" + 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 PointWhere { + equals: Float + gt: Float + gte: Float + lt: Float + lte: Float + } + + 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 RelatedNodeEdge { + cursor: String + node: RelatedNode + } + + input RelatedNodeEdgeWhere { + AND: [RelatedNodeEdgeWhere!] + NOT: RelatedNodeEdgeWhere + OR: [RelatedNodeEdgeWhere!] + node: RelatedNodeWhere + } + + type RelatedNodeOperation { + connection(after: String, first: Int): RelatedNodeConnection + } + + input RelatedNodeOperationWhere { + AND: [RelatedNodeOperationWhere!] + NOT: RelatedNodeOperationWhere + OR: [RelatedNodeOperationWhere!] + edges: RelatedNodeEdgeWhere + } + + type RelatedNodeProperties { + cartesianPoint: CartesianPoint! + cartesianPointNullable: CartesianPoint + point: Point! + pointNullable: Point + } + + input RelatedNodePropertiesWhere { + AND: [RelatedNodePropertiesWhere!] + NOT: RelatedNodePropertiesWhere + OR: [RelatedNodePropertiesWhere!] + cartesianPoint: CartesianPointWhere + cartesianPointNullable: CartesianPointWhere + point: PointWhere + pointNullable: PointWhere + } + + input RelatedNodeWhere { + AND: [RelatedNodeWhere!] + NOT: RelatedNodeWhere + OR: [RelatedNodeWhere!] + cartesianPoint: CartesianPointWhere + cartesianPointNullable: CartesianPointWhere + point: PointWhere + pointNullable: PointWhere + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index a4b64efd99..7aebc6385c 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -20,6 +20,7 @@ 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 () => { @@ -54,7 +55,9 @@ describe("Temporals", () => { } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); - const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getAuraSchema())); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); expect(printedSchema).toMatchInlineSnapshot(` "schema { diff --git a/packages/graphql/tests/api-v6/tck/filters/types/array/temporals-array.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/array/temporals-array.test.ts new file mode 100644 index 0000000000..e5ea1ce283 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/array/temporals-array.test.ts @@ -0,0 +1,629 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { + dateTimeNullable: [DateTime] + dateTime: [DateTime!] + localDateTimeNullable: [LocalDateTime] + localDateTime: [LocalDateTime!] + durationNullable: [Duration] + duration: [Duration!] + timeNullable: [Time] + time: [Time!] + localTimeNullable: [LocalTime] + localTime: [LocalTime!] + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNodeProperties @relationshipProperties { + dateTimeNullable: [DateTime] + dateTime: [DateTime!] + localDateTimeNullable: [LocalDateTime] + localDateTime: [LocalDateTime!] + durationNullable: [Duration] + duration: [Duration!] + timeNullable: [Time] + time: [Time!] + localTimeNullable: [LocalTime] + localTime: [LocalTime!] + } + + type RelatedNode @node { + dateTimeNullable: [DateTime] + dateTime: [DateTime!] + localDateTimeNullable: [LocalDateTime] + localDateTime: [LocalDateTime!] + durationNullable: [Duration] + duration: [Duration!] + timeNullable: [Time] + time: [Time!] + localTimeNullable: [LocalTime] + localTime: [LocalTime!] + } + `; + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("should filter temporal array types - Top-Level", async () => { + const query = /* GraphQL */ ` + query { + typeNodes( + 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"] } + dateTimeNullable: { equals: ["2015-06-24T12:50:35.556+0100"] } + localDateTimeNullable: { equals: ["2003-09-14T12:00:00"] } + durationNullable: { equals: ["P1Y"] } + timeNullable: { equals: ["22:00:15.555"] } + localTimeNullable: { equals: ["12:50:35.556"] } + } + } + } + ) { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + dateTimeNullable + localDateTimeNullable + durationNullable + timeNullable + localTimeNullable + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:TypeNode) + WHERE (this0.dateTimeNullable = $param0 AND this0.dateTime = $param1 AND this0.localDateTimeNullable = $param2 AND this0.localDateTime = $param3 AND this0.durationNullable = $param4 AND this0.duration = $param5 AND this0.timeNullable = $param6 AND this0.time = $param7 AND this0.localTimeNullable = $param8 AND this0.localTime = $param9) + 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, dateTimeNullable: [var2 IN this0.dateTimeNullable | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this0.localDateTimeNullable, durationNullable: this0.durationNullable, timeNullable: this0.timeNullable, localTimeNullable: this0.localTimeNullable, __resolveType: \\"TypeNode\\" } }) AS var3 + } + RETURN { connection: { edges: var3, 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\\": 2015, + \\"month\\": 6, + \\"day\\": 24, + \\"hour\\": 11, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param2\\": [ + { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + } + ], + \\"param3\\": [ + { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + } + ], + \\"param4\\": [ + { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } + ], + \\"param5\\": [ + { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } + ], + \\"param6\\": [ + { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param7\\": [ + { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param8\\": [ + { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + ], + \\"param9\\": [ + { + \\"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"] } + dateTimeNullable: { equals: ["2015-06-24T12:50:35.556+0100"] } + localDateTimeNullable: { equals: ["2003-09-14T12:00:00"] } + durationNullable: { equals: ["P1Y"] } + timeNullable: { equals: ["22:00:15.555"] } + localTimeNullable: { equals: ["12:50:35.556"] } + } + } + } + ) { + connection { + edges { + node { + dateTime + localDateTime + duration + time + localTime + dateTimeNullable + localDateTimeNullable + durationNullable + timeNullable + localTimeNullable + } + } + } + } + } + } + } + } + } + `; + + 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]->(relatedNode:RelatedNode) + WHERE (relatedNode.dateTimeNullable = $param0 AND relatedNode.dateTime = $param1 AND relatedNode.localDateTimeNullable = $param2 AND relatedNode.localDateTime = $param3 AND relatedNode.durationNullable = $param4 AND relatedNode.duration = $param5 AND relatedNode.timeNullable = $param6 AND relatedNode.time = $param7 AND relatedNode.localTimeNullable = $param8 AND relatedNode.localTime = $param9) + WITH collect({ node: relatedNode, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS relatedNode, edge.relationship AS this1 + RETURN collect({ node: { dateTime: [var2 IN relatedNode.dateTime | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: relatedNode.localDateTime, duration: relatedNode.duration, time: relatedNode.time, localTime: relatedNode.localTime, dateTimeNullable: [var3 IN relatedNode.dateTimeNullable | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: relatedNode.localDateTimeNullable, durationNullable: relatedNode.durationNullable, timeNullable: relatedNode.timeNullable, localTimeNullable: relatedNode.localTimeNullable, __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\\": 2015, + \\"month\\": 6, + \\"day\\": 24, + \\"hour\\": 11, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param2\\": [ + { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + } + ], + \\"param3\\": [ + { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + } + ], + \\"param4\\": [ + { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } + ], + \\"param5\\": [ + { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } + ], + \\"param6\\": [ + { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param7\\": [ + { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param8\\": [ + { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + ], + \\"param9\\": [ + { + \\"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"] } + dateTimeNullable: { equals: ["2015-06-24T12:50:35.556+0100"] } + localDateTimeNullable: { equals: ["2003-09-14T12:00:00"] } + durationNullable: { equals: ["P1Y"] } + timeNullable: { equals: ["22:00:15.555"] } + localTimeNullable: { equals: ["12:50:35.556"] } + } + } + } + ) { + connection { + edges { + properties { + dateTime + localDateTime + duration + time + localTime + dateTimeNullable + localDateTimeNullable + durationNullable + timeNullable + localTimeNullable + } + } + } + } + } + } + } + } + } + `; + + 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]->(relatedNode:RelatedNode) + WHERE (this1.dateTimeNullable = $param0 AND this1.dateTime = $param1 AND this1.localDateTimeNullable = $param2 AND this1.localDateTime = $param3 AND this1.durationNullable = $param4 AND this1.duration = $param5 AND this1.timeNullable = $param6 AND this1.time = $param7 AND this1.localTimeNullable = $param8 AND this1.localTime = $param9) + WITH collect({ node: relatedNode, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS relatedNode, edge.relationship AS this1 + RETURN collect({ properties: { dateTime: [var2 IN this1.dateTime | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: this1.localDateTime, duration: this1.duration, time: this1.time, localTime: this1.localTime, dateTimeNullable: [var3 IN this1.dateTimeNullable | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this1.localDateTimeNullable, durationNullable: this1.durationNullable, timeNullable: this1.timeNullable, localTimeNullable: this1.localTimeNullable }, node: { __id: id(relatedNode), __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\\": 2015, + \\"month\\": 6, + \\"day\\": 24, + \\"hour\\": 11, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param2\\": [ + { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + } + ], + \\"param3\\": [ + { + \\"year\\": 2003, + \\"month\\": 9, + \\"day\\": 14, + \\"hour\\": 12, + \\"minute\\": 0, + \\"second\\": 0, + \\"nanosecond\\": 0 + } + ], + \\"param4\\": [ + { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } + ], + \\"param5\\": [ + { + \\"months\\": 12, + \\"days\\": 0, + \\"seconds\\": { + \\"low\\": 0, + \\"high\\": 0 + }, + \\"nanoseconds\\": { + \\"low\\": 0, + \\"high\\": 0 + } + } + ], + \\"param6\\": [ + { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param7\\": [ + { + \\"hour\\": 22, + \\"minute\\": 0, + \\"second\\": 15, + \\"nanosecond\\": 555000000, + \\"timeZoneOffsetSeconds\\": 0 + } + ], + \\"param8\\": [ + { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + ], + \\"param9\\": [ + { + \\"hour\\": 12, + \\"minute\\": 50, + \\"second\\": 35, + \\"nanosecond\\": 556000000 + } + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts new file mode 100644 index 0000000000..66d5c8244c --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.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 { 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: { + 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) + 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: 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]->(relatedNode:RelatedNode) + WHERE (relatedNode.dateTime = $param0 AND relatedNode.localDateTime > $param1 AND relatedNode.duration >= $param2 AND relatedNode.time < $param3 AND relatedNode.localTime <= $param4) + WITH collect({ node: relatedNode, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS relatedNode, edge.relationship AS this1 + RETURN collect({ node: { dateTime: apoc.date.convertFormat(toString(relatedNode.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: relatedNode.localDateTime, duration: relatedNode.duration, time: relatedNode.time, localTime: relatedNode.localTime, __resolveType: \\"RelatedNode\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { relatedNode: var3, __resolveType: \\"TypeNode\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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]->(relatedNode:RelatedNode) + WHERE (this1.dateTime = $param0 AND this1.localDateTime > $param1 AND this1.duration >= $param2 AND this1.time < $param3 AND this1.localTime <= $param4) + WITH collect({ node: relatedNode, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS relatedNode, 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(relatedNode), __resolveType: \\"RelatedNode\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { relatedNode: var3, __resolveType: \\"TypeNode\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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/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..2ca20c20ac --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/types/array/temporals-array.test.ts @@ -0,0 +1,243 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { + dateTimeNullable: [DateTime] + dateTime: [DateTime!] + localDateTimeNullable: [LocalDateTime] + localDateTime: [LocalDateTime!] + durationNullable: [Duration] + duration: [Duration!] + timeNullable: [Time] + time: [Time!] + localTimeNullable: [LocalTime] + localTime: [LocalTime!] + relatedNode: [RelatedNode!]! + @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") + } + + type RelatedNodeProperties @relationshipProperties { + dateTimeNullable: [DateTime] + dateTime: [DateTime!] + localDateTimeNullable: [LocalDateTime] + localDateTime: [LocalDateTime!] + durationNullable: [Duration] + duration: [Duration!] + timeNullable: [Time] + time: [Time!] + localTimeNullable: [LocalTime] + localTime: [LocalTime!] + } + + type RelatedNode @node { + dateTimeNullable: [DateTime] + dateTime: [DateTime!] + localDateTimeNullable: [LocalDateTime] + localDateTime: [LocalDateTime!] + durationNullable: [Duration] + duration: [Duration!] + timeNullable: [Time] + time: [Time!] + localTimeNullable: [LocalTime] + 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 { + dateTimeNullable + dateTime + localDateTimeNullable + localDateTime + durationNullable + duration + timeNullable + time + localTimeNullable + 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: { dateTimeNullable: [var1 IN this0.dateTimeNullable | apoc.date.convertFormat(toString(var1), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var2 IN this0.dateTime | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this0.localDateTimeNullable, localDateTime: this0.localDateTime, durationNullable: this0.durationNullable, duration: this0.duration, timeNullable: this0.timeNullable, time: this0.time, localTimeNullable: this0.localTimeNullable, localTime: this0.localTime, __resolveType: \\"TypeNode\\" } }) AS var3 + } + RETURN { connection: { edges: var3, 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 { + dateTimeNullable + dateTime + localDateTimeNullable + localDateTime + durationNullable + duration + timeNullable + time + localTimeNullable + 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]->(relatedNode:RelatedNode) + WITH collect({ node: relatedNode, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS relatedNode, edge.relationship AS this1 + RETURN collect({ node: { dateTimeNullable: [var2 IN relatedNode.dateTimeNullable | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var3 IN relatedNode.dateTime | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: relatedNode.localDateTimeNullable, localDateTime: relatedNode.localDateTime, durationNullable: relatedNode.durationNullable, duration: relatedNode.duration, timeNullable: relatedNode.timeNullable, time: relatedNode.time, localTimeNullable: relatedNode.localTimeNullable, localTime: relatedNode.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 { + dateTimeNullable + dateTime + localDateTimeNullable + localDateTime + durationNullable + duration + timeNullable + time + localTimeNullable + 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]->(relatedNode:RelatedNode) + WITH collect({ node: relatedNode, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS relatedNode, edge.relationship AS this1 + RETURN collect({ properties: { dateTimeNullable: [var2 IN this1.dateTimeNullable | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var3 IN this1.dateTime | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this1.localDateTimeNullable, localDateTime: this1.localDateTime, durationNullable: this1.durationNullable, duration: this1.duration, timeNullable: this1.timeNullable, time: this1.time, localTimeNullable: this1.localTimeNullable, localTime: this1.localTime }, node: { __id: id(relatedNode), __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/temporals.test.ts b/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts index 3179e766eb..8a8869cf0f 100644 --- a/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts @@ -32,6 +32,24 @@ describe("Temporal types", () => { 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 } `; @@ -40,22 +58,10 @@ describe("Temporal types", () => { }); }); - test("Filter temporals types", async () => { + test("should be possible to querying temporal fields - Top Level", async () => { const query = /* GraphQL */ ` query { - typeNodes( - 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" } - } - } - } - ) { + typeNodes { connection { edges { node { @@ -73,10 +79,8 @@ describe("Temporal types", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor 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 { @@ -88,53 +92,122 @@ describe("Temporal types", () => { 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 + 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]->(relatedNode:RelatedNode) + WITH collect({ node: relatedNode, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS relatedNode, edge.relationship AS this1 + RETURN collect({ node: { dateTime: apoc.date.convertFormat(toString(relatedNode.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: relatedNode.localDateTime, duration: relatedNode.duration, time: relatedNode.time, localTime: relatedNode.localTime, __resolveType: \\"RelatedNode\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { relatedNode: var3, __resolveType: \\"TypeNode\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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]->(relatedNode:RelatedNode) + WITH collect({ node: relatedNode, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS relatedNode, 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(relatedNode), __resolveType: \\"RelatedNode\\" } }) AS var2 } - }, - \\"param3\\": { - \\"hour\\": 22, - \\"minute\\": 0, - \\"second\\": 15, - \\"nanosecond\\": 555000000, - \\"timeZoneOffsetSeconds\\": 0 - }, - \\"param4\\": { - \\"hour\\": 12, - \\"minute\\": 50, - \\"second\\": 35, - \\"nanosecond\\": 556000000 + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 } - }" + RETURN collect({ node: { relatedNode: var3, __resolveType: \\"TypeNode\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); }); }); 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")}`); + } +} From d5f77b7efb24fdc4265b53800b465da0966f50a5 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 10 Jun 2024 15:55:32 +0100 Subject: [PATCH 052/177] implement point filter operators, move tck tests --- .../api-v6/queryIRFactory/FilterFactory.ts | 34 +- .../api-v6/queryIRFactory/FilterOperators.ts | 21 + .../queryIRFactory/ReadOperationFactory.ts | 13 + .../resolve-tree-parser/ResolveTreeParser.ts | 43 +- .../resolve-tree-parser/graphql-tree.ts | 27 +- .../api-v6/schema-generation/SchemaBuilder.ts | 13 +- .../schema-types/StaticSchemaTypes.ts | 28 +- ...ibuteField.ts => SpatialAttributeField.ts} | 4 +- .../{PointFilter.ts => SpatialFilter.ts} | 2 +- .../queryAST/factory/FieldFactory.ts | 7 +- .../queryAST/factory/FilterFactory.ts | 6 +- .../tests/api-v6/schema/types/spatial.test.ts | 46 +- .../api-v6/tck/directives/alias/query.test.ts | 2 - .../tck/filters/array/array-filters.test.ts | 1 - .../filters/filters-on-relationships.test.ts | 3 - .../logical-filters/and-filter.test.ts | 2 - .../logical-filters/not-filter.test.ts | 2 - .../filters/logical-filters/or-filter.test.ts | 3 - .../api-v6/tck/filters/nested/all.test.ts | 3 - .../api-v6/tck/filters/nested/none.test.ts | 3 - .../api-v6/tck/filters/nested/single.test.ts | 3 - .../api-v6/tck/filters/nested/some.test.ts | 3 - .../tck/filters/top-level-filters.test.ts | 1 - .../tck/filters/types/temporals.test.ts | 6 +- .../tests/api-v6/tck/pagination/first.test.ts | 1 - .../tck/projection/relationship.test.ts | 2 - .../tck/projection/simple-query.test.ts | 1 - .../api-v6/tck/projection/types/point.test.ts | 522 ++++++++++++++++++ 28 files changed, 732 insertions(+), 70 deletions(-) rename packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/{PointAttributeField.ts => SpatialAttributeField.ts} (94%) rename packages/graphql/src/translate/queryAST/ast/filters/property-filters/{PointFilter.ts => SpatialFilter.ts} (98%) create mode 100644 packages/graphql/tests/api-v6/tck/projection/types/point.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index 37240ce376..2b77cb612a 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -26,7 +26,9 @@ import { RelationshipAdapter } from "../../schema-model/relationship/model-adapt 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 { getFilterOperator, getRelationshipOperator } from "./FilterOperators"; import type { GraphQLAttributeFilters, @@ -217,16 +219,40 @@ export class FilterFactory { return this.createPropertyFilters(attributeAdapter, filters, "relationship"); }); } - + // TODO: remove adapter from here private createPropertyFilters( attribute: AttributeAdapter, filters: GraphQLAttributeFilters, attachedTo: "node" | "relationship" = "node" - ): PropertyFilter[] { + ): Filter[] { 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 (!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, diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts index 20d046857a..b7e846cf7b 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts @@ -1,7 +1,9 @@ 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); } @@ -13,6 +15,25 @@ export function getFilterOperator(attribute: AttributeAdapter, operator: string) 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 { diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 3afbc28430..2b7fcf36b2 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -28,6 +28,7 @@ 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"; @@ -39,6 +40,7 @@ import type { GraphQLSortArgument, GraphQLTree, GraphQLTreeEdgeProperties, + GraphQLTreeLeafField, GraphQLTreeNode, GraphQLTreeReadOperation, GraphQLTreeSortElement, @@ -177,6 +179,7 @@ export class ReadOperationFactory { Object.values(propertiesTree.fields).map((rawField) => { const attribute = target.findAttribute(rawField.name); if (attribute) { + const field = rawField as GraphQLTreeLeafField; const attributeAdapter = new AttributeAdapter(attribute); if (attributeAdapter.typeHelper.isDateTime()) { return new DateTimeField({ @@ -184,7 +187,17 @@ export class ReadOperationFactory { attribute: attributeAdapter, }); } + if (attributeAdapter.typeHelper.isSpatial()) { + /* const typeName = attributeAdapter.typeHelper.isList() + ? (attributeAdapter.type as ListType).ofType.name + : attributeAdapter.type.name; */ + return new SpatialAttributeField({ + alias: rawField.alias, + attribute: attributeAdapter, + crs: Boolean(field?.fields?.crs), + }); + } return new AttributeField({ alias: rawField.alias, attribute: attributeAdapter, diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 6489e6ab8d..012b9c1058 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -18,6 +18,7 @@ */ import type { ResolveTree } from "graphql-parse-resolve-info"; +import type { Attribute } from "../../../schema-model/attribute/Attribute"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; import { findFieldByName } from "./find-field-by-name"; @@ -31,7 +32,9 @@ import type { GraphQLTreeEdgeProperties, GraphQLTreeLeafField, GraphQLTreeNode, + GraphQLTreePoint, GraphQLTreeReadOperation, + GraphQLTreeScalarField, GraphQLTreeSortElement, } from "./graphql-tree"; @@ -75,8 +78,34 @@ export abstract class ResolveTreeParser protected parseAttributeField( resolveTree: ResolveTree, entity: ConcreteEntity | Relationship - ): GraphQLTreeLeafField | undefined { + ): GraphQLTreeLeafField | GraphQLTreePoint | undefined { if (entity.hasAttribute(resolveTree.name)) { + const attribute = entity.findAttribute(resolveTree.name) as Attribute; + if (attribute.type.name === "Point") { + const longitude = findFieldByName(resolveTree, "Point", "longitude"); + const latitude = findFieldByName(resolveTree, "Point", "latitude"); + const height = findFieldByName(resolveTree, "Point", "height"); + const crs = findFieldByName(resolveTree, "Point", "crs"); + const srid = findFieldByName(resolveTree, "Point", "srid"); + + const pointField: GraphQLTreePoint = { + 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), + }, + }; + + return pointField; + } + if (attribute.type.name === "CartesianPoint") { + throw new ResolveTreeParserError("CartesianPoint is not supported"); + } return { alias: resolveTree.alias, args: resolveTree.args, @@ -258,3 +287,15 @@ export class RelationshipResolveTreeParser extends ResolveTreeParser = { NOT?: LogicalOperation; } & T; -export type StringFilters = { +export type StringFilters = LogicalOperation<{ equals?: string; in?: string[]; matches?: string; contains?: string; startsWith?: string; endsWith?: string; -}; +}>; -export type NumberFilters = { +export type NumberFilters = LogicalOperation<{ equals?: string; in?: string[]; lt?: string; lte?: string; gt?: string; gte?: string; -}; +}>; export type RelationshipFilters = { edges?: { @@ -111,10 +111,27 @@ export interface GraphQLTreeEdgeProperties extends GraphQLTreeElement { fields: Record; } -export interface GraphQLTreeLeafField extends GraphQLTreeElement { +/* export interface GraphQLTreeLeafField extends GraphQLTreeElement { fields: undefined; name: string; } + */ + +export type GraphQLTreeLeafField = GraphQLTreeScalarField | GraphQLTreePoint; +export interface GraphQLTreeScalarField extends GraphQLTreeElement { + fields: undefined; + 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 GraphQLSortArgument { edges: GraphQLSortEdgeArgument[]; diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index 909298b628..015d6ce8a0 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -17,7 +17,16 @@ * limitations under the License. */ -import type { GraphQLList, GraphQLNamedInputType, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema } from "graphql"; +import type { + GraphQLInputObjectType, + GraphQLInputType, + GraphQLList, + GraphQLNamedInputType, + GraphQLNonNull, + GraphQLObjectType, + GraphQLScalarType, + GraphQLSchema, +} from "graphql"; import type { EnumTypeComposer, InputTypeComposer, @@ -89,7 +98,7 @@ export class SchemaBuilder { string, | EnumTypeComposer | string - | GraphQLNamedInputType + | GraphQLInputType | GraphQLList | GraphQLNonNull | WrappedComposer 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 index 7331f0c030..18ad5e404d 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { GraphQLScalarType } from "graphql"; +import type { GraphQLInputType, GraphQLScalarType } from "graphql"; import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; @@ -32,6 +32,9 @@ import { } from "../../../graphql/scalars"; import type { SchemaBuilder } from "../SchemaBuilder"; +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"; @@ -458,21 +461,34 @@ class StaticFilterTypes { }); } + // TODO: Discuss, distance operator. public get cartesianPointWhere(): InputTypeComposer { - return this.schemaBuilder.getOrCreateInputType("CartesianPointWhere", (_itc) => { + return this.schemaBuilder.getOrCreateInputType("CartesianPointWhere", (itc) => { return { fields: { - ...this.createNumericOperators(GraphQLFloat), + ...this.createBooleanOperators(itc), + //...this.createNumericOperators(CartesianPointDistance), // commented out as equals is of different type than the rest + distance: CartesianPointInput, + in: toGraphQLList(toGraphQLNonNull(CartesianPointInput)), }, }; }); } + // TODO: Discuss, distance operator. public get pointWhere(): InputTypeComposer { - return this.schemaBuilder.getOrCreateInputType("PointWhere", (_itc) => { + return this.schemaBuilder.getOrCreateInputType("PointWhere", (itc) => { return { fields: { - ...this.createNumericOperators(GraphQLFloat), + ...this.createBooleanOperators(itc), + //...this.createNumericOperators(PointDistance), // commented out as equals is of different type than the rest + equals: PointInput, + lt: PointDistance, + lte: PointDistance, + gt: PointDistance, + gte: PointDistance, + distance: PointDistance, + in: toGraphQLList(toGraphQLNonNull(PointInput)), }, }; }); @@ -488,7 +504,7 @@ class StaticFilterTypes { }; } - private createNumericOperators(type: GraphQLScalarType): Record { + private createNumericOperators(type: GraphQLInputType): Record { return { equals: type, lt: type, 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/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/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 4a125a756d..0def2c1698 100644 --- a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts @@ -37,8 +37,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"; @@ -188,8 +188,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/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 11ec40ecce..fce26a433e 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -70,12 +70,19 @@ describe("Spatial Types", () => { z: Float } + \\"\\"\\"Input type for a cartesian point\\"\\"\\" + input CartesianPointInput { + x: Float! + y: Float! + z: Float + } + input CartesianPointWhere { - equals: Float - gt: Float - gte: Float - lt: Float - lte: Float + AND: [CartesianPointWhere!] + NOT: CartesianPointWhere + OR: [CartesianPointWhere!] + distance: CartesianPointInput + in: [CartesianPointInput!] } type NodeType { @@ -188,12 +195,31 @@ describe("Spatial Types", () => { srid: Int! } + \\"\\"\\"Input type for a point with a distance\\"\\"\\" + input PointDistance { + \\"\\"\\"The distance in metres to be used when comparing two points\\"\\"\\" + distance: Float! + point: PointInput! + } + + \\"\\"\\"Input type for a point\\"\\"\\" + input PointInput { + height: Float + latitude: Float! + longitude: Float! + } + input PointWhere { - equals: Float - gt: Float - gte: Float - lt: Float - lte: Float + AND: [PointWhere!] + NOT: PointWhere + OR: [PointWhere!] + distance: PointDistance + equals: PointInput + gt: PointDistance + gte: PointDistance + in: [PointInput!] + lt: PointDistance + lte: PointDistance } type Query { diff --git a/packages/graphql/tests/api-v6/tck/directives/alias/query.test.ts b/packages/graphql/tests/api-v6/tck/directives/alias/query.test.ts index 34919e1e8a..f72193ba7b 100644 --- a/packages/graphql/tests/api-v6/tck/directives/alias/query.test.ts +++ b/packages/graphql/tests/api-v6/tck/directives/alias/query.test.ts @@ -67,7 +67,6 @@ describe("Alias directive", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WITH collect({ node: this0 }) AS edges @@ -116,7 +115,6 @@ describe("Alias directive", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WITH collect({ node: this0 }) AS edges 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 index a684aa3853..11d2648c16 100644 --- 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 @@ -54,7 +54,6 @@ describe("Array filters", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE this0.alternativeTitles = $param0 diff --git a/packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts b/packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts index b7edd6b49a..f8140e80ec 100644 --- a/packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts @@ -70,7 +70,6 @@ describe("Relationship", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WITH collect({ node: this0 }) AS edges @@ -131,7 +130,6 @@ describe("Relationship", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WITH collect({ node: this0 }) AS edges @@ -201,7 +199,6 @@ describe("Relationship", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WITH collect({ node: this0 }) AS edges 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 index f100d3845b..6ed4dff1df 100644 --- 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 @@ -62,7 +62,6 @@ describe("AND filters", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE (this0.title = $param0 AND this0.year = $param1) @@ -111,7 +110,6 @@ describe("AND filters", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE (this0.title = $param0 AND this0.year = $param1) 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 index 4767743776..dc777d63da 100644 --- 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 @@ -55,7 +55,6 @@ describe("NOT filters", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE NOT (this0.title = $param0) @@ -96,7 +95,6 @@ describe("NOT filters", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE NOT (this0.title = $param0 AND this0.year = $param1) 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 index def04a44b1..97248fe9b4 100644 --- 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 @@ -62,7 +62,6 @@ describe("OR filters", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE (this0.title = $param0 OR this0.year = $param1) @@ -111,7 +110,6 @@ describe("OR filters", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE (this0.title = $param0 OR this0.year = $param1) @@ -156,7 +154,6 @@ describe("OR filters", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE (this0.title = $param0 OR this0.year = $param1) 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 index 55a1cf6046..6e8a8b4dc4 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts @@ -63,7 +63,6 @@ describe("Nested Filters with all", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE (EXISTS { @@ -112,7 +111,6 @@ describe("Nested Filters with all", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE (EXISTS { @@ -177,7 +175,6 @@ describe("Nested Filters with all", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE (EXISTS { 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 index 748107a12f..c458d940a8 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts @@ -63,7 +63,6 @@ describe("Nested Filters with none", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE NOT (EXISTS { @@ -109,7 +108,6 @@ describe("Nested Filters with none", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE NOT (EXISTS { @@ -171,7 +169,6 @@ describe("Nested Filters with none", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE EXISTS { 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 index d4ec37e8b4..da2f464c1a 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts @@ -65,7 +65,6 @@ describe("Nested Filters with single", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor 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) @@ -108,7 +107,6 @@ describe("Nested Filters with single", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor 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) @@ -167,7 +165,6 @@ describe("Nested Filters with single", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor 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) 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 index b877d46304..7051742b54 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts @@ -63,7 +63,6 @@ describe("Nested Filters with some", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE EXISTS { @@ -109,7 +108,6 @@ describe("Nested Filters with some", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE EXISTS { @@ -171,7 +169,6 @@ describe("Nested Filters with some", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE EXISTS { 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 index 3d784bc086..9bdf789029 100644 --- 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 @@ -61,7 +61,6 @@ describe("Top level filters", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WHERE (this0.title = $param0 AND this0.year = $param1 AND this0.runtime = $param2) diff --git a/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts index 66d5c8244c..88c72705ac 100644 --- a/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts @@ -92,7 +92,7 @@ describe("Temporal types", () => { 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) + 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 { @@ -206,7 +206,7 @@ describe("Temporal types", () => { CALL { WITH this0 MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) - WHERE (relatedNode.dateTime = $param0 AND relatedNode.localDateTime > $param1 AND relatedNode.duration >= $param2 AND relatedNode.time < $param3 AND relatedNode.localTime <= $param4) + WHERE (relatedNode.dateTime = $param0 AND relatedNode.localDateTime > $param1 AND (datetime() + relatedNode.duration) >= (datetime() + $param2) AND relatedNode.time < $param3 AND relatedNode.localTime <= $param4) WITH collect({ node: relatedNode, relationship: this1 }) AS edges WITH edges, size(edges) AS totalCount CALL { @@ -324,7 +324,7 @@ describe("Temporal types", () => { CALL { WITH this0 MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) - WHERE (this1.dateTime = $param0 AND this1.localDateTime > $param1 AND this1.duration >= $param2 AND this1.time < $param3 AND this1.localTime <= $param4) + 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: relatedNode, relationship: this1 }) AS edges WITH edges, size(edges) AS totalCount CALL { diff --git a/packages/graphql/tests/api-v6/tck/pagination/first.test.ts b/packages/graphql/tests/api-v6/tck/pagination/first.test.ts index f3049c9c75..5cbe2e6ee6 100644 --- a/packages/graphql/tests/api-v6/tck/pagination/first.test.ts +++ b/packages/graphql/tests/api-v6/tck/pagination/first.test.ts @@ -64,7 +64,6 @@ describe("Pagination - First argument", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WITH collect({ node: this0 }) AS edges diff --git a/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts b/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts index 2a1288f69a..87a12b6894 100644 --- a/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts @@ -70,7 +70,6 @@ describe("Relationship", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WITH collect({ node: this0 }) AS edges @@ -126,7 +125,6 @@ describe("Relationship", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WITH collect({ node: this0 }) AS edges 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 index eae20da21b..a742612f2a 100644 --- a/packages/graphql/tests/api-v6/tck/projection/simple-query.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/simple-query.test.ts @@ -53,7 +53,6 @@ describe("Simple Query", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); - // NOTE: Order of these subqueries have been reversed after refactor expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) WITH collect({ node: this0 }) AS edges 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..b0f14f2829 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/projection/types/point.test.ts @@ -0,0 +1,522 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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"; + +// packages/graphql/tests/tck/types/point.test.ts +describe("Cypher Points", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + type PointContainer @node { + id: String + point: Point + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("Simple Point EQUALS query", async () => { + const query = /* GraphQL */ ` + { + pointContainers(where: { edges: { node: { point: { equals: { longitude: 1.0, latitude: 2.0 } } } } }) { + connection { + edges { + node { + point { + longitude + latitude + crs + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:PointContainer) + WHERE this0.point = 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: { point: CASE + WHEN this0.point IS NOT NULL THEN { point: this0.point, crs: this0.point.crs } + ELSE NULL + END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { + const query = /* GraphQL */ ` + { + pointContainers( + where: { edges: { node: { point: { NOT: { equals: { longitude: 1.0, latitude: 2.0 } } } } } } + ) { + connection { + edges { + node { + point { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:PointContainer) + WHERE NOT (this0.point = 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: { point: CASE + WHEN this0.point IS NOT NULL THEN { point: this0.point } + ELSE NULL + END, __resolveType: \\"PointContainer\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"longitude\\": 1, + \\"latitude\\": 2 + } + }" + `); + }); + + test("Simple Point IN query", async () => { + const query = /* GraphQL */ ` + { + pointContainers(where: { edges: { node: { point: { in: [{ longitude: 1.0, latitude: 2.0 }] } } } }) { + connection { + edges { + node { + point { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:PointContainer) + WHERE this0.point 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: { point: CASE + WHEN this0.point IS NOT NULL THEN { point: this0.point } + ELSE NULL + END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { + const query = /* GraphQL */ ` + { + pointContainers( + where: { edges: { node: { point: { NOT: { in: [{ longitude: 1.0, latitude: 2.0 }] } } } } } + ) { + connection { + edges { + node { + point { + longitude + latitude + crs + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:PointContainer) + WHERE NOT (this0.point 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: { point: CASE + WHEN this0.point IS NOT NULL THEN { point: this0.point, crs: this0.point.crs } + ELSE NULL + END, __resolveType: \\"PointContainer\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"longitude\\": 1, + \\"latitude\\": 2 + } + ] + }" + `); + }); + + describe("tests using distance or point.distance", () => { + test("Simple Point LT query", async () => { + const query = /* GraphQL */ ` + { + pointContainers( + where: { + edges: { + node: { point: { lt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } + } + } + ) { + connection { + edges { + node { + point { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:PointContainer) + WHERE point.distance(this0.point, 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: { point: CASE + WHEN this0.point IS NOT NULL THEN { point: this0.point } + ELSE NULL + END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { + const query = /* GraphQL */ ` + { + pointContainers( + where: { + edges: { + node: { point: { lte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } + } + } + ) { + connection { + edges { + node { + point { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:PointContainer) + WHERE point.distance(this0.point, 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: { point: CASE + WHEN this0.point IS NOT NULL THEN { point: this0.point } + ELSE NULL + END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { + const query = /* GraphQL */ ` + { + pointContainers( + where: { + edges: { + node: { point: { gt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } + } + } + ) { + connection { + edges { + node { + point { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:PointContainer) + WHERE point.distance(this0.point, 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: { point: CASE + WHEN this0.point IS NOT NULL THEN { point: this0.point } + ELSE NULL + END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { + const query = /* GraphQL */ ` + { + pointContainers( + where: { + edges: { + node: { point: { gte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } + } + } + ) { + connection { + edges { + node { + point { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:PointContainer) + WHERE point.distance(this0.point, 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: { point: CASE + WHEN this0.point IS NOT NULL THEN { point: this0.point } + ELSE NULL + END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { + const query = /* GraphQL */ ` + { + pointContainers( + where: { + edges: { + node: { + point: { distance: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 }} + } + } + } + ) { + connection { + edges { + node { + point { + longitude + latitude + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { v6Api: true }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:PointContainer) + WHERE point.distance(this0.point, 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: { point: CASE + WHEN this0.point IS NOT NULL THEN { point: this0.point } + ELSE NULL + END, __resolveType: \\"PointContainer\\" } }) 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 + } + }" + `); + }); + }); +}); From 3360877b57026e17e13a172da527b506061f6f6d Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 12 Jun 2024 14:38:53 +0100 Subject: [PATCH 053/177] complete spatial types with all tests --- .../queryIRFactory/ReadOperationFactory.ts | 4 - .../resolve-tree-parser/ResolveTreeParser.ts | 40 +- .../resolve-tree-parser/graphql-tree.ts | 19 +- .../schema-types/StaticSchemaTypes.ts | 75 ++- .../filter-schema-types/FilterSchemaTypes.ts | 10 +- .../cartesian-point-2d-equals.int.test.ts | 107 ++++ .../cartesian-point-3d-equals.int.test.ts | 106 ++++ .../cartesian-point-2d-equals.int.test.ts | 156 ++++++ .../cartesian-point-2d-gt.int.test.ts | 148 +++++ .../cartesian-point-2d-in.int.test.ts | 147 +++++ .../cartesian-point-2d-lt.int.test.ts | 151 ++++++ .../cartesian-point-3d-equals.int.test.ts | 157 ++++++ .../cartesian-point-3d-gt.int.test.ts | 146 +++++ .../cartesian-point-3d-lt.int.test.ts | 147 +++++ .../point/array/point-2d-equals.int.test.ts | 108 ++++ .../point/array/point-3d-equals.int.test.ts | 106 ++++ .../types/point/point-2d-equals.int.test.ts | 156 ++++++ .../types/point/point-2d-gt.int.test.ts | 151 ++++++ .../types/point/point-2d-in.int.test.ts | 147 +++++ .../types/point/point-2d-lt.int.test.ts | 151 ++++++ .../types/point/point-3d-equals.int.test.ts | 156 ++++++ .../types/point/point-3d-gt.int.test.ts | 146 +++++ .../types/point/point-3d-lt.int.test.ts | 146 +++++ .../cartesian-point-2d.int.test.ts | 112 ++++ .../cartesian-point-3d.int.test.ts | 113 ++++ .../types/point/point-2d.int.test.ts | 112 ++++ .../types/point/point-3d.int.test.ts | 112 ++++ .../tests/api-v6/schema/types/spatial.test.ts | 13 +- .../filters/types/cartesian-filters.test.ts | 489 +++++++++++++++++ .../tck/filters/types/point-filters.test.ts | 507 ++++++++++++++++++ ...rals.test.ts => temporals-filters.test.ts} | 0 .../tck/projection/types/cartesian.test.ts | 161 ++++++ .../api-v6/tck/projection/types/point.test.ts | 427 ++------------- .../types/point-cartesian.int.test.ts | 1 - .../tests/integration/types/point.int.test.ts | 261 --------- .../graphql/tests/tck/types/point.test.ts | 325 +---------- 36 files changed, 4284 insertions(+), 1029 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/array/cartesian-point-2d-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/array/cartesian-point-3d-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-gt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-in.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-lt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-gt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-lt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/point/array/point-2d-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/point/array/point-3d-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-gt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-in.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-lt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-gt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-lt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/projection/types/cartesian-point/cartesian-point-2d.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/projection/types/cartesian-point/cartesian-point-3d.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/projection/types/point/point-2d.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/projection/types/point/point-3d.int.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/cartesian-filters.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/point-filters.test.ts rename packages/graphql/tests/api-v6/tck/filters/types/{temporals.test.ts => temporals-filters.test.ts} (100%) create mode 100644 packages/graphql/tests/api-v6/tck/projection/types/cartesian.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 2b7fcf36b2..23413c1659 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -188,10 +188,6 @@ export class ReadOperationFactory { }); } if (attributeAdapter.typeHelper.isSpatial()) { - /* const typeName = attributeAdapter.typeHelper.isList() - ? (attributeAdapter.type as ListType).ofType.name - : attributeAdapter.type.name; */ - return new SpatialAttributeField({ alias: rawField.alias, attribute: attributeAdapter, diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 012b9c1058..31d1dc0cd5 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -18,6 +18,8 @@ */ import type { ResolveTree } from "graphql-parse-resolve-info"; +import { CartesianPoint } from "../../../graphql/objects/CartesianPoint"; +import { Point } from "../../../graphql/objects/Point"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; @@ -27,6 +29,7 @@ import type { GraphQLReadOperationArgs, GraphQLSortArgument, GraphQLSortEdgeArgument, + GraphQLTreeCartesianPoint, GraphQLTreeConnection, GraphQLTreeEdge, GraphQLTreeEdgeProperties, @@ -37,6 +40,7 @@ import type { GraphQLTreeScalarField, GraphQLTreeSortElement, } from "./graphql-tree"; +import { ListType } from "../../../schema-model/attribute/AttributeType"; export abstract class ResolveTreeParser { protected entity: T; @@ -81,12 +85,13 @@ export abstract class ResolveTreeParser ): GraphQLTreeLeafField | GraphQLTreePoint | undefined { if (entity.hasAttribute(resolveTree.name)) { const attribute = entity.findAttribute(resolveTree.name) as Attribute; - if (attribute.type.name === "Point") { - const longitude = findFieldByName(resolveTree, "Point", "longitude"); - const latitude = findFieldByName(resolveTree, "Point", "latitude"); - const height = findFieldByName(resolveTree, "Point", "height"); - const crs = findFieldByName(resolveTree, "Point", "crs"); - const srid = findFieldByName(resolveTree, "Point", "srid"); + const wrappedTypeName = attribute.type instanceof ListType ? attribute.type.ofType.name : attribute.type.name; + if (wrappedTypeName === "Point") { + 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"); const pointField: GraphQLTreePoint = { alias: resolveTree.alias, @@ -103,8 +108,27 @@ export abstract class ResolveTreeParser return pointField; } - if (attribute.type.name === "CartesianPoint") { - throw new ResolveTreeParserError("CartesianPoint is not supported"); + if (wrappedTypeName === "CartesianPoint") { + 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"); + + const cartesianPointField: GraphQLTreeCartesianPoint = { + 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), + }, + }; + + return cartesianPointField; } return { alias: resolveTree.alias, diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index c33a8793e5..982fa23f06 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -111,13 +111,7 @@ export interface GraphQLTreeEdgeProperties extends GraphQLTreeElement { fields: Record; } -/* export interface GraphQLTreeLeafField extends GraphQLTreeElement { - fields: undefined; - name: string; -} - */ - -export type GraphQLTreeLeafField = GraphQLTreeScalarField | GraphQLTreePoint; +export type GraphQLTreeLeafField = GraphQLTreeScalarField | GraphQLTreePoint | GraphQLTreeCartesianPoint; export interface GraphQLTreeScalarField extends GraphQLTreeElement { fields: undefined; name: string; @@ -133,6 +127,17 @@ export interface GraphQLTreePoint extends GraphQLTreeElement { 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; +} + export interface GraphQLSortArgument { edges: GraphQLSortEdgeArgument[]; } 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 index 18ad5e404d..36fa778561 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -32,6 +32,7 @@ import { } from "../../../graphql/scalars"; import type { SchemaBuilder } from "../SchemaBuilder"; +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"; @@ -302,23 +303,8 @@ class StaticFilterTypes { } public getBooleanListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("StringListWhereNullable", () => { - return { - fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLBoolean)), - }, - }; - }); - } - - return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { - return { - fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLBoolean)), - }, - }; - }); + // TODO: verify correctness of this + return this.getStringListWhere(nullable); } public get booleanWhere(): InputTypeComposer { @@ -461,34 +447,77 @@ class StaticFilterTypes { }); } - // TODO: Discuss, distance operator. + public getCartesianListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhereNullable", () => { + return { + fields: { + equals: toGraphQLList(CartesianPointInput), + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhere", () => { + return { + fields: { + equals: toGraphQLList(toGraphQLNonNull(CartesianPointInput)), + }, + }; + }); + } + + public getPointListWhere(nullable: boolean): InputTypeComposer { + if (nullable) { + return this.schemaBuilder.getOrCreateInputType("PointListPointWhereNullable", () => { + return { + fields: { + equals: toGraphQLList(PointInput), + }, + }; + }); + } + + return this.schemaBuilder.getOrCreateInputType("PointListPointWhere", () => { + return { + fields: { + equals: toGraphQLList(toGraphQLNonNull(PointInput)), + }, + }; + }); + } + + // TODO: Discuss distance operator and SpatialOperators in general as the API it may be improved. public get cartesianPointWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("CartesianPointWhere", (itc) => { return { fields: { ...this.createBooleanOperators(itc), - //...this.createNumericOperators(CartesianPointDistance), // commented out as equals is of different type than the rest - distance: CartesianPointInput, + equals: CartesianPointInput, in: toGraphQLList(toGraphQLNonNull(CartesianPointInput)), + lt: CartesianPointDistance, + lte: CartesianPointDistance, + gt: CartesianPointDistance, + gte: CartesianPointDistance, + distance: CartesianPointDistance, }, }; }); } - // TODO: Discuss, distance operator. + // TODO: Discuss distance operator and SpatialOperators in general as the API it may be improved. public get pointWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("PointWhere", (itc) => { return { fields: { ...this.createBooleanOperators(itc), - //...this.createNumericOperators(PointDistance), // commented out as equals is of different type than the rest equals: PointInput, + in: toGraphQLList(toGraphQLNonNull(PointInput)), lt: PointDistance, lte: PointDistance, gt: PointDistance, gte: PointDistance, distance: PointDistance, - in: toGraphQLList(toGraphQLNonNull(PointInput)), }, }; }); 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 index 157a889200..0924cb90f4 100644 --- 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 @@ -199,18 +199,16 @@ export abstract class FilterSchemaTypes { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 }; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + + 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..6e7ee3912e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/array/cartesian-point-3d-equals.int.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 array EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -0.127758, y: 51.507351, z: 0 }; + const Rome = { x: 12.496365, y: 41.902782, z: 0 }; + const Paris = { x: 2.352222, y: 48.856613, 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), 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..1599cfc136 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-equals.int.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 type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("CartesianPoint 2d EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 }; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + + 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..9f24515a12 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-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"; + +describe("CartesianPoint 2d GT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 }; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + + 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 equalsResult = await testHelper.executeGraphQL(query); + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..a5dbfe6bef --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-in.int.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 IN", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 }; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + + 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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: {}, + }); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: {}, + }); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..892d38c0f6 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-2d-lt.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("CartesianPoint 2d LT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { x: -14221.955504767046, y: 6711533.711877272 }; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; + const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + + 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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: { x: Paris.x, y: Paris.y, distance }, + }); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: { x: Paris.x, y: Paris.y, distance }, + }); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..871fc76c89 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-equals.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 type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("CartesianPoint 3d EQ", () => { + 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 }; + const Paris = { x: 261848.15527273554, y: 6250566.54904563, 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 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..1a7920f50e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-gt.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 type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("CartesianPoint 2d GT", () => { + 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 }; + const Paris = { x: 261848.15527273554, y: 6250566.54904563, 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 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..c41df6388c --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/cartesian-point/cartesian-point-3d-lt.int.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 LT", () => { + 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 }; + const Paris = { x: 261848.15527273554, y: 6250566.54904563, 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 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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/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..d16d5e01d5 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/array/point-2d-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"; + +describe("Point 2d array EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 }; + const Rome = { longitude: 12.496365, latitude: 41.902782 }; + const Paris = { longitude: 2.352222, latitude: 48.856613 }; + + 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..e1e07c015a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/array/point-3d-equals.int.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 array EQ", () => { + 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 }; + const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 }; + + 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}, height: ${London.height} }, { longitude: ${Paris.longitude}, latitude: ${Paris.latitude}, height: ${Paris.height} }] } } } }) { + 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: 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..7bcb0a781c --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-equals.int.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 type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Point 2d EQ", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 }; + const Rome = { longitude: 12.496365, latitude: 41.902782 }; + const Paris = { longitude: 2.352222, latitude: 48.856613 }; + + 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..03d162c86a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-gt.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("Point 2d GT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 }; + const Rome = { longitude: 12.496365, latitude: 41.902782 }; + const Paris = { longitude: 2.352222, latitude: 48.856613 }; + + 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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, + }); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, + }); + + 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", + }, + }, + }, + ], + }, + }, + }); + }); +}); 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..ec941fa9bf --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-in.int.test.ts @@ -0,0 +1,147 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 IN", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 }; + const Rome = { longitude: 12.496365, latitude: 41.902782 }; + const Paris = { longitude: 2.352222, latitude: 48.856613 }; + + 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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: {}, + }); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: {}, + }); + + 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", + }, + }, + }, + ], + }, + }, + }); + }); +}); 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..b04e854544 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-2d-lt.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("Point 2d LT", () => { + const testHelper = new TestHelper({ v6Api: true }); + + let Location: UniqueType; + const London = { longitude: -0.127758, latitude: 51.507351 }; + const Rome = { longitude: 12.496365, latitude: 41.902782 }; + const Paris = { longitude: 2.352222, latitude: 48.856613 }; + + 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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, + }); + + 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 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 equalsResult = await testHelper.executeGraphQL(query, { + variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, + }); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..e29750466e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-equals.int.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 type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Point 3d EQ", () => { + 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 }; + const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 }; + + 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 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: 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 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: 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..3343a774cc --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-gt.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 type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Point 2d GT", () => { + 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 }; + const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 }; + 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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 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: 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..408c071f5e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/point/point-3d-lt.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 type { UniqueType } from "../../../../../utils/graphql-types"; +import { TestHelper } from "../../../../../utils/tests-helper"; + +describe("Point 2d LT", () => { + 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 }; + const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 }; + 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 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: 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 equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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/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..6ea0a622e0 --- /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 }; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; + + 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(); + }); + // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-2d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + y + x + z + crs + # srid + } + } + } + } + + } + } + `; + + const equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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..761203a565 --- /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(); + }); + // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-3d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + y + x + z + crs + # srid + } + } + } + } + + } + } + `; + + const equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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/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..3298b206b2 --- /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 }; + const Rome = { longitude: 12.496365, latitude: 41.902782 }; + + 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(); + }); + // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-2d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + # srid + } + } + } + } + + } + } + `; + + 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", + // 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..1ce749c425 --- /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(); + }); + // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-3d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + # srid + } + } + } + } + + } + } + `; + + 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: London.height, + crs: "wgs-84-3d", + // srid: 4326, + }, + }, + }, + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: Rome.height, + crs: "wgs-84-3d", + //srid: 4326, + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index fce26a433e..7f3ab7663d 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -70,6 +70,12 @@ describe("Spatial Types", () => { z: Float } + \\"\\"\\"Input type for a cartesian point with a distance\\"\\"\\" + input CartesianPointDistance { + distance: Float! + point: CartesianPointInput! + } + \\"\\"\\"Input type for a cartesian point\\"\\"\\" input CartesianPointInput { x: Float! @@ -81,8 +87,13 @@ describe("Spatial Types", () => { AND: [CartesianPointWhere!] NOT: CartesianPointWhere OR: [CartesianPointWhere!] - distance: CartesianPointInput + distance: CartesianPointDistance + equals: CartesianPointInput + gt: CartesianPointDistance + gte: CartesianPointDistance in: [CartesianPointInput!] + lt: CartesianPointDistance + lte: CartesianPointDistance } type NodeType { 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..fe5d8fb63c --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/cartesian-filters.test.ts @@ -0,0 +1,489 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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("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: { edges: { 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: { edges: { 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: { edges: { 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: { edges: { 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: { edges: { 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: { edges: { 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: { edges: { 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: { edges: { 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: { edges: { 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/point-filters.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/point-filters.test.ts new file mode 100644 index 0000000000..8e6b090c78 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/filters/types/point-filters.test.ts @@ -0,0 +1,507 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 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: { edges: { 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: { edges: { 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: { edges: { 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: { edges: { 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: { + edges: { 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: { + edges: { 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: { + edges: { 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: { + edges: { 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: { + edges: { + 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/temporals.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/temporals-filters.test.ts similarity index 100% rename from packages/graphql/tests/api-v6/tck/filters/types/temporals.test.ts rename to packages/graphql/tests/api-v6/tck/filters/types/temporals-filters.test.ts 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 index b0f14f2829..c534378c69 100644 --- a/packages/graphql/tests/api-v6/tck/projection/types/point.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/types/point.test.ts @@ -20,16 +20,15 @@ import { Neo4jGraphQL } from "../../../../../src"; import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; -// packages/graphql/tests/tck/types/point.test.ts -describe("Cypher Points", () => { +describe("Point projection", () => { let typeDefs: string; let neoSchema: Neo4jGraphQL; beforeAll(() => { typeDefs = /* GraphQL */ ` - type PointContainer @node { + type Location @node { id: String - point: Point + value: Point } `; @@ -38,17 +37,17 @@ describe("Cypher Points", () => { }); }); - test("Simple Point EQUALS query", async () => { + test("point coordinates", async () => { const query = /* GraphQL */ ` { - pointContainers(where: { edges: { node: { point: { equals: { longitude: 1.0, latitude: 2.0 } } } } }) { + locations { connection { edges { node { - point { + value { longitude latitude - crs + height } } } @@ -60,44 +59,36 @@ describe("Cypher Points", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:PointContainer) - WHERE this0.point = point($param0) + "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: { point: CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point, crs: this0.point.crs } + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } ELSE NULL - END, __resolveType: \\"PointContainer\\" } }) AS var1 + END, __resolveType: \\"Location\\" } }) AS var1 } RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"longitude\\": 1, - \\"latitude\\": 2 - } - }" - `); + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); }); - test("Simple Point NOT EQUALS query", async () => { + test("point crs", async () => { const query = /* GraphQL */ ` { - pointContainers( - where: { edges: { node: { point: { NOT: { equals: { longitude: 1.0, latitude: 2.0 } } } } } } - ) { + locations { connection { edges { node { - point { + value { longitude latitude + height + crs } } } @@ -109,42 +100,36 @@ describe("Cypher Points", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:PointContainer) - WHERE NOT (this0.point = point($param0)) + "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: { point: CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point } + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value, crs: this0.value.crs } ELSE NULL - END, __resolveType: \\"PointContainer\\" } }) AS var1 + END, __resolveType: \\"Location\\" } }) AS var1 } RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" `); - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"longitude\\": 1, - \\"latitude\\": 2 - } - }" - `); + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); }); - test("Simple Point IN query", async () => { + test("point srid", async () => { const query = /* GraphQL */ ` { - pointContainers(where: { edges: { node: { point: { in: [{ longitude: 1.0, latitude: 2.0 }] } } } }) { + locations { connection { edges { node { - point { + value { longitude latitude + height + srid } } } @@ -156,367 +141,21 @@ describe("Cypher Points", () => { const result = await translateQuery(neoSchema, query, { v6Api: true }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:PointContainer) - WHERE this0.point IN [var1 IN $param0 | point(var1)] + "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: { point: CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point } + RETURN collect({ node: { value: CASE + WHEN this0.value IS NOT NULL THEN { point: this0.value } ELSE NULL - END, __resolveType: \\"PointContainer\\" } }) AS var2 + END, __resolveType: \\"Location\\" } }) AS var1 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": [ - { - \\"longitude\\": 1, - \\"latitude\\": 2 - } - ] - }" - `); - }); - - test("Simple Point NOT IN query", async () => { - const query = /* GraphQL */ ` - { - pointContainers( - where: { edges: { node: { point: { NOT: { in: [{ longitude: 1.0, latitude: 2.0 }] } } } } } - ) { - connection { - edges { - node { - point { - longitude - latitude - crs - } - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query, { v6Api: true }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:PointContainer) - WHERE NOT (this0.point 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: { point: CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point, crs: this0.point.crs } - ELSE NULL - END, __resolveType: \\"PointContainer\\" } }) AS var2 - } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": [ - { - \\"longitude\\": 1, - \\"latitude\\": 2 - } - ] - }" + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" `); - }); - - describe("tests using distance or point.distance", () => { - test("Simple Point LT query", async () => { - const query = /* GraphQL */ ` - { - pointContainers( - where: { - edges: { - node: { point: { lt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } - } - } - ) { - connection { - edges { - node { - point { - longitude - latitude - } - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query, { v6Api: true }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:PointContainer) - WHERE point.distance(this0.point, 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: { point: CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point } - ELSE NULL - END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { - const query = /* GraphQL */ ` - { - pointContainers( - where: { - edges: { - node: { point: { lte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } - } - } - ) { - connection { - edges { - node { - point { - longitude - latitude - } - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query, { v6Api: true }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:PointContainer) - WHERE point.distance(this0.point, 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: { point: CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point } - ELSE NULL - END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { - const query = /* GraphQL */ ` - { - pointContainers( - where: { - edges: { - node: { point: { gt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } - } - } - ) { - connection { - edges { - node { - point { - longitude - latitude - } - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query, { v6Api: true }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:PointContainer) - WHERE point.distance(this0.point, 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: { point: CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point } - ELSE NULL - END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { - const query = /* GraphQL */ ` - { - pointContainers( - where: { - edges: { - node: { point: { gte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } - } - } - ) { - connection { - edges { - node { - point { - longitude - latitude - } - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query, { v6Api: true }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:PointContainer) - WHERE point.distance(this0.point, 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: { point: CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point } - ELSE NULL - END, __resolveType: \\"PointContainer\\" } }) 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 query", async () => { - const query = /* GraphQL */ ` - { - pointContainers( - where: { - edges: { - node: { - point: { distance: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 }} - } - } - } - ) { - connection { - edges { - node { - point { - longitude - latitude - } - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query, { v6Api: true }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this0:PointContainer) - WHERE point.distance(this0.point, 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: { point: CASE - WHEN this0.point IS NOT NULL THEN { point: this0.point } - ELSE NULL - END, __resolveType: \\"PointContainer\\" } }) 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 - } - }" - `); - }); + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); }); }); diff --git a/packages/graphql/tests/integration/types/point-cartesian.int.test.ts b/packages/graphql/tests/integration/types/point-cartesian.int.test.ts index bf7abf29a0..6cd42d80fc 100644 --- a/packages/graphql/tests/integration/types/point-cartesian.int.test.ts +++ b/packages/graphql/tests/integration/types/point-cartesian.int.test.ts @@ -55,7 +55,6 @@ describe("CartesianPoint", () => { x y z - crs } } } 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/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 { From df805e2cc83d40ba33abf403b9e83caa702dbead Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 13 Jun 2024 09:18:58 +0100 Subject: [PATCH 054/177] move temporal tests --- .../types/date/array/date-equals.int.test.ts | 101 +++++++++++ .../types/date/date-equals.int.test.ts | 92 ++++++++++ .../filters/types/date/date-in.int.test.ts | 157 ++++++++++++++++++ .../filters/types/date/date-lt.int.test.ts | 92 ++++++++++ .../array/duration-equals.int.test.ts | 118 +++++++++++++ .../duration/duration-equals.int.test.ts | 100 +++++++++++ .../types/duration/duration-lt.int.test.ts | 102 ++++++++++++ .../array/localdatetime-equals.int.test.ts | 102 ++++++++++++ .../localdatetime-equals.int.test.ts | 95 +++++++++++ .../types/time/array/time-equals.int.test.ts | 101 +++++++++++ .../types/time/time-equals.int.test.ts | 92 ++++++++++ .../tests/integration/types/date.int.test.ts | 41 +---- .../integration/types/duration.int.test.ts | 47 +----- .../types/localdatetime.int.test.ts | 35 ---- .../tests/integration/types/time.int.test.ts | 43 ----- 15 files changed, 1154 insertions(+), 164 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/date/array/date-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/date/date-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/date/date-in.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/date/date-lt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/duration/array/duration-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/duration/duration-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/duration/duration-lt.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/localdatetime/array/localdatetime-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/localdatetime/localdatetime-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/time/array/time-equals.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/filters/types/time/time-equals.int.test.ts 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..95582d27c9 --- /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: { edges: { 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..4b43644143 --- /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 datetime1 = neo4jDriver.types.Date.fromStandardDate(date1); + const datetime2 = 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, datetime2 } + ); + + await testHelper.initNeo4jGraphQL({ typeDefs }); + + const query = /* GraphQL */ ` + query { + ${Movie.plural}(where: { edges: { node: { date: { equals: "${datetime1.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: datetime1.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..25722e6c8c --- /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: { edges: { 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: { edges: { 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..155b19d6ff --- /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: { edges: { 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/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..826ada9d30 --- /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: { edges: { 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..d67e09a0bb --- /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: { edges: { 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..f945458862 --- /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: { edges: { 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..4d43c6b85d --- /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: { edges: { 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..5aee69a68c --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/types/localdatetime/localdatetime-equals.int.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { edges: { 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/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..a686537697 --- /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: { edges: { 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..485f1a45a4 --- /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: { edges: { 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/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/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) => { From 6018ac9f7befec1a7932d7b4ad7f4895042e7838 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 13 Jun 2024 11:24:50 +0100 Subject: [PATCH 055/177] unify pagination implementation, improve reliability pagination-after test --- .../queryIRFactory/ReadOperationFactory.ts | 27 ++++++++++-------- .../pagination/first-after.int.test.ts | 28 +++++++++++++++---- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index d07a629c91..26232a1a16 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -37,6 +37,7 @@ import { filterTruthy } from "../../utils/utils"; import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { FilterFactory } from "./FilterFactory"; import type { + GraphQLConnectionArgs, GraphQLSortArgument, GraphQLTree, GraphQLTreeEdgeProperties, @@ -88,11 +89,7 @@ export class ReadOperationFactory { const nodeResolveTree = connectionTree.fields.edges?.fields.node; const sortArgument = connectionTree.args.sort; - const firstArgument = connectionTree.args.first; - const afterArgument = connectionTree.args.after ? cursorToOffset(connectionTree.args.after) : undefined; - - const hasPagination = firstArgument || afterArgument; - const pagination = hasPagination ? new Pagination({ limit: firstArgument, skip: afterArgument }) : undefined; + const pagination = this.getPagination(connectionTree.args); const nodeFields = this.getNodeFields(entity, nodeResolveTree); const sortInputFields = this.getSortInputFields({ @@ -139,17 +136,14 @@ export class ReadOperationFactory { const nodeResolveTree = connectionTree.fields.edges?.fields.node; const propertiesResolveTree = connectionTree.fields.edges?.fields.properties; const relTarget = relationshipAdapter.target.entity; - const nodeFields = this.getNodeFields(relTarget, nodeResolveTree); + const edgeFields = this.getAttributeFields(relationship, propertiesResolveTree); const sortArgument = connectionTree.args.sort; - const sortInputFields = this.getSortInputFields({ entity: relTarget, relationship, sortArgument }); - - const firstArgument = connectionTree.args.first; - const afterArgument = connectionTree.args.after ? cursorToOffset(connectionTree.args.after) : undefined; + const pagination = this.getPagination(connectionTree.args); - const hasPagination = firstArgument || afterArgument; - const pagination = hasPagination ? new Pagination({ limit: firstArgument, skip: afterArgument }) : undefined; + const nodeFields = this.getNodeFields(relTarget, nodeResolveTree); + const sortInputFields = this.getSortInputFields({ entity: relTarget, relationship, sortArgument }); return new V6ReadOperation({ target: relationshipAdapter.target, @@ -168,6 +162,15 @@ export class ReadOperationFactory { }); } + private getPagination(connectionTreeArgs: GraphQLConnectionArgs): Pagination | undefined { + const firstArgument = connectionTreeArgs.first; + const afterArgument = connectionTreeArgs.after ? cursorToOffset(connectionTreeArgs.after) : undefined; + const hasPagination = firstArgument ?? afterArgument; + if (hasPagination) { + return new Pagination({ limit: firstArgument, skip: afterArgument }); + } + } + private getAttributeFields(target: ConcreteEntity, propertiesTree: GraphQLTreeNode | undefined): Field[]; private getAttributeFields(target: Relationship, propertiesTree: GraphQLTreeEdgeProperties | undefined): Field[]; private getAttributeFields( 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 index 50503fb052..90246a2794 100644 --- 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 @@ -48,18 +48,24 @@ describe("Pagination with first and after", () => { await testHelper.close(); }); - test("Get movies with first and after argument", async () => { - const afterCursor = offsetToCursor(2); + test.only("Get movies with first and after argument", async () => { + const afterCursor = offsetToCursor(4); const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(first: 2, after: "${afterCursor}") { + connection(first: 1, after: "${afterCursor}", sort: { edges: { node: { title: ASC } } }) { edges { node { title } } - + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } + } } } @@ -70,7 +76,19 @@ describe("Pagination with first and after", () => { expect(gqlResult.data).toEqual({ [Movie.plural]: { connection: { - edges: expect.toBeArrayOfSize(2), + edges: [ + { + node: { + title: "The Matrix 5", + }, + }, + ], + pageInfo: { + endCursor: offsetToCursor(5), + hasNextPage: false, + hasPreviousPage: true, + startCursor: offsetToCursor(5), + }, }, }, }); From 90d8777bb44a9f8b96bd6055de21f98d2decb112 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 13 Jun 2024 11:51:09 +0100 Subject: [PATCH 056/177] remove connectComponentsPlanner argument from the Context --- packages/graphql/src/types/index.ts | 8 ++++---- packages/graphql/src/utils/execute.test.ts | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/graphql/src/types/index.ts b/packages/graphql/src/types/index.ts index 7f9e5afc89..e2b6676890 100644 --- a/packages/graphql/src/types/index.ts +++ b/packages/graphql/src/types/index.ts @@ -256,12 +256,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", From acb6659f8e02dc3fef5dd328ae257b6b73669f54 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 13 Jun 2024 11:56:40 +0100 Subject: [PATCH 057/177] Create five-turtles-grab.md --- .changeset/five-turtles-grab.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/five-turtles-grab.md 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` From dbda630a658b7d2e04c9a2407813fb085509e1a1 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 13 Jun 2024 14:12:06 +0100 Subject: [PATCH 058/177] fix sorting for not sortable types after merge --- .../src/api-v6/schema-generation/SchemaBuilder.ts | 2 -- .../schema-types/EntitySchemaTypes.ts | 11 ++++------- .../schema-types/TopLevelEntitySchemaTypes.ts | 5 +++-- .../graphql/tests/api-v6/schema/types/spatial.test.ts | 2 ++ 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index 015d6ce8a0..d7b8df0871 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -18,10 +18,8 @@ */ import type { - GraphQLInputObjectType, GraphQLInputType, GraphQLList, - GraphQLNamedInputType, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index af42df9883..c885f791e8 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import type { GraphQLScalarType } from "graphql"; import { GraphQLInt, GraphQLString } from "graphql"; import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; import { connectionOperationResolver } from "../../resolvers/connection-operation-resolver"; @@ -46,22 +47,18 @@ export abstract class EntitySchemaTypes { public get connectionOperation(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connectionOperation, () => { - const args = { + const args: { first: GraphQLScalarType; after: GraphQLScalarType; sort?: InputTypeComposer } = { first: GraphQLInt, after: GraphQLString, }; if (this.isSortable()) { - args["sort"] = this.connectionSort; + args.sort = this.connectionSort; } return { fields: { connection: { type: this.connection, - args: { - sort: this.connectionSort, - first: GraphQLInt, - after: GraphQLString, - }, + args: args, resolve: connectionOperationResolver, }, }, 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 index 42044de803..184d7b80ac 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -30,7 +30,6 @@ import { ScalarType, } from "../../../schema-model/attribute/AttributeType"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; -//import { attributeAdapterToComposeFields } from "../../../schema/to-compose"; import { idResolver } from "../../../schema/resolvers/field/id"; import { numericalResolver } from "../../../schema/resolvers/field/numerical"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; @@ -130,7 +129,9 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes 0; } diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 7f3ab7663d..7761e65aee 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -191,8 +191,10 @@ describe("Spatial Types", () => { } type PageInfo { + endCursor: String hasNextPage: Boolean hasPreviousPage: Boolean + startCursor: String } \\"\\"\\" From a2da2a642ccefe993f82776df9c9e2de7fd7e4aa Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 13 Jun 2024 14:22:03 +0100 Subject: [PATCH 059/177] revert point-cartesian.int.test --- .../graphql/tests/integration/types/point-cartesian.int.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/graphql/tests/integration/types/point-cartesian.int.test.ts b/packages/graphql/tests/integration/types/point-cartesian.int.test.ts index 6cd42d80fc..bf7abf29a0 100644 --- a/packages/graphql/tests/integration/types/point-cartesian.int.test.ts +++ b/packages/graphql/tests/integration/types/point-cartesian.int.test.ts @@ -55,6 +55,7 @@ describe("CartesianPoint", () => { x y z + crs } } } From de8118f6c37e5847eea727d7347804356cea0ac5 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 13 Jun 2024 14:28:31 +0100 Subject: [PATCH 060/177] cleaning up --- .../graphql/src/schema-model/attribute/AttributeTypeHelper.ts | 4 +--- packages/graphql/src/schema-model/parser/parse-attribute.ts | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts b/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts index e3da7867eb..d63fb5641f 100644 --- a/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts +++ b/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts @@ -23,12 +23,10 @@ import { InputType, InterfaceType, ListType, - Neo4jSpatialType, - // Neo4jCartesianPointType, Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - //Neo4jPointType, + Neo4jSpatialType, ObjectType, ScalarType, UnionType, diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 838b81d110..6a4c279a7c 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -29,12 +29,10 @@ import { InputType, InterfaceType, ListType, - //Neo4jCartesianPointType, Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, Neo4jSpatialType, - // Neo4jPointType, ObjectType, ScalarType, UnionType, From 33bbb794f6bafa366323e66b616c9c27ea943f95 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 14 Jun 2024 14:16:57 +0100 Subject: [PATCH 061/177] add tck/integration test for @limit directive --- .../queryIRFactory/ReadOperationFactory.ts | 29 +- .../directives/alias/query-alias.int.test.ts | 2 +- .../directives/limit/query-limit.int.test.ts | 333 +++++++++++++++ .../{query.test.ts => query-alias.test.ts} | 0 .../tck/directives/limit/query-limit.test.ts | 382 ++++++++++++++++++ .../integration/rfcs/query-limits.int.test.ts | 179 -------- .../tests/tck/rfcs/query-limits.test.ts | 380 ----------------- 7 files changed, 739 insertions(+), 566 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/directives/limit/query-limit.int.test.ts rename packages/graphql/tests/api-v6/tck/directives/alias/{query.test.ts => query-alias.test.ts} (100%) create mode 100644 packages/graphql/tests/api-v6/tck/directives/limit/query-limit.test.ts delete mode 100644 packages/graphql/tests/integration/rfcs/query-limits.int.test.ts delete mode 100644 packages/graphql/tests/tck/rfcs/query-limits.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 9bc6035318..d5c74516b9 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -19,6 +19,7 @@ import { cursorToOffset } from "graphql-relay"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import type { LimitAnnotation } from "../../schema-model/annotation/LimitAnnotation"; import { 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"; @@ -48,6 +49,7 @@ import type { GraphQLTreeSortElement, } from "./resolve-tree-parser/graphql-tree"; +import { Integer } from "neo4j-driver"; export class ReadOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; private filterFactory: FilterFactory; @@ -91,7 +93,7 @@ export class ReadOperationFactory { const nodeResolveTree = connectionTree.fields.edges?.fields.node; const sortArgument = connectionTree.args.sort; - const pagination = this.getPagination(connectionTree.args); + const pagination = this.getPagination(connectionTree.args, entity); const nodeFields = this.getNodeFields(entity, nodeResolveTree); const sortInputFields = this.getSortInputFields({ @@ -137,12 +139,12 @@ export class ReadOperationFactory { // Fields const nodeResolveTree = connectionTree.fields.edges?.fields.node; const propertiesResolveTree = connectionTree.fields.edges?.fields.properties; - const relTarget = relationshipAdapter.target.entity; const edgeFields = this.getAttributeFields(relationship, propertiesResolveTree); const sortArgument = connectionTree.args.sort; - const pagination = this.getPagination(connectionTree.args); + 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 }); @@ -164,13 +166,28 @@ export class ReadOperationFactory { }); } - private getPagination(connectionTreeArgs: GraphQLConnectionArgs): Pagination | undefined { + private getPagination(connectionTreeArgs: GraphQLConnectionArgs, entity: ConcreteEntity): Pagination | undefined { const firstArgument = connectionTreeArgs.first; const afterArgument = connectionTreeArgs.after ? cursorToOffset(connectionTreeArgs.after) : undefined; const hasPagination = firstArgument ?? afterArgument; - if (hasPagination) { - return new Pagination({ limit: firstArgument, skip: afterArgument }); + const limitAnnotation = entity.annotations.limit; + if (hasPagination || limitAnnotation) { + const limit = this.calculatePaginationLimitArgument(firstArgument, limitAnnotation); + return new Pagination({ limit, skip: afterArgument }); + } + } + + private calculatePaginationLimitArgument( + firstArgument: Integer | undefined, + limitAnnotation: LimitAnnotation | undefined + ): number | undefined { + if (firstArgument && limitAnnotation?.max) { + return Math.min(firstArgument.toNumber(), limitAnnotation.max); + } + if (firstArgument && firstArgument.toNumber() >= (limitAnnotation?.max ?? Number.MAX_SAFE_INTEGER)) { + return limitAnnotation?.max; } + return firstArgument?.toNumber() ?? limitAnnotation?.default ?? limitAnnotation?.max; } private getAttributeFields(target: ConcreteEntity, propertiesTree: GraphQLTreeNode | undefined): Field[]; 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 index 5fd1d583c6..3491afc7e5 100644 --- 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 @@ -30,7 +30,7 @@ describe("@alias directive", () => { Movie = testHelper.createUniqueType("Movie"); Director = testHelper.createUniqueType("Director"); - const typeDefs = ` + const typeDefs = /* GraphQL */ ` type ${Director} @node { name: String nameAgain: String @alias(property: "name") 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/tck/directives/alias/query.test.ts b/packages/graphql/tests/api-v6/tck/directives/alias/query-alias.test.ts similarity index 100% rename from packages/graphql/tests/api-v6/tck/directives/alias/query.test.ts rename to packages/graphql/tests/api-v6/tck/directives/alias/query-alias.test.ts 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..ce76dd1246 --- /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]-(actors:Person) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + LIMIT $param1 + RETURN collect({ node: { id: actors.id, __resolveType: \\"Person\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { id: this0.id, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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]-(actors:Person) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + LIMIT $param1 + RETURN collect({ node: { id: actors.id, __resolveType: \\"Person\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { id: this0.id, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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]-(shows:Show) + WITH collect({ node: shows, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS shows, edge.relationship AS this1 + WITH * + LIMIT $param0 + RETURN collect({ node: { id: shows.id, __resolveType: \\"Show\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { name: this0.name, shows: var3, __resolveType: \\"Festival\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 2, + \\"high\\": 0 + } + }" + `); + }); +}); 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/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 - } - }" - `); - }); - }); -}); From 607d95ecaacc919f87af2d933b6f58ebb63d3e1d Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 17 Jun 2024 14:52:37 +0100 Subject: [PATCH 062/177] clean-up sclalar support --- .../resolve-tree-parser/ResolveTreeParser.ts | 89 ++++++++++--------- .../schema-types/StaticSchemaTypes.ts | 20 ++--- .../filter-schema-types/FilterSchemaTypes.ts | 4 - .../model-adapters/AttributeAdapter.test.ts | 2 - .../schema-model/parser/parse-attribute.ts | 2 - .../tests/api-v6/schema/types/array.test.ts | 19 ++-- 6 files changed, 67 insertions(+), 69 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 31d1dc0cd5..bbe1688bcc 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -21,6 +21,7 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import { CartesianPoint } from "../../../graphql/objects/CartesianPoint"; import { Point } from "../../../graphql/objects/Point"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; +import { ListType } from "../../../schema-model/attribute/AttributeType"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; import { findFieldByName } from "./find-field-by-name"; @@ -40,7 +41,6 @@ import type { GraphQLTreeScalarField, GraphQLTreeSortElement, } from "./graphql-tree"; -import { ListType } from "../../../schema-model/attribute/AttributeType"; export abstract class ResolveTreeParser { protected entity: T; @@ -85,50 +85,13 @@ export abstract class ResolveTreeParser ): GraphQLTreeLeafField | GraphQLTreePoint | undefined { 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; + const wrappedTypeName = + attribute.type instanceof ListType ? attribute.type.ofType.name : attribute.type.name; if (wrappedTypeName === "Point") { - 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"); - - const pointField: GraphQLTreePoint = { - 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), - }, - }; - - return pointField; + return this.parsePointField(resolveTree); } if (wrappedTypeName === "CartesianPoint") { - 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"); - - const cartesianPointField: GraphQLTreeCartesianPoint = { - 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), - }, - }; - - return cartesianPointField; + return this.parseCartesianPointField(resolveTree); } return { alias: resolveTree.alias, @@ -139,6 +102,48 @@ export abstract class ResolveTreeParser } } + private 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), + }, + }; + } + + private 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), + }, + }; + } + private parseConnection(resolveTree: ResolveTree): GraphQLTreeConnection { const entityTypes = this.entity.typeNames; const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges"); 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 index 5747235730..1c32df3a7c 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -21,6 +21,13 @@ import type { GraphQLInputType, GraphQLScalarType } from "graphql"; import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; +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 { GraphQLBigInt, GraphQLDate, @@ -31,14 +38,6 @@ import { GraphQLTime, } from "../../../graphql/scalars"; import type { SchemaBuilder } from "../SchemaBuilder"; - -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 { toGraphQLList } from "../utils/to-graphql-list"; import { toGraphQLNonNull } from "../utils/to-graphql-non-null"; @@ -309,11 +308,6 @@ class StaticFilterTypes { }); } - public getBooleanListWhere(nullable: boolean): InputTypeComposer { - // TODO: verify correctness of this - return this.getStringListWhere(nullable); - } - public get booleanWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("BooleanWhere", (itc) => { return { 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 index 0924cb90f4..06e932a24f 100644 --- 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 @@ -96,10 +96,6 @@ export abstract class FilterSchemaTypes { equals: [BigInt] } + input BooleanWhere { + AND: [BooleanWhere!] + NOT: BooleanWhere + OR: [BooleanWhere!] + equals: Boolean + } + \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" scalar Date @@ -321,8 +328,8 @@ describe("Scalars", () => { OR: [NodeTypeWhere!] bigIntList: BigIntListWhere bigIntListNullable: BigIntListWhereNullable - booleanList: StringListWhere - booleanListNullable: StringListWhereNullable + booleanList: BooleanWhere + booleanListNullable: BooleanWhere dateList: DateListWhere dateListNullable: DateListWhereNullable dateTimeList: DateTimeListWhere @@ -446,8 +453,8 @@ describe("Scalars", () => { OR: [RelatedNodePropertiesWhere!] bigIntList: BigIntListWhere bigIntListNullable: BigIntListWhereNullable - booleanList: StringListWhere - booleanListNullable: StringListWhereNullable + booleanList: BooleanWhere + booleanListNullable: BooleanWhere dateList: DateListWhere dateListNullable: DateListWhereNullable dateTimeList: DateTimeListWhere @@ -476,8 +483,8 @@ describe("Scalars", () => { OR: [RelatedNodeWhere!] bigIntList: BigIntListWhere bigIntListNullable: BigIntListWhereNullable - booleanList: StringListWhere - booleanListNullable: StringListWhereNullable + booleanList: BooleanWhere + booleanListNullable: BooleanWhere dateList: DateListWhere dateListNullable: DateListWhereNullable dateTimeList: DateTimeListWhere From 8e568763c3e629af9a5e1887a2a20f2bd800d512 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 14 Jun 2024 14:16:57 +0100 Subject: [PATCH 063/177] add tck/integration test for @limit directive --- .../queryIRFactory/ReadOperationFactory.ts | 29 +- .../directives/alias/query-alias.int.test.ts | 2 +- .../directives/limit/query-limit.int.test.ts | 333 +++++++++++++++ .../{query.test.ts => query-alias.test.ts} | 0 .../tck/directives/limit/query-limit.test.ts | 382 ++++++++++++++++++ .../integration/rfcs/query-limits.int.test.ts | 179 -------- .../tests/tck/rfcs/query-limits.test.ts | 380 ----------------- 7 files changed, 739 insertions(+), 566 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/directives/limit/query-limit.int.test.ts rename packages/graphql/tests/api-v6/tck/directives/alias/{query.test.ts => query-alias.test.ts} (100%) create mode 100644 packages/graphql/tests/api-v6/tck/directives/limit/query-limit.test.ts delete mode 100644 packages/graphql/tests/integration/rfcs/query-limits.int.test.ts delete mode 100644 packages/graphql/tests/tck/rfcs/query-limits.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 9bc6035318..d5c74516b9 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -19,6 +19,7 @@ import { cursorToOffset } from "graphql-relay"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import type { LimitAnnotation } from "../../schema-model/annotation/LimitAnnotation"; import { 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"; @@ -48,6 +49,7 @@ import type { GraphQLTreeSortElement, } from "./resolve-tree-parser/graphql-tree"; +import { Integer } from "neo4j-driver"; export class ReadOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; private filterFactory: FilterFactory; @@ -91,7 +93,7 @@ export class ReadOperationFactory { const nodeResolveTree = connectionTree.fields.edges?.fields.node; const sortArgument = connectionTree.args.sort; - const pagination = this.getPagination(connectionTree.args); + const pagination = this.getPagination(connectionTree.args, entity); const nodeFields = this.getNodeFields(entity, nodeResolveTree); const sortInputFields = this.getSortInputFields({ @@ -137,12 +139,12 @@ export class ReadOperationFactory { // Fields const nodeResolveTree = connectionTree.fields.edges?.fields.node; const propertiesResolveTree = connectionTree.fields.edges?.fields.properties; - const relTarget = relationshipAdapter.target.entity; const edgeFields = this.getAttributeFields(relationship, propertiesResolveTree); const sortArgument = connectionTree.args.sort; - const pagination = this.getPagination(connectionTree.args); + 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 }); @@ -164,13 +166,28 @@ export class ReadOperationFactory { }); } - private getPagination(connectionTreeArgs: GraphQLConnectionArgs): Pagination | undefined { + private getPagination(connectionTreeArgs: GraphQLConnectionArgs, entity: ConcreteEntity): Pagination | undefined { const firstArgument = connectionTreeArgs.first; const afterArgument = connectionTreeArgs.after ? cursorToOffset(connectionTreeArgs.after) : undefined; const hasPagination = firstArgument ?? afterArgument; - if (hasPagination) { - return new Pagination({ limit: firstArgument, skip: afterArgument }); + const limitAnnotation = entity.annotations.limit; + if (hasPagination || limitAnnotation) { + const limit = this.calculatePaginationLimitArgument(firstArgument, limitAnnotation); + return new Pagination({ limit, skip: afterArgument }); + } + } + + private calculatePaginationLimitArgument( + firstArgument: Integer | undefined, + limitAnnotation: LimitAnnotation | undefined + ): number | undefined { + if (firstArgument && limitAnnotation?.max) { + return Math.min(firstArgument.toNumber(), limitAnnotation.max); + } + if (firstArgument && firstArgument.toNumber() >= (limitAnnotation?.max ?? Number.MAX_SAFE_INTEGER)) { + return limitAnnotation?.max; } + return firstArgument?.toNumber() ?? limitAnnotation?.default ?? limitAnnotation?.max; } private getAttributeFields(target: ConcreteEntity, propertiesTree: GraphQLTreeNode | undefined): Field[]; 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 index 5fd1d583c6..3491afc7e5 100644 --- 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 @@ -30,7 +30,7 @@ describe("@alias directive", () => { Movie = testHelper.createUniqueType("Movie"); Director = testHelper.createUniqueType("Director"); - const typeDefs = ` + const typeDefs = /* GraphQL */ ` type ${Director} @node { name: String nameAgain: String @alias(property: "name") 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/tck/directives/alias/query.test.ts b/packages/graphql/tests/api-v6/tck/directives/alias/query-alias.test.ts similarity index 100% rename from packages/graphql/tests/api-v6/tck/directives/alias/query.test.ts rename to packages/graphql/tests/api-v6/tck/directives/alias/query-alias.test.ts 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..ce76dd1246 --- /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]-(actors:Person) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + LIMIT $param1 + RETURN collect({ node: { id: actors.id, __resolveType: \\"Person\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { id: this0.id, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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]-(actors:Person) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + WITH * + LIMIT $param1 + RETURN collect({ node: { id: actors.id, __resolveType: \\"Person\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { id: this0.id, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, 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]-(shows:Show) + WITH collect({ node: shows, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS shows, edge.relationship AS this1 + WITH * + LIMIT $param0 + RETURN collect({ node: { id: shows.id, __resolveType: \\"Show\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { name: this0.name, shows: var3, __resolveType: \\"Festival\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 2, + \\"high\\": 0 + } + }" + `); + }); +}); 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/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 - } - }" - `); - }); - }); -}); From 642f1fa27a5a2fa7a24c42ae10cd80000a316e71 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 18 Jun 2024 10:26:30 +0100 Subject: [PATCH 064/177] simplify calcualePaginationLimitArgument --- .../src/api-v6/queryIRFactory/ReadOperationFactory.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index d5c74516b9..cb6e9ecf83 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -49,7 +49,7 @@ import type { GraphQLTreeSortElement, } from "./resolve-tree-parser/graphql-tree"; -import { Integer } from "neo4j-driver"; +import type { Integer } from "neo4j-driver"; export class ReadOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; private filterFactory: FilterFactory; @@ -177,6 +177,12 @@ export class ReadOperationFactory { } } + /** + * 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 @@ -184,9 +190,6 @@ export class ReadOperationFactory { if (firstArgument && limitAnnotation?.max) { return Math.min(firstArgument.toNumber(), limitAnnotation.max); } - if (firstArgument && firstArgument.toNumber() >= (limitAnnotation?.max ?? Number.MAX_SAFE_INTEGER)) { - return limitAnnotation?.max; - } return firstArgument?.toNumber() ?? limitAnnotation?.default ?? limitAnnotation?.max; } From 678e0df9a2c115cdf016282f84c71e0aa6805be9 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 18 Jun 2024 16:18:23 +0100 Subject: [PATCH 065/177] Add relay id field in nodes --- .../queryIRFactory/ReadOperationFactory.ts | 13 +- .../resolve-tree-parser/ResolveTreeParser.ts | 13 +- .../api-v6/resolvers/global-id-resolver.ts | 24 +++ .../schema-types/TopLevelEntitySchemaTypes.ts | 14 +- .../src/schema-model/entity/ConcreteEntity.ts | 9 + .../relayId/relayId-projection.int.test.ts | 178 ++++++++++++++++++ .../api-v6/schema/directives/relayId.test.ts | 135 +++++++++++++ 7 files changed, 382 insertions(+), 4 deletions(-) create mode 100644 packages/graphql/src/api-v6/resolvers/global-id-resolver.ts create mode 100644 packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection.int.test.ts create mode 100644 packages/graphql/tests/api-v6/schema/directives/relayId.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 9bc6035318..97bc3728e5 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -19,8 +19,9 @@ import { cursorToOffset } from "graphql-relay"; 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 { 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"; @@ -185,7 +186,15 @@ export class ReadOperationFactory { return filterTruthy( Object.values(propertiesTree.fields).map((rawField) => { - const attribute = target.findAttribute(rawField.name); + 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 as GraphQLTreeLeafField; const attributeAdapter = new AttributeAdapter(attribute); diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index bbe1688bcc..dea62b8689 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -22,7 +22,7 @@ import { CartesianPoint } from "../../../graphql/objects/CartesianPoint"; import { Point } from "../../../graphql/objects/Point"; import type { Attribute } from "../../../schema-model/attribute/Attribute"; import { ListType } from "../../../schema-model/attribute/AttributeType"; -import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; import { findFieldByName } from "./find-field-by-name"; import type { @@ -83,6 +83,17 @@ export abstract class ResolveTreeParser resolveTree: ResolveTree, entity: ConcreteEntity | Relationship ): GraphQLTreeLeafField | GraphQLTreePoint | undefined { + if (resolveTree.name === "id") { + if (entity instanceof ConcreteEntity && entity.globalIdField) { + return { + alias: entity.globalIdField.name, + args: resolveTree.args, + name: resolveTree.name, + fields: undefined, + }; + } + } + if (entity.hasAttribute(resolveTree.name)) { const attribute = entity.findAttribute(resolveTree.name) as Attribute; const wrappedTypeName = diff --git a/packages/graphql/src/api-v6/resolvers/global-id-resolver.ts b/packages/graphql/src/api-v6/resolvers/global-id-resolver.ts new file mode 100644 index 0000000000..d4e21083df --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/global-id-resolver.ts @@ -0,0 +1,24 @@ +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 to globalId*/ +export function generateGlobalIdResolver({ 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/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts index 184d7b80ac..267a482819 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -33,6 +33,7 @@ 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 { generateGlobalIdResolver } from "../../resolvers/global-id-resolver"; import type { TopLevelEntityTypeNames } from "../../schema-model/graphql-type-names/TopLevelEntityTypeNames"; import type { FieldDefinition, GraphQLResolver, SchemaBuilder } from "../SchemaBuilder"; import { EntitySchemaTypes } from "./EntitySchemaTypes"; @@ -184,7 +185,18 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes }> { diff --git a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts index 59cd4b4882..2e516dd634 100644 --- a/packages/graphql/src/schema-model/entity/ConcreteEntity.ts +++ b/packages/graphql/src/schema-model/entity/ConcreteEntity.ts @@ -76,6 +76,15 @@ export class ConcreteEntity implements Entity { 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; } 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..83159d212a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection.int.test.ts @@ -0,0 +1,178 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 = ` + type ${Movie} @node { + 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} @node { + dbId: ID! @id @unique @relayId + name: String! + } + + type ${Actor} @node { + dbId: ID! @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 = ` + 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", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }); + // 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/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts new file mode 100644 index 0000000000..e9965d2509 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + type Movie { + dbId: ID! + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + edges: [MovieEdgeSort!] + } + + type MovieEdge { + cursor: String + node: Movie + } + + input MovieEdgeSort { + node: MovieSort + } + + input MovieEdgeWhere { + AND: [MovieEdgeWhere!] + NOT: MovieEdgeWhere + OR: [MovieEdgeWhere!] + node: MovieWhere + } + + type MovieOperation { + connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + edges: MovieEdgeWhere + } + + input MovieSort { + dbId: SortDirection + title: SortDirection + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + dbId: IDWhere + title: StringWhere + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean + hasPreviousPage: Boolean + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); +}); From 8047aa4302ceddeee25315eb23440a0efd308a1d Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 19 Jun 2024 11:02:28 +0100 Subject: [PATCH 066/177] RelayID filters --- .../api-v6/queryIRFactory/FilterFactory.ts | 27 ++- .../resolve-tree-parser/ResolveTreeParser.ts | 28 ++- .../schema-types/StaticSchemaTypes.ts | 12 + .../schema-types/TopLevelEntitySchemaTypes.ts | 6 +- .../filter-schema-types/FilterSchemaTypes.ts | 13 +- .../TopLevelFilterSchemaTypes.ts | 1 + ...-projection-with-database-name.int.test.ts | 6 +- ...Id-projection-with-field-alias.int.test.ts | 6 +- .../relayId/relayId-projection.int.test.ts | 17 -- .../relayId/relayId-projection.int.test.ts | 206 ------------------ 10 files changed, 80 insertions(+), 242 deletions(-) rename packages/graphql/tests/{ => api-v6}/integration/directives/relayId/relayId-projection-with-database-name.int.test.ts (97%) rename packages/graphql/tests/{ => api-v6}/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts (97%) delete mode 100644 packages/graphql/tests/integration/directives/relayId/relayId-projection.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index 2b77cb612a..dd4e319ec2 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -18,6 +18,7 @@ */ 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"; @@ -29,6 +30,7 @@ import { LogicalFilter } from "../../translate/queryAST/ast/filters/LogicalFilte 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 { getFilterOperator, getRelationshipOperator } from "./FilterOperators"; import type { GraphQLAttributeFilters, @@ -161,8 +163,14 @@ export class FilterFactory { where?: Record; }): Filter[] { return Object.entries(where).flatMap(([fieldName, filters]) => { - // TODO: Logical filters here - const attribute = entity.findAttribute(fieldName); + 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 @@ -178,6 +186,20 @@ export class FilterFactory { }); } + /** 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); @@ -219,6 +241,7 @@ export class FilterFactory { return this.createPropertyFilters(attributeAdapter, filters, "relationship"); }); } + // TODO: remove adapter from here private createPropertyFilters( attribute: AttributeAdapter, diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index dea62b8689..d9eeb10210 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -83,15 +83,9 @@ export abstract class ResolveTreeParser resolveTree: ResolveTree, entity: ConcreteEntity | Relationship ): GraphQLTreeLeafField | GraphQLTreePoint | undefined { - if (resolveTree.name === "id") { - if (entity instanceof ConcreteEntity && entity.globalIdField) { - return { - alias: entity.globalIdField.name, - args: resolveTree.args, - name: resolveTree.name, - fields: undefined, - }; - } + const globalIdField = this.parseGlobalIdField(resolveTree, entity); + if (globalIdField) { + return globalIdField; } if (entity.hasAttribute(resolveTree.name)) { @@ -113,6 +107,22 @@ export abstract class ResolveTreeParser } } + private 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, + }; + } + } + } + private parsePointField(resolveTree: ResolveTree): GraphQLTreePoint { const longitude = findFieldByName(resolveTree, Point.name, "longitude"); const latitude = findFieldByName(resolveTree, Point.name, "latitude"); 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 index 1c32df3a7c..b00f9db176 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -117,6 +117,18 @@ class StaticFilterTypes { }); } + public get globalIdWhere(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType("GlobalIdWhere", (itc) => { + return { + fields: { + // ...this.createBooleanOperators(itc), + equals: GraphQLString, + // in: toGraphQLList(toGraphQLNonNull(GraphQLString)), + }, + }; + }); + } + public get dateWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("DateWhere", (itc) => { return { 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 index 267a482819..a908bceddd 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -187,6 +187,11 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes): void { const globalIdField = this.entity.globalIdField; if (globalIdField) { fields["id"] = { @@ -196,7 +201,6 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes }> { 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 index 06e932a24f..d79186956a 100644 --- 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 @@ -29,6 +29,7 @@ import { Neo4jSpatialType, 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"; @@ -82,7 +83,17 @@ export abstract class FilterSchemaTypes { + 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) { 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 index 72e69a3b59..faa8fd8896 100644 --- 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 @@ -64,6 +64,7 @@ export class TopLevelFilterSchemaTypes extends FilterSchemaTypes { - const testHelper = new TestHelper(); + const testHelper = new TestHelper({ v6Api: true }); let movieDatabaseID: string; let genreDatabaseID: string; let actorDatabaseID: string; diff --git a/packages/graphql/tests/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts similarity index 97% rename from packages/graphql/tests/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts rename to packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts index 6d9612979a..ed0fe0d0b3 100644 --- a/packages/graphql/tests/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts @@ -18,12 +18,12 @@ */ import { generate } from "randomstring"; -import { toGlobalId } from "../../../../src/utils/global-ids"; -import { TestHelper } from "../../../utils/tests-helper"; +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(); + const testHelper = new TestHelper({ v6Api: true }); let movieDatabaseID: string; let genreDatabaseID: string; let actorDatabaseID: string; 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 index 83159d212a..83e75bea07 100644 --- 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 @@ -157,22 +157,5 @@ describe("RelayId projection", () => { ], }, }); - // 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.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 })); - }); -}); From 82711acb66d948fa26b737b2c964ae2b0a9c0bdc Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 19 Jun 2024 11:03:17 +0100 Subject: [PATCH 067/177] add relationship validation for 1to1 cardinality --- .../api-v6/validation/validate-document.ts | 276 ++++++++++++++++++ .../api-v6/validation/validate-v6-document.ts | 254 ++++++++++++++++ packages/graphql/src/classes/Neo4jGraphQL.ts | 18 +- .../features/valid-relationship-n-n.ts | 65 +++++ 4 files changed, 612 insertions(+), 1 deletion(-) create mode 100644 packages/graphql/src/api-v6/validation/validate-document.ts create mode 100644 packages/graphql/src/api-v6/validation/validate-v6-document.ts create mode 100644 packages/graphql/src/schema/validation/custom-rules/features/valid-relationship-n-n.ts diff --git a/packages/graphql/src/api-v6/validation/validate-document.ts b/packages/graphql/src/api-v6/validation/validate-document.ts new file mode 100644 index 0000000000..d4e97e973e --- /dev/null +++ b/packages/graphql/src/api-v6/validation/validate-document.ts @@ -0,0 +1,276 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { type IResolvers } from "@graphql-tools/utils"; +import type { + DefinitionNode, + DocumentNode, + EnumTypeDefinitionNode, + FieldDefinitionNode, + GraphQLDirective, + GraphQLNamedType, + InputValueDefinitionNode, + InterfaceTypeDefinitionNode, + ObjectTypeDefinitionNode, + TypeNode, + UnionTypeDefinitionNode, +} from "graphql"; +import { GraphQLSchema, Kind, extendSchema, specifiedDirectives, validateSchema } from "graphql"; +import { specifiedSDLRules } from "graphql/validation/specifiedRules"; +import pluralize from "pluralize"; +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 { directiveIsValid } from "../../schema/validation//custom-rules/directives/valid-directive"; +import { ValidDirectiveAtFieldLocation } from "../../schema/validation/custom-rules/directives/valid-directive-field-location"; +import { ValidJwtDirectives } from "../../schema/validation/custom-rules/features/valid-jwt-directives"; +import { ValidRelationshipProperties } from "../../schema/validation/custom-rules/features/valid-relationship-properties"; +import { ValidRelayID } from "../../schema/validation/custom-rules/features/valid-relay-id"; +import { ReservedTypeNames } from "../../schema/validation/custom-rules/valid-types/reserved-type-names"; +import { + DirectiveCombinationValid, + SchemaOrTypeDirectives, +} 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 { isRootType } from "../../utils/is-root-type"; + +function filterDocument(document: DocumentNode): DocumentNode { + const nodeNames = document.definitions + .filter((definition) => { + if (definition.kind === Kind.OBJECT_TYPE_DEFINITION) { + if (!isRootType(definition)) { + return true; + } + } + return false; + }) + .map((definition) => (definition as ObjectTypeDefinitionNode).name.value); + + const getArgumentType = (type: TypeNode): string => { + if (type.kind === Kind.LIST_TYPE) { + return getArgumentType(type.type); + } + + if (type.kind === Kind.NON_NULL_TYPE) { + return getArgumentType(type.type); + } + + return type.name.value; + }; + + const filterInputTypes = ( + fields: readonly InputValueDefinitionNode[] | undefined + ): InputValueDefinitionNode[] | undefined => { + return fields?.filter((f) => { + const type = getArgumentType(f.type); + + const nodeMatch = + /(?.+)(?:ConnectInput|ConnectWhere|CreateInput|DeleteInput|DisconnectInput|Options|RelationInput|Sort|UpdateInput|Where)/gm.exec( + type + ); + if (nodeMatch?.groups?.nodeName) { + if (nodeNames.includes(nodeMatch.groups.nodeName)) { + return false; + } + } + + return true; + }); + }; + + const filterFields = (fields: readonly FieldDefinitionNode[] | undefined): FieldDefinitionNode[] | undefined => { + return fields + ?.filter((field) => { + const type = getArgumentType(field.type); + const match = /(?:Create|Update)(?.+)MutationResponse/gm.exec(type); + if (match?.groups?.nodeName) { + if (nodeNames.map((nodeName) => pluralize(nodeName)).includes(match.groups.nodeName)) { + return false; + } + } + return true; + }) + .map((field) => { + return { + ...field, + arguments: filterInputTypes(field.arguments), + }; + }); + }; + + const filteredDocument: DocumentNode = { + ...document, + definitions: document.definitions.reduce((res: DefinitionNode[], def) => { + if (def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { + const fields = filterInputTypes(def.fields); + + if (!fields?.length) { + return res; + } + + return [ + ...res, + { + ...def, + fields, + }, + ]; + } + + if (def.kind === Kind.OBJECT_TYPE_DEFINITION || def.kind === Kind.INTERFACE_TYPE_DEFINITION) { + if (!def.fields?.length) { + return [...res, def]; + } + + const fields = filterFields(def.fields); + if (!fields?.length) { + return res; + } + + return [ + ...res, + { + ...def, + fields, + }, + ]; + } + + return [...res, def]; + }, []), + }; + + return filteredDocument; +} + +function runNeo4jValidationRules({ + schema, + document, + extra, + userCustomResolvers, + features, +}: { + schema: GraphQLSchema; + document: DocumentNode; + extra: { + enums?: EnumTypeDefinitionNode[]; + interfaces?: InterfaceTypeDefinitionNode[]; + unions?: UnionTypeDefinitionNode[]; + objects?: ObjectTypeDefinitionNode[]; + }; + userCustomResolvers?: IResolvers | Array; + features: Neo4jFeaturesSettings | undefined; +}) { + const errors = validateSDL( + document, + [ + ...specifiedSDLRules, + directiveIsValid(extra, features?.populatedBy?.callbacks), + ValidDirectiveAtFieldLocation, + DirectiveCombinationValid, + // SchemaOrTypeDirectives, + // ValidJwtDirectives, + // ValidRelayID, + ValidRelationshipProperties, + // ValidRelationshipDeclaration, + // ValidFieldTypes, + ReservedTypeNames, + // ValidObjectType, + // ValidDirectiveInheritance, + // DirectiveArgumentOfCorrectType(false), + // WarnIfAuthorizationFeatureDisabled(features?.authorization), + WarnIfListOfListsFieldDefinition, + // WarnIfAMaxLimitCanBeBypassedThroughInterface(), + // WarnObjectFieldsWithoutResolver({ + // customResolvers: asArray(userCustomResolvers ?? []), + // }), + ], + schema + ); + const filteredErrors = errors.filter((e) => e.message !== "Query root type must be provided."); + if (filteredErrors.length) { + throw filteredErrors; + } +} + +export function validateV6Document({ + document, + features, + additionalDefinitions, + userCustomResolvers, +}: { + document: DocumentNode; + features: Neo4jFeaturesSettings | undefined; + additionalDefinitions: { + additionalDirectives?: Array; + additionalTypes?: Array; + enums?: EnumTypeDefinitionNode[]; + interfaces?: InterfaceTypeDefinitionNode[]; + unions?: UnionTypeDefinitionNode[]; + objects?: ObjectTypeDefinitionNode[]; + }; + userCustomResolvers?: IResolvers | Array; +}): void { + const filteredDocument = filterDocument(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 || []), + ], + }); + + runNeo4jValidationRules({ + schema: schemaToExtend, + document: filteredDocument, + extra, + userCustomResolvers, + features, + }); + + 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/api-v6/validation/validate-v6-document.ts b/packages/graphql/src/api-v6/validation/validate-v6-document.ts new file mode 100644 index 0000000000..0e2b2722ab --- /dev/null +++ b/packages/graphql/src/api-v6/validation/validate-v6-document.ts @@ -0,0 +1,254 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + DefinitionNode, + DocumentNode, + EnumTypeDefinitionNode, + FieldDefinitionNode, + GraphQLDirective, + GraphQLNamedType, + InputValueDefinitionNode, + InterfaceTypeDefinitionNode, + ObjectTypeDefinitionNode, + TypeNode, + UnionTypeDefinitionNode, +} from "graphql"; +import { GraphQLSchema, Kind, extendSchema, specifiedDirectives, validateSchema } from "graphql"; +import { specifiedSDLRules } from "graphql/validation/specifiedRules"; +import pluralize from "pluralize"; +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 { directiveIsValid } from "../../schema/validation/custom-rules/directives/valid-directive"; +import { ValidDirectiveAtFieldLocation } from "../../schema/validation/custom-rules/directives/valid-directive-field-location"; +import { ValidRelationshipNtoN } from "../../schema/validation/custom-rules/features/valid-relationship-n-n"; +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 { isRootType } from "../../utils/is-root-type"; + +function filterDocument(document: DocumentNode): DocumentNode { + const nodeNames = document.definitions + .filter((definition) => { + if (definition.kind === Kind.OBJECT_TYPE_DEFINITION) { + if (!isRootType(definition)) { + return true; + } + } + return false; + }) + .map((definition) => (definition as ObjectTypeDefinitionNode).name.value); + + const getArgumentType = (type: TypeNode): string => { + if (type.kind === Kind.LIST_TYPE) { + return getArgumentType(type.type); + } + + if (type.kind === Kind.NON_NULL_TYPE) { + return getArgumentType(type.type); + } + + return type.name.value; + }; + + const filterInputTypes = ( + fields: readonly InputValueDefinitionNode[] | undefined + ): InputValueDefinitionNode[] | undefined => { + return fields?.filter((f) => { + const type = getArgumentType(f.type); + + const nodeMatch = + /(?.+)(?:ConnectInput|ConnectWhere|CreateInput|DeleteInput|DisconnectInput|Options|RelationInput|Sort|UpdateInput|Where)/gm.exec( + type + ); + if (nodeMatch?.groups?.nodeName) { + if (nodeNames.includes(nodeMatch.groups.nodeName)) { + return false; + } + } + + return true; + }); + }; + + const filterFields = (fields: readonly FieldDefinitionNode[] | undefined): FieldDefinitionNode[] | undefined => { + return fields + ?.filter((field) => { + const type = getArgumentType(field.type); + const match = /(?:Create|Update)(?.+)MutationResponse/gm.exec(type); + if (match?.groups?.nodeName) { + if (nodeNames.map((nodeName) => pluralize(nodeName)).includes(match.groups.nodeName)) { + return false; + } + } + return true; + }) + .map((field) => { + return { + ...field, + arguments: filterInputTypes(field.arguments), + }; + }); + }; + + const filteredDocument: DocumentNode = { + ...document, + definitions: document.definitions.reduce((res: DefinitionNode[], def) => { + if (def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { + const fields = filterInputTypes(def.fields); + + if (!fields?.length) { + return res; + } + + return [ + ...res, + { + ...def, + fields, + }, + ]; + } + + if (def.kind === Kind.OBJECT_TYPE_DEFINITION || def.kind === Kind.INTERFACE_TYPE_DEFINITION) { + if (!def.fields?.length) { + return [...res, def]; + } + + const fields = filterFields(def.fields); + if (!fields?.length) { + return res; + } + + return [ + ...res, + { + ...def, + fields, + }, + ]; + } + + return [...res, def]; + }, []), + }; + + return filteredDocument; +} + +function runNeo4jGraphQLValidationRules({ + schema, + document, + extra, + features, +}: { + schema: GraphQLSchema; + document: DocumentNode; + extra: { + enums?: EnumTypeDefinitionNode[]; + interfaces?: InterfaceTypeDefinitionNode[]; + unions?: UnionTypeDefinitionNode[]; + objects?: ObjectTypeDefinitionNode[]; + }; + features: Neo4jFeaturesSettings | undefined; +}) { + const errors = validateSDL( + document, + [ + ...specifiedSDLRules, + ValidRelationshipNtoN, + directiveIsValid(extra), + ValidDirectiveAtFieldLocation, + DirectiveCombinationValid, + ValidRelationshipProperties, + ReservedTypeNames, + WarnIfListOfListsFieldDefinition, + ], + schema + ); + const filteredErrors = errors.filter((e) => e.message !== "Query root type must be provided."); + if (filteredErrors.length) { + throw filteredErrors; + } +} + +export function validateV6Document({ + document, + features, + additionalDefinitions, +}: { + document: DocumentNode; + features: Neo4jFeaturesSettings | undefined; + additionalDefinitions: { + additionalDirectives?: GraphQLDirective[]; + additionalTypes?: GraphQLNamedType[]; + enums?: EnumTypeDefinitionNode[]; + interfaces?: InterfaceTypeDefinitionNode[]; + unions?: UnionTypeDefinitionNode[]; + objects?: ObjectTypeDefinitionNode[]; + }; +}): void { + const filteredDocument = filterDocument(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, + features, + }); + + 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 d846010e71..4bfa361903 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -27,6 +27,7 @@ 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"; @@ -37,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"; @@ -116,9 +117,24 @@ class Neo4jGraphQL { public async getSchema(): Promise { 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(); diff --git a/packages/graphql/src/schema/validation/custom-rules/features/valid-relationship-n-n.ts b/packages/graphql/src/schema/validation/custom-rules/features/valid-relationship-n-n.ts new file mode 100644 index 0000000000..4cf726d1ce --- /dev/null +++ b/packages/graphql/src/schema/validation/custom-rules/features/valid-relationship-n-n.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, TypeNode } from "graphql"; +import { Kind } from "graphql"; +import type { SDLValidationContext } from "graphql/validation/ValidationContext"; +import { relationshipDirective } from "../../../../graphql/directives"; +import { DocumentValidationError, assertValid, createGraphQLError } from "../utils/document-validation-error"; +import { getPathToNode } from "../utils/path-parser"; + +export function ValidRelationshipNtoN(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(() => { + if (!isListType(type)) { + throw new DocumentValidationError(`@relationship can only be used on List target`, []); + } + }); + const [pathToNode] = getPathToNode(path, ancestors); + if (!isValid) { + context.reportError( + createGraphQLError({ + nodes: [fieldDefinitionNode], + path: [...pathToNode, fieldDefinitionNode.name.value, ...errorPath], + errorMsg, + }) + ); + } + }, + }; +} + +function isListType(type: TypeNode): boolean { + if (type.kind === Kind.NON_NULL_TYPE) { + return type.type.kind === Kind.LIST_TYPE; + } + return type.kind === Kind.LIST_TYPE; +} From e63a8d782b9446baa1e3c8fe31ba3b263ab959cb Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 19 Jun 2024 11:04:40 +0100 Subject: [PATCH 068/177] fix import Neo4jGraphQL --- packages/graphql/src/classes/Neo4jGraphQL.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index 4bfa361903..7b14acfdb0 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -39,7 +39,7 @@ import { wrapQueryAndMutation } from "../schema/resolvers/composition/wrap-query import { wrapSubscription, type WrapSubscriptionArgs } from "../schema/resolvers/composition/wrap-subscription"; import { defaultFieldResolver } from "../schema/resolvers/field/defaultField"; import { validateUserDefinition } from "../schema/validation/schema-validation"; -import { validateDocument } from "../schema/validation/validate-document"; +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"; From c2c4defa89fd92fed930489961b03d4a7b93cdf7 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 19 Jun 2024 15:59:02 +0100 Subject: [PATCH 069/177] relayId tests --- .../alias-relayId/alias-relayId.int.test.ts | 161 ++++++++++++++ .../relayId/relayId-filters.int.test.ts | 169 ++++++++++++++ ...-projection-with-database-name.int.test.ts | 207 ----------------- ...Id-projection-with-field-alias.int.test.ts | 209 ------------------ .../relayId/relayId-projection.int.test.ts | 89 ++++++++ 5 files changed, 419 insertions(+), 416 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/combinations/alias-relayId/alias-relayId.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/directives/relayId/relayId-filters.int.test.ts delete mode 100644 packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection-with-database-name.int.test.ts delete mode 100644 packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts 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..3f2b0ad2e1 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/combinations/alias-relayId/alias-relayId.int.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 { 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 = ` + type ${Movie} @node { + 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} @node { + dbId: ID! @id @unique @relayId + name: String! + } + + type ${Actor} @node { + dbId: ID! @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 = ` + 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", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }); + }); +}); 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..e68dc53e70 --- /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 = ` + type ${Movie} @node { + 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} @node { + dbId: ID! @id @unique @relayId + name: String! + } + + type ${Actor} @node { + dbId: ID! @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 = ` + query { + ${Movie.plural}(where: { + edges: { + 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?.[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", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection-with-database-name.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection-with-database-name.int.test.ts deleted file mode 100644 index 015eeebe01..0000000000 --- a/packages/graphql/tests/api-v6/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({ 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 = ` - 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/api-v6/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection-with-field-alias.int.test.ts deleted file mode 100644 index ed0fe0d0b3..0000000000 --- a/packages/graphql/tests/api-v6/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({ 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 = ` - 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/api-v6/integration/directives/relayId/relayId-projection.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/relayId/relayId-projection.int.test.ts index 83e75bea07..4e9b71b9ba 100644 --- 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 @@ -158,4 +158,93 @@ describe("RelayId projection", () => { }, }); }); + + test("should return the correct relayId ids using the connection API with aliased fields", async () => { + const connectionQuery = ` + 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", + }, + }, + ], + }, + }, + }, + }, + ], + }, + }); + }); }); From 8dd02cc08ad1a5a0c01a3c6f36420373705cc132 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 20 Jun 2024 12:33:31 +0100 Subject: [PATCH 070/177] PR comment changes --- .../schema-types/StaticSchemaTypes.ts | 5 +- .../alias-relayId/alias-relayId.int.test.ts | 68 ++++++++++--------- .../relayId/relayId-filters.int.test.ts | 68 ++++++++++--------- 3 files changed, 73 insertions(+), 68 deletions(-) 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 index b00f9db176..6dd630931b 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -118,11 +118,12 @@ class StaticFilterTypes { } public get globalIdWhere(): InputTypeComposer { - return this.schemaBuilder.getOrCreateInputType("GlobalIdWhere", (itc) => { + return this.schemaBuilder.getOrCreateInputType("GlobalIdWhere", (_itc) => { return { fields: { - // ...this.createBooleanOperators(itc), equals: GraphQLString, + // TODO: Boolean fields and IN operator: + // ...this.createBooleanOperators(itc), // in: toGraphQLList(toGraphQLNonNull(GraphQLString)), }, }; 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 index 3f2b0ad2e1..8d0217b05f 100644 --- 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 @@ -31,7 +31,7 @@ describe("RelayId projection with alias directive", () => { const Actor = testHelper.createUniqueType("Actor"); beforeAll(async () => { - const typeDefs = ` + const typeDefs = /* GraphQL */ ` type ${Movie} @node { dbId: ID! @id @unique @relayId @alias(property: "serverId") title: String! @@ -71,7 +71,7 @@ describe("RelayId projection with alias directive", () => { }); test("should return the correct relayId ids using the connection API", async () => { - const connectionQuery = ` + const connectionQuery = /* GraphQL */ ` query { ${Movie.plural} { connection { @@ -118,43 +118,45 @@ describe("RelayId projection with alias directive", () => { 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", + 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", + actors: { + connection: { + edges: [ + { + node: { + id: actorGlobalId, + dbId: actorDatabaseID, + 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 index e68dc53e70..b04eefb061 100644 --- 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 @@ -31,7 +31,7 @@ describe("RelayId projection with filters", () => { const Actor = testHelper.createUniqueType("Actor"); beforeAll(async () => { - const typeDefs = ` + const typeDefs = /* GraphQL */ ` type ${Movie} @node { dbId: ID! @id @unique @relayId title: String! @@ -77,7 +77,7 @@ describe("RelayId projection with filters", () => { const genreGlobalId = toGlobalId({ typeName: Genre.name, field: "dbId", id: genreDatabaseID }); const actorGlobalId = toGlobalId({ typeName: Actor.name, field: "dbId", id: actorDatabaseID }); - const connectionQuery = ` + const connectionQuery = /* GraphQL */ ` query { ${Movie.plural}(where: { edges: { @@ -126,43 +126,45 @@ describe("RelayId projection with filters", () => { expect(connectionQueryResult.errors).toBeUndefined(); expect(connectionQueryResult.data).toBeDefined(); - 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", + 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", + actors: { + connection: { + edges: [ + { + node: { + id: actorGlobalId, + dbId: actorDatabaseID, + name: "Keanu", + }, }, - }, - ], + ], + }, }, }, }, - }, - ], + ], + }, }, }); }); From b97bd542f182ba67798b1de9b4d52e35f37b354f Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 19 Jun 2024 13:57:20 +0100 Subject: [PATCH 071/177] introduce validation to v6 API --- .../api-v6/validation/rules/valid-limit.ts | 57 ++++ .../validation/rules/valid-relationship.ts | 107 +++++++ .../api-v6/validation/validate-document.ts | 276 ------------------ .../api-v6/validation/validate-v6-document.ts | 139 +-------- packages/graphql/src/classes/Neo4jGraphQL.ts | 2 + .../features/valid-relationship-n-n.ts | 65 ----- .../schema/validation/validate-document.ts | 2 +- .../invalid-schema/invalid-limit.test.ts | 101 +++++++ .../invalid-relationship.test.ts | 142 +++++++++ 9 files changed, 418 insertions(+), 473 deletions(-) create mode 100644 packages/graphql/src/api-v6/validation/rules/valid-limit.ts create mode 100644 packages/graphql/src/api-v6/validation/rules/valid-relationship.ts delete mode 100644 packages/graphql/src/api-v6/validation/validate-document.ts delete mode 100644 packages/graphql/src/schema/validation/custom-rules/features/valid-relationship-n-n.ts create mode 100644 packages/graphql/tests/api-v6/schema/invalid-schema/invalid-limit.test.ts create mode 100644 packages/graphql/tests/api-v6/schema/invalid-schema/invalid-relationship.test.ts 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-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-document.ts b/packages/graphql/src/api-v6/validation/validate-document.ts deleted file mode 100644 index d4e97e973e..0000000000 --- a/packages/graphql/src/api-v6/validation/validate-document.ts +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { type IResolvers } from "@graphql-tools/utils"; -import type { - DefinitionNode, - DocumentNode, - EnumTypeDefinitionNode, - FieldDefinitionNode, - GraphQLDirective, - GraphQLNamedType, - InputValueDefinitionNode, - InterfaceTypeDefinitionNode, - ObjectTypeDefinitionNode, - TypeNode, - UnionTypeDefinitionNode, -} from "graphql"; -import { GraphQLSchema, Kind, extendSchema, specifiedDirectives, validateSchema } from "graphql"; -import { specifiedSDLRules } from "graphql/validation/specifiedRules"; -import pluralize from "pluralize"; -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 { directiveIsValid } from "../../schema/validation//custom-rules/directives/valid-directive"; -import { ValidDirectiveAtFieldLocation } from "../../schema/validation/custom-rules/directives/valid-directive-field-location"; -import { ValidJwtDirectives } from "../../schema/validation/custom-rules/features/valid-jwt-directives"; -import { ValidRelationshipProperties } from "../../schema/validation/custom-rules/features/valid-relationship-properties"; -import { ValidRelayID } from "../../schema/validation/custom-rules/features/valid-relay-id"; -import { ReservedTypeNames } from "../../schema/validation/custom-rules/valid-types/reserved-type-names"; -import { - DirectiveCombinationValid, - SchemaOrTypeDirectives, -} 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 { isRootType } from "../../utils/is-root-type"; - -function filterDocument(document: DocumentNode): DocumentNode { - const nodeNames = document.definitions - .filter((definition) => { - if (definition.kind === Kind.OBJECT_TYPE_DEFINITION) { - if (!isRootType(definition)) { - return true; - } - } - return false; - }) - .map((definition) => (definition as ObjectTypeDefinitionNode).name.value); - - const getArgumentType = (type: TypeNode): string => { - if (type.kind === Kind.LIST_TYPE) { - return getArgumentType(type.type); - } - - if (type.kind === Kind.NON_NULL_TYPE) { - return getArgumentType(type.type); - } - - return type.name.value; - }; - - const filterInputTypes = ( - fields: readonly InputValueDefinitionNode[] | undefined - ): InputValueDefinitionNode[] | undefined => { - return fields?.filter((f) => { - const type = getArgumentType(f.type); - - const nodeMatch = - /(?.+)(?:ConnectInput|ConnectWhere|CreateInput|DeleteInput|DisconnectInput|Options|RelationInput|Sort|UpdateInput|Where)/gm.exec( - type - ); - if (nodeMatch?.groups?.nodeName) { - if (nodeNames.includes(nodeMatch.groups.nodeName)) { - return false; - } - } - - return true; - }); - }; - - const filterFields = (fields: readonly FieldDefinitionNode[] | undefined): FieldDefinitionNode[] | undefined => { - return fields - ?.filter((field) => { - const type = getArgumentType(field.type); - const match = /(?:Create|Update)(?.+)MutationResponse/gm.exec(type); - if (match?.groups?.nodeName) { - if (nodeNames.map((nodeName) => pluralize(nodeName)).includes(match.groups.nodeName)) { - return false; - } - } - return true; - }) - .map((field) => { - return { - ...field, - arguments: filterInputTypes(field.arguments), - }; - }); - }; - - const filteredDocument: DocumentNode = { - ...document, - definitions: document.definitions.reduce((res: DefinitionNode[], def) => { - if (def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { - const fields = filterInputTypes(def.fields); - - if (!fields?.length) { - return res; - } - - return [ - ...res, - { - ...def, - fields, - }, - ]; - } - - if (def.kind === Kind.OBJECT_TYPE_DEFINITION || def.kind === Kind.INTERFACE_TYPE_DEFINITION) { - if (!def.fields?.length) { - return [...res, def]; - } - - const fields = filterFields(def.fields); - if (!fields?.length) { - return res; - } - - return [ - ...res, - { - ...def, - fields, - }, - ]; - } - - return [...res, def]; - }, []), - }; - - return filteredDocument; -} - -function runNeo4jValidationRules({ - schema, - document, - extra, - userCustomResolvers, - features, -}: { - schema: GraphQLSchema; - document: DocumentNode; - extra: { - enums?: EnumTypeDefinitionNode[]; - interfaces?: InterfaceTypeDefinitionNode[]; - unions?: UnionTypeDefinitionNode[]; - objects?: ObjectTypeDefinitionNode[]; - }; - userCustomResolvers?: IResolvers | Array; - features: Neo4jFeaturesSettings | undefined; -}) { - const errors = validateSDL( - document, - [ - ...specifiedSDLRules, - directiveIsValid(extra, features?.populatedBy?.callbacks), - ValidDirectiveAtFieldLocation, - DirectiveCombinationValid, - // SchemaOrTypeDirectives, - // ValidJwtDirectives, - // ValidRelayID, - ValidRelationshipProperties, - // ValidRelationshipDeclaration, - // ValidFieldTypes, - ReservedTypeNames, - // ValidObjectType, - // ValidDirectiveInheritance, - // DirectiveArgumentOfCorrectType(false), - // WarnIfAuthorizationFeatureDisabled(features?.authorization), - WarnIfListOfListsFieldDefinition, - // WarnIfAMaxLimitCanBeBypassedThroughInterface(), - // WarnObjectFieldsWithoutResolver({ - // customResolvers: asArray(userCustomResolvers ?? []), - // }), - ], - schema - ); - const filteredErrors = errors.filter((e) => e.message !== "Query root type must be provided."); - if (filteredErrors.length) { - throw filteredErrors; - } -} - -export function validateV6Document({ - document, - features, - additionalDefinitions, - userCustomResolvers, -}: { - document: DocumentNode; - features: Neo4jFeaturesSettings | undefined; - additionalDefinitions: { - additionalDirectives?: Array; - additionalTypes?: Array; - enums?: EnumTypeDefinitionNode[]; - interfaces?: InterfaceTypeDefinitionNode[]; - unions?: UnionTypeDefinitionNode[]; - objects?: ObjectTypeDefinitionNode[]; - }; - userCustomResolvers?: IResolvers | Array; -}): void { - const filteredDocument = filterDocument(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 || []), - ], - }); - - runNeo4jValidationRules({ - schema: schemaToExtend, - document: filteredDocument, - extra, - userCustomResolvers, - features, - }); - - 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/api-v6/validation/validate-v6-document.ts b/packages/graphql/src/api-v6/validation/validate-v6-document.ts index 0e2b2722ab..b3d1c0f0fe 100644 --- a/packages/graphql/src/api-v6/validation/validate-v6-document.ts +++ b/packages/graphql/src/api-v6/validation/validate-v6-document.ts @@ -18,21 +18,16 @@ */ import type { - DefinitionNode, DocumentNode, EnumTypeDefinitionNode, - FieldDefinitionNode, GraphQLDirective, GraphQLNamedType, - InputValueDefinitionNode, InterfaceTypeDefinitionNode, ObjectTypeDefinitionNode, - TypeNode, UnionTypeDefinitionNode, } from "graphql"; -import { GraphQLSchema, Kind, extendSchema, specifiedDirectives, validateSchema } from "graphql"; +import { GraphQLSchema, extendSchema, specifiedDirectives, validateSchema } from "graphql"; import { specifiedSDLRules } from "graphql/validation/specifiedRules"; -import pluralize from "pluralize"; import * as directives from "../../graphql/directives"; import { typeDependantDirectivesScaffolds } from "../../graphql/directives/type-dependant-directives/scaffolds"; import { SortDirection } from "../../graphql/enums/SortDirection"; @@ -43,131 +38,18 @@ 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 { directiveIsValid } from "../../schema/validation/custom-rules/directives/valid-directive"; -import { ValidDirectiveAtFieldLocation } from "../../schema/validation/custom-rules/directives/valid-directive-field-location"; -import { ValidRelationshipNtoN } from "../../schema/validation/custom-rules/features/valid-relationship-n-n"; 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 { isRootType } from "../../utils/is-root-type"; - -function filterDocument(document: DocumentNode): DocumentNode { - const nodeNames = document.definitions - .filter((definition) => { - if (definition.kind === Kind.OBJECT_TYPE_DEFINITION) { - if (!isRootType(definition)) { - return true; - } - } - return false; - }) - .map((definition) => (definition as ObjectTypeDefinitionNode).name.value); - - const getArgumentType = (type: TypeNode): string => { - if (type.kind === Kind.LIST_TYPE) { - return getArgumentType(type.type); - } - - if (type.kind === Kind.NON_NULL_TYPE) { - return getArgumentType(type.type); - } - - return type.name.value; - }; - - const filterInputTypes = ( - fields: readonly InputValueDefinitionNode[] | undefined - ): InputValueDefinitionNode[] | undefined => { - return fields?.filter((f) => { - const type = getArgumentType(f.type); - - const nodeMatch = - /(?.+)(?:ConnectInput|ConnectWhere|CreateInput|DeleteInput|DisconnectInput|Options|RelationInput|Sort|UpdateInput|Where)/gm.exec( - type - ); - if (nodeMatch?.groups?.nodeName) { - if (nodeNames.includes(nodeMatch.groups.nodeName)) { - return false; - } - } - - return true; - }); - }; - - const filterFields = (fields: readonly FieldDefinitionNode[] | undefined): FieldDefinitionNode[] | undefined => { - return fields - ?.filter((field) => { - const type = getArgumentType(field.type); - const match = /(?:Create|Update)(?.+)MutationResponse/gm.exec(type); - if (match?.groups?.nodeName) { - if (nodeNames.map((nodeName) => pluralize(nodeName)).includes(match.groups.nodeName)) { - return false; - } - } - return true; - }) - .map((field) => { - return { - ...field, - arguments: filterInputTypes(field.arguments), - }; - }); - }; - - const filteredDocument: DocumentNode = { - ...document, - definitions: document.definitions.reduce((res: DefinitionNode[], def) => { - if (def.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION) { - const fields = filterInputTypes(def.fields); - - if (!fields?.length) { - return res; - } - - return [ - ...res, - { - ...def, - fields, - }, - ]; - } - - if (def.kind === Kind.OBJECT_TYPE_DEFINITION || def.kind === Kind.INTERFACE_TYPE_DEFINITION) { - if (!def.fields?.length) { - return [...res, def]; - } - - const fields = filterFields(def.fields); - if (!fields?.length) { - return res; - } - - return [ - ...res, - { - ...def, - fields, - }, - ]; - } - - return [...res, def]; - }, []), - }; - - return filteredDocument; -} +import { ValidLimit } from "./rules/valid-limit"; +import { ValidRelationship } from "./rules/valid-relationship"; function runNeo4jGraphQLValidationRules({ schema, document, - extra, - features, }: { schema: GraphQLSchema; document: DocumentNode; @@ -177,15 +59,13 @@ function runNeo4jGraphQLValidationRules({ unions?: UnionTypeDefinitionNode[]; objects?: ObjectTypeDefinitionNode[]; }; - features: Neo4jFeaturesSettings | undefined; }) { const errors = validateSDL( document, [ ...specifiedSDLRules, - ValidRelationshipNtoN, - directiveIsValid(extra), - ValidDirectiveAtFieldLocation, + ValidRelationship, + ValidLimit, DirectiveCombinationValid, ValidRelationshipProperties, ReservedTypeNames, @@ -193,15 +73,13 @@ function runNeo4jGraphQLValidationRules({ ], schema ); - const filteredErrors = errors.filter((e) => e.message !== "Query root type must be provided."); - if (filteredErrors.length) { - throw filteredErrors; + if (errors.length) { + throw errors; } } export function validateV6Document({ document, - features, additionalDefinitions, }: { document: DocumentNode; @@ -215,7 +93,7 @@ export function validateV6Document({ objects?: ObjectTypeDefinitionNode[]; }; }): void { - const filteredDocument = filterDocument(document); + const filteredDocument = document; const { additionalDirectives, additionalTypes, ...extra } = additionalDefinitions; const schemaToExtend = new GraphQLSchema({ directives: [ @@ -241,7 +119,6 @@ export function validateV6Document({ schema: schemaToExtend, document: filteredDocument, extra, - features, }); const schema = extendSchema(schemaToExtend, filteredDocument); diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index 7b14acfdb0..e625ce963b 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -121,6 +121,7 @@ class Neo4jGraphQL { @Memoize() public getAuraSchema(): Promise { const document = this.normalizeTypeDefinitions(this.typeDefs); + if (this.validate) { const { enumTypes: enums, @@ -135,6 +136,7 @@ class Neo4jGraphQL { additionalDefinitions: { enums, interfaces, unions, objects }, }); } + this.schemaModel = this.generateSchemaModel(document, true); const schemaGenerator = new SchemaGenerator(); diff --git a/packages/graphql/src/schema/validation/custom-rules/features/valid-relationship-n-n.ts b/packages/graphql/src/schema/validation/custom-rules/features/valid-relationship-n-n.ts deleted file mode 100644 index 4cf726d1ce..0000000000 --- a/packages/graphql/src/schema/validation/custom-rules/features/valid-relationship-n-n.ts +++ /dev/null @@ -1,65 +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 { ASTVisitor, FieldDefinitionNode, TypeNode } from "graphql"; -import { Kind } from "graphql"; -import type { SDLValidationContext } from "graphql/validation/ValidationContext"; -import { relationshipDirective } from "../../../../graphql/directives"; -import { DocumentValidationError, assertValid, createGraphQLError } from "../utils/document-validation-error"; -import { getPathToNode } from "../utils/path-parser"; - -export function ValidRelationshipNtoN(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(() => { - if (!isListType(type)) { - throw new DocumentValidationError(`@relationship can only be used on List target`, []); - } - }); - const [pathToNode] = getPathToNode(path, ancestors); - if (!isValid) { - context.reportError( - createGraphQLError({ - nodes: [fieldDefinitionNode], - path: [...pathToNode, fieldDefinitionNode.name.value, ...errorPath], - errorMsg, - }) - ); - } - }, - }; -} - -function isListType(type: TypeNode): boolean { - if (type.kind === Kind.NON_NULL_TYPE) { - return type.type.kind === Kind.LIST_TYPE; - } - return type.kind === Kind.LIST_TYPE; -} 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/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-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(); + }); +}); From de98ad09e4b2c22d6f5ddc44eb1878afb844e38e Mon Sep 17 00:00:00 2001 From: angrykoala Date: Mon, 24 Jun 2024 14:44:48 +0100 Subject: [PATCH 072/177] Add node query endpoint --- .../api-v6/schema-generation/SchemaBuilder.ts | 31 +++++++++++++++++-- .../schema-generation/SchemaGenerator.ts | 23 ++++++++++++++ .../schema-types/StaticSchemaTypes.ts | 18 ++++++++++- .../schema-types/TopLevelEntitySchemaTypes.ts | 8 ++++- .../api-v6/schema/directives/relayId.test.ts | 8 ++++- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index d7b8df0871..d7536af9cf 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -28,6 +28,7 @@ import type { import type { EnumTypeComposer, InputTypeComposer, + InterfaceTypeComposer, ListComposer, NonNullComposer, ObjectTypeComposer, @@ -76,10 +77,33 @@ export class SchemaBuilder { 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); } @@ -144,17 +168,20 @@ export class SchemaBuilder { type, args, resolver, + description, }: { name: string; - type: ObjectTypeComposer; - args: Record; + type: ObjectTypeComposer | InterfaceTypeComposer; + args: Record; resolver: (...args: any[]) => any; + description?: string; }): void { this.composer.Query.addFields({ [name]: { type: type, args, resolve: resolver, + description, }, }); } diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index c3e9ef9282..af498fac01 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -36,9 +36,32 @@ export class SchemaGenerator { public generate(schemaModel: Neo4jGraphQLSchemaModel): GraphQLSchema { const staticTypes = new StaticSchemaTypes({ schemaBuilder: this.schemaBuilder }); this.generateEntityTypes(schemaModel, staticTypes); + // Taken from makeAugmentedSchema + // const hasGlobalNodes = addGlobalNodeFields(nodes, composer, schemaModel.concreteEntities); + this.generateGlobalNodeQuery(schemaModel, staticTypes); + return this.schemaBuilder.build(); } + private generateGlobalNodeQuery(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { + for (const entity of schemaModel.entities.values()) { + if (entity.isConcreteEntity() && entity.globalIdField) { + this.schemaBuilder.addQueryField({ + name: "node", + type: staticTypes.globalNodeInterface, + args: { + id: "ID!", + }, + description: "Fetches an object given its ID", + resolver() { + console.log("RESOLVER"); + }, + }); + return; + } + } + } + private generateEntityTypes(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { const resultMap = new Map(); const schemaTypes = new SchemaTypes({ 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 index 6dd630931b..158a83ca27 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -19,7 +19,13 @@ import type { GraphQLInputType, GraphQLScalarType } from "graphql"; import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; -import type { EnumTypeComposer, InputTypeComposer, ListComposer, ObjectTypeComposer } from "graphql-compose"; +import type { + EnumTypeComposer, + InputTypeComposer, + InterfaceTypeComposer, + ListComposer, + ObjectTypeComposer, +} from "graphql-compose"; import { Memoize } from "typescript-memoize"; import { CartesianPointDistance } from "../../../graphql/input-objects/CartesianPointDistance"; import { CartesianPointInput } from "../../../graphql/input-objects/CartesianPointInput"; @@ -76,6 +82,16 @@ export class StaticSchemaTypes { public get sortDirection(): EnumTypeComposer { return this.schemaBuilder.createEnumType("SortDirection", ["ASC", "DESC"]); } + + public get globalNodeInterface(): InterfaceTypeComposer { + return this.schemaBuilder.getOrCreateInterfaceType("Node", () => { + return { + fields: { + id: "ID!", + }, + }; + }); + } } class StaticFilterTypes { 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 index a908bceddd..9bf7fc31ab 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -18,7 +18,7 @@ */ import { type GraphQLResolveInfo } from "graphql"; -import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; +import type { InputTypeComposer, InterfaceTypeComposer, ObjectTypeComposer } 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"; @@ -107,8 +107,14 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { startsWith: ID } - type Movie { + type Movie implements Node { dbId: ID! id: ID! title: String @@ -110,6 +110,10 @@ describe("RelayId", () => { title: StringWhere } + interface Node { + id: ID! + } + type PageInfo { endCursor: String hasNextPage: Boolean @@ -119,6 +123,8 @@ describe("RelayId", () => { type Query { movies(where: MovieOperationWhere): MovieOperation + \\"\\"\\"Fetches an object given its ID\\"\\"\\" + node(id: ID!): Node } enum SortDirection { From d136dfda6c8eee76c90c6d1a614c7af9806ce443 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 25 Jun 2024 10:29:32 +0100 Subject: [PATCH 073/177] move global id resolver --- ...al-id-resolver.ts => global-id-field-resolver.ts} | 2 +- .../src/api-v6/schema-generation/SchemaGenerator.ts | 12 +++++++++--- .../schema-types/TopLevelEntitySchemaTypes.ts | 4 ++-- 3 files changed, 12 insertions(+), 6 deletions(-) rename packages/graphql/src/api-v6/resolvers/{global-id-resolver.ts => global-id-field-resolver.ts} (90%) diff --git a/packages/graphql/src/api-v6/resolvers/global-id-resolver.ts b/packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts similarity index 90% rename from packages/graphql/src/api-v6/resolvers/global-id-resolver.ts rename to packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts index d4e21083df..f0191bfe5c 100644 --- a/packages/graphql/src/api-v6/resolvers/global-id-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts @@ -4,7 +4,7 @@ import type { ConnectionQueryArgs } from "../../types"; import { toGlobalId } from "../../utils/global-ids"; /** Maps the database id to globalId*/ -export function generateGlobalIdResolver({ entity }: { entity: ConcreteEntity }) { +export function generateGlobalIdFieldResolver({ entity }: { entity: ConcreteEntity }) { return function resolve(source, _args: ConnectionQueryArgs, _ctx, _info: GraphQLResolveInfo) { const globalAttribute = entity.globalIdField; if (!globalAttribute) { diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index af498fac01..11e818c00e 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -17,9 +17,10 @@ * limitations under the License. */ -import type { GraphQLSchema } from "graphql"; +import type { GraphQLResolveInfo, GraphQLSchema } from "graphql"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; import { generateReadResolver } from "../resolvers/read-resolver"; import { SchemaBuilder } from "./SchemaBuilder"; import { SchemaTypes } from "./schema-types/SchemaTypes"; @@ -46,6 +47,10 @@ export class SchemaGenerator { private generateGlobalNodeQuery(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { for (const entity of schemaModel.entities.values()) { if (entity.isConcreteEntity() && entity.globalIdField) { + // const globalEntities = schemaModel.concreteEntities + // .map((e) => new ConcreteEntityAdapter(e)) + // .filter((e) => e.isGlobalNode()); + this.schemaBuilder.addQueryField({ name: "node", type: staticTypes.globalNodeInterface, @@ -53,8 +58,9 @@ export class SchemaGenerator { id: "ID!", }, description: "Fetches an object given its ID", - resolver() { - console.log("RESOLVER"); + resolver(_root: any, args: any, context: Neo4jGraphQLTranslationContext, info: GraphQLResolveInfo) { + // TODO + return undefined; }, }); return; 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 index 9bf7fc31ab..c6e8c03073 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -33,7 +33,7 @@ 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 { generateGlobalIdResolver } from "../../resolvers/global-id-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 { EntitySchemaTypes } from "./EntitySchemaTypes"; @@ -204,7 +204,7 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes Date: Tue, 25 Jun 2024 14:09:52 +0100 Subject: [PATCH 074/177] Global node endpoint --- .../GlobalNodeResolveTreeParser.ts | 68 +++++++++++++++++++ .../parse-resolve-info-tree.ts | 12 ++++ .../resolvers/global-id-field-resolver.ts | 2 +- .../api-v6/resolvers/global-node-resolver.ts | 51 ++++++++++++++ .../api-v6/schema-generation/SchemaBuilder.ts | 5 ++ .../schema-generation/SchemaGenerator.ts | 13 ++-- packages/graphql/src/types/index.ts | 4 ++ 7 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts create mode 100644 packages/graphql/src/api-v6/resolvers/global-node-resolver.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts new file mode 100644 index 0000000000..cfabfa63fa --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts @@ -0,0 +1,68 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; +import type { GraphQLTreeReadOperation } from "./graphql-tree"; + +export class GlobalNodeResolveTreeParser extends TopLevelResolveTreeParser { + constructor({ entity }: { entity: ConcreteEntity }) { + super({ entity: entity }); + } + + /** Parse a resolveTree into a Neo4j GraphQLTree */ + public parseOperation(resolveTree: ResolveTree): GraphQLTreeReadOperation { + const entityTypes = this.targetNode.typeNames; + resolveTree.fieldsByTypeName[entityTypes.node] = { + ...resolveTree.fieldsByTypeName["Node"], + ...resolveTree.fieldsByTypeName[entityTypes.node], + }; + const node = resolveTree ? this.parseNode(resolveTree) : undefined; + + return { + alias: resolveTree.alias, + args: { + where: { + edges: { + 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-resolve-info-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-resolve-info-tree.ts index 9edac75c0d..6c7eff8292 100644 --- 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 @@ -19,6 +19,7 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import { GlobalNodeResolveTreeParser } from "./GlobalNodeResolveTreeParser"; import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; import type { GraphQLTree } from "./graphql-tree"; @@ -32,3 +33,14 @@ export function parseResolveInfoTree({ const parser = new TopLevelResolveTreeParser({ entity }); return parser.parseOperation(resolveTree); } + +export function parseGlobalNodeResolveInfoTree({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTree { + const parser = new GlobalNodeResolveTreeParser({ entity }); + return parser.parseOperation(resolveTree); +} 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 index f0191bfe5c..6602a6de52 100644 --- a/packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts @@ -3,7 +3,7 @@ import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; import type { ConnectionQueryArgs } from "../../types"; import { toGlobalId } from "../../utils/global-ids"; -/** Maps the database id to globalId*/ +/** 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; 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..4dd7f478ec --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts @@ -0,0 +1,51 @@ +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-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/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index d7536af9cf..cc1d29ecd0 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -110,6 +110,11 @@ export class SchemaBuilder { if (description) { tc.setDescription(description); } + + // This is used for global node, not sure if needed for other interfaces + tc.setResolveType((obj) => { + return obj.__resolveType; + }); }); } diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index 11e818c00e..588ecacc1b 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -17,10 +17,10 @@ * limitations under the License. */ -import type { GraphQLResolveInfo, GraphQLSchema } from "graphql"; +import type { GraphQLSchema } from "graphql"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; -import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; +import { generateGlobalNodeResolver } from "../resolvers/global-node-resolver"; import { generateReadResolver } from "../resolvers/read-resolver"; import { SchemaBuilder } from "./SchemaBuilder"; import { SchemaTypes } from "./schema-types/SchemaTypes"; @@ -47,9 +47,7 @@ export class SchemaGenerator { private generateGlobalNodeQuery(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { for (const entity of schemaModel.entities.values()) { if (entity.isConcreteEntity() && entity.globalIdField) { - // const globalEntities = schemaModel.concreteEntities - // .map((e) => new ConcreteEntityAdapter(e)) - // .filter((e) => e.isGlobalNode()); + const globalEntities = schemaModel.concreteEntities.filter((e) => e.globalIdField); this.schemaBuilder.addQueryField({ name: "node", @@ -58,10 +56,7 @@ export class SchemaGenerator { id: "ID!", }, description: "Fetches an object given its ID", - resolver(_root: any, args: any, context: Neo4jGraphQLTranslationContext, info: GraphQLResolveInfo) { - // TODO - return undefined; - }, + resolver: generateGlobalNodeResolver({ globalEntities }), }); return; } diff --git a/packages/graphql/src/types/index.ts b/packages/graphql/src/types/index.ts index e2b6676890..01b6fa89d1 100644 --- a/packages/graphql/src/types/index.ts +++ b/packages/graphql/src/types/index.ts @@ -217,6 +217,10 @@ export interface ConnectionQueryArgs { sort?: ConnectionSortArg[]; } +export interface GlobalNodeArgs { + id: string; +} + /** * Representation of the options arg * passed to resolvers. From f8bf5825e9cc08bd002c99d8bc3ba3a93025b07a Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 28 Jun 2024 13:49:28 +0100 Subject: [PATCH 075/177] Add tests on global node endpoint --- .../relayId/global-node-query.int.test.ts | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 packages/graphql/tests/api-v6/integration/directives/relayId/global-node-query.int.test.ts 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..79bb9391c9 --- /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 = ` + type ${Movie} @node { + 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} @node { + dbId: ID! @id @unique @relayId + name: String! + } + + type ${Actor} @node { + dbId: ID! @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 = ` + 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 = ` + 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", + }, + }, + ], + }, + }, + }, + }); + }); +}); From d7462a34cddc52a9fd0678d3fdb68bc34138b24c Mon Sep 17 00:00:00 2001 From: angrykoala Date: Mon, 1 Jul 2024 11:23:38 +0100 Subject: [PATCH 076/177] Improve global node generator --- .../schema-generation/SchemaGenerator.ts | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index 588ecacc1b..b47d6cbc4d 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -37,32 +37,11 @@ export class SchemaGenerator { public generate(schemaModel: Neo4jGraphQLSchemaModel): GraphQLSchema { const staticTypes = new StaticSchemaTypes({ schemaBuilder: this.schemaBuilder }); this.generateEntityTypes(schemaModel, staticTypes); - // Taken from makeAugmentedSchema - // const hasGlobalNodes = addGlobalNodeFields(nodes, composer, schemaModel.concreteEntities); this.generateGlobalNodeQuery(schemaModel, staticTypes); return this.schemaBuilder.build(); } - private generateGlobalNodeQuery(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { - for (const entity of schemaModel.entities.values()) { - if (entity.isConcreteEntity() && entity.globalIdField) { - const globalEntities = schemaModel.concreteEntities.filter((e) => e.globalIdField); - - this.schemaBuilder.addQueryField({ - name: "node", - type: staticTypes.globalNodeInterface, - args: { - id: "ID!", - }, - description: "Fetches an object given its ID", - resolver: generateGlobalNodeResolver({ globalEntities }), - }); - return; - } - } - } - private generateEntityTypes(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { const resultMap = new Map(); const schemaTypes = new SchemaTypes({ @@ -86,4 +65,20 @@ export class SchemaGenerator { entitySchemaTypes.addTopLevelQueryField(resolver); } } + + private generateGlobalNodeQuery(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { + const globalEntities = schemaModel.concreteEntities.filter((e) => e.globalIdField); + + if (globalEntities.length > 0) { + this.schemaBuilder.addQueryField({ + name: "node", + type: staticTypes.globalNodeInterface, + args: { + id: "ID!", + }, + description: "Fetches an object given its ID", + resolver: generateGlobalNodeResolver({ globalEntities }), + }); + } + } } From 3b34d0552edcb3e9d24562e1c3399b59b5eb4b5e Mon Sep 17 00:00:00 2001 From: angrykoala Date: Mon, 1 Jul 2024 13:43:19 +0100 Subject: [PATCH 077/177] Use typeComposer types for scalar types --- .../api-v6/schema-generation/SchemaBuilder.ts | 18 ++++--- .../schema-generation/SchemaBuilderTypes.ts | 34 +++++++++++++ .../schema-generation/SchemaGenerator.ts | 35 ++++++++----- .../schema-types/EntitySchemaTypes.ts | 10 ++-- .../schema-types/RelatedEntitySchemaTypes.ts | 2 +- .../schema-types/StaticSchemaTypes.ts | 51 ++++++++++--------- .../schema-types/TopLevelEntitySchemaTypes.ts | 4 +- 7 files changed, 101 insertions(+), 53 deletions(-) create mode 100644 packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index cc1d29ecd0..1ff2e8697a 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -32,20 +32,22 @@ import type { ListComposer, NonNullComposer, ObjectTypeComposer, + ScalarTypeComposer, } from "graphql-compose"; import { SchemaComposer } from "graphql-compose"; +import { SchemaBuilderTypes } from "./SchemaBuilderTypes"; -export type TypeDefinition = string | ListComposer | ObjectTypeComposer; +export type TypeDefinition = string | WrappedComposer; type ObjectOrInputTypeComposer = ObjectTypeComposer | InputTypeComposer; -type ListOrNullComposer = +type ListOrNullComposer = | ListComposer | ListComposer> | NonNullComposer | NonNullComposer>; -type WrappedComposer = T | ListOrNullComposer; +type WrappedComposer = T | ListOrNullComposer; export type GraphQLResolver = (...args) => any; @@ -58,10 +60,12 @@ export type FieldDefinition = { }; 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 { @@ -75,7 +79,7 @@ export class SchemaBuilder { public getOrCreateObjectType( name: string, onCreate: () => { - fields: Record>; + fields: Record>; description?: string; iface?: InterfaceTypeComposer; } @@ -97,7 +101,7 @@ export class SchemaBuilder { public getOrCreateInterfaceType( name: string, onCreate: () => { - fields: Record>; + fields: Record>; description?: string; } ): InterfaceTypeComposer { @@ -128,7 +132,7 @@ export class SchemaBuilder { | GraphQLInputType | GraphQLList | GraphQLNonNull - | WrappedComposer + | WrappedComposer >; description?: string; } @@ -177,7 +181,7 @@ export class SchemaBuilder { }: { name: string; type: ObjectTypeComposer | InterfaceTypeComposer; - args: Record; + args: Record>; resolver: (...args: any[]) => any; description?: string; }): void { 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..a7a6a77de3 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts @@ -0,0 +1,34 @@ +import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; +import type { SchemaComposer } from "graphql-compose"; +import { ScalarTypeComposer } from "graphql-compose"; +import { Memoize } from "typescript-memoize"; + +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 string(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLString, this.composer); + } + @Memoize() + public get boolean(): ScalarTypeComposer { + return new ScalarTypeComposer(GraphQLBoolean, this.composer); + } +} diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index b47d6cbc4d..0e535f76e8 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { GraphQLSchema } from "graphql"; +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"; @@ -29,25 +29,29 @@ import { TopLevelEntitySchemaTypes } from "./schema-types/TopLevelEntitySchemaTy 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 staticTypes = new StaticSchemaTypes({ schemaBuilder: this.schemaBuilder }); - this.generateEntityTypes(schemaModel, staticTypes); - this.generateGlobalNodeQuery(schemaModel, staticTypes); + const entityTypesMap = this.generateEntityTypes(schemaModel); + this.generateTopLevelQueryFields(entityTypesMap); + + this.generateGlobalNodeQueryField(schemaModel); return this.schemaBuilder.build(); } - private generateEntityTypes(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { - const resultMap = new Map(); + private generateEntityTypes(schemaModel: Neo4jGraphQLSchemaModel): Map { + const entityTypesMap = new Map(); const schemaTypes = new SchemaTypes({ - staticTypes, - entitySchemas: resultMap, + staticTypes: this.staticTypes, + entitySchemas: entityTypesMap, }); + for (const entity of schemaModel.entities.values()) { if (entity.isConcreteEntity()) { const entitySchemaTypes = new TopLevelEntitySchemaTypes({ @@ -55,10 +59,15 @@ export class SchemaGenerator { schemaBuilder: this.schemaBuilder, schemaTypes, }); - resultMap.set(entity, entitySchemaTypes); + entityTypesMap.set(entity, entitySchemaTypes); } } - for (const [entity, entitySchemaTypes] of resultMap.entries()) { + + return entityTypesMap; + } + + private generateTopLevelQueryFields(entityTypesMap: Map): void { + for (const [entity, entitySchemaTypes] of entityTypesMap.entries()) { const resolver = generateReadResolver({ entity, }); @@ -66,15 +75,15 @@ export class SchemaGenerator { } } - private generateGlobalNodeQuery(schemaModel: Neo4jGraphQLSchemaModel, staticTypes: StaticSchemaTypes): void { + private generateGlobalNodeQueryField(schemaModel: Neo4jGraphQLSchemaModel): void { const globalEntities = schemaModel.concreteEntities.filter((e) => e.globalIdField); if (globalEntities.length > 0) { this.schemaBuilder.addQueryField({ name: "node", - type: staticTypes.globalNodeInterface, + type: this.staticTypes.globalNodeInterface, args: { - id: "ID!", + 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/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index c885f791e8..e562b57b60 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -17,9 +17,7 @@ * limitations under the License. */ -import type { GraphQLScalarType } from "graphql"; -import { GraphQLInt, GraphQLString } from "graphql"; -import type { InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; +import type { InputTypeComposer, ObjectTypeComposer, ScalarTypeComposer } from "graphql-compose"; import { connectionOperationResolver } from "../../resolvers/connection-operation-resolver"; import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; @@ -47,9 +45,9 @@ export abstract class EntitySchemaTypes { public get connectionOperation(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connectionOperation, () => { - const args: { first: GraphQLScalarType; after: GraphQLScalarType; sort?: InputTypeComposer } = { - first: GraphQLInt, - after: GraphQLString, + const args: { first: ScalarTypeComposer; after: ScalarTypeComposer; sort?: InputTypeComposer } = { + first: this.schemaBuilder.types.int, + after: this.schemaBuilder.types.string, }; if (this.isSortable()) { args.sort = this.connectionSort; 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 index 3e1ea63ad1..89a0c8741d 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -69,7 +69,7 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { return { fields: { - hasNextPage: "Boolean", - hasPreviousPage: "Boolean", - startCursor: "String", - endCursor: "String", + hasNextPage: this.schemaBuilder.types.boolean, + hasPreviousPage: this.schemaBuilder.types.boolean, + startCursor: this.schemaBuilder.types.string, + endCursor: this.schemaBuilder.types.string, }, }; }); @@ -87,7 +88,7 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateInterfaceType("Node", () => { return { fields: { - id: "ID!", + id: this.schemaBuilder.types.id.NonNull, }, }; }); @@ -106,7 +107,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("StringListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLString), + equals: this.schemaBuilder.types.string.List, }, }; }); @@ -115,7 +116,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLString)), + equals: this.schemaBuilder.types.string.NonNull.List, }, }; }); @@ -126,8 +127,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createStringOperators(GraphQLString), - in: toGraphQLList(toGraphQLNonNull(GraphQLString)), + ...this.createStringOperators(this.schemaBuilder.types.string), + in: this.schemaBuilder.types.string.NonNull.List, }, }; }); @@ -137,7 +138,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("GlobalIdWhere", (_itc) => { return { fields: { - equals: GraphQLString, + equals: this.schemaBuilder.types.string, // TODO: Boolean fields and IN operator: // ...this.createBooleanOperators(itc), // in: toGraphQLList(toGraphQLNonNull(GraphQLString)), @@ -375,8 +376,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createStringOperators(GraphQLID), - in: toGraphQLList(toGraphQLNonNull(GraphQLID)), + ...this.createStringOperators(this.schemaBuilder.types.id), + in: this.schemaBuilder.types.id.NonNull.List, }, }; }); @@ -387,7 +388,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("IntListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLInt), + equals: this.schemaBuilder.types.int.List, }, }; }); @@ -396,7 +397,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("IntListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLInt)), + equals: this.schemaBuilder.types.int.NonNull.List, }, }; }); @@ -406,8 +407,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createNumericOperators(GraphQLInt), - in: toGraphQLList(toGraphQLNonNull(GraphQLInt)), + ...this.createNumericOperators(this.schemaBuilder.types.int), + in: this.schemaBuilder.types.int.NonNull.List, }, }; }); @@ -450,7 +451,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("FloatListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLFloat), + equals: this.schemaBuilder.types.float.List, }, }; }); @@ -459,7 +460,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("FloatListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLFloat)), + equals: this.schemaBuilder.types.float.NonNull.List, }, }; }); @@ -470,8 +471,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createNumericOperators(GraphQLFloat), - in: toGraphQLList(toGraphQLNonNull(GraphQLFloat)), + ...this.createNumericOperators(this.schemaBuilder.types.float), + in: this.schemaBuilder.types.float.NonNull.List, }, }; }); @@ -553,7 +554,7 @@ class StaticFilterTypes { }); } - private createStringOperators(type: GraphQLScalarType): Record { + private createStringOperators(type: ScalarTypeComposer): Record { return { equals: type, // matches: type, @@ -563,7 +564,9 @@ class StaticFilterTypes { }; } - private createNumericOperators(type: GraphQLInputType): Record { + private createNumericOperators( + type: GraphQLInputType | ScalarTypeComposer + ): Record { return { equals: type, lt: type, 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 index c6e8c03073..4626463fec 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -86,7 +86,7 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes Date: Wed, 3 Jul 2024 10:37:51 +0100 Subject: [PATCH 078/177] comment out the spatial filters waiting for proper design --- .../schema-types/StaticSchemaTypes.ts | 152 +++++++++--------- .../filter-schema-types/FilterSchemaTypes.ts | 44 +++-- .../cartesian-point-2d-equals.int.test.ts | 3 +- .../cartesian-point-3d-equals.int.test.ts | 3 +- .../cartesian-point-2d-equals.int.test.ts | 3 +- .../cartesian-point-2d-gt.int.test.ts | 3 +- .../cartesian-point-2d-in.int.test.ts | 3 +- .../cartesian-point-2d-lt.int.test.ts | 3 +- .../cartesian-point-3d-equals.int.test.ts | 3 +- .../cartesian-point-3d-gt.int.test.ts | 3 +- .../cartesian-point-3d-lt.int.test.ts | 3 +- .../point/array/point-2d-equals.int.test.ts | 3 +- .../point/array/point-3d-equals.int.test.ts | 5 +- .../types/point/point-2d-equals.int.test.ts | 3 +- .../types/point/point-2d-gt.int.test.ts | 3 +- .../types/point/point-2d-in.int.test.ts | 3 +- .../types/point/point-2d-lt.int.test.ts | 3 +- .../types/point/point-3d-equals.int.test.ts | 3 +- .../types/point/point-3d-gt.int.test.ts | 3 +- .../types/point/point-3d-lt.int.test.ts | 3 +- .../tests/api-v6/schema/types/spatial.test.ts | 65 -------- .../filters/types/cartesian-filters.test.ts | 3 +- .../tck/filters/types/point-filters.test.ts | 3 +- 23 files changed, 135 insertions(+), 188 deletions(-) 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 index fdc85744d7..b327db5073 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -28,10 +28,6 @@ import type { ScalarTypeComposer, } from "graphql-compose"; import { Memoize } from "typescript-memoize"; -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"; @@ -478,81 +474,79 @@ class StaticFilterTypes { }); } - public getCartesianListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhereNullable", () => { - return { - fields: { - equals: toGraphQLList(CartesianPointInput), - }, - }; - }); - } - - return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhere", () => { - return { - fields: { - equals: toGraphQLList(toGraphQLNonNull(CartesianPointInput)), - }, - }; - }); - } - - public getPointListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("PointListPointWhereNullable", () => { - return { - fields: { - equals: toGraphQLList(PointInput), - }, - }; - }); - } - - return this.schemaBuilder.getOrCreateInputType("PointListPointWhere", () => { - return { - fields: { - equals: toGraphQLList(toGraphQLNonNull(PointInput)), - }, - }; - }); - } - - // TODO: Discuss distance operator and SpatialOperators in general as the API it may be improved. - 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, - }, - }; - }); - } - - // TODO: Discuss distance operator and SpatialOperators in general as the API it may be improved. - 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, - }, - }; - }); - } + // public getCartesianListWhere(nullable: boolean): InputTypeComposer { + // if (nullable) { + // return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhereNullable", () => { + // return { + // fields: { + // equals: toGraphQLList(CartesianPointInput), + // }, + // }; + // }); + // } + + // return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhere", () => { + // return { + // fields: { + // equals: toGraphQLList(toGraphQLNonNull(CartesianPointInput)), + // }, + // }; + // }); + // } + + // public getPointListWhere(nullable: boolean): InputTypeComposer { + // if (nullable) { + // return this.schemaBuilder.getOrCreateInputType("PointListPointWhereNullable", () => { + // return { + // fields: { + // equals: toGraphQLList(PointInput), + // }, + // }; + // }); + // } + + // 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 { 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 index d79186956a..942815d20e 100644 --- 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 @@ -24,9 +24,7 @@ import { GraphQLBuiltInScalarType, ListType, Neo4jGraphQLNumberType, - Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - Neo4jSpatialType, ScalarType, } from "../../../../schema-model/attribute/AttributeType"; import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; @@ -99,9 +97,9 @@ export abstract class FilterSchemaTypes { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 6e7ee3912e..04240b2a07 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../../utils/tests-helper"; -describe("CartesianPoint 2d array EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 3d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 1599cfc136..6df09e24e6 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 9f24515a12..6910452c74 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d GT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index a5dbfe6bef..febe645034 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d IN", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d IN", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 892d38c0f6..57296e87b9 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d LT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 871fc76c89..fc7e64ddb8 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 3d EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 3d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 1a7920f50e..b92ffd423b 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d GT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 3d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index c41df6388c..e4eb726d84 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d LT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 3d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index d16d5e01d5..e3b122c532 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../../utils/tests-helper"; -describe("Point 2d array EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index e1e07c015a..8143427379 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../../utils/tests-helper"; -describe("Point 2d array EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 3d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; @@ -50,7 +51,7 @@ describe("Point 2d array EQ", () => { afterEach(async () => { await testHelper.close(); }); - test("wgs-84-2d point filter by EQ", async () => { + 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} }] } } } }) { 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 index 7bcb0a781c..ffc83cce72 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 03d162c86a..bd6445bc85 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d GT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index ec941fa9bf..70db3a7359 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d IN", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d IN", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index b04e854544..5b1482bca8 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d LT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index e29750466e..232c19f2fe 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 3d EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 3d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 3343a774cc..39b2e2763b 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d GT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 3d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 408c071f5e..6e6a9d4e55 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d LT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 3d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 7761e65aee..ed28e0921a 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -70,32 +70,6 @@ describe("Spatial Types", () => { z: Float } - \\"\\"\\"Input type for a cartesian point with a distance\\"\\"\\" - input CartesianPointDistance { - distance: Float! - point: CartesianPointInput! - } - - \\"\\"\\"Input type for a cartesian point\\"\\"\\" - input CartesianPointInput { - x: Float! - y: Float! - z: Float - } - - input CartesianPointWhere { - AND: [CartesianPointWhere!] - NOT: CartesianPointWhere - OR: [CartesianPointWhere!] - distance: CartesianPointDistance - equals: CartesianPointInput - gt: CartesianPointDistance - gte: CartesianPointDistance - in: [CartesianPointInput!] - lt: CartesianPointDistance - lte: CartesianPointDistance - } - type NodeType { cartesianPoint: CartesianPoint! cartesianPointNullable: CartesianPoint @@ -183,10 +157,6 @@ describe("Spatial Types", () => { AND: [NodeTypeWhere!] NOT: NodeTypeWhere OR: [NodeTypeWhere!] - cartesianPoint: CartesianPointWhere - cartesianPointNullable: CartesianPointWhere - point: PointWhere - pointNullable: PointWhere relatedNode: NodeTypeRelatedNodeNestedOperationWhere } @@ -208,33 +178,6 @@ describe("Spatial Types", () => { srid: Int! } - \\"\\"\\"Input type for a point with a distance\\"\\"\\" - input PointDistance { - \\"\\"\\"The distance in metres to be used when comparing two points\\"\\"\\" - distance: Float! - point: PointInput! - } - - \\"\\"\\"Input type for a point\\"\\"\\" - input PointInput { - height: Float - latitude: Float! - longitude: Float! - } - - input PointWhere { - AND: [PointWhere!] - NOT: PointWhere - OR: [PointWhere!] - distance: PointDistance - equals: PointInput - gt: PointDistance - gte: PointDistance - in: [PointInput!] - lt: PointDistance - lte: PointDistance - } - type Query { nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation @@ -286,20 +229,12 @@ describe("Spatial Types", () => { AND: [RelatedNodePropertiesWhere!] NOT: RelatedNodePropertiesWhere OR: [RelatedNodePropertiesWhere!] - cartesianPoint: CartesianPointWhere - cartesianPointNullable: CartesianPointWhere - point: PointWhere - pointNullable: PointWhere } input RelatedNodeWhere { AND: [RelatedNodeWhere!] NOT: RelatedNodeWhere OR: [RelatedNodeWhere!] - cartesianPoint: CartesianPointWhere - cartesianPointNullable: CartesianPointWhere - point: PointWhere - pointNullable: PointWhere }" `); }); 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 index fe5d8fb63c..80f8b843af 100644 --- 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 @@ -20,7 +20,8 @@ import { Neo4jGraphQL } from "../../../../../src"; import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; -describe("CartesianPoint filters", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint filters", () => { let typeDefs: string; let neoSchema: Neo4jGraphQL; 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 index 8e6b090c78..c739e9afbe 100644 --- 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 @@ -20,7 +20,8 @@ import { Neo4jGraphQL } from "../../../../../src"; import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; -describe("Point filters", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point filters", () => { let typeDefs: string; let neoSchema: Neo4jGraphQL; From 190ff925069327b233f19d1957e51b2cdb1d8746 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 3 Jul 2024 10:38:34 +0100 Subject: [PATCH 079/177] fix relayId tests (no longer 1to1 rel supported) --- .../combinations/alias-relayId/alias-relayId.int.test.ts | 2 +- .../directives/relayId/global-node-query.int.test.ts | 8 ++++---- .../directives/relayId/relayId-filters.int.test.ts | 2 +- .../directives/relayId/relayId-projection.int.test.ts | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) 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 index 8d0217b05f..1894937ca4 100644 --- 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 @@ -35,7 +35,7 @@ describe("RelayId projection with alias directive", () => { type ${Movie} @node { dbId: ID! @id @unique @relayId @alias(property: "serverId") title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) } 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 index 79bb9391c9..5aa5e5b4ef 100644 --- 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 @@ -31,11 +31,11 @@ describe("Global node query", () => { const Actor = testHelper.createUniqueType("Actor"); beforeAll(async () => { - const typeDefs = ` + const typeDefs = /* GraphQL */ ` type ${Movie} @node { dbId: ID! @id @unique @relayId title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) } @@ -70,7 +70,7 @@ describe("Global node query", () => { 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 = ` + const connectionQuery = /* GraphQL */ ` query { node(id: "${movieGlobalId}") { ... on ${Movie} { @@ -97,7 +97,7 @@ describe("Global node query", () => { 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 = ` + const connectionQuery = /* GraphQL */ ` query { node(id: "${movieGlobalId}") { ... on ${Movie} { 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 index b04eefb061..5e5bb8d45e 100644 --- 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 @@ -35,7 +35,7 @@ describe("RelayId projection with filters", () => { type ${Movie} @node { dbId: ID! @id @unique @relayId title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) } 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 index 4e9b71b9ba..207c53f651 100644 --- 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 @@ -31,11 +31,11 @@ describe("RelayId projection", () => { const Actor = testHelper.createUniqueType("Actor"); beforeAll(async () => { - const typeDefs = ` + const typeDefs = /* GraphQL */ ` type ${Movie} @node { dbId: ID! @id @unique @relayId title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) } @@ -71,7 +71,7 @@ describe("RelayId projection", () => { }); test("should return the correct relayId ids using the connection API", async () => { - const connectionQuery = ` + const connectionQuery = /* GraphQL */ ` query { ${Movie.plural} { connection { @@ -160,7 +160,7 @@ describe("RelayId projection", () => { }); test("should return the correct relayId ids using the connection API with aliased fields", async () => { - const connectionQuery = ` + const connectionQuery = /* GraphQL */ ` query { ${Movie.plural} { connection { From 254342c6dd4669c73104dcc25609041aae954132 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 5 Jul 2024 11:56:39 +0100 Subject: [PATCH 080/177] Fix tests for version 6.x --- .../api-v6/queryIRFactory/FilterOperators.ts | 20 ++++++++++++++++++- .../connection-operation-resolver.ts | 19 ++++++++++++++++++ .../resolvers/global-id-field-resolver.ts | 19 ++++++++++++++++++ .../api-v6/resolvers/global-node-resolver.ts | 19 ++++++++++++++++++ .../schema-generation/SchemaBuilderTypes.ts | 19 ++++++++++++++++++ .../schema-types/SchemaTypes.ts | 19 ++++++++++++++++++ .../alias-relayId/alias-relayId.int.test.ts | 2 +- .../relayId/global-node-query.int.test.ts | 4 ++-- .../relayId/relayId-filters.int.test.ts | 2 +- .../relayId/relayId-projection.int.test.ts | 4 ++-- .../pagination/first-after.int.test.ts | 2 +- 11 files changed, 121 insertions(+), 8 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts index b7e846cf7b..679db95f39 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterOperators.ts @@ -1,9 +1,27 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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); } diff --git a/packages/graphql/src/api-v6/resolvers/connection-operation-resolver.ts b/packages/graphql/src/api-v6/resolvers/connection-operation-resolver.ts index a3b7220c5b..19fa210da6 100644 --- a/packages/graphql/src/api-v6/resolvers/connection-operation-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/connection-operation-resolver.ts @@ -1,3 +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 { GraphQLResolveInfo } from "graphql"; import type { PageInfo } from "graphql-relay"; import { createConnectionWithEdgeProperties } from "../../schema/pagination"; 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 index 6602a6de52..692e964917 100644 --- a/packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/global-id-field-resolver.ts @@ -1,3 +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 { GraphQLResolveInfo } from "graphql"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; import type { ConnectionQueryArgs } from "../../types"; diff --git a/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts b/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts index 4dd7f478ec..38eb1f2a34 100644 --- a/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts @@ -1,3 +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 { GraphQLResolveInfo } from "graphql"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; import type { GlobalNodeArgs } from "../../types"; diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts index a7a6a77de3..cf40e00391 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts @@ -1,3 +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 { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; import type { SchemaComposer } from "graphql-compose"; import { ScalarTypeComposer } from "graphql-compose"; 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 index 05dbc20bf9..1f6ff01f3d 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/SchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/SchemaTypes.ts @@ -1,3 +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 { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { StaticSchemaTypes } from "./StaticSchemaTypes"; import type { TopLevelEntitySchemaTypes } from "./TopLevelEntitySchemaTypes"; 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 index 8d0217b05f..1894937ca4 100644 --- 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 @@ -35,7 +35,7 @@ describe("RelayId projection with alias directive", () => { type ${Movie} @node { dbId: ID! @id @unique @relayId @alias(property: "serverId") title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) } 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 index 79bb9391c9..1148ffca59 100644 --- 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 @@ -31,11 +31,11 @@ describe("Global node query", () => { const Actor = testHelper.createUniqueType("Actor"); beforeAll(async () => { - const typeDefs = ` + const typeDefs = /* GraphQL */ ` type ${Movie} @node { dbId: ID! @id @unique @relayId title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) } 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 index b04eefb061..5e5bb8d45e 100644 --- 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 @@ -35,7 +35,7 @@ describe("RelayId projection with filters", () => { type ${Movie} @node { dbId: ID! @id @unique @relayId title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) } 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 index 4e9b71b9ba..27091d6787 100644 --- 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 @@ -31,11 +31,11 @@ describe("RelayId projection", () => { const Actor = testHelper.createUniqueType("Actor"); beforeAll(async () => { - const typeDefs = ` + const typeDefs = /* GraphQL */ ` type ${Movie} @node { dbId: ID! @id @unique @relayId title: String! - genre: ${Genre}! @relationship(type: "HAS_GENRE", direction: OUT) + genre: [${Genre}!]! @relationship(type: "HAS_GENRE", direction: OUT) actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: OUT) } 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 index 90246a2794..a6a13dfaab 100644 --- 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 @@ -48,7 +48,7 @@ describe("Pagination with first and after", () => { await testHelper.close(); }); - test.only("Get movies with first and after argument", async () => { + test("Get movies with first and after argument", async () => { const afterCursor = offsetToCursor(4); const query = /* GraphQL */ ` query { From feb334389f850428417449fdb68c97a51f3443b2 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 5 Jul 2024 13:09:44 +0100 Subject: [PATCH 081/177] Fix compatibility issues with graphQL 14 --- .../api-v6/schema-generation/utils/to-graphql-list.ts | 2 +- .../schema-generation/utils/to-graphql-non-null.ts | 2 +- packages/graphql/src/classes/Subgraph.ts | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-list.ts b/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-list.ts index 6462331a06..f0faa50b05 100644 --- a/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-list.ts +++ b/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-list.ts @@ -20,6 +20,6 @@ import type { GraphQLType } from "graphql"; import { GraphQLList } from "graphql"; -export function toGraphQLList(type: T): GraphQLList { +export function toGraphQLList(type: T): GraphQLList { return new GraphQLList(type); } diff --git a/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts b/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts index 4590cecec5..d1c54a0408 100644 --- a/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts +++ b/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts @@ -20,6 +20,6 @@ import type { GraphQLType } from "graphql"; import { GraphQLNonNull } from "graphql"; -export function toGraphQLNonNull(type: T): GraphQLNonNull { +export function toGraphQLNonNull(type: T): GraphQLNonNull { return new GraphQLNonNull(type); } 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) { From c6125cb18ec25280be7e3303c673f150590ba740 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 5 Jul 2024 14:11:45 +0100 Subject: [PATCH 082/177] Logical filters in node --- .../api-v6/queryIRFactory/FilterFactory.ts | 45 ++++++++++++++++-- .../resolve-tree-parser/graphql-tree.ts | 4 +- .../src/api-v6/resolvers/read-resolver.ts | 1 - .../logical-filters/and-filter.test.ts | 46 +++++++++++++++++++ .../logical-filters/not-filter.test.ts | 44 ++++++++++++++++++ .../filters/logical-filters/or-filter.test.ts | 2 +- 6 files changed, 135 insertions(+), 7 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index dd4e319ec2..f8c7121740 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -36,6 +36,7 @@ import type { GraphQLAttributeFilters, GraphQLEdgeWhereArgs, GraphQLNodeFilters, + GraphQLNodeWhereArgs, GraphQLWhereArgs, RelationshipFilters, } from "./resolve-tree-parser/graphql-tree"; @@ -139,8 +140,8 @@ export class FilterFactory { if (where.length === 0) { return []; } - const nestedFilters = where.flatMap((orWhere: GraphQLEdgeWhereArgs) => { - return this.createEdgeFilters({ entity, relationship, edgeWhere: orWhere }); + const nestedFilters = where.flatMap((logicalWhere: GraphQLEdgeWhereArgs) => { + return this.createEdgeFilters({ entity, relationship, edgeWhere: logicalWhere }); }); if (nestedFilters.length > 0) { @@ -160,9 +161,19 @@ export class FilterFactory { entity, }: { entity: ConcreteEntity; - where?: Record; + where?: GraphQLNodeWhereArgs; }): Filter[] { - return Object.entries(where).flatMap(([fieldName, filters]) => { + const andFilters = this.createLogicalNodeFilters(entity, "AND", where.AND); + const orFilters = this.createLogicalNodeFilters(entity, "OR", where.OR); + const notFilters = this.createLogicalNodeFilters(entity, "NOT", where.NOT ? [where.NOT] : undefined); + + const nodePropertiesFilters = Object.entries(where).flatMap(([fieldName, filtersWithLogical]) => { + if (["AND", "OR", "NOT"].includes(fieldName)) { + return []; + } + + let filters = filtersWithLogical as GraphQLNodeFilters; + let attribute: Attribute | undefined; if (fieldName === "id" && entity.globalIdField) { attribute = entity.globalIdField; @@ -184,6 +195,32 @@ export class FilterFactory { } return []; }); + + return [...andFilters, ...orFilters, ...notFilters, ...nodePropertiesFilters]; + } + + private createLogicalNodeFilters( + entity: ConcreteEntity, + operation: LogicalOperators, + where: GraphQLNodeWhereArgs[] = [] + ): [] | [Filter] { + if (where.length === 0) { + return []; + } + const nestedFilters = where.flatMap((logicalWhere) => { + return this.createNodeFilter({ entity, where: logicalWhere }); + }); + + if (nestedFilters.length > 0) { + return [ + new LogicalFilter({ + operation, + filters: nestedFilters, + }), + ]; + } + + return []; } /** Transforms globalId filters into normal property filters */ diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index 982fa23f06..67383d7c72 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -75,9 +75,11 @@ export type GraphQLWhereArgs = LogicalOperation<{ edges?: GraphQLEdgeWhereArgs; }>; +export type GraphQLNodeWhereArgs = LogicalOperation>; + export type GraphQLEdgeWhereArgs = LogicalOperation<{ properties?: Record; - node?: Record; + node?: GraphQLNodeWhereArgs; }>; export type GraphQLAttributeFilters = StringFilters | NumberFilters; diff --git a/packages/graphql/src/api-v6/resolvers/read-resolver.ts b/packages/graphql/src/api-v6/resolvers/read-resolver.ts index 53e505ebd7..969a1b8681 100644 --- a/packages/graphql/src/api-v6/resolvers/read-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/read-resolver.ts @@ -36,7 +36,6 @@ export function generateReadResolver({ entity }: { entity: ConcreteEntity }) { context.resolveTree = resolveTree; const graphQLTree = parseResolveInfoTree({ resolveTree: context.resolveTree, entity }); - const { cypher, params } = translateReadOperation({ context: context, graphQLTree, 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 index 6ed4dff1df..ed795bc84d 100644 --- 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 @@ -134,4 +134,50 @@ describe("AND filters", () => { }" `); }); + + test("AND logical filter in nodes", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { + edges: { 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 index dc777d63da..3617e14328 100644 --- 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 @@ -119,4 +119,48 @@ describe("NOT filters", () => { }" `); }); + + test("NOT logical filter on nodes", async () => { + const query = /* GraphQL */ ` + query { + movies( + where: { edges: { 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 index 97248fe9b4..040584c827 100644 --- 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 @@ -135,7 +135,7 @@ describe("OR filters", () => { `); }); - test.skip("OR logical filter in nodes", async () => { + test("OR logical filter in nodes", async () => { const query = /* GraphQL */ ` query { movies( From f1d8e0e32605a4749b91f85eed419a8bd935fecc Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 3 Jul 2024 10:37:51 +0100 Subject: [PATCH 083/177] comment out the spatial filters waiting for proper design --- .../schema-types/StaticSchemaTypes.ts | 152 +++++++++--------- .../filter-schema-types/FilterSchemaTypes.ts | 44 +++-- .../cartesian-point-2d-equals.int.test.ts | 3 +- .../cartesian-point-3d-equals.int.test.ts | 3 +- .../cartesian-point-2d-equals.int.test.ts | 3 +- .../cartesian-point-2d-gt.int.test.ts | 3 +- .../cartesian-point-2d-in.int.test.ts | 3 +- .../cartesian-point-2d-lt.int.test.ts | 3 +- .../cartesian-point-3d-equals.int.test.ts | 3 +- .../cartesian-point-3d-gt.int.test.ts | 3 +- .../cartesian-point-3d-lt.int.test.ts | 3 +- .../point/array/point-2d-equals.int.test.ts | 3 +- .../point/array/point-3d-equals.int.test.ts | 5 +- .../types/point/point-2d-equals.int.test.ts | 3 +- .../types/point/point-2d-gt.int.test.ts | 3 +- .../types/point/point-2d-in.int.test.ts | 3 +- .../types/point/point-2d-lt.int.test.ts | 3 +- .../types/point/point-3d-equals.int.test.ts | 3 +- .../types/point/point-3d-gt.int.test.ts | 3 +- .../types/point/point-3d-lt.int.test.ts | 3 +- .../tests/api-v6/schema/types/spatial.test.ts | 65 -------- .../filters/types/cartesian-filters.test.ts | 3 +- .../tck/filters/types/point-filters.test.ts | 3 +- 23 files changed, 135 insertions(+), 188 deletions(-) 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 index fdc85744d7..b327db5073 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -28,10 +28,6 @@ import type { ScalarTypeComposer, } from "graphql-compose"; import { Memoize } from "typescript-memoize"; -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"; @@ -478,81 +474,79 @@ class StaticFilterTypes { }); } - public getCartesianListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhereNullable", () => { - return { - fields: { - equals: toGraphQLList(CartesianPointInput), - }, - }; - }); - } - - return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhere", () => { - return { - fields: { - equals: toGraphQLList(toGraphQLNonNull(CartesianPointInput)), - }, - }; - }); - } - - public getPointListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("PointListPointWhereNullable", () => { - return { - fields: { - equals: toGraphQLList(PointInput), - }, - }; - }); - } - - return this.schemaBuilder.getOrCreateInputType("PointListPointWhere", () => { - return { - fields: { - equals: toGraphQLList(toGraphQLNonNull(PointInput)), - }, - }; - }); - } - - // TODO: Discuss distance operator and SpatialOperators in general as the API it may be improved. - 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, - }, - }; - }); - } - - // TODO: Discuss distance operator and SpatialOperators in general as the API it may be improved. - 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, - }, - }; - }); - } + // public getCartesianListWhere(nullable: boolean): InputTypeComposer { + // if (nullable) { + // return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhereNullable", () => { + // return { + // fields: { + // equals: toGraphQLList(CartesianPointInput), + // }, + // }; + // }); + // } + + // return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhere", () => { + // return { + // fields: { + // equals: toGraphQLList(toGraphQLNonNull(CartesianPointInput)), + // }, + // }; + // }); + // } + + // public getPointListWhere(nullable: boolean): InputTypeComposer { + // if (nullable) { + // return this.schemaBuilder.getOrCreateInputType("PointListPointWhereNullable", () => { + // return { + // fields: { + // equals: toGraphQLList(PointInput), + // }, + // }; + // }); + // } + + // 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 { 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 index d79186956a..942815d20e 100644 --- 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 @@ -24,9 +24,7 @@ import { GraphQLBuiltInScalarType, ListType, Neo4jGraphQLNumberType, - Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - Neo4jSpatialType, ScalarType, } from "../../../../schema-model/attribute/AttributeType"; import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; @@ -99,9 +97,9 @@ export abstract class FilterSchemaTypes { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 6e7ee3912e..04240b2a07 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../../utils/tests-helper"; -describe("CartesianPoint 2d array EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 3d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 1599cfc136..6df09e24e6 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 9f24515a12..6910452c74 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d GT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index a5dbfe6bef..febe645034 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d IN", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d IN", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 892d38c0f6..57296e87b9 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d LT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 2d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 871fc76c89..fc7e64ddb8 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 3d EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 3d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 1a7920f50e..b92ffd423b 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d GT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 3d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index c41df6388c..e4eb726d84 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d LT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint 3d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index d16d5e01d5..e3b122c532 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../../utils/tests-helper"; -describe("Point 2d array EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index e1e07c015a..8143427379 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../../utils/tests-helper"; -describe("Point 2d array EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 3d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; @@ -50,7 +51,7 @@ describe("Point 2d array EQ", () => { afterEach(async () => { await testHelper.close(); }); - test("wgs-84-2d point filter by EQ", async () => { + 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} }] } } } }) { 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 index 7bcb0a781c..ffc83cce72 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 03d162c86a..bd6445bc85 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d GT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index ec941fa9bf..70db3a7359 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d IN", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d IN", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index b04e854544..5b1482bca8 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d LT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 2d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index e29750466e..232c19f2fe 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 3d EQ", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 3d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 3343a774cc..39b2e2763b 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d GT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 3d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; 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 index 408c071f5e..6e6a9d4e55 100644 --- 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 @@ -20,7 +20,8 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d LT", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point 3d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 7761e65aee..ed28e0921a 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -70,32 +70,6 @@ describe("Spatial Types", () => { z: Float } - \\"\\"\\"Input type for a cartesian point with a distance\\"\\"\\" - input CartesianPointDistance { - distance: Float! - point: CartesianPointInput! - } - - \\"\\"\\"Input type for a cartesian point\\"\\"\\" - input CartesianPointInput { - x: Float! - y: Float! - z: Float - } - - input CartesianPointWhere { - AND: [CartesianPointWhere!] - NOT: CartesianPointWhere - OR: [CartesianPointWhere!] - distance: CartesianPointDistance - equals: CartesianPointInput - gt: CartesianPointDistance - gte: CartesianPointDistance - in: [CartesianPointInput!] - lt: CartesianPointDistance - lte: CartesianPointDistance - } - type NodeType { cartesianPoint: CartesianPoint! cartesianPointNullable: CartesianPoint @@ -183,10 +157,6 @@ describe("Spatial Types", () => { AND: [NodeTypeWhere!] NOT: NodeTypeWhere OR: [NodeTypeWhere!] - cartesianPoint: CartesianPointWhere - cartesianPointNullable: CartesianPointWhere - point: PointWhere - pointNullable: PointWhere relatedNode: NodeTypeRelatedNodeNestedOperationWhere } @@ -208,33 +178,6 @@ describe("Spatial Types", () => { srid: Int! } - \\"\\"\\"Input type for a point with a distance\\"\\"\\" - input PointDistance { - \\"\\"\\"The distance in metres to be used when comparing two points\\"\\"\\" - distance: Float! - point: PointInput! - } - - \\"\\"\\"Input type for a point\\"\\"\\" - input PointInput { - height: Float - latitude: Float! - longitude: Float! - } - - input PointWhere { - AND: [PointWhere!] - NOT: PointWhere - OR: [PointWhere!] - distance: PointDistance - equals: PointInput - gt: PointDistance - gte: PointDistance - in: [PointInput!] - lt: PointDistance - lte: PointDistance - } - type Query { nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation @@ -286,20 +229,12 @@ describe("Spatial Types", () => { AND: [RelatedNodePropertiesWhere!] NOT: RelatedNodePropertiesWhere OR: [RelatedNodePropertiesWhere!] - cartesianPoint: CartesianPointWhere - cartesianPointNullable: CartesianPointWhere - point: PointWhere - pointNullable: PointWhere } input RelatedNodeWhere { AND: [RelatedNodeWhere!] NOT: RelatedNodeWhere OR: [RelatedNodeWhere!] - cartesianPoint: CartesianPointWhere - cartesianPointNullable: CartesianPointWhere - point: PointWhere - pointNullable: PointWhere }" `); }); 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 index fe5d8fb63c..80f8b843af 100644 --- 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 @@ -20,7 +20,8 @@ import { Neo4jGraphQL } from "../../../../../src"; import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; -describe("CartesianPoint filters", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("CartesianPoint filters", () => { let typeDefs: string; let neoSchema: Neo4jGraphQL; 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 index 8e6b090c78..c739e9afbe 100644 --- 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 @@ -20,7 +20,8 @@ import { Neo4jGraphQL } from "../../../../../src"; import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; -describe("Point filters", () => { +// Skip Spatial types waiting for the new operator design +describe.skip("Point filters", () => { let typeDefs: string; let neoSchema: Neo4jGraphQL; From 718db8467c5413579934d32d094fc31b87f959d7 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 3 Jul 2024 10:38:34 +0100 Subject: [PATCH 084/177] fix relayId tests (no longer 1to1 rel supported) --- .../directives/relayId/global-node-query.int.test.ts | 4 ++-- .../directives/relayId/relayId-projection.int.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 index 1148ffca59..5aa5e5b4ef 100644 --- 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 @@ -70,7 +70,7 @@ describe("Global node query", () => { 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 = ` + const connectionQuery = /* GraphQL */ ` query { node(id: "${movieGlobalId}") { ... on ${Movie} { @@ -97,7 +97,7 @@ describe("Global node query", () => { 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 = ` + const connectionQuery = /* GraphQL */ ` query { node(id: "${movieGlobalId}") { ... on ${Movie} { 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 index 27091d6787..207c53f651 100644 --- 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 @@ -71,7 +71,7 @@ describe("RelayId projection", () => { }); test("should return the correct relayId ids using the connection API", async () => { - const connectionQuery = ` + const connectionQuery = /* GraphQL */ ` query { ${Movie.plural} { connection { @@ -160,7 +160,7 @@ describe("RelayId projection", () => { }); test("should return the correct relayId ids using the connection API with aliased fields", async () => { - const connectionQuery = ` + const connectionQuery = /* GraphQL */ ` query { ${Movie.plural} { connection { From 9ba241e25e4cc6ecbdd4afd78bdf4dc625ed929a Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 5 Jul 2024 14:54:20 +0100 Subject: [PATCH 085/177] disable lint error for skipped tests on spatial type filtering --- .../cartesian-point/array/cartesian-point-2d-equals.int.test.ts | 1 + .../cartesian-point/array/cartesian-point-3d-equals.int.test.ts | 1 + .../types/cartesian-point/cartesian-point-2d-equals.int.test.ts | 1 + .../types/cartesian-point/cartesian-point-2d-gt.int.test.ts | 1 + .../types/cartesian-point/cartesian-point-2d-in.int.test.ts | 1 + .../types/cartesian-point/cartesian-point-2d-lt.int.test.ts | 1 + .../types/cartesian-point/cartesian-point-3d-equals.int.test.ts | 1 + .../types/cartesian-point/cartesian-point-3d-gt.int.test.ts | 1 + .../types/cartesian-point/cartesian-point-3d-lt.int.test.ts | 1 + 9 files changed, 9 insertions(+) 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 index 3b4ac7edcf..c08cbc9a80 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 04240b2a07..3631acbb22 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 6df09e24e6..ef76334bc0 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 6910452c74..4521acbca4 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index febe645034..fd6eef4e7a 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 57296e87b9..cc1a81e71c 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index fc7e64ddb8..195bbfc09e 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index b92ffd423b..4263eaa60b 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index e4eb726d84..d684515d75 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); From f2f744962028af63ca7fdf32b7e50919eec8d4d0 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 5 Jul 2024 16:10:27 +0100 Subject: [PATCH 086/177] Remove unused types in schema builder --- .../api-v6/schema-generation/SchemaBuilder.ts | 19 +++++-------------- .../schema-types/RelatedEntitySchemaTypes.ts | 10 +++++----- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index 1ff2e8697a..aff40f80b8 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -17,14 +17,7 @@ * limitations under the License. */ -import type { - GraphQLInputType, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLScalarType, - GraphQLSchema, -} from "graphql"; +import type { GraphQLInputType, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema } from "graphql"; import type { EnumTypeComposer, InputTypeComposer, @@ -79,7 +72,7 @@ export class SchemaBuilder { public getOrCreateObjectType( name: string, onCreate: () => { - fields: Record>; + fields: Record>; description?: string; iface?: InterfaceTypeComposer; } @@ -101,7 +94,7 @@ export class SchemaBuilder { public getOrCreateInterfaceType( name: string, onCreate: () => { - fields: Record>; + fields: Record>; description?: string; } ): InterfaceTypeComposer { @@ -128,9 +121,7 @@ export class SchemaBuilder { fields: Record< string, | EnumTypeComposer - | string | GraphQLInputType - | GraphQLList | GraphQLNonNull | WrappedComposer >; @@ -150,7 +141,7 @@ export class SchemaBuilder { public createInputObjectType( name: string, - fields: Record>, + fields: Record>, description?: string ): InputTypeComposer { return this.composer.createInputTC({ @@ -181,7 +172,7 @@ export class SchemaBuilder { }: { name: string; type: ObjectTypeComposer | InterfaceTypeComposer; - args: Record>; + args: Record>; resolver: (...args: any[]) => any; description?: string; }): void { 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 index 89a0c8741d..7ca07b791e 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -133,9 +133,9 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { + private getRelationshipFieldsDefinition(): Record { const entityAttributes = this.getRelationshipFields().map((attribute) => new AttributeAdapter(attribute)); - return attributeAdapterToComposeFields(entityAttributes, new Map()) as Record; + return attributeAdapterToComposeFields(entityAttributes, new Map()); } private getRelationshipSortFields(): Record { @@ -151,9 +151,9 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes - field.type.name === GraphQLBuiltInScalarType[GraphQLBuiltInScalarType[field.type.name]] || - field.type.name === Neo4jGraphQLNumberType[Neo4jGraphQLNumberType[field.type.name]] || - field.type.name === Neo4jGraphQLTemporalType[Neo4jGraphQLTemporalType[field.type.name]] + field.type.name === GraphQLBuiltInScalarType[field.type.name] || + field.type.name === Neo4jGraphQLNumberType[field.type.name] || + field.type.name === Neo4jGraphQLTemporalType[field.type.name] ); } From cdc6aa2cf9165eaa7579e797c72fa11050bdcc13 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 5 Jul 2024 17:45:45 +0100 Subject: [PATCH 087/177] disable skip warnings on point filters --- .../filters/types/point/array/point-2d-equals.int.test.ts | 1 + .../filters/types/point/array/point-3d-equals.int.test.ts | 1 + .../integration/filters/types/point/point-2d-equals.int.test.ts | 1 + .../integration/filters/types/point/point-2d-gt.int.test.ts | 1 + .../integration/filters/types/point/point-2d-in.int.test.ts | 1 + .../integration/filters/types/point/point-2d-lt.int.test.ts | 1 + .../integration/filters/types/point/point-3d-equals.int.test.ts | 1 + .../integration/filters/types/point/point-3d-gt.int.test.ts | 1 + .../integration/filters/types/point/point-3d-lt.int.test.ts | 1 + .../tests/api-v6/tck/filters/types/cartesian-filters.test.ts | 1 + .../graphql/tests/api-v6/tck/filters/types/point-filters.test.ts | 1 + 11 files changed, 11 insertions(+) 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 index e3b122c532..c5e41f8bb4 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 8143427379..c3f6a8d621 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index ffc83cce72..13374e26c0 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index bd6445bc85..f9d3292ceb 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 70db3a7359..fe01db7ca2 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 5b1482bca8..08208cc54d 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 232c19f2fe..4a662684d5 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 39b2e2763b..615df873cd 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 6e6a9d4e55..8430c7f284 100644 --- 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 @@ -21,6 +21,7 @@ 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 }); 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 index 80f8b843af..808b5bb293 100644 --- 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 @@ -21,6 +21,7 @@ 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; 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 index c739e9afbe..0c1e23f3b8 100644 --- 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 @@ -21,6 +21,7 @@ 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; From e7156e3d5f2ad9e951180e37f8a409c57e3ffbad Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Tue, 9 Jul 2024 14:17:52 +0200 Subject: [PATCH 088/177] feat: remove edges from top-level filtering and sort in schema types --- .../schema-types/TopLevelEntitySchemaTypes.ts | 14 ++++- .../filter-schema-types/FilterSchemaTypes.ts | 20 ++++++- .../api-v6/schema/directives/relayId.test.ts | 15 +---- .../tests/api-v6/schema/relationship.test.ts | 60 +++---------------- .../tests/api-v6/schema/simple.test.ts | 60 +++---------------- .../tests/api-v6/schema/types/array.test.ts | 18 +----- .../tests/api-v6/schema/types/scalars.test.ts | 30 ++-------- .../tests/api-v6/schema/types/spatial.test.ts | 18 +----- .../api-v6/schema/types/temporals.test.ts | 30 ++-------- 9 files changed, 61 insertions(+), 204 deletions(-) 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 index 4626463fec..4b28efb2f6 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -75,12 +75,22 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { + return { + fields: { + node: this.nodeSort.NonNull.List, + }, + }; + }); + } + protected get edge(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.edge, () => { return { @@ -219,7 +229,7 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { + 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) => { diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index b0a769aab9..bd50911104 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -66,7 +66,7 @@ describe("RelayId", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: [MovieSort!] } type MovieEdge { @@ -74,17 +74,6 @@ describe("RelayId", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - - input MovieEdgeWhere { - AND: [MovieEdgeWhere!] - NOT: MovieEdgeWhere - OR: [MovieEdgeWhere!] - node: MovieWhere - } - type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } @@ -93,7 +82,7 @@ describe("RelayId", () => { AND: [MovieOperationWhere!] NOT: MovieOperationWhere OR: [MovieOperationWhere!] - edges: MovieEdgeWhere + node: MovieWhere } input MovieSort { diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 9621c2a672..e10a7c7ae0 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -55,7 +55,7 @@ describe("Relationships", () => { } input ActorConnectionSort { - edges: [ActorEdgeSort!] + node: [ActorSort!] } type ActorEdge { @@ -63,17 +63,6 @@ describe("Relationships", () => { node: Actor } - input ActorEdgeSort { - node: ActorSort - } - - input ActorEdgeWhere { - AND: [ActorEdgeWhere!] - NOT: ActorEdgeWhere - OR: [ActorEdgeWhere!] - node: ActorWhere - } - type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo @@ -135,7 +124,7 @@ describe("Relationships", () => { AND: [ActorOperationWhere!] NOT: ActorOperationWhere OR: [ActorOperationWhere!] - edges: ActorEdgeWhere + node: ActorWhere } input ActorSort { @@ -214,7 +203,7 @@ describe("Relationships", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: [MovieSort!] } type MovieEdge { @@ -222,17 +211,6 @@ describe("Relationships", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - - input MovieEdgeWhere { - AND: [MovieEdgeWhere!] - NOT: MovieEdgeWhere - OR: [MovieEdgeWhere!] - node: MovieWhere - } - type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } @@ -241,7 +219,7 @@ describe("Relationships", () => { AND: [MovieOperationWhere!] NOT: MovieOperationWhere OR: [MovieOperationWhere!] - edges: MovieEdgeWhere + node: MovieWhere } input MovieSort { @@ -337,7 +315,7 @@ describe("Relationships", () => { } input ActorConnectionSort { - edges: [ActorEdgeSort!] + node: [ActorSort!] } type ActorEdge { @@ -345,17 +323,6 @@ describe("Relationships", () => { node: Actor } - input ActorEdgeSort { - node: ActorSort - } - - input ActorEdgeWhere { - AND: [ActorEdgeWhere!] - NOT: ActorEdgeWhere - OR: [ActorEdgeWhere!] - node: ActorWhere - } - type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo @@ -420,7 +387,7 @@ describe("Relationships", () => { AND: [ActorOperationWhere!] NOT: ActorOperationWhere OR: [ActorOperationWhere!] - edges: ActorEdgeWhere + node: ActorWhere } input ActorSort { @@ -514,7 +481,7 @@ describe("Relationships", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: [MovieSort!] } type MovieEdge { @@ -522,17 +489,6 @@ describe("Relationships", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - - input MovieEdgeWhere { - AND: [MovieEdgeWhere!] - NOT: MovieEdgeWhere - OR: [MovieEdgeWhere!] - node: MovieWhere - } - type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } @@ -541,7 +497,7 @@ describe("Relationships", () => { AND: [MovieOperationWhere!] NOT: MovieOperationWhere OR: [MovieOperationWhere!] - edges: MovieEdgeWhere + node: MovieWhere } input MovieSort { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index dc4748cd9b..5bcc990c12 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -49,7 +49,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: [MovieSort!] } type MovieEdge { @@ -57,17 +57,6 @@ describe("Simple Aura-API", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - - input MovieEdgeWhere { - AND: [MovieEdgeWhere!] - NOT: MovieEdgeWhere - OR: [MovieEdgeWhere!] - node: MovieWhere - } - type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } @@ -76,7 +65,7 @@ describe("Simple Aura-API", () => { AND: [MovieOperationWhere!] NOT: MovieOperationWhere OR: [MovieOperationWhere!] - edges: MovieEdgeWhere + node: MovieWhere } input MovieSort { @@ -148,7 +137,7 @@ describe("Simple Aura-API", () => { } input ActorConnectionSort { - edges: [ActorEdgeSort!] + node: [ActorSort!] } type ActorEdge { @@ -156,17 +145,6 @@ describe("Simple Aura-API", () => { node: Actor } - input ActorEdgeSort { - node: ActorSort - } - - input ActorEdgeWhere { - AND: [ActorEdgeWhere!] - NOT: ActorEdgeWhere - OR: [ActorEdgeWhere!] - node: ActorWhere - } - type ActorOperation { connection(after: String, first: Int, sort: ActorConnectionSort): ActorConnection } @@ -175,7 +153,7 @@ describe("Simple Aura-API", () => { AND: [ActorOperationWhere!] NOT: ActorOperationWhere OR: [ActorOperationWhere!] - edges: ActorEdgeWhere + node: ActorWhere } input ActorSort { @@ -199,7 +177,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: [MovieSort!] } type MovieEdge { @@ -207,17 +185,6 @@ describe("Simple Aura-API", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - - input MovieEdgeWhere { - AND: [MovieEdgeWhere!] - NOT: MovieEdgeWhere - OR: [MovieEdgeWhere!] - node: MovieWhere - } - type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } @@ -226,7 +193,7 @@ describe("Simple Aura-API", () => { AND: [MovieOperationWhere!] NOT: MovieOperationWhere OR: [MovieOperationWhere!] - edges: MovieEdgeWhere + node: MovieWhere } input MovieSort { @@ -299,7 +266,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: [MovieSort!] } type MovieEdge { @@ -307,17 +274,6 @@ describe("Simple Aura-API", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - - input MovieEdgeWhere { - AND: [MovieEdgeWhere!] - NOT: MovieEdgeWhere - OR: [MovieEdgeWhere!] - node: MovieWhere - } - type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } @@ -326,7 +282,7 @@ describe("Simple Aura-API", () => { AND: [MovieOperationWhere!] NOT: MovieOperationWhere OR: [MovieOperationWhere!] - edges: MovieEdgeWhere + node: MovieWhere } input MovieSort { diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 9a8ee20972..9eec46258c 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -257,13 +257,6 @@ describe("Scalars", () => { node: NodeType } - input NodeTypeEdgeWhere { - AND: [NodeTypeEdgeWhere!] - NOT: NodeTypeEdgeWhere - OR: [NodeTypeEdgeWhere!] - node: NodeTypeWhere - } - type NodeTypeOperation { connection(after: String, first: Int): NodeTypeConnection } @@ -272,7 +265,7 @@ describe("Scalars", () => { AND: [NodeTypeOperationWhere!] NOT: NodeTypeOperationWhere OR: [NodeTypeOperationWhere!] - edges: NodeTypeEdgeWhere + node: NodeTypeWhere } type NodeTypeRelatedNodeConnection { @@ -402,13 +395,6 @@ describe("Scalars", () => { node: RelatedNode } - input RelatedNodeEdgeWhere { - AND: [RelatedNodeEdgeWhere!] - NOT: RelatedNodeEdgeWhere - OR: [RelatedNodeEdgeWhere!] - node: RelatedNodeWhere - } - type RelatedNodeOperation { connection(after: String, first: Int): RelatedNodeConnection } @@ -417,7 +403,7 @@ describe("Scalars", () => { AND: [RelatedNodeOperationWhere!] NOT: RelatedNodeOperationWhere OR: [RelatedNodeOperationWhere!] - edges: RelatedNodeEdgeWhere + node: RelatedNodeWhere } type RelatedNodeProperties { diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index b2e75c6163..f9fe022853 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -163,7 +163,7 @@ describe("Scalars", () => { } input NodeTypeConnectionSort { - edges: [NodeTypeEdgeSort!] + node: [NodeTypeSort!] } type NodeTypeEdge { @@ -171,17 +171,6 @@ describe("Scalars", () => { node: NodeType } - input NodeTypeEdgeSort { - node: NodeTypeSort - } - - input NodeTypeEdgeWhere { - AND: [NodeTypeEdgeWhere!] - NOT: NodeTypeEdgeWhere - OR: [NodeTypeEdgeWhere!] - node: NodeTypeWhere - } - type NodeTypeOperation { connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection } @@ -190,7 +179,7 @@ describe("Scalars", () => { AND: [NodeTypeOperationWhere!] NOT: NodeTypeOperationWhere OR: [NodeTypeOperationWhere!] - edges: NodeTypeEdgeWhere + node: NodeTypeWhere } type NodeTypeRelatedNodeConnection { @@ -316,7 +305,7 @@ describe("Scalars", () => { } input RelatedNodeConnectionSort { - edges: [RelatedNodeEdgeSort!] + node: [RelatedNodeSort!] } type RelatedNodeEdge { @@ -324,17 +313,6 @@ describe("Scalars", () => { node: RelatedNode } - input RelatedNodeEdgeSort { - node: RelatedNodeSort - } - - input RelatedNodeEdgeWhere { - AND: [RelatedNodeEdgeWhere!] - NOT: RelatedNodeEdgeWhere - OR: [RelatedNodeEdgeWhere!] - node: RelatedNodeWhere - } - type RelatedNodeOperation { connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection } @@ -343,7 +321,7 @@ describe("Scalars", () => { AND: [RelatedNodeOperationWhere!] NOT: RelatedNodeOperationWhere OR: [RelatedNodeOperationWhere!] - edges: RelatedNodeEdgeWhere + node: RelatedNodeWhere } type RelatedNodeProperties { diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index ed28e0921a..0968c85d6d 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -88,13 +88,6 @@ describe("Spatial Types", () => { node: NodeType } - input NodeTypeEdgeWhere { - AND: [NodeTypeEdgeWhere!] - NOT: NodeTypeEdgeWhere - OR: [NodeTypeEdgeWhere!] - node: NodeTypeWhere - } - type NodeTypeOperation { connection(after: String, first: Int): NodeTypeConnection } @@ -103,7 +96,7 @@ describe("Spatial Types", () => { AND: [NodeTypeOperationWhere!] NOT: NodeTypeOperationWhere OR: [NodeTypeOperationWhere!] - edges: NodeTypeEdgeWhere + node: NodeTypeWhere } type NodeTypeRelatedNodeConnection { @@ -200,13 +193,6 @@ describe("Spatial Types", () => { node: RelatedNode } - input RelatedNodeEdgeWhere { - AND: [RelatedNodeEdgeWhere!] - NOT: RelatedNodeEdgeWhere - OR: [RelatedNodeEdgeWhere!] - node: RelatedNodeWhere - } - type RelatedNodeOperation { connection(after: String, first: Int): RelatedNodeConnection } @@ -215,7 +201,7 @@ describe("Spatial Types", () => { AND: [RelatedNodeOperationWhere!] NOT: RelatedNodeOperationWhere OR: [RelatedNodeOperationWhere!] - edges: RelatedNodeEdgeWhere + node: RelatedNodeWhere } type RelatedNodeProperties { diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 73b6615993..9fc5af86fe 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -157,7 +157,7 @@ describe("Temporals", () => { } input NodeTypeConnectionSort { - edges: [NodeTypeEdgeSort!] + node: [NodeTypeSort!] } type NodeTypeEdge { @@ -165,17 +165,6 @@ describe("Temporals", () => { node: NodeType } - input NodeTypeEdgeSort { - node: NodeTypeSort - } - - input NodeTypeEdgeWhere { - AND: [NodeTypeEdgeWhere!] - NOT: NodeTypeEdgeWhere - OR: [NodeTypeEdgeWhere!] - node: NodeTypeWhere - } - type NodeTypeOperation { connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection } @@ -184,7 +173,7 @@ describe("Temporals", () => { AND: [NodeTypeOperationWhere!] NOT: NodeTypeOperationWhere OR: [NodeTypeOperationWhere!] - edges: NodeTypeEdgeWhere + node: NodeTypeWhere } type NodeTypeRelatedNodeConnection { @@ -292,7 +281,7 @@ describe("Temporals", () => { } input RelatedNodeConnectionSort { - edges: [RelatedNodeEdgeSort!] + node: [RelatedNodeSort!] } type RelatedNodeEdge { @@ -300,17 +289,6 @@ describe("Temporals", () => { node: RelatedNode } - input RelatedNodeEdgeSort { - node: RelatedNodeSort - } - - input RelatedNodeEdgeWhere { - AND: [RelatedNodeEdgeWhere!] - NOT: RelatedNodeEdgeWhere - OR: [RelatedNodeEdgeWhere!] - node: RelatedNodeWhere - } - type RelatedNodeOperation { connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection } @@ -319,7 +297,7 @@ describe("Temporals", () => { AND: [RelatedNodeOperationWhere!] NOT: RelatedNodeOperationWhere OR: [RelatedNodeOperationWhere!] - edges: RelatedNodeEdgeWhere + node: RelatedNodeWhere } type RelatedNodeProperties { From 319d9a2423dc85d8310ccab2efe69056786c2e05 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Mon, 8 Jul 2024 16:26:05 +0100 Subject: [PATCH 089/177] Move nested filtering tests --- .../filters/nested/all.int.test.ts | 39 + .../filters/nested/none.int.test.ts | 34 + .../filters/nested/single.int.test.ts | 44 + .../filters/nested/some.int.test.ts | 44 + .../api-v6/integration/issues/190.int.test.ts | 250 ++++++ .../filtering/advanced-filtering.int.test.ts | 848 ------------------ .../tests/integration/issues/190.int.test.ts | 150 ---- packages/graphql/tests/tck/issues/190.test.ts | 129 --- 8 files changed, 411 insertions(+), 1127 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/issues/190.int.test.ts delete mode 100644 packages/graphql/tests/integration/issues/190.int.test.ts delete mode 100644 packages/graphql/tests/tck/issues/190.test.ts 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 index 72cfb57fac..fe6ea7ee6b 100644 --- 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 @@ -167,4 +167,43 @@ describe("Relationship filters with all", () => { }, }); }); + + test("filter by nested node with all and NOT operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { edges: { node: { actors: { edges: { all: { 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 index 0edfbc9dc4..a80cab42e7 100644 --- 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 @@ -172,4 +172,38 @@ describe("Relationship filters with none", () => { }, }); }); + + test("filter by nested node with none and NOT operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { edges: { node: { actors: { edges: { none: { 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 index ac93f19454..a8a83cdd75 100644 --- 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 @@ -169,4 +169,48 @@ describe("Relationship filters with single", () => { }, }); }); + + test("filter by nested node with single and NOT operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { edges: { node: { actors: { edges: { single: { 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 index 43c64d53ec..9d162a5201 100644 --- 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 @@ -182,4 +182,48 @@ describe("Relationship filters with some", () => { }, }); }); + + test("filter by nested node with some and NOT operator", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { edges: { node: { actors: { edges: { some: { 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/issues/190.int.test.ts b/packages/graphql/tests/api-v6/integration/issues/190.int.test.ts new file mode 100644 index 0000000000..39d3fa3a6a --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/issues/190.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 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: { edges: { node: { demographics: { edges: { some: {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: { + edges: { + node: { + demographics: { + edges: { + some: { + 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/filtering/advanced-filtering.int.test.ts b/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts index 0a5d4ad3a1..9eadb3f5f4 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", () => { @@ -331,853 +330,6 @@ describe("Advanced Filtering", () => { }); 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"); 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/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\\" - }" - `); - }); -}); From 0828428c88226c3f8dc9e1a3ac5545aec6a0eaf4 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Mon, 8 Jul 2024 18:05:00 +0100 Subject: [PATCH 090/177] Fix null filters --- .../api-v6/queryIRFactory/FilterFactory.ts | 5 +- .../resolve-tree-parser/graphql-tree.ts | 4 +- .../src/api-v6/resolvers/read-resolver.ts | 1 - .../filters/null-filtering.int.test.ts | 166 ++++++++++++++++++ .../filtering/advanced-filtering.int.test.ts | 156 ---------------- 5 files changed, 172 insertions(+), 160 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/null-filtering.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index f8c7121740..f9f39ea837 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -282,9 +282,12 @@ export class FilterFactory { // TODO: remove adapter from here private createPropertyFilters( attribute: AttributeAdapter, - filters: GraphQLAttributeFilters, + 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({ diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index 67383d7c72..06b5e66d86 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -75,10 +75,10 @@ export type GraphQLWhereArgs = LogicalOperation<{ edges?: GraphQLEdgeWhereArgs; }>; -export type GraphQLNodeWhereArgs = LogicalOperation>; +export type GraphQLNodeWhereArgs = LogicalOperation>; export type GraphQLEdgeWhereArgs = LogicalOperation<{ - properties?: Record; + properties?: Record; node?: GraphQLNodeWhereArgs; }>; diff --git a/packages/graphql/src/api-v6/resolvers/read-resolver.ts b/packages/graphql/src/api-v6/resolvers/read-resolver.ts index 969a1b8681..0ccae39a14 100644 --- a/packages/graphql/src/api-v6/resolvers/read-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/read-resolver.ts @@ -34,7 +34,6 @@ export function generateReadResolver({ entity }: { entity: ConcreteEntity }) { ) { const resolveTree = getNeo4jResolveTree(info, { args }); context.resolveTree = resolveTree; - const graphQLTree = parseResolveInfoTree({ resolveTree: context.resolveTree, entity }); const { cypher, params } = translateReadOperation({ context: context, 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..e34fa67edb --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/null-filtering.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 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: { + edges: { + 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: { + edges: { + 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: { + edges: { + 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/integration/filtering/advanced-filtering.int.test.ts b/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts index 9eadb3f5f4..8ce6fa425a 100644 --- a/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts +++ b/packages/graphql/tests/integration/filtering/advanced-filtering.int.test.ts @@ -328,160 +328,4 @@ describe("Advanced Filtering", () => { ); }); }); - - describe("Relationship/Connection Filtering", () => { - 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); - }); - }); }); From 4ba98f6ca4889ede453856a69d46ed3906e79231 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Mon, 8 Jul 2024 18:23:17 +0100 Subject: [PATCH 091/177] Fix implicit AND filters --- .../api-v6/queryIRFactory/FilterFactory.ts | 24 +++++- .../filters/logical/or-filter.test.ts | 39 +++++++++ .../filtering/operations.int.test.ts | 81 ------------------- 3 files changed, 60 insertions(+), 84 deletions(-) delete mode 100644 packages/graphql/tests/integration/filtering/operations.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index f9f39ea837..bc58df45ce 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -68,7 +68,7 @@ export class FilterFactory { const edgeFilters = this.createEdgeFilters({ entity, relationship, edgeWhere: where.edges }); - return [...edgeFilters, ...andFilters, ...orFilters, ...notFilters]; + return [...this.mergeFiltersWithAnd(edgeFilters), ...andFilters, ...orFilters, ...notFilters]; } private createLogicalFilters({ @@ -128,7 +128,13 @@ export class FilterFactory { relationship, }); } - return [...nodeFilters, ...edgePropertiesFilters, ...andFilters, ...orFilters, ...notFilters]; + return [ + ...this.mergeFiltersWithAnd(nodeFilters), + ...edgePropertiesFilters, + ...andFilters, + ...orFilters, + ...notFilters, + ]; } private createLogicalEdgeFilters( @@ -196,7 +202,7 @@ export class FilterFactory { return []; }); - return [...andFilters, ...orFilters, ...notFilters, ...nodePropertiesFilters]; + return [...andFilters, ...orFilters, ...notFilters, ...this.mergeFiltersWithAnd(nodePropertiesFilters)]; } private createLogicalNodeFilters( @@ -325,4 +331,16 @@ export class FilterFactory { }); }); } + + private mergeFiltersWithAnd(filters: Filter[]): Filter[] { + if (filters.length > 1) { + return [ + new LogicalFilter({ + operation: "AND", + filters: filters, + }), + ]; + } + return filters; + } } diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts index 09efe6f979..a6a596b6be 100644 --- a/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts +++ b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts @@ -190,4 +190,43 @@ describe("Filters OR", () => { }, }); }); + + test("top level OR filter combined with implicit AND", async () => { + const query = /* GraphQL */ ` + query { + ${Movie.plural}( + where: { + OR: [ + { edges: { node: { title: { equals: "The Matrix" }, year: { equals: 2001 } } } } + { edges: { 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/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 }, - ]) - ); - }); -}); From fe18eae494d2ad41fa34ac816d2e34e785daa300 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 9 Jul 2024 10:34:44 +0100 Subject: [PATCH 092/177] Move sort and filter tests --- .../integration/filters/typename.int.test.ts | 86 +++ .../integration/sort/sort-filter.int.test.ts | 263 ++++++++ .../api-v6/integration/sort/sort.int.test.ts | 8 +- .../relationship-properties/read.int.test.ts | 617 ------------------ .../tests/integration/sort.int.test.ts | 413 ------------ 5 files changed, 350 insertions(+), 1037 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/typename.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/sort/sort-filter.int.test.ts delete mode 100644 packages/graphql/tests/integration/relationship-properties/read.int.test.ts diff --git a/packages/graphql/tests/api-v6/integration/filters/typename.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/typename.int.test.ts new file mode 100644 index 0000000000..e92e3c2baa --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/filters/typename.int.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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("Filter by typename", () => { + 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: { + edges: { + 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/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..bc654adea5 --- /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: { edges: { node: { title: { in: ["The Matrix 2", "The Matrix 4"] } } } }) { + connection(sort: { edges: { 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: { edges: { node: { title: { in: ["The Matrix 2", "The Matrix 4"] } } } }) { + connection(sort: { edges: { 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.int.test.ts b/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts index 52c932b4b6..1b7dd1ac23 100644 --- a/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts @@ -155,7 +155,7 @@ describe("Sort", () => { }); }); - test("should be able to sort by multiple criteria", async () => { + test("should be able to sort by multiple criteria, not in the selection set", async () => { const query = /* GraphQL */ ` query { ${Movie.plural} { @@ -164,7 +164,6 @@ describe("Sort", () => { node { title description - ratings } } @@ -183,14 +182,12 @@ describe("Sort", () => { node: { title: "The Matrix", description: "DVD edition", - ratings: 5, }, }, { node: { title: "The Matrix", description: "Cinema edition", - ratings: 4, }, }, @@ -198,21 +195,18 @@ describe("Sort", () => { 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/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/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); - }); - }); - }); }); }); From c99425758d09f3071893fd38b84af84cf1115393 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 9 Jul 2024 10:55:41 +0100 Subject: [PATCH 093/177] Update multi-database tests --- .../database}/multi-database.int.test.ts | 117 +++++++++++------- 1 file changed, 75 insertions(+), 42 deletions(-) rename packages/graphql/tests/{integration => api-v6/integration/database}/multi-database.int.test.ts (66%) 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 66% 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..c3cbab6606 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: { edges: { 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: { edges: { 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 = ` query { - ${Movie.plural}(where: { id: "${id}" }) { - id + ${Movie.plural}(where: { edges: { 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 = ` query { - ${Movie.plural}(where: { id: "${id}" }) { - id + ${Movie.plural}(where: { edges: { 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, + }, + }, + ], + }, + }, + }); }); }); From 04d967277aaae9fc2672da5e42f6191f55fd7dc3 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 9 Jul 2024 11:56:36 +0100 Subject: [PATCH 094/177] update errors tests --- .../integration/errors.int.test.ts | 16 ++-- .../integration/filters/typename.int.test.ts | 86 ------------------- 2 files changed, 11 insertions(+), 91 deletions(-) rename packages/graphql/tests/{ => api-v6}/integration/errors.int.test.ts (79%) delete mode 100644 packages/graphql/tests/api-v6/integration/filters/typename.int.test.ts 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/typename.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/typename.int.test.ts deleted file mode 100644 index e92e3c2baa..0000000000 --- a/packages/graphql/tests/api-v6/integration/filters/typename.int.test.ts +++ /dev/null @@ -1,86 +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("Filter by typename", () => { - 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: { - edges: { - 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", - }, - }, - ], - }, - }, - }); - }); -}); From 946773aca113a07918d3d171ef9216c000db5827 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Tue, 9 Jul 2024 15:47:37 +0200 Subject: [PATCH 095/177] wip --- .../resolve-tree-parser/ResolveTreeParser.ts | 2 +- .../TopLevelResolveTreeParser.ts | 33 ++++++++++++++++++- .../resolve-tree-parser/graphql-tree.ts | 16 +++++++-- .../parse-resolve-info-tree.ts | 8 ++--- .../tck/filters/top-level-filters.test.ts | 5 ++- 5 files changed, 53 insertions(+), 11 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index d9eeb10210..73d255ca1f 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -165,7 +165,7 @@ export abstract class ResolveTreeParser }; } - private parseConnection(resolveTree: ResolveTree): GraphQLTreeConnection { + protected parseConnection(resolveTree: ResolveTree): GraphQLTreeConnection { const entityTypes = this.entity.typeNames; const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges"); const edgeResolveTree = edgesResolveTree ? this.parseEdges(edgesResolveTree) : undefined; diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts index 9e8b4c573c..767018f91f 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts @@ -21,13 +21,44 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { ResolveTreeParser } from "./ResolveTreeParser"; import { findFieldByName } from "./find-field-by-name"; -import type { GraphQLTreeEdge } from "./graphql-tree"; +import type { + GraphQLReadOperationArgsTopLevel, + GraphQLTreeEdge, + GraphQLTreeReadOperationTopLevel, +} from "./graphql-tree"; export class TopLevelResolveTreeParser extends ResolveTreeParser { protected get targetNode(): ConcreteEntity { return this.entity; } + /** Parse a resolveTree into a Neo4j GraphQLTree */ + public parseOperationTopLevel(resolveTree: ResolveTree): GraphQLTreeReadOperationTopLevel { + const connectionResolveTree = findFieldByName( + resolveTree, + this.entity.typeNames.connectionOperation, + "connection" + ); + + const connection = connectionResolveTree ? this.parseConnection(connectionResolveTree) : undefined; + const connectionOperationArgs = this.parseOperationArgsTopLevel(resolveTree.args); + return { + alias: resolveTree.alias, + args: connectionOperationArgs, + name: resolveTree.name, + fields: { + connection, + }, + }; + } + + private parseOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLReadOperationArgsTopLevel { + // Not properly parsed, assuming the type is the same + return { + where: resolveTreeArgs.where, + }; + } + protected parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { const edgeType = this.entity.typeNames.edge; diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index 67383d7c72..f56aeba6c6 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -19,7 +19,7 @@ import type { Integer } from "neo4j-driver"; -export type GraphQLTree = GraphQLTreeReadOperation; +export type GraphQLTree = GraphQLTreeReadOperationTopLevel; interface GraphQLTreeElement { alias: string; @@ -59,12 +59,24 @@ export type RelationshipFilters = { }; }; +export interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { + name: string; + fields: { + connection?: GraphQLTreeConnection; + }; + args: GraphQLReadOperationArgsTopLevel; +} + export interface GraphQLTreeReadOperation extends GraphQLTreeElement { + name: string; fields: { connection?: GraphQLTreeConnection; }; args: GraphQLReadOperationArgs; - name: string; +} + +export interface GraphQLReadOperationArgsTopLevel { + where?: GraphQLNodeWhereArgs; } export interface GraphQLReadOperationArgs { 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 index 6c7eff8292..47494629e4 100644 --- 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 @@ -21,7 +21,7 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { GlobalNodeResolveTreeParser } from "./GlobalNodeResolveTreeParser"; import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; -import type { GraphQLTree } from "./graphql-tree"; +import type { GraphQLTree, GraphQLTreeReadOperation } from "./graphql-tree"; export function parseResolveInfoTree({ resolveTree, @@ -31,7 +31,7 @@ export function parseResolveInfoTree({ entity: ConcreteEntity; }): GraphQLTree { const parser = new TopLevelResolveTreeParser({ entity }); - return parser.parseOperation(resolveTree); + return parser.parseOperationTopLevel(resolveTree); } export function parseGlobalNodeResolveInfoTree({ @@ -40,7 +40,7 @@ export function parseGlobalNodeResolveInfoTree({ }: { resolveTree: ResolveTree; entity: ConcreteEntity; -}): GraphQLTree { +}): GraphQLTreeReadOperation { const parser = new GlobalNodeResolveTreeParser({ entity }); - return parser.parseOperation(resolveTree); + return parser.parseOperation(resolveTree); // TODO: This needs to be parseOperationTopLevel } 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 index 9bdf789029..193589e112 100644 --- 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 @@ -35,6 +35,7 @@ describe("Top level filters", () => { neoSchema = new Neo4jGraphQL({ typeDefs, + debug: true, }); }); @@ -43,9 +44,7 @@ describe("Top level filters", () => { query { movies( where: { - edges: { - node: { title: { equals: "The Matrix" }, year: { equals: 100 }, runtime: { equals: 90.5 } } - } + node: { title: { equals: "The Matrix" }, year: { equals: 100 }, runtime: { equals: 90.5 } } } ) { connection { From f445b14392512741922f29358ad5b4146ef21f2b Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 10 Jul 2024 11:22:43 +0100 Subject: [PATCH 096/177] WIP - move issues integration tests --- .../integration/issues/360.int.test.ts | 66 +++++-- .../integration/issues/433.int.test.ts | 56 ++++-- .../integration/issues/560.int.test.ts | 105 +++++----- .../api-v6/integration/issues/582.int.test.ts | 172 ++++++++++++++++ .../connections/nested.int.test.ts | 184 ------------------ .../tests/integration/issues/582.int.test.ts | 120 ------------ 6 files changed, 317 insertions(+), 386 deletions(-) rename packages/graphql/tests/{ => api-v6}/integration/issues/360.int.test.ts (69%) rename packages/graphql/tests/{ => api-v6}/integration/issues/433.int.test.ts (58%) rename packages/graphql/tests/{ => api-v6}/integration/issues/560.int.test.ts (53%) create mode 100644 packages/graphql/tests/api-v6/integration/issues/582.int.test.ts delete mode 100644 packages/graphql/tests/integration/connections/nested.int.test.ts delete mode 100644 packages/graphql/tests/integration/issues/582.int.test.ts 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 69% 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..aa88bb1e96 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 @@ -47,8 +45,14 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { const query = ` query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { - ${type.plural}(where: { AND: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { - id + ${type.plural}(where: { edges: { 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 @@ -86,8 +96,14 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { const query = ` query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { - ${type.plural}(where: { OR: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { - id + ${type.plural}(where: { edges: { 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} { + type ${type.name} @node { id: ID! name: String start: DateTime @@ -128,8 +150,14 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { const query = ` query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { - ${type.plural}(where: { OR: [{ start_GTE: $rangeStart }, { start_LTE: $rangeEnd }, { activity: $activity }] }) { - id + ${type.plural}(where: { edges: { 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..0ada097a29 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: {edges: {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..27bba7bcdb --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/issues/582.int.test.ts @@ -0,0 +1,172 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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: { + edges: { + node: { + type: { equals: "Cat" }, + children: { + edges: { + some: { + node: { + type: { equals: "Dog" }, + parents: { + edges: { + some: { + 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: { + edges: { + node: { + type: { equals: "Cat" }, + children: { + edges: { + some: { + node: { + type: { equals: "Dog" }, + parents: { + edges: { + some: { + node: { + type: { equals: "Bird" }, + children: { + edges: { + some: { + 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/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/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", - }); - }); -}); From d007319a953c595f673c5feaf39afceac4fe6dbd Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 10 Jul 2024 13:27:48 +0100 Subject: [PATCH 097/177] WIP update top level filtering --- .../queryIRFactory/ReadOperationFactory.ts | 10 ++------ .../GlobalNodeResolveTreeParser.ts | 24 ++++++++++++++++++- .../TopLevelResolveTreeParser.ts | 2 +- .../parse-resolve-info-tree.ts | 6 ++--- .../translators/translate-read-operation.ts | 4 ++-- .../tck/filters/top-level-filters.test.ts | 1 - 6 files changed, 31 insertions(+), 16 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 128544b185..544b64879d 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -20,8 +20,8 @@ import { cursorToOffset } from "graphql-relay"; import type { Integer } from "neo4j-driver"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; -import type { Attribute } from "../../schema-model/attribute/Attribute"; 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"; @@ -60,13 +60,7 @@ export class ReadOperationFactory { this.filterFactory = new FilterFactory(schemaModel); } - public createAST({ - graphQLTree, - entity, - }: { - graphQLTree: GraphQLTreeReadOperation; - entity: ConcreteEntity; - }): QueryAST { + public createAST({ graphQLTree, entity }: { graphQLTree: GraphQLTree; entity: ConcreteEntity }): QueryAST { const operation = this.generateOperation({ graphQLTree, entity, diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts index cfabfa63fa..87c733821e 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts @@ -20,13 +20,35 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; -import type { GraphQLTreeReadOperation } from "./graphql-tree"; +import { findFieldByName } from "./find-field-by-name"; +import type { GraphQLTreeReadOperation, GraphQLTreeReadOperationTopLevel } from "./graphql-tree"; export class GlobalNodeResolveTreeParser extends TopLevelResolveTreeParser { constructor({ entity }: { entity: ConcreteEntity }) { super({ entity: entity }); } + /** Parse a resolveTree into a Neo4j GraphQLTree */ + public parseOperationTopLevel(resolveTree: ResolveTree): GraphQLTreeReadOperationTopLevel { + // FIXME + const connectionResolveTree = findFieldByName( + resolveTree, + this.entity.typeNames.connectionOperation, + "connection" + ); + + const connection = connectionResolveTree ? this.parseConnection(connectionResolveTree) : undefined; + const connectionOperationArgs = this.parseOperationArgsTopLevel(resolveTree.args); + return { + alias: resolveTree.alias, + args: connectionOperationArgs, + name: resolveTree.name, + fields: { + connection, + }, + }; + } + /** Parse a resolveTree into a Neo4j GraphQLTree */ public parseOperation(resolveTree: ResolveTree): GraphQLTreeReadOperation { const entityTypes = this.targetNode.typeNames; diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts index 767018f91f..e335266e77 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts @@ -52,7 +52,7 @@ export class TopLevelResolveTreeParser extends ResolveTreeParser }; } - private parseOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLReadOperationArgsTopLevel { + protected parseOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLReadOperationArgsTopLevel { // Not properly parsed, assuming the type is the same return { where: resolveTreeArgs.where, 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 index 47494629e4..898877cacc 100644 --- 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 @@ -21,7 +21,7 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { GlobalNodeResolveTreeParser } from "./GlobalNodeResolveTreeParser"; import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; -import type { GraphQLTree, GraphQLTreeReadOperation } from "./graphql-tree"; +import type { GraphQLTree } from "./graphql-tree"; export function parseResolveInfoTree({ resolveTree, @@ -40,7 +40,7 @@ export function parseGlobalNodeResolveInfoTree({ }: { resolveTree: ResolveTree; entity: ConcreteEntity; -}): GraphQLTreeReadOperation { +}): GraphQLTree { const parser = new GlobalNodeResolveTreeParser({ entity }); - return parser.parseOperation(resolveTree); // TODO: This needs to be parseOperationTopLevel + return parser.parseOperationTopLevel(resolveTree); // TODO: This needs to be parseOperationTopLevel } diff --git a/packages/graphql/src/api-v6/translators/translate-read-operation.ts b/packages/graphql/src/api-v6/translators/translate-read-operation.ts index 235aab916a..067519c1a9 100644 --- a/packages/graphql/src/api-v6/translators/translate-read-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-read-operation.ts @@ -23,7 +23,7 @@ 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 { GraphQLTreeReadOperation } from "../queryIRFactory/resolve-tree-parser/graphql-tree"; +import type { GraphQLTree } from "../queryIRFactory/resolve-tree-parser/graphql-tree"; const debug = Debug(DEBUG_TRANSLATE); @@ -33,7 +33,7 @@ export function translateReadOperation({ graphQLTree, }: { context: Neo4jGraphQLTranslationContext; - graphQLTree: GraphQLTreeReadOperation; + graphQLTree: GraphQLTree; entity: ConcreteEntity; }): Cypher.CypherResult { const readFactory = new ReadOperationFactory(context.schemaModel); 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 index 193589e112..20b5173acc 100644 --- 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 @@ -35,7 +35,6 @@ describe("Top level filters", () => { neoSchema = new Neo4jGraphQL({ typeDefs, - debug: true, }); }); From cd71378cdb96aac3177cf92de9fabb32877e1336 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 10 Jul 2024 16:46:38 +0100 Subject: [PATCH 098/177] Remove edges from top level filters --- .../api-v6/queryIRFactory/FilterFactory.ts | 48 +++++++++++++++ .../queryIRFactory/ReadOperationFactory.ts | 2 +- .../GlobalNodeResolveTreeParser.ts | 32 ++-------- .../resolve-tree-parser/graphql-tree.ts | 6 +- .../parse-resolve-info-tree.ts | 2 +- .../schema-types/TopLevelEntitySchemaTypes.ts | 18 +++--- .../relayId/relayId-filters.int.test.ts | 6 +- .../filters/logical/and-filter.test.ts | 51 ++-------------- .../filters/logical/not-filter.test.ts | 50 +-------------- .../filters/logical/or-filter.test.ts | 53 +--------------- .../filters/nested/all.int.test.ts | 6 +- .../filters/nested/none.int.test.ts | 6 +- .../filters/nested/single.int.test.ts | 6 +- .../filters/nested/some.int.test.ts | 6 +- .../filters/top-level-filters.int.test.ts | 8 +-- .../types/boolean/boolean-equals.int.test.ts | 6 +- .../types/date/array/date-equals.int.test.ts | 2 +- .../types/date/date-equals.int.test.ts | 2 +- .../filters/types/date/date-in.int.test.ts | 4 +- .../filters/types/date/date-lt.int.test.ts | 2 +- .../array/datetime-equals.int.test.ts | 10 ++- .../datetime/datetime-equals.int.test.ts | 2 +- .../array/duration-equals.int.test.ts | 2 +- .../duration/duration-equals.int.test.ts | 2 +- .../types/duration/duration-lt.int.test.ts | 2 +- .../array/localdatetime-equals.int.test.ts | 4 +- .../localdatetime-equals.int.test.ts | 4 +- .../number/array/number-equals.int.test.ts | 4 +- .../types/number/number-equals.int.test.ts | 4 +- .../types/number/number-gt.int.test.ts | 8 +-- .../types/number/number-in.int.test.ts | 4 +- .../types/number/number-lt.int.test.ts | 8 +-- .../types/string/string-contains.int.test.ts | 4 +- .../types/string/string-ends-with-int.test.ts | 4 +- .../types/string/string-equals.int.test.ts | 4 +- .../types/string/string-in.int.test.ts | 4 +- .../string/string-starts-with.int.test.ts | 4 +- .../types/time/array/time-equals.int.test.ts | 2 +- .../types/time/time-equals.int.test.ts | 2 +- .../api-v6/schema/directives/relayId.test.ts | 6 +- .../tests/api-v6/schema/relationship.test.ts | 24 ++++++-- .../tests/api-v6/schema/simple.test.ts | 24 ++++++-- .../tests/api-v6/schema/types/scalars.test.ts | 12 +++- .../api-v6/schema/types/temporals.test.ts | 12 +++- .../tck/filters/array/array-filters.test.ts | 2 +- .../logical-filters/and-filter.test.ts | 61 +------------------ .../logical-filters/not-filter.test.ts | 50 +-------------- .../filters/logical-filters/or-filter.test.ts | 59 +----------------- .../api-v6/tck/filters/nested/all.test.ts | 28 +++------ .../api-v6/tck/filters/nested/none.test.ts | 28 +++------ .../api-v6/tck/filters/nested/single.test.ts | 30 +++------ .../api-v6/tck/filters/nested/some.test.ts | 28 +++------ .../types/array/temporals-array.test.ts | 24 ++++---- .../filters/types/cartesian-filters.test.ts | 24 +++----- .../tck/filters/types/point-filters.test.ts | 30 +++------ .../filters/types/temporals-filters.test.ts | 14 ++--- 56 files changed, 284 insertions(+), 566 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index f8c7121740..bdf72160b5 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -38,6 +38,7 @@ import type { GraphQLNodeFilters, GraphQLNodeWhereArgs, GraphQLWhereArgs, + GraphQLWhereArgsTopLevel, RelationshipFilters, } from "./resolve-tree-parser/graphql-tree"; @@ -71,6 +72,53 @@ export class FilterFactory { return [...edgeFilters, ...andFilters, ...orFilters, ...notFilters]; } + public createTopLevelFilters({ + where = {}, + entity, + }: { + entity: ConcreteEntity; + where?: GraphQLWhereArgsTopLevel; + }): Filter[] { + const andFilters = this.createTopLevelLogicalFilters({ operation: "AND", entity, where: where.AND }); + const orFilters = this.createTopLevelLogicalFilters({ operation: "OR", entity, where: where.OR }); + const notFilters = this.createTopLevelLogicalFilters({ + operation: "NOT", + entity, + where: where.NOT ? [where.NOT] : undefined, + }); + + const edgeFilters = this.createNodeFilter({ entity, where: where.node }); + return [...edgeFilters, ...andFilters, ...orFilters, ...notFilters]; + } + + private createTopLevelLogicalFilters({ + where = [], + operation, + entity, + }: { + entity: ConcreteEntity; + operation: LogicalOperators; + where?: GraphQLWhereArgsTopLevel[]; + }): [] | [Filter] { + if (where.length === 0) { + return []; + } + const nestedFilters = where.flatMap((orWhere: GraphQLWhereArgsTopLevel) => { + return this.createTopLevelFilters({ entity, where: orWhere }); + }); + + if (nestedFilters.length > 0) { + return [ + new LogicalFilter({ + operation, + filters: nestedFilters, + }), + ]; + } + + return []; + } + private createLogicalFilters({ where = [], relationship, diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 544b64879d..48c1a4fa66 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -104,7 +104,7 @@ export class ReadOperationFactory { }, pagination, sortFields: sortInputFields, - filters: this.filterFactory.createFilters({ entity, where: graphQLTree.args.where }), + filters: this.filterFactory.createTopLevelFilters({ entity, where: graphQLTree.args.where }), }); } diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts index 87c733821e..14d5e5b926 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts @@ -20,8 +20,7 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; -import { findFieldByName } from "./find-field-by-name"; -import type { GraphQLTreeReadOperation, GraphQLTreeReadOperationTopLevel } from "./graphql-tree"; +import type { GraphQLTree } from "./graphql-tree"; export class GlobalNodeResolveTreeParser extends TopLevelResolveTreeParser { constructor({ entity }: { entity: ConcreteEntity }) { @@ -29,28 +28,7 @@ export class GlobalNodeResolveTreeParser extends TopLevelResolveTreeParser { } /** Parse a resolveTree into a Neo4j GraphQLTree */ - public parseOperationTopLevel(resolveTree: ResolveTree): GraphQLTreeReadOperationTopLevel { - // FIXME - const connectionResolveTree = findFieldByName( - resolveTree, - this.entity.typeNames.connectionOperation, - "connection" - ); - - const connection = connectionResolveTree ? this.parseConnection(connectionResolveTree) : undefined; - const connectionOperationArgs = this.parseOperationArgsTopLevel(resolveTree.args); - return { - alias: resolveTree.alias, - args: connectionOperationArgs, - name: resolveTree.name, - fields: { - connection, - }, - }; - } - - /** Parse a resolveTree into a Neo4j GraphQLTree */ - public parseOperation(resolveTree: ResolveTree): GraphQLTreeReadOperation { + public parseTopLevelOperation(resolveTree: ResolveTree): GraphQLTree { const entityTypes = this.targetNode.typeNames; resolveTree.fieldsByTypeName[entityTypes.node] = { ...resolveTree.fieldsByTypeName["Node"], @@ -62,10 +40,8 @@ export class GlobalNodeResolveTreeParser extends TopLevelResolveTreeParser { alias: resolveTree.alias, args: { where: { - edges: { - node: { - id: { equals: resolveTree.args.id as any }, - }, + node: { + id: { equals: resolveTree.args.id as any }, }, }, }, diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index f56aeba6c6..cd19738089 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -76,7 +76,7 @@ export interface GraphQLTreeReadOperation extends GraphQLTreeElement { } export interface GraphQLReadOperationArgsTopLevel { - where?: GraphQLNodeWhereArgs; + where?: GraphQLWhereArgsTopLevel; } export interface GraphQLReadOperationArgs { @@ -87,6 +87,10 @@ export type GraphQLWhereArgs = LogicalOperation<{ edges?: GraphQLEdgeWhereArgs; }>; +export type GraphQLWhereArgsTopLevel = LogicalOperation<{ + node?: GraphQLNodeWhereArgs; +}>; + export type GraphQLNodeWhereArgs = LogicalOperation>; export type GraphQLEdgeWhereArgs = LogicalOperation<{ 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 index 898877cacc..e1c01849a4 100644 --- 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 @@ -42,5 +42,5 @@ export function parseGlobalNodeResolveInfoTree({ entity: ConcreteEntity; }): GraphQLTree { const parser = new GlobalNodeResolveTreeParser({ entity }); - return parser.parseOperationTopLevel(resolveTree); // TODO: This needs to be parseOperationTopLevel + return parser.parseTopLevelOperation(resolveTree); // TODO: This needs to be parseOperationTopLevel } 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 index 4b28efb2f6..999de96486 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -81,15 +81,15 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { - return { - fields: { - node: this.nodeSort.NonNull.List, - }, - }; - }); - } + // protected get connectionSort(): InputTypeComposer { + // return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { + // return { + // fields: { + // node: this.nodeSort.NonNull.List, + // }, + // }; + // }); + // } protected get edge(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.edge, () => { 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 index 5e5bb8d45e..6cbb9fe1c3 100644 --- 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 @@ -80,10 +80,8 @@ describe("RelayId projection with filters", () => { const connectionQuery = /* GraphQL */ ` query { ${Movie.plural}(where: { - edges: { - node: { - id: { equals: "${movieGlobalId}"} - } + node: { + id: { equals: "${movieGlobalId}"} } }) { connection { diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts index 141eb3b5ae..2f92941f14 100644 --- a/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts +++ b/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts @@ -65,8 +65,8 @@ describe("Filters AND", () => { ${Movie.plural}( where: { AND: [ - { edges: { node: { runtime: { equals: 90.5 } } } } - { edges: { node: { year: { equals: 1999 } } } } + { node: { runtime: { equals: 90.5 } } } + { node: { year: { equals: 1999 } } } ] } ) { @@ -110,8 +110,8 @@ describe("Filters AND", () => { where: { AND: { AND: [ - { edges: { node: { runtime: { equals: 90.5 } } } } - { edges: { node: { year: { equals: 1999 } } } } + { node: { runtime: { equals: 90.5 } } } + { node: { year: { equals: 1999 } } } ] } } @@ -148,47 +148,4 @@ describe("Filters AND", () => { }, }); }); - - test("AND filter in edges by node", async () => { - const query = /* GraphQL */ ` - query { - ${Movie.plural}( - where: { - edges: { - 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.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.test.ts index 8a65e60f5f..ce1f76aa21 100644 --- a/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.test.ts +++ b/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.test.ts @@ -61,55 +61,7 @@ describe("Filters NOT", () => { test("top level NOT filter by node", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}( - where: { - NOT: - { edges: { 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", - }, - }, - ]), - }, - }, - }); - }); - - test("NOT filter in edges by node", async () => { - const query = /* GraphQL */ ` - query { - ${Movie.plural}( - where: { - edges: { - NOT: { node: { title: { equals: "The Matrix" } } } - } - } - ) { + ${Movie.plural}(where: { NOT: { node: { title: { equals: "The Matrix" } } } }) { connection { edges { node { diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts index 09efe6f979..0ea264c5b2 100644 --- a/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts +++ b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts @@ -62,12 +62,7 @@ describe("Filters OR", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { - OR: [ - { edges: { node: { title: { equals: "The Matrix" } } } } - { edges: { node: { year: { equals: 2001 } } } } - ] - } + where: { OR: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 2001 } } }] } ) { connection { edges { @@ -108,52 +103,6 @@ describe("Filters OR", () => { ${Movie.plural}( where: { OR: { - OR: [ - { edges: { node: { title: { equals: "The Matrix" } } } } - { edges: { 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("OR filter in edges by node", async () => { - const query = /* GraphQL */ ` - query { - ${Movie.plural}( - where: { - edges: { OR: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 2001 } } }] } } 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 index 72cfb57fac..d4a25b69c7 100644 --- 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 @@ -65,7 +65,7 @@ describe("Relationship filters with all", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { all: { node: { name: { equals: "Keanu" } } } } } } } } + where: { node: { actors: { edges: { all: { node: { name: { equals: "Keanu" } } } } } } } ) { connection { edges { @@ -99,7 +99,7 @@ describe("Relationship filters with all", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { all: { properties: { year: { equals: 1999 } } } } } } } } + where: { node: { actors: { edges: { all: { properties: { year: { equals: 1999 } } } } } } } ) { connection { edges { @@ -133,7 +133,7 @@ describe("Relationship filters with all", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { all: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } } + where: { node: { actors: { edges: { all: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } ) { connection { edges { 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 index 0edfbc9dc4..a3e1441a7b 100644 --- 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 @@ -65,7 +65,7 @@ describe("Relationship filters with none", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { none: { node: { name: { equals: "Keanu" } } } } } } } } + where: { node: { actors: { edges: { none: { node: { name: { equals: "Keanu" } } } } } } } ) { connection { edges { @@ -104,7 +104,7 @@ describe("Relationship filters with none", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { none: { properties: { year: { equals: 1999 } } } } } } } } + where: { node: { actors: { edges: { none: { properties: { year: { equals: 1999 } } } } } } } ) { connection { edges { @@ -143,7 +143,7 @@ describe("Relationship filters with none", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { none: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } } + where: { node: { actors: { edges: { none: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } ) { connection { edges { 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 index ac93f19454..f4d3ae7a2c 100644 --- 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 @@ -67,7 +67,7 @@ describe("Relationship filters with single", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { single: { node: { name: { equals: "Keanu" } } } } } } } } + where: { node: { actors: { edges: { single: { node: { name: { equals: "Keanu" } } } } } } } ) { connection { edges { @@ -101,7 +101,7 @@ describe("Relationship filters with single", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { single: { properties: { year: { equals: 1999 } } } } } } } } + where: { node: { actors: { edges: { single: { properties: { year: { equals: 1999 } } } } } } } ) { connection { edges { @@ -135,7 +135,7 @@ describe("Relationship filters with single", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { single: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } } + where: { node: { actors: { edges: { single: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } ) { connection { edges { 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 index 43c64d53ec..071459bdc3 100644 --- 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 @@ -65,7 +65,7 @@ describe("Relationship filters with some", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { some: { node: { name: { equals: "Keanu" } } } } } } } } + where: { node: { actors: { edges: { some: { node: { name: { equals: "Keanu" } } } } } } } ) { connection { edges { @@ -104,7 +104,7 @@ describe("Relationship filters with some", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { some: { properties: { year: { equals: 1999 } } } } } } } } + where: { node: { actors: { edges: { some: { properties: { year: { equals: 1999 } } } } } } } ) { connection { edges { @@ -143,7 +143,7 @@ describe("Relationship filters with some", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { some: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } } + where: { node: { actors: { edges: { some: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } ) { connection { edges { 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 index 5d90d04abd..07fb4c6c48 100644 --- 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 @@ -49,13 +49,7 @@ describe("Top level filters", () => { test("should be able to get a Movie", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}( - where: { - edges: { - node: { year: { equals: 1999 }, runtime: { equals: 90.5 } } - } - } - ) { + ${Movie.plural}(where: { node: { year: { equals: 1999 }, runtime: { equals: 90.5 } } }) { connection { edges { node { 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 index 6c0db0dd28..f4b01a6c2c 100644 --- 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 @@ -50,7 +50,7 @@ describe("Boolean Filtering", () => { test("filter by true", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { equals: true } } } }) { + ${Movie.plural}(where: { node: { value: { equals: true } } }) { connection { edges { node { @@ -82,7 +82,7 @@ describe("Boolean Filtering", () => { test("filter by false", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { equals: false } } } }) { + ${Movie.plural}(where: { node: { value: { equals: false } } }) { connection { edges { node { @@ -114,7 +114,7 @@ describe("Boolean Filtering", () => { test("filter by NOT", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { equals: true } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { equals: true } } } }) { connection { edges { node { 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 index 95582d27c9..afd4348f1b 100644 --- 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 @@ -62,7 +62,7 @@ describe("Date array - Equals", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); const query = /* GraphQL */ ` query movies($date1: Date!, $date3: Date!) { - ${Movie.plural}(where: { edges: { node: { date: { equals: [$date1, $date3] }} }}) { + ${Movie.plural}(where: { node: { date: { equals: [$date1, $date3] }}}) { connection{ edges { node { 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 index 4b43644143..6dffaf6410 100644 --- 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 @@ -58,7 +58,7 @@ describe("Date - Equals", () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { date: { equals: "${datetime1.toString()}" }} }}) { + ${Movie.plural}(where: { node: { date: { equals: "${datetime1.toString()}" }} }) { connection{ edges { node { 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 index 25722e6c8c..2fa3a3e560 100644 --- 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 @@ -62,7 +62,7 @@ describe("Date - IN", () => { query { ${ Movie.plural - }(where: { edges: { node: { date: { in: ["${neoDate1.toString()}", "${neoDate3.toString()}"] }} }}) { + }(where: { node: { date: { in: ["${neoDate1.toString()}", "${neoDate3.toString()}"] }} }) { connection{ edges { node { @@ -123,7 +123,7 @@ describe("Date - IN", () => { query { ${ Movie.plural - }(where: { edges: { node: { date: { NOT: { in: ["${neoDate1.toString()}", "${neoDate3.toString()}"] } }} }}) { + }(where: { node: { date: { NOT: { in: ["${neoDate1.toString()}", "${neoDate3.toString()}"] } }} }) { connection{ edges { node { 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 index 155b19d6ff..390cd60613 100644 --- 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 @@ -58,7 +58,7 @@ describe("Date - LT", () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { date: { lt: "${neoDate2.toString()}" }} }}) { + ${Movie.plural}(where: { node: { date: { lt: "${neoDate2.toString()}" }}}) { connection{ edges { node { 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 index 4ed3cee923..3a4d7955b3 100644 --- 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 @@ -44,9 +44,11 @@ describe("DateTime array - Equals", () => { 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 datetime1 = [ + neo4jDriver.types.DateTime.fromStandardDate(date1), + neo4jDriver.types.DateTime.fromStandardDate(date3), + ]; const datetime2 = [neo4jDriver.types.DateTime.fromStandardDate(date2)]; - await testHelper.executeCypher( ` @@ -60,7 +62,9 @@ describe("DateTime array - Equals", () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { datetime: { equals: ["${date1.toISOString()}", "${date3.toISOString()}"] }} }}) { + ${ + Movie.plural + }(where: { node: { datetime: { equals: ["${date1.toISOString()}", "${date3.toISOString()}"] }}}) { connection{ edges { node { 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 index 8b095c2b0d..e4269b4970 100644 --- 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 @@ -58,7 +58,7 @@ describe("DateTime - Equals", () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { datetime: { equals: "${date1.toISOString()}" }} }}) { + ${Movie.plural}(where: { node: { datetime: { equals: "${date1.toISOString()}" }}}) { connection{ edges { node { 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 index 826ada9d30..7b49531b49 100644 --- 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 @@ -79,7 +79,7 @@ describe("Duration array - Equals", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); const query = /* GraphQL */ ` query movies($date1: Duration!, $date3: Duration!) { - ${Movie.plural}(where: { edges: { node: { duration: { equals: [$date1, $date3] }} }}) { + ${Movie.plural}(where: { node: { duration: { equals: [$date1, $date3] }} }) { connection{ edges { node { 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 index d67e09a0bb..2fb214828f 100644 --- 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 @@ -66,7 +66,7 @@ describe("Duration - Equals", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { duration: { equals: "${duration1.toString()}" }} }}) { + ${Movie.plural}(where: { node: { duration: { equals: "${duration1.toString()}" }} }) { connection{ edges { node { 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 index f945458862..2c9ecabc53 100644 --- 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 @@ -68,7 +68,7 @@ describe("Duration - LT", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { duration: { lt: "${duration2.toString()}" }} }}) { + ${Movie.plural}(where: { node: { duration: { lt: "${duration2.toString()}" }} }) { connection{ edges { node { 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 index 4d43c6b85d..6d9e26324f 100644 --- 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 @@ -48,7 +48,7 @@ describe("LocalDateTime array - Equals", () => { const date3 = new Date("2026-09-17T11:49:48.322Z"); const localdatetime3 = neo4jDriver.types.LocalDateTime.fromStandardDate(date3); - + const dateList1 = [localdatetime1, localdatetime3]; const dateList2 = [localdatetime2]; @@ -63,7 +63,7 @@ describe("LocalDateTime array - Equals", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); const query = /* GraphQL */ ` query movies($date1: LocalDateTime!, $date3: LocalDateTime!) { - ${Movie.plural}(where: { edges: { node: { localDateTime: { equals: [$date1, $date3] }} }}) { + ${Movie.plural}(where: { node: { localDateTime: { equals: [$date1, $date3] }}}) { connection{ edges { node { 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 index 5aee69a68c..c37af05cd4 100644 --- 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 @@ -59,9 +59,7 @@ describe("LocalDateTime - Equals", () => { const query = /* GraphQL */ ` query { - ${ - Movie.plural - }(where: { edges: { node: { localDateTime: { equals: "${localdatetime1.toString()}" }} }}) { + ${Movie.plural}(where: { node: { localDateTime: { equals: "${localdatetime1.toString()}" }} }) { connection{ edges { node { 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 index bf50d7d8b3..96aac3fe34 100644 --- 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 @@ -52,7 +52,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering array - 'equals test.each(["list", "listNullable"])("%s filter by 'equals'", async (field) => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { ${field}: { equals: [2001, 2000] } } } }) { + ${Movie.plural}(where: { node: { ${field}: { equals: [2001, 2000] } } }) { connection { edges { node { @@ -84,7 +84,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering array - 'equals test.each(["list", "listNullable"])("%s filter by NOT 'equals'", async (field) => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { ${field}: { equals: [2001, 2000] } } } } }) { + ${Movie.plural}(where: { NOT: { node: { ${field}: { equals: [2001, 2000] } } } }) { connection { edges { node { 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 index 48cabe886d..7c366ca142 100644 --- 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 @@ -51,7 +51,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'equals'", (t test("filter by 'equals'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { equals: 2001 } } } }) { + ${Movie.plural}(where: { node: { value: { equals: 2001 } } }) { connection { edges { node { @@ -83,7 +83,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'equals'", (t test("filter by NOT 'equals'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { equals: 2001 } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { equals: 2001 } } } }) { connection { edges { node { 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 index b089e53a31..5e993cdb5f 100644 --- 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 @@ -51,7 +51,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'gt' and 'gte test("filter by 'gt'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { gt: 1999 } } } }) { + ${Movie.plural}(where: { node: { value: { gt: 1999 } } }) { connection { edges { node { @@ -83,7 +83,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'gt' and 'gte test("filter by NOT 'gt'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { gt: 1999 } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { gt: 1999 } } } }) { connection { edges { node { @@ -120,7 +120,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'gt' and 'gte test("filter by 'gte'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { gte: 1999 } } } }) { + ${Movie.plural}(where: { node: { value: { gte: 1999 } } }) { connection { edges { node { @@ -157,7 +157,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'gt' and 'gte test("filter by NOT 'gte'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { gte: 1999 } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { gte: 1999 } } } }) { connection { edges { node { 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 index 315996ce32..79e73d5482 100644 --- 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 @@ -51,7 +51,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'in'", (type) test("filter by 'in'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { in: [1999, 2001] } } } }) { + ${Movie.plural}(where: { node: { value: { in: [1999, 2001] } } }) { connection { edges { node { @@ -88,7 +88,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering - 'in'", (type) test("filter by NOT 'in'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { in: [1999, 2001] } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { in: [1999, 2001] } } } }) { connection { edges { node { 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 index fb8b0051dc..3a1f3314d0 100644 --- 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 @@ -51,7 +51,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering", (type) => { test("filter by 'lt'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { lt: 1999 } } } }) { + ${Movie.plural}(where: { node: { value: { lt: 1999 } } }) { connection { edges { node { @@ -83,7 +83,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering", (type) => { test("filter by NOT 'lt'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { lt: 1999 } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { lt: 1999 } } } }) { connection { edges { node { @@ -120,7 +120,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering", (type) => { test("filter by 'lte'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { lte: 1999 } } } }) { + ${Movie.plural}(where: { node: { value: { lte: 1999 } } }) { connection { edges { node { @@ -157,7 +157,7 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering", (type) => { test("filter by NOT 'lte'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { lte: 1999 } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { lte: 1999 } } } }) { connection { edges { node { 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 index af59c41da1..1f40884515 100644 --- 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 @@ -50,7 +50,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'contains'", (type) => test("filter by 'contains'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { contains: "Matrix" } } } }) { + ${Movie.plural}(where: { node: { value: { contains: "Matrix" } } }) { connection { edges { node { @@ -87,7 +87,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'contains'", (type) => test("filter by NOT 'contains'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { contains: "Matrix" } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { contains: "Matrix" } } } }) { connection { edges { node { 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 index 02d7a30662..bd8ab73b0b 100644 --- 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 @@ -50,7 +50,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'endsWith'", (type) => test("filter by 'endsWith'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { endsWith: "The Matrix" } } } }) { + ${Movie.plural}(where: { node: { value: { endsWith: "The Matrix" } } }) { connection { edges { node { @@ -87,7 +87,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'endsWith'", (type) => test("filter by NOT 'endsWith'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { endsWith: "The Matrix" } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { endsWith: "The Matrix" } } } }) { connection { edges { node { 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 index 7d3fd5a052..e55b36f56e 100644 --- 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 @@ -50,7 +50,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'equals'", (type) => { test("filter by 'equals'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { equals: "The Matrix" } } } }) { + ${Movie.plural}(where: { node: { value: { equals: "The Matrix" } } }) { connection { edges { node { @@ -82,7 +82,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'equals'", (type) => { test("filter by NOT 'equals'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { equals: "The Matrix" } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { equals: "The Matrix" } } } }) { connection { edges { node { 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 index 5a0d3bbc6f..c6ed46f1e4 100644 --- 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 @@ -50,7 +50,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'in'", (type) => { test("filter by 'in'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { in: ["The Matrix", "The Matrix 2"] } } } }) { + ${Movie.plural}(where: { node: { value: { in: ["The Matrix", "The Matrix 2"] } } }) { connection { edges { node { @@ -87,7 +87,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'in'", (type) => { test("filter by NOT 'in'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { in: ["The Matrix", "The Matrix 2"] } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { in: ["The Matrix", "The Matrix 2"] } } } }) { connection { edges { node { 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 index fce8cfdff0..1fb90317a1 100644 --- 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 @@ -50,7 +50,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'startsWith'", (type) = test("filter by 'startsWith'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { value: { startsWith: "The" } } } }) { + ${Movie.plural}(where: { node: { value: { startsWith: "The" } } }) { connection { edges { node { @@ -87,7 +87,7 @@ describe.each(["ID", "String"] as const)("%s Filtering - 'startsWith'", (type) = test("filter by NOT 'startsWith'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { NOT: { node: { value: { startsWith: "The" } } } } }) { + ${Movie.plural}(where: { NOT: { node: { value: { startsWith: "The" } } } }) { connection { edges { node { 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 index a686537697..140f495df7 100644 --- 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 @@ -62,7 +62,7 @@ describe("Time array - Equals", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); const query = /* GraphQL */ ` query movies($date1: Time!, $date3: Time!) { - ${Movie.plural}(where: { edges: { node: { time: { equals: [$date1, $date3] }} }}) { + ${Movie.plural}(where: { node: { time: { equals: [$date1, $date3] }} }) { connection{ edges { node { 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 index 485f1a45a4..07b1aa6ac1 100644 --- 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 @@ -58,7 +58,7 @@ describe("Time - Equals", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { time: { equals: "${time1.toString()}" }} }}) { + ${Movie.plural}(where: { node: { time: { equals: "${time1.toString()}" }}}) { connection{ edges { node { diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index bd50911104..18f06bd9fb 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -66,7 +66,7 @@ describe("RelayId", () => { } input MovieConnectionSort { - node: [MovieSort!] + edges: [MovieEdgeSort!] } type MovieEdge { @@ -74,6 +74,10 @@ describe("RelayId", () => { node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index e10a7c7ae0..aa30d881a7 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -55,7 +55,7 @@ describe("Relationships", () => { } input ActorConnectionSort { - node: [ActorSort!] + edges: [ActorEdgeSort!] } type ActorEdge { @@ -63,6 +63,10 @@ describe("Relationships", () => { node: Actor } + input ActorEdgeSort { + node: ActorSort + } + type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo @@ -203,7 +207,7 @@ describe("Relationships", () => { } input MovieConnectionSort { - node: [MovieSort!] + edges: [MovieEdgeSort!] } type MovieEdge { @@ -211,6 +215,10 @@ describe("Relationships", () => { node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } @@ -315,7 +323,7 @@ describe("Relationships", () => { } input ActorConnectionSort { - node: [ActorSort!] + edges: [ActorEdgeSort!] } type ActorEdge { @@ -323,6 +331,10 @@ describe("Relationships", () => { node: Actor } + input ActorEdgeSort { + node: ActorSort + } + type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo @@ -481,7 +493,7 @@ describe("Relationships", () => { } input MovieConnectionSort { - node: [MovieSort!] + edges: [MovieEdgeSort!] } type MovieEdge { @@ -489,6 +501,10 @@ describe("Relationships", () => { node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 5bcc990c12..6338fac0b6 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -49,7 +49,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - node: [MovieSort!] + edges: [MovieEdgeSort!] } type MovieEdge { @@ -57,6 +57,10 @@ describe("Simple Aura-API", () => { node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } @@ -137,7 +141,7 @@ describe("Simple Aura-API", () => { } input ActorConnectionSort { - node: [ActorSort!] + edges: [ActorEdgeSort!] } type ActorEdge { @@ -145,6 +149,10 @@ describe("Simple Aura-API", () => { node: Actor } + input ActorEdgeSort { + node: ActorSort + } + type ActorOperation { connection(after: String, first: Int, sort: ActorConnectionSort): ActorConnection } @@ -177,7 +185,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - node: [MovieSort!] + edges: [MovieEdgeSort!] } type MovieEdge { @@ -185,6 +193,10 @@ describe("Simple Aura-API", () => { node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } @@ -266,7 +278,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - node: [MovieSort!] + edges: [MovieEdgeSort!] } type MovieEdge { @@ -274,6 +286,10 @@ describe("Simple Aura-API", () => { node: Movie } + input MovieEdgeSort { + node: MovieSort + } + type MovieOperation { connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection } diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index f9fe022853..fbef044c1e 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -163,7 +163,7 @@ describe("Scalars", () => { } input NodeTypeConnectionSort { - node: [NodeTypeSort!] + edges: [NodeTypeEdgeSort!] } type NodeTypeEdge { @@ -171,6 +171,10 @@ describe("Scalars", () => { node: NodeType } + input NodeTypeEdgeSort { + node: NodeTypeSort + } + type NodeTypeOperation { connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection } @@ -305,7 +309,7 @@ describe("Scalars", () => { } input RelatedNodeConnectionSort { - node: [RelatedNodeSort!] + edges: [RelatedNodeEdgeSort!] } type RelatedNodeEdge { @@ -313,6 +317,10 @@ describe("Scalars", () => { node: RelatedNode } + input RelatedNodeEdgeSort { + node: RelatedNodeSort + } + type RelatedNodeOperation { connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection } diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 9fc5af86fe..f4844070d7 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -157,7 +157,7 @@ describe("Temporals", () => { } input NodeTypeConnectionSort { - node: [NodeTypeSort!] + edges: [NodeTypeEdgeSort!] } type NodeTypeEdge { @@ -165,6 +165,10 @@ describe("Temporals", () => { node: NodeType } + input NodeTypeEdgeSort { + node: NodeTypeSort + } + type NodeTypeOperation { connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection } @@ -281,7 +285,7 @@ describe("Temporals", () => { } input RelatedNodeConnectionSort { - node: [RelatedNodeSort!] + edges: [RelatedNodeEdgeSort!] } type RelatedNodeEdge { @@ -289,6 +293,10 @@ describe("Temporals", () => { node: RelatedNode } + input RelatedNodeEdgeSort { + node: RelatedNodeSort + } + type RelatedNodeOperation { connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection } 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 index 11d2648c16..87826c17f3 100644 --- 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 @@ -40,7 +40,7 @@ describe("Array filters", () => { test("array filters", async () => { const query = /* GraphQL */ ` query { - movies(where: { edges: { node: { alternativeTitles: { equals: ["potato"] } } } }) { + movies(where: { node: { alternativeTitles: { equals: ["potato"] } } }) { connection { edges { node { 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 index ed795bc84d..24b30aa632 100644 --- 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 @@ -42,60 +42,7 @@ describe("AND filters", () => { const query = /* GraphQL */ ` query { movies( - where: { - AND: [ - { edges: { node: { title: { equals: "The Matrix" } } } } - { edges: { 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 on edges", async () => { - const query = /* GraphQL */ ` - query { - movies( - where: { - edges: { - AND: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 100 } } }] - } - } + where: { AND: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 100 } } }] } ) { connection { edges { @@ -138,11 +85,7 @@ describe("AND filters", () => { test("AND logical filter in nodes", async () => { const query = /* GraphQL */ ` query { - movies( - where: { - edges: { node: { AND: [{ title: { equals: "The Matrix" } }, { year: { equals: 100 } }] } } - } - ) { + movies(where: { node: { AND: [{ title: { equals: "The Matrix" } }, { year: { equals: 100 } }] } }) { connection { edges { node { 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 index 3617e14328..73644ff926 100644 --- 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 @@ -41,7 +41,7 @@ describe("NOT filters", () => { test("NOT logical filter in where", async () => { const query = /* GraphQL */ ` query { - movies(where: { NOT: { edges: { node: { title: { equals: "The Matrix" } } } } }) { + movies(where: { NOT: { node: { title: { equals: "The Matrix" } } } }) { connection { edges { node { @@ -76,56 +76,10 @@ describe("NOT filters", () => { `); }); - test("NOT logical filter on edges", async () => { - const query = /* GraphQL */ ` - query { - movies( - where: { edges: { NOT: { node: { 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 - } - }" - `); - }); - test("NOT logical filter on nodes", async () => { const query = /* GraphQL */ ` query { - movies( - where: { edges: { node: { NOT: { title: { equals: "The Matrix" }, year: { equals: 100 } } } } } - ) { + movies(where: { node: { NOT: { title: { equals: "The Matrix" }, year: { equals: 100 } } } }) { connection { edges { node { 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 index 040584c827..eefd3cbd86 100644 --- 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 @@ -42,60 +42,7 @@ describe("OR filters", () => { const query = /* GraphQL */ ` query { movies( - where: { - OR: [ - { edges: { node: { title: { equals: "The Matrix" } } } } - { edges: { 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 edges", async () => { - const query = /* GraphQL */ ` - query { - movies( - where: { - edges: { - OR: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 100 } } }] - } - } + where: { OR: [{ node: { title: { equals: "The Matrix" } } }, { node: { year: { equals: 100 } } }] } ) { connection { edges { @@ -138,9 +85,7 @@ describe("OR filters", () => { test("OR logical filter in nodes", async () => { const query = /* GraphQL */ ` query { - movies( - where: { edges: { node: { OR: [{ title: { equals: "The Matrix" } }, { year: { equals: 100 } }] } } } - ) { + movies(where: { node: { OR: [{ title: { equals: "The Matrix" } }, { year: { equals: 100 } }] } }) { connection { edges { node { 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 index 6e8a8b4dc4..c197c6fdf7 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts @@ -47,9 +47,7 @@ describe("Nested Filters with all", () => { test("query nested relationship with all filter", async () => { const query = /* GraphQL */ ` query { - movies( - where: { edges: { node: { actors: { edges: { all: { node: { name: { equals: "Keanu" } } } } } } } } - ) { + movies(where: { node: { actors: { edges: { all: { node: { name: { equals: "Keanu" } } } } } } }) { connection { edges { node { @@ -93,11 +91,7 @@ describe("Nested Filters with all", () => { test("query nested relationship properties with all filter", async () => { const query = /* GraphQL */ ` query { - movies( - where: { - edges: { node: { actors: { edges: { all: { properties: { year: { equals: 1999 } } } } } } } - } - ) { + movies(where: { node: { actors: { edges: { all: { properties: { year: { equals: 1999 } } } } } } }) { connection { edges { node { @@ -146,16 +140,14 @@ describe("Nested Filters with all", () => { query { movies( where: { - edges: { - node: { - actors: { - edges: { - all: { - OR: [ - { node: { name: { equals: "Keanu" } } } - { node: { name: { endsWith: "eeves" } } } - ] - } + node: { + actors: { + edges: { + all: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "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 index c458d940a8..2efc518a18 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts @@ -47,9 +47,7 @@ describe("Nested Filters with none", () => { test("query nested relationship with none filter", async () => { const query = /* GraphQL */ ` query { - movies( - where: { edges: { node: { actors: { edges: { none: { node: { name: { equals: "Keanu" } } } } } } } } - ) { + movies(where: { node: { actors: { edges: { none: { node: { name: { equals: "Keanu" } } } } } } }) { connection { edges { node { @@ -90,11 +88,7 @@ describe("Nested Filters with none", () => { test("query nested relationship properties with none filter", async () => { const query = /* GraphQL */ ` query { - movies( - where: { - edges: { node: { actors: { edges: { none: { properties: { year: { equals: 1999 } } } } } } } - } - ) { + movies(where: { node: { actors: { edges: { none: { properties: { year: { equals: 1999 } } } } } } }) { connection { edges { node { @@ -140,16 +134,14 @@ describe("Nested Filters with none", () => { query { movies( where: { - edges: { - node: { - actors: { - edges: { - some: { - OR: [ - { node: { name: { equals: "Keanu" } } } - { node: { name: { endsWith: "eeves" } } } - ] - } + node: { + actors: { + edges: { + some: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "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 index da2f464c1a..4045b3b826 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts @@ -47,11 +47,7 @@ describe("Nested Filters with single", () => { test("query nested relationship with single filter", async () => { const query = /* GraphQL */ ` query { - movies( - where: { - edges: { node: { actors: { edges: { single: { node: { name: { equals: "Keanu" } } } } } } } - } - ) { + movies(where: { node: { actors: { edges: { single: { node: { name: { equals: "Keanu" } } } } } } }) { connection { edges { node { @@ -89,11 +85,7 @@ describe("Nested Filters with single", () => { test("query nested relationship properties with single filter", async () => { const query = /* GraphQL */ ` query { - movies( - where: { - edges: { node: { actors: { edges: { single: { properties: { year: { equals: 1999 } } } } } } } - } - ) { + movies(where: { node: { actors: { edges: { single: { properties: { year: { equals: 1999 } } } } } } }) { connection { edges { node { @@ -136,16 +128,14 @@ describe("Nested Filters with single", () => { query { movies( where: { - edges: { - node: { - actors: { - edges: { - single: { - OR: [ - { node: { name: { equals: "Keanu" } } } - { node: { name: { endsWith: "eeves" } } } - ] - } + node: { + actors: { + edges: { + single: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "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 index 7051742b54..7721c13a47 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts @@ -47,9 +47,7 @@ describe("Nested Filters with some", () => { test("query nested relationship with some filter", async () => { const query = /* GraphQL */ ` query { - movies( - where: { edges: { node: { actors: { edges: { some: { node: { name: { equals: "Keanu" } } } } } } } } - ) { + movies(where: { node: { actors: { edges: { some: { node: { name: { equals: "Keanu" } } } } } } }) { connection { edges { node { @@ -90,11 +88,7 @@ describe("Nested Filters with some", () => { test("query nested relationship properties with some filter", async () => { const query = /* GraphQL */ ` query { - movies( - where: { - edges: { node: { actors: { edges: { some: { properties: { year: { equals: 1999 } } } } } } } - } - ) { + movies(where: { node: { actors: { edges: { some: { properties: { year: { equals: 1999 } } } } } } }) { connection { edges { node { @@ -140,16 +134,14 @@ describe("Nested Filters with some", () => { query { movies( where: { - edges: { - node: { - actors: { - edges: { - some: { - OR: [ - { node: { name: { equals: "Keanu" } } } - { node: { name: { endsWith: "eeves" } } } - ] - } + node: { + actors: { + edges: { + some: { + OR: [ + { node: { name: { equals: "Keanu" } } } + { node: { name: { endsWith: "eeves" } } } + ] } } } diff --git a/packages/graphql/tests/api-v6/tck/filters/types/array/temporals-array.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/array/temporals-array.test.ts index e5ea1ce283..7231f9526c 100644 --- a/packages/graphql/tests/api-v6/tck/filters/types/array/temporals-array.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/types/array/temporals-array.test.ts @@ -77,19 +77,17 @@ describe("Temporal types", () => { query { typeNodes( 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"] } - dateTimeNullable: { equals: ["2015-06-24T12:50:35.556+0100"] } - localDateTimeNullable: { equals: ["2003-09-14T12:00:00"] } - durationNullable: { equals: ["P1Y"] } - timeNullable: { equals: ["22:00:15.555"] } - localTimeNullable: { equals: ["12:50:35.556"] } - } + 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"] } + dateTimeNullable: { equals: ["2015-06-24T12:50:35.556+0100"] } + localDateTimeNullable: { equals: ["2003-09-14T12:00:00"] } + durationNullable: { equals: ["P1Y"] } + timeNullable: { equals: ["22:00:15.555"] } + localTimeNullable: { equals: ["12:50:35.556"] } } } ) { 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 index 808b5bb293..aa3a0ded88 100644 --- 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 @@ -42,7 +42,7 @@ describe.skip("CartesianPoint filters", () => { test("CartesianPoint EQUALS", async () => { const query = /* GraphQL */ ` { - locations(where: { edges: { node: { value: { equals: { x: 1.0, y: 2.0 } } } } }) { + locations(where: { node: { value: { equals: { x: 1.0, y: 2.0 } } } }) { connection { edges { node { @@ -90,7 +90,7 @@ describe.skip("CartesianPoint filters", () => { test("Simple Point NOT EQUALS", async () => { const query = /* GraphQL */ ` { - locations(where: { edges: { node: { value: { NOT: { equals: { x: 1.0, y: 2.0 } } } } } }) { + locations(where: { node: { value: { NOT: { equals: { x: 1.0, y: 2.0 } } } }) { connection { edges { node { @@ -137,7 +137,7 @@ describe.skip("CartesianPoint filters", () => { test("Simple Point IN", async () => { const query = /* GraphQL */ ` { - locations(where: { edges: { node: { value: { in: [{ x: 1.0, y: 2.0 }] } } } }) { + locations(where: { node: { value: { in: [{ x: 1.0, y: 2.0 }] } } }) { connection { edges { node { @@ -186,7 +186,7 @@ describe.skip("CartesianPoint filters", () => { test("Simple Point NOT IN", async () => { const query = /* GraphQL */ ` { - locations(where: { edges: { node: { value: { NOT: { in: [{ x: 1.0, y: 2.0 }] } } } } }) { + locations(where: { node: { value: { NOT: { in: [{ x: 1.0, y: 2.0 }] } } } }) { connection { edges { node { @@ -236,7 +236,7 @@ describe.skip("CartesianPoint filters", () => { test("Simple Point LT", async () => { const query = /* GraphQL */ ` { - locations(where: { edges: { node: { value: { lt: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } } }) { + locations(where: { node: { value: { lt: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { connection { edges { node { @@ -286,9 +286,7 @@ describe.skip("CartesianPoint filters", () => { test("Simple Point LTE", async () => { const query = /* GraphQL */ ` { - locations( - where: { edges: { node: { value: { lte: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } } } - ) { + locations(where: { node: { value: { lte: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { connection { edges { node { @@ -338,7 +336,7 @@ describe.skip("CartesianPoint filters", () => { test("Simple Point GT", async () => { const query = /* GraphQL */ ` { - locations(where: { edges: { node: { value: { gt: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } } }) { + locations(where: { node: { value: { gt: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { connection { edges { node { @@ -388,9 +386,7 @@ describe.skip("CartesianPoint filters", () => { test("Simple Point GTE", async () => { const query = /* GraphQL */ ` { - locations( - where: { edges: { node: { value: { gte: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } } } - ) { + locations(where: { node: { value: { gte: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { connection { edges { node { @@ -440,9 +436,7 @@ describe.skip("CartesianPoint filters", () => { test("Simple Point DISTANCE EQ", async () => { const query = /* GraphQL */ ` { - locations( - where: { edges: { node: { value: { distance: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } } } - ) { + locations(where: { node: { value: { distance: { point: { x: 1.1, y: 2.2 }, distance: 3.3 } } } }) { connection { edges { node { 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 index 0c1e23f3b8..b66d350959 100644 --- 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 @@ -42,7 +42,7 @@ describe.skip("Point filters", () => { test("Simple Point EQUALS", async () => { const query = /* GraphQL */ ` { - locations(where: { edges: { node: { value: { equals: { longitude: 1.0, latitude: 2.0 } } } } }) { + locations(where: { node: { value: { equals: { longitude: 1.0, latitude: 2.0 } } } }) { connection { edges { node { @@ -90,9 +90,7 @@ describe.skip("Point filters", () => { test("Simple Point NOT EQUALS", async () => { const query = /* GraphQL */ ` { - locations( - where: { edges: { node: { value: { NOT: { equals: { longitude: 1.0, latitude: 2.0 } } } } } } - ) { + locations(where: { node: { value: { NOT: { equals: { longitude: 1.0, latitude: 2.0 } } } } }) { connection { edges { node { @@ -139,7 +137,7 @@ describe.skip("Point filters", () => { test("Simple Point IN", async () => { const query = /* GraphQL */ ` { - locations(where: { edges: { node: { value: { in: [{ longitude: 1.0, latitude: 2.0 }] } } } }) { + locations(where: { node: { value: { in: [{ longitude: 1.0, latitude: 2.0 }] } } }) { connection { edges { node { @@ -188,7 +186,7 @@ describe.skip("Point filters", () => { test("Simple Point NOT IN", async () => { const query = /* GraphQL */ ` { - locations(where: { edges: { node: { value: { NOT: { in: [{ longitude: 1.0, latitude: 2.0 }] } } } } }) { + locations(where: { node: { value: { NOT: { in: [{ longitude: 1.0, latitude: 2.0 }] } } } }) { connection { edges { node { @@ -239,9 +237,7 @@ describe.skip("Point filters", () => { const query = /* GraphQL */ ` { locations( - where: { - edges: { node: { value: { lt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } - } + where: { node: { value: { lt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } ) { connection { edges { @@ -293,9 +289,7 @@ describe.skip("Point filters", () => { const query = /* GraphQL */ ` { locations( - where: { - edges: { node: { value: { lte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } - } + where: { node: { value: { lte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } ) { connection { edges { @@ -347,9 +341,7 @@ describe.skip("Point filters", () => { const query = /* GraphQL */ ` { locations( - where: { - edges: { node: { value: { gt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } - } + where: { node: { value: { gt: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } ) { connection { edges { @@ -401,9 +393,7 @@ describe.skip("Point filters", () => { const query = /* GraphQL */ ` { locations( - where: { - edges: { node: { value: { gte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } - } + where: { node: { value: { gte: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } ) { connection { edges { @@ -456,9 +446,7 @@ describe.skip("Point filters", () => { { locations( where: { - edges: { - node: { value: { distance: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } - } + node: { value: { distance: { point: { longitude: 1.1, latitude: 2.2 }, distance: 3.3 } } } } ) { connection { diff --git a/packages/graphql/tests/api-v6/tck/filters/types/temporals-filters.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/temporals-filters.test.ts index 88c72705ac..a09c925f04 100644 --- a/packages/graphql/tests/api-v6/tck/filters/types/temporals-filters.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/types/temporals-filters.test.ts @@ -62,14 +62,12 @@ describe("Temporal types", () => { query { typeNodes( 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" } - } + 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" } } } ) { From 7a7929748a326d0e8f92a5c1c434bc421f96b5c9 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 10 Jul 2024 17:54:42 +0100 Subject: [PATCH 099/177] Remove top level edge from sorting and change sort to be an array --- .../queryIRFactory/ReadOperationFactory.ts | 68 +++++++++++++++---- .../resolve-tree-parser/ResolveTreeParser.ts | 47 ++++++------- .../TopLevelResolveTreeParser.ts | 35 +++++++++- .../resolve-tree-parser/graphql-tree.ts | 18 ++++- .../schema-types/EntitySchemaTypes.ts | 18 +++-- .../schema-types/TopLevelEntitySchemaTypes.ts | 18 ++--- .../sort-pagination.int.test.ts | 4 +- .../directives/alias/sort-alias.int.test.ts | 6 +- .../alias/sort-relationship-alias.int.test.ts | 16 ++--- .../pagination/first-after.int.test.ts | 2 +- .../sort/sort-relationship.int.test.ts | 16 ++--- .../api-v6/integration/sort/sort.int.test.ts | 6 +- .../api-v6/schema/directives/relayId.test.ts | 8 +-- .../tests/api-v6/schema/relationship.test.ts | 48 +++++-------- .../tests/api-v6/schema/simple.test.ts | 32 +++------ .../tests/api-v6/schema/types/scalars.test.ts | 20 ++---- .../api-v6/schema/types/temporals.test.ts | 20 ++---- .../tests/api-v6/tck/sort/sort-alias.test.ts | 4 +- .../tck/sort/sort-relationship-alias.test.ts | 16 ++--- .../api-v6/tck/sort/sort-relationship.test.ts | 18 ++--- .../tests/api-v6/tck/sort/sort.test.ts | 4 +- 21 files changed, 231 insertions(+), 193 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 48c1a4fa66..5d35e612ff 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -42,7 +42,9 @@ import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { FilterFactory } from "./FilterFactory"; import type { GraphQLConnectionArgs, + GraphQLConnectionArgsTopLevel, GraphQLSortArgument, + GraphQLSortEdgeArgument, GraphQLTree, GraphQLTreeEdgeProperties, GraphQLTreeLeafField, @@ -89,9 +91,8 @@ export class ReadOperationFactory { 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.getSortInputFields({ + const sortInputFields = this.getTopLevelSortInputFields({ entity, sortArgument, }); @@ -161,7 +162,10 @@ export class ReadOperationFactory { }); } - private getPagination(connectionTreeArgs: GraphQLConnectionArgs, entity: ConcreteEntity): Pagination | undefined { + private getPagination( + connectionTreeArgs: GraphQLConnectionArgs | GraphQLConnectionArgsTopLevel, + entity: ConcreteEntity + ): Pagination | undefined { const firstArgument = connectionTreeArgs.first; const afterArgument = connectionTreeArgs.after ? cursorToOffset(connectionTreeArgs.after) : undefined; const hasPagination = firstArgument ?? afterArgument; @@ -264,24 +268,60 @@ export class ReadOperationFactory { }: { entity: ConcreteEntity; relationship?: Relationship; - sortArgument: GraphQLSortArgument | undefined; + sortArgument: GraphQLSortArgument[] | 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: GraphQLSortEdgeArgument[] | undefined; }): Array<{ edge: PropertySort[]; node: PropertySort[] }> { if (!sortArgument) { return []; } - return sortArgument.edges.map((edge): { edge: PropertySort[]; node: PropertySort[] } => { - const nodeSortFields = edge.node ? this.getPropertiesSort({ target: entity, sortArgument: edge.node }) : []; - const edgeSortFields = - edge.properties && relationship - ? this.getPropertiesSort({ target: relationship, sortArgument: edge.properties }) - : []; - return { - edge: edgeSortFields, - node: nodeSortFields, - }; + return sortArgument.map((edges): { edge: PropertySort[]; node: PropertySort[] } => { + return this.getEdgeSortInput({ + entity, + relationship, + edges, + }); }); } + private getEdgeSortInput({ + entity, + relationship, + edges, + }: { + entity: ConcreteEntity; + relationship?: Relationship; + edges: GraphQLSortEdgeArgument; + }): { 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, diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 73d255ca1f..567281527a 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -222,13 +222,12 @@ export abstract class ResolveTreeParser } private parseConnectionArgs(resolveTreeArgs: { [str: string]: any }): GraphQLConnectionArgs { - let sortArg: GraphQLSortArgument | undefined; + let sortArg: GraphQLSortArgument[] | undefined; if (resolveTreeArgs.sort) { - sortArg = { - edges: this.parseSortEdges(resolveTreeArgs.sort.edges), - }; + sortArg = resolveTreeArgs.sort.map((sortArg): GraphQLSortArgument => { + return { edges: this.parseSortEdges(sortArg.edges) }; + }); } - return { sort: sortArg, first: resolveTreeArgs.first, @@ -236,28 +235,24 @@ export abstract class ResolveTreeParser }; } - private parseSortEdges( - sortEdges: Array<{ - node: Record | undefined; - properties: Record | undefined; - }> - ): GraphQLSortEdgeArgument[] { - return sortEdges.map((edge) => { - const sortFields: GraphQLSortEdgeArgument = {}; - const nodeFields = edge.node; - - if (nodeFields) { - const fields = this.parseSort(this.targetNode, nodeFields); - sortFields.node = fields; - } - const edgeProperties = edge.properties; + protected parseSortEdges(sortEdges: { + node: Record | undefined; + properties: Record | undefined; + }): GraphQLSortEdgeArgument { + const sortFields: GraphQLSortEdgeArgument = {}; + const nodeFields = sortEdges.node; - if (edgeProperties) { - const fields = this.parseSort(this.entity, edgeProperties); - sortFields.properties = fields; - } - return sortFields; - }); + if (nodeFields) { + const fields = this.parseSort(this.targetNode, nodeFields); + sortFields.node = fields; + } + const edgeProperties = sortEdges.properties; + + if (edgeProperties) { + const fields = this.parseSort(this.entity, edgeProperties); + sortFields.properties = fields; + } + return sortFields; } private parseSort( diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts index e335266e77..b732eae73d 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts @@ -22,7 +22,10 @@ import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity import { ResolveTreeParser } from "./ResolveTreeParser"; import { findFieldByName } from "./find-field-by-name"; import type { + GraphQLConnectionArgsTopLevel, GraphQLReadOperationArgsTopLevel, + GraphQLSortEdgeArgument, + GraphQLTreeConnectionTopLevel, GraphQLTreeEdge, GraphQLTreeReadOperationTopLevel, } from "./graphql-tree"; @@ -40,7 +43,7 @@ export class TopLevelResolveTreeParser extends ResolveTreeParser "connection" ); - const connection = connectionResolveTree ? this.parseConnection(connectionResolveTree) : undefined; + const connection = connectionResolveTree ? this.parseTopLevelConnection(connectionResolveTree) : undefined; const connectionOperationArgs = this.parseOperationArgsTopLevel(resolveTree.args); return { alias: resolveTree.alias, @@ -52,6 +55,36 @@ export class TopLevelResolveTreeParser extends ResolveTreeParser }; } + private parseTopLevelConnection(resolveTree: ResolveTree): GraphQLTreeConnectionTopLevel { + const entityTypes = this.entity.typeNames; + const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges"); + const edgeResolveTree = edgesResolveTree ? this.parseEdges(edgesResolveTree) : undefined; + const connectionArgs = this.parseConnectionArgsTopLevel(resolveTree.args); + + return { + alias: resolveTree.alias, + args: connectionArgs, + fields: { + edges: edgeResolveTree, + }, + }; + } + + private parseConnectionArgsTopLevel(resolveTreeArgs: { [str: string]: any }): GraphQLConnectionArgsTopLevel { + let sortArg: GraphQLSortEdgeArgument[] | undefined; + if (resolveTreeArgs.sort) { + sortArg = resolveTreeArgs.sort.map((sortArg) => { + return this.parseSortEdges(sortArg); + }); + } + + return { + sort: sortArg, + first: resolveTreeArgs.first, + after: resolveTreeArgs.after, + }; + } + protected parseOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLReadOperationArgsTopLevel { // Not properly parsed, assuming the type is the same return { diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index cd19738089..5df7251e37 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -62,7 +62,7 @@ export type RelationshipFilters = { export interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { name: string; fields: { - connection?: GraphQLTreeConnection; + connection?: GraphQLTreeConnectionTopLevel; }; args: GraphQLReadOperationArgsTopLevel; } @@ -108,8 +108,20 @@ export interface GraphQLTreeConnection extends GraphQLTreeElement { args: GraphQLConnectionArgs; } +export interface GraphQLTreeConnectionTopLevel extends GraphQLTreeElement { + fields: { + edges?: GraphQLTreeEdge; + }; + args: GraphQLConnectionArgsTopLevel; +} + export interface GraphQLConnectionArgs { - sort?: GraphQLSortArgument; + sort?: GraphQLSortArgument[]; + first?: Integer; + after?: string; +} +export interface GraphQLConnectionArgsTopLevel { + sort?: GraphQLSortEdgeArgument[]; first?: Integer; after?: string; } @@ -157,7 +169,7 @@ export interface GraphQLTreeCartesianPoint extends GraphQLTreeElement { } export interface GraphQLSortArgument { - edges: GraphQLSortEdgeArgument[]; + edges: GraphQLSortEdgeArgument; } export interface GraphQLSortEdgeArgument { diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts index e562b57b60..d1935a8304 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts @@ -17,7 +17,13 @@ * limitations under the License. */ -import type { InputTypeComposer, ObjectTypeComposer, ScalarTypeComposer } from "graphql-compose"; +import type { + InputTypeComposer, + ListComposer, + NonNullComposer, + ObjectTypeComposer, + ScalarTypeComposer, +} from "graphql-compose"; import { connectionOperationResolver } from "../../resolvers/connection-operation-resolver"; import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; import type { SchemaBuilder } from "../SchemaBuilder"; @@ -45,12 +51,16 @@ export abstract class EntitySchemaTypes { public get connectionOperation(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connectionOperation, () => { - const args: { first: ScalarTypeComposer; after: ScalarTypeComposer; sort?: InputTypeComposer } = { + 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; + args.sort = this.connectionSort.NonNull.List; } return { fields: { @@ -79,7 +89,7 @@ export abstract class EntitySchemaTypes { return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { return { fields: { - edges: this.edgeSort.NonNull.List, + edges: this.edgeSort, }, }; }); 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 index 999de96486..3e8e2ce3be 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -81,15 +81,15 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { - // return { - // fields: { - // node: this.nodeSort.NonNull.List, - // }, - // }; - // }); - // } + protected get connectionSort(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { + return { + fields: { + node: this.nodeSort, + }, + }; + }); + } protected get edge(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.edge, () => { 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 index f5340c85fb..501c6c8fac 100644 --- 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 @@ -54,7 +54,7 @@ describe("Sort", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(sort: { edges: { node: { title: ASC } } }, first: 3) { + connection(sort: { node: { title: ASC } }, first: 3) { edges { node { title @@ -97,7 +97,7 @@ describe("Sort", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(sort: { edges: { node: { title: DESC } } }, first: 3) { + connection(sort: { node: { title: DESC } }, first: 3) { edges { node { title 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 index e46a01bc7b..5e200ea891 100644 --- 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 @@ -53,7 +53,7 @@ describe("Sort with alias", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(sort: { edges: { node: { title: ASC } } }) { + connection(sort: { node: { title: ASC } }) { edges { node { title @@ -106,7 +106,7 @@ describe("Sort with alias", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(sort: { edges: { node: { title: DESC } } }) { + connection(sort: { node: { title: DESC } }) { edges { node { title @@ -159,7 +159,7 @@ describe("Sort with alias", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(sort: { edges: [{ node: { title: ASC } }, { node: { ratings: DESC } }] }) { + connection(sort: [{ node: { title: ASC } }, { node: { ratings: DESC } }]) { edges { node { title 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 index ff8add1000..69fac93fb6 100644 --- 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 @@ -224,7 +224,7 @@ describe("Sort relationship with alias", () => { node { name movies { - connection(sort: { edges: [{ node: { title: ASC } }, { node: { ratings: DESC } }] }) { + connection(sort: [{edges: { node: { title: ASC } }}, {edges: { node: { ratings: DESC } }}]) { edges { node { title @@ -310,7 +310,7 @@ describe("Sort relationship with alias", () => { node { name movies { - connection(sort: { edges: [{ properties: { role: DESC } }, { properties: { year: ASC } } ] }) { + connection(sort: [{edges:{ properties: { role: DESC } }}, {edges:{ properties: { year: ASC } }} ]) { edges { properties { year @@ -391,13 +391,13 @@ describe("Sort relationship with alias", () => { node { name movies { - connection(sort: { edges: [ - { properties: { role: DESC } }, - { node: { title: ASC } }, - { properties: { year: DESC } }, - { node: { description: ASC } } + connection(sort: [ + {edges: { properties: { role: DESC } } }, + {edges: { node: { title: ASC } } }, + {edges: { properties: { year: DESC } } }, + {edges: { node: { description: ASC } } } - ] }) { + ]) { edges { properties { year 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 index a6a13dfaab..a9a65f7088 100644 --- 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 @@ -53,7 +53,7 @@ describe("Pagination with first and after", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(first: 1, after: "${afterCursor}", sort: { edges: { node: { title: ASC } } }) { + connection(first: 1, after: "${afterCursor}", sort: { node: { title: ASC } }) { edges { node { title 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 index a4d34ee365..f407dc0ee4 100644 --- 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 @@ -224,7 +224,7 @@ describe("Sort relationship", () => { node { name movies { - connection(sort: { edges: [{ node: { title: ASC } }, { node: { ratings: DESC } }] }) { + connection(sort: [{edges: { node: { title: ASC } }}, {edges: { node: { ratings: DESC } }}]) { edges { node { title @@ -310,7 +310,7 @@ describe("Sort relationship", () => { node { name movies { - connection(sort: { edges: [{ properties: { role: DESC } }, { properties: { year: ASC } } ] }) { + connection(sort: [{edges:{ properties: { role: DESC } }}, {edges:{ properties: { year: ASC } }} ]) { edges { properties { year @@ -391,13 +391,13 @@ describe("Sort relationship", () => { node { name movies { - connection(sort: { edges: [ - { properties: { role: DESC } }, - { node: { title: ASC } }, - { properties: { year: DESC } }, - { node: { description: ASC } } + connection(sort: [ + {edges: { properties: { role: DESC } }}, + {edges: { node: { title: ASC } }}, + {edges: { properties: { year: DESC } }}, + {edges: { node: { description: ASC } }} - ] }) { + ]) { edges { properties { year 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 index 52c932b4b6..566aaa0d53 100644 --- a/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/sort/sort.int.test.ts @@ -53,7 +53,7 @@ describe("Sort", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(sort: { edges: { node: { title: ASC } } }) { + connection(sort: { node: { title: ASC } }) { edges { node { title @@ -106,7 +106,7 @@ describe("Sort", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(sort: { edges: { node: { title: DESC } } }) { + connection(sort: { node: { title: DESC } }) { edges { node { title @@ -159,7 +159,7 @@ describe("Sort", () => { const query = /* GraphQL */ ` query { ${Movie.plural} { - connection(sort: { edges: [{ node: { title: ASC } }, { node: { ratings: DESC } }] }) { + connection(sort: [{ node: { title: ASC } }, { node: { ratings: DESC } }] ) { edges { node { title diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index 18f06bd9fb..76f4954286 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -66,7 +66,7 @@ describe("RelayId", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: MovieSort } type MovieEdge { @@ -74,12 +74,8 @@ describe("RelayId", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - type MovieOperation { - connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection } input MovieOperationWhere { diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index aa30d881a7..12a2d9b8de 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -55,7 +55,7 @@ describe("Relationships", () => { } input ActorConnectionSort { - edges: [ActorEdgeSort!] + node: ActorSort } type ActorEdge { @@ -63,17 +63,13 @@ describe("Relationships", () => { node: Actor } - input ActorEdgeSort { - node: ActorSort - } - type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo } input ActorMoviesConnectionSort { - edges: [ActorMoviesEdgeSort!] + edges: ActorMoviesEdgeSort } type ActorMoviesEdge { @@ -110,7 +106,7 @@ describe("Relationships", () => { } type ActorMoviesOperation { - connection(after: String, first: Int, sort: ActorMoviesConnectionSort): ActorMoviesConnection + connection(after: String, first: Int, sort: [ActorMoviesConnectionSort!]): ActorMoviesConnection } input ActorMoviesOperationWhere { @@ -121,7 +117,7 @@ describe("Relationships", () => { } type ActorOperation { - connection(after: String, first: Int, sort: ActorConnectionSort): ActorConnection + connection(after: String, first: Int, sort: [ActorConnectionSort!]): ActorConnection } input ActorOperationWhere { @@ -154,7 +150,7 @@ describe("Relationships", () => { } input MovieActorsConnectionSort { - edges: [MovieActorsEdgeSort!] + edges: MovieActorsEdgeSort } type MovieActorsEdge { @@ -191,7 +187,7 @@ describe("Relationships", () => { } type MovieActorsOperation { - connection(after: String, first: Int, sort: MovieActorsConnectionSort): MovieActorsConnection + connection(after: String, first: Int, sort: [MovieActorsConnectionSort!]): MovieActorsConnection } input MovieActorsOperationWhere { @@ -207,7 +203,7 @@ describe("Relationships", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: MovieSort } type MovieEdge { @@ -215,12 +211,8 @@ describe("Relationships", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - type MovieOperation { - connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection } input MovieOperationWhere { @@ -323,7 +315,7 @@ describe("Relationships", () => { } input ActorConnectionSort { - edges: [ActorEdgeSort!] + node: ActorSort } type ActorEdge { @@ -331,17 +323,13 @@ describe("Relationships", () => { node: Actor } - input ActorEdgeSort { - node: ActorSort - } - type ActorMoviesConnection { edges: [ActorMoviesEdge] pageInfo: PageInfo } input ActorMoviesConnectionSort { - edges: [ActorMoviesEdgeSort!] + edges: ActorMoviesEdgeSort } type ActorMoviesEdge { @@ -381,7 +369,7 @@ describe("Relationships", () => { } type ActorMoviesOperation { - connection(after: String, first: Int, sort: ActorMoviesConnectionSort): ActorMoviesConnection + connection(after: String, first: Int, sort: [ActorMoviesConnectionSort!]): ActorMoviesConnection } input ActorMoviesOperationWhere { @@ -392,7 +380,7 @@ describe("Relationships", () => { } type ActorOperation { - connection(after: String, first: Int, sort: ActorConnectionSort): ActorConnection + connection(after: String, first: Int, sort: [ActorConnectionSort!]): ActorConnection } input ActorOperationWhere { @@ -437,7 +425,7 @@ describe("Relationships", () => { } input MovieActorsConnectionSort { - edges: [MovieActorsEdgeSort!] + edges: MovieActorsEdgeSort } type MovieActorsEdge { @@ -477,7 +465,7 @@ describe("Relationships", () => { } type MovieActorsOperation { - connection(after: String, first: Int, sort: MovieActorsConnectionSort): MovieActorsConnection + connection(after: String, first: Int, sort: [MovieActorsConnectionSort!]): MovieActorsConnection } input MovieActorsOperationWhere { @@ -493,7 +481,7 @@ describe("Relationships", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: MovieSort } type MovieEdge { @@ -501,12 +489,8 @@ describe("Relationships", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - type MovieOperation { - connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection } input MovieOperationWhere { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 6338fac0b6..d081708719 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -49,7 +49,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: MovieSort } type MovieEdge { @@ -57,12 +57,8 @@ describe("Simple Aura-API", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - type MovieOperation { - connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection } input MovieOperationWhere { @@ -141,7 +137,7 @@ describe("Simple Aura-API", () => { } input ActorConnectionSort { - edges: [ActorEdgeSort!] + node: ActorSort } type ActorEdge { @@ -149,12 +145,8 @@ describe("Simple Aura-API", () => { node: Actor } - input ActorEdgeSort { - node: ActorSort - } - type ActorOperation { - connection(after: String, first: Int, sort: ActorConnectionSort): ActorConnection + connection(after: String, first: Int, sort: [ActorConnectionSort!]): ActorConnection } input ActorOperationWhere { @@ -185,7 +177,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: MovieSort } type MovieEdge { @@ -193,12 +185,8 @@ describe("Simple Aura-API", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - type MovieOperation { - connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection } input MovieOperationWhere { @@ -278,7 +266,7 @@ describe("Simple Aura-API", () => { } input MovieConnectionSort { - edges: [MovieEdgeSort!] + node: MovieSort } type MovieEdge { @@ -286,12 +274,8 @@ describe("Simple Aura-API", () => { node: Movie } - input MovieEdgeSort { - node: MovieSort - } - type MovieOperation { - connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + connection(after: String, first: Int, sort: [MovieConnectionSort!]): MovieConnection } input MovieOperationWhere { diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index fbef044c1e..90be897df2 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -163,7 +163,7 @@ describe("Scalars", () => { } input NodeTypeConnectionSort { - edges: [NodeTypeEdgeSort!] + node: NodeTypeSort } type NodeTypeEdge { @@ -171,12 +171,8 @@ describe("Scalars", () => { node: NodeType } - input NodeTypeEdgeSort { - node: NodeTypeSort - } - type NodeTypeOperation { - connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection + connection(after: String, first: Int, sort: [NodeTypeConnectionSort!]): NodeTypeConnection } input NodeTypeOperationWhere { @@ -192,7 +188,7 @@ describe("Scalars", () => { } input NodeTypeRelatedNodeConnectionSort { - edges: [NodeTypeRelatedNodeEdgeSort!] + edges: NodeTypeRelatedNodeEdgeSort } type NodeTypeRelatedNodeEdge { @@ -232,7 +228,7 @@ describe("Scalars", () => { } type NodeTypeRelatedNodeOperation { - connection(after: String, first: Int, sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection + connection(after: String, first: Int, sort: [NodeTypeRelatedNodeConnectionSort!]): NodeTypeRelatedNodeConnection } input NodeTypeRelatedNodeOperationWhere { @@ -309,7 +305,7 @@ describe("Scalars", () => { } input RelatedNodeConnectionSort { - edges: [RelatedNodeEdgeSort!] + node: RelatedNodeSort } type RelatedNodeEdge { @@ -317,12 +313,8 @@ describe("Scalars", () => { node: RelatedNode } - input RelatedNodeEdgeSort { - node: RelatedNodeSort - } - type RelatedNodeOperation { - connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection + connection(after: String, first: Int, sort: [RelatedNodeConnectionSort!]): RelatedNodeConnection } input RelatedNodeOperationWhere { diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index f4844070d7..afd4afc8fe 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -157,7 +157,7 @@ describe("Temporals", () => { } input NodeTypeConnectionSort { - edges: [NodeTypeEdgeSort!] + node: NodeTypeSort } type NodeTypeEdge { @@ -165,12 +165,8 @@ describe("Temporals", () => { node: NodeType } - input NodeTypeEdgeSort { - node: NodeTypeSort - } - type NodeTypeOperation { - connection(after: String, first: Int, sort: NodeTypeConnectionSort): NodeTypeConnection + connection(after: String, first: Int, sort: [NodeTypeConnectionSort!]): NodeTypeConnection } input NodeTypeOperationWhere { @@ -186,7 +182,7 @@ describe("Temporals", () => { } input NodeTypeRelatedNodeConnectionSort { - edges: [NodeTypeRelatedNodeEdgeSort!] + edges: NodeTypeRelatedNodeEdgeSort } type NodeTypeRelatedNodeEdge { @@ -226,7 +222,7 @@ describe("Temporals", () => { } type NodeTypeRelatedNodeOperation { - connection(after: String, first: Int, sort: NodeTypeRelatedNodeConnectionSort): NodeTypeRelatedNodeConnection + connection(after: String, first: Int, sort: [NodeTypeRelatedNodeConnectionSort!]): NodeTypeRelatedNodeConnection } input NodeTypeRelatedNodeOperationWhere { @@ -285,7 +281,7 @@ describe("Temporals", () => { } input RelatedNodeConnectionSort { - edges: [RelatedNodeEdgeSort!] + node: RelatedNodeSort } type RelatedNodeEdge { @@ -293,12 +289,8 @@ describe("Temporals", () => { node: RelatedNode } - input RelatedNodeEdgeSort { - node: RelatedNodeSort - } - type RelatedNodeOperation { - connection(after: String, first: Int, sort: RelatedNodeConnectionSort): RelatedNodeConnection + connection(after: String, first: Int, sort: [RelatedNodeConnectionSort!]): RelatedNodeConnection } input RelatedNodeOperationWhere { 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 index b8e9905e22..245be651f3 100644 --- a/packages/graphql/tests/api-v6/tck/sort/sort-alias.test.ts +++ b/packages/graphql/tests/api-v6/tck/sort/sort-alias.test.ts @@ -41,7 +41,7 @@ describe("Sort with alias", () => { const query = /* GraphQL */ ` query { movies { - connection(sort: { edges: { node: { title: DESC } } }) { + connection(sort: { node: { title: DESC } }) { edges { node { title @@ -76,7 +76,7 @@ describe("Sort with alias", () => { const query = /* GraphQL */ ` query { movies { - connection(sort: { edges: [{ node: { title: DESC } }, { node: { ratings: DESC } }] }) { + connection(sort: [{ node: { title: DESC } }, { node: { ratings: DESC } }]) { edges { node { title 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 index 8bd3ddabda..e60886c015 100644 --- 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 @@ -115,7 +115,9 @@ describe("Sort relationship with alias", () => { node { title actors { - connection(sort: { edges: [{ node: { name: DESC } }, { node: { age: DESC } }] }) { + connection( + sort: [{ edges: { node: { name: DESC } } }, { edges: { node: { age: DESC } } }] + ) { edges { node { name @@ -230,13 +232,11 @@ describe("Sort relationship with alias", () => { title actors { connection( - sort: { - edges: [ - { properties: { year: DESC } } - { node: { name: ASC } } - { properties: { role: ASC } } - ] - } + sort: [ + { edges: { properties: { year: DESC } } } + { edges: { node: { name: ASC } } } + { edges: { properties: { role: ASC } } } + ] ) { edges { node { 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 index a1bcc2a632..e54d0dc77c 100644 --- a/packages/graphql/tests/api-v6/tck/sort/sort-relationship.test.ts +++ b/packages/graphql/tests/api-v6/tck/sort/sort-relationship.test.ts @@ -115,7 +115,9 @@ describe("Sort relationship", () => { node { title actors { - connection(sort: { edges: [{ node: { name: DESC } }, { node: { age: DESC } }] }) { + connection( + sort: [{ edges: { node: { name: DESC } } }, { edges: { node: { age: DESC } } }] + ) { edges { node { name @@ -220,7 +222,7 @@ describe("Sort relationship", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); }); - test("should respect input order on sorting", async () => { + test("Should respect input order on sorting", async () => { const query = /* GraphQL */ ` query { movies { @@ -230,13 +232,11 @@ describe("Sort relationship", () => { title actors { connection( - sort: { - edges: [ - { properties: { year: DESC } } - { node: { name: ASC } } - { properties: { role: ASC } } - ] - } + sort: [ + { edges: { properties: { year: DESC } } } + { edges: { node: { name: ASC } } } + { edges: { properties: { role: ASC } } } + ] ) { edges { node { diff --git a/packages/graphql/tests/api-v6/tck/sort/sort.test.ts b/packages/graphql/tests/api-v6/tck/sort/sort.test.ts index b382d40caa..08174e251c 100644 --- a/packages/graphql/tests/api-v6/tck/sort/sort.test.ts +++ b/packages/graphql/tests/api-v6/tck/sort/sort.test.ts @@ -41,7 +41,7 @@ describe("Sort", () => { const query = /* GraphQL */ ` query { movies { - connection(sort: { edges: { node: { title: DESC } } }) { + connection(sort: { node: { title: DESC } }) { edges { node { title @@ -76,7 +76,7 @@ describe("Sort", () => { const query = /* GraphQL */ ` query { movies { - connection(sort: { edges: [{ node: { title: DESC } }, { node: { ratings: DESC } }] }) { + connection(sort: [{ node: { title: DESC } }, { node: { ratings: DESC } }]) { edges { node { title From 2e193b70624e43dc80826412a0a85ee18bb88d68 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 10 Jul 2024 17:55:24 +0100 Subject: [PATCH 100/177] Remove commented code --- .../resolve-tree-parser/parse-resolve-info-tree.ts | 2 +- .../schema-types/filter-schema-types/FilterSchemaTypes.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) 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 index e1c01849a4..4fa0160a28 100644 --- 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 @@ -42,5 +42,5 @@ export function parseGlobalNodeResolveInfoTree({ entity: ConcreteEntity; }): GraphQLTree { const parser = new GlobalNodeResolveTreeParser({ entity }); - return parser.parseTopLevelOperation(resolveTree); // TODO: This needs to be parseOperationTopLevel + return parser.parseTopLevelOperation(resolveTree); } 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 index 53ef685401..c5d895ef6b 100644 --- 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 @@ -53,8 +53,6 @@ export abstract class FilterSchemaTypes Date: Thu, 11 Jul 2024 10:14:49 +0100 Subject: [PATCH 101/177] Update tests to remove top level edges --- .../database/multi-database.int.test.ts | 12 ++-- .../filters/logical/or-filter.test.ts | 4 +- .../filters/nested/all.int.test.ts | 2 +- .../filters/nested/none.int.test.ts | 2 +- .../filters/nested/single.int.test.ts | 2 +- .../filters/nested/some.int.test.ts | 2 +- .../filters/null-filtering.int.test.ts | 12 +--- .../api-v6/integration/issues/190.int.test.ts | 16 +++-- .../api-v6/integration/issues/360.int.test.ts | 14 ++--- .../api-v6/integration/issues/433.int.test.ts | 2 +- .../api-v6/integration/issues/582.int.test.ts | 62 +++++++++---------- .../integration/sort/sort-filter.int.test.ts | 8 +-- 12 files changed, 63 insertions(+), 75 deletions(-) diff --git a/packages/graphql/tests/api-v6/integration/database/multi-database.int.test.ts b/packages/graphql/tests/api-v6/integration/database/multi-database.int.test.ts index c3cbab6606..e9ccb0bddc 100644 --- a/packages/graphql/tests/api-v6/integration/database/multi-database.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/database/multi-database.int.test.ts @@ -88,7 +88,7 @@ describe("multi-database", () => { const query = ` query { - ${Movie.plural}(where: { edges: { node: { id: { equals: "${id}"}}}}) { + ${Movie.plural}(where: { node: { id: { equals: "${id}"}}}) { connection { edges { node { @@ -118,7 +118,7 @@ describe("multi-database", () => { const query = ` query { - ${Movie.plural}(where: { edges: { node: { id: { equals: "${id}"}}}}) { + ${Movie.plural}(where: { node: { id: { equals: "${id}"}}}) { connection { edges { node { @@ -158,9 +158,9 @@ describe("multi-database", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); - const query = ` + const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { id: { equals: "${id}"}}}}) { + ${Movie.plural}(where: { node: { id: { equals: "${id}"}}}) { connection { edges { node { @@ -192,9 +192,9 @@ describe("multi-database", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); - const query = ` + const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { id: { equals: "${id}"}}}}) { + ${Movie.plural}(where: { node: { id: { equals: "${id}"}}}) { connection { edges { node { diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts index 2c7c0583e4..988bec3e66 100644 --- a/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts +++ b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts @@ -146,8 +146,8 @@ describe("Filters OR", () => { ${Movie.plural}( where: { OR: [ - { edges: { node: { title: { equals: "The Matrix" }, year: { equals: 2001 } } } } - { edges: { node: { year: { equals: 2002 } } } } + { node: { title: { equals: "The Matrix" }, year: { equals: 2001 } } } + { node: { year: { equals: 2002 } } } ] } ) { 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 index 8aa52493a0..39bc92ba84 100644 --- 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 @@ -172,7 +172,7 @@ describe("Relationship filters with all", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { all: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } } + where: { node: { actors: { edges: { all: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } ) { connection { edges { 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 index 0dfcdc56d5..1e48882d40 100644 --- 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 @@ -177,7 +177,7 @@ describe("Relationship filters with none", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { none: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } } + where: { node: { actors: { edges: { none: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } ) { connection { edges { 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 index 1e290c6524..b3affd3d00 100644 --- 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 @@ -174,7 +174,7 @@ describe("Relationship filters with single", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { single: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } } + where: { node: { actors: { edges: { single: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } ) { connection { edges { 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 index 5ae18a90d4..f7baa3a696 100644 --- 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 @@ -187,7 +187,7 @@ describe("Relationship filters with some", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { edges: { node: { actors: { edges: { some: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } } + where: { node: { actors: { edges: { some: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } ) { connection { edges { 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 index e34fa67edb..d007db5562 100644 --- 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 @@ -50,9 +50,7 @@ describe("Null filtering", () => { query { ${Movie.plural}( where: { - edges: { - node: { optional: { equals: null } } - } + node: { optional: { equals: null } } } ) { connection { @@ -88,9 +86,7 @@ describe("Null filtering", () => { query { ${Movie.plural}( where: { - edges: { - node: { optional: { NOT: { equals: null } } } - } + node: { optional: { NOT: { equals: null } } } } ) { connection { @@ -126,9 +122,7 @@ describe("Null filtering", () => { query { ${Movie.plural}( where: { - edges: { - node: { optional: null } - } + node: { optional: null } } ) { connection { 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 index 39d3fa3a6a..145cc44777 100644 --- a/packages/graphql/tests/api-v6/integration/issues/190.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/issues/190.int.test.ts @@ -67,7 +67,7 @@ describe("https://github.com/neo4j/graphql/issues/190", () => { test("Example 1", async () => { const query = /* GraphQL */ ` query { - ${User.plural}(where: { edges: { node: { demographics: { edges: { some: {node: { type: { equals: "Gender" }, value: { equals: "Female" } } } } } } } }) { + ${User.plural}(where: { node: { demographics: { edges: { some: {node: { type: { equals: "Gender" }, value: { equals: "Female" } } } } } } }) { connection { edges { node { @@ -139,14 +139,12 @@ describe("https://github.com/neo4j/graphql/issues/190", () => { query { ${User.plural} ( where: { - edges: { - node: { - demographics: { - edges: { - some: { - node: { - OR: [{ type: {equals: "Gender"}, value:{equals: "Female"} }, { type: {equals: "State"} }, { type: {equals: "Age"} }] - } + node: { + demographics: { + edges: { + some: { + node: { + OR: [{ type: {equals: "Gender"}, value:{equals: "Female"} }, { type: {equals: "State"} }, { type: {equals: "Age"} }] } } } diff --git a/packages/graphql/tests/api-v6/integration/issues/360.int.test.ts b/packages/graphql/tests/api-v6/integration/issues/360.int.test.ts index aa88bb1e96..d2cc4bd8f6 100644 --- a/packages/graphql/tests/api-v6/integration/issues/360.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/issues/360.int.test.ts @@ -43,9 +43,9 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { typeDefs, }); - const query = ` + const query = /* GraphQL */ ` query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { - ${type.plural}(where: { edges: { node: { AND: [{ start: { gte: $rangeStart } }, { start: { lte: $rangeEnd } }, { activity: { equals: $activity } }] } } }) { + ${type.plural}(where: { node: { AND: [{ start: { gte: $rangeStart } }, { start: { lte: $rangeEnd } }, { activity: { equals: $activity } }] } }) { connection { edges { node { @@ -94,9 +94,9 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { typeDefs, }); - const query = ` + const query = /* GraphQL */ ` query ($rangeStart: DateTime, $rangeEnd: DateTime, $activity: String) { - ${type.plural}(where: { edges: { node: { OR: [{ start: { gte: $rangeStart } }, { start: { lte: $rangeEnd } }, { activity: { equals: $activity } }] } } }) { + ${type.plural}(where: { node: { OR: [{ start: { gte: $rangeStart } }, { start: { lte: $rangeEnd } }, { activity: { equals: $activity } }] } }) { connection { edges { node { @@ -131,7 +131,7 @@ describe("https://github.com/neo4j/graphql/issues/360", () => { test("should recreate given test in issue and return correct results", async () => { const type = testHelper.createUniqueType("Event"); - const typeDefs = ` + const typeDefs = /* GraphQL */ ` type ${type.name} @node { id: ID! name: String @@ -148,9 +148,9 @@ 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: { edges: { node: { OR: [{ start: { gte: $rangeStart } }, { start: { lte: $rangeEnd } }, { activity: { equals: $activity } }] } } }) { + ${type.plural}(where: { node: { OR: [{ start: { gte: $rangeStart } }, { start: { lte: $rangeEnd } }, { activity: { equals: $activity } }] } }) { connection { edges { node { diff --git a/packages/graphql/tests/api-v6/integration/issues/433.int.test.ts b/packages/graphql/tests/api-v6/integration/issues/433.int.test.ts index 0ada097a29..ab0baebe60 100644 --- a/packages/graphql/tests/api-v6/integration/issues/433.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/issues/433.int.test.ts @@ -61,7 +61,7 @@ describe("https://github.com/neo4j/graphql/issues/433", () => { const query = ` query { - ${Movie.plural}(where: {edges: {node: {title: {equals: "${movieTitle}"}}}}) { + ${Movie.plural}(where: {node: {title: {equals: "${movieTitle}"}}}) { connection { edges { node { 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 index 27bba7bcdb..6841500e9b 100644 --- a/packages/graphql/tests/api-v6/integration/issues/582.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/issues/582.int.test.ts @@ -71,20 +71,18 @@ describe("https://github.com/neo4j/graphql/issues/582", () => { const gqlResult = await testHelper.executeGraphQL(query, { variableValues: { where: { - edges: { - node: { - type: { equals: "Cat" }, - children: { - edges: { - some: { - node: { - type: { equals: "Dog" }, - parents: { - edges: { - some: { - node: { - type: { equals: "Bird" }, - }, + node: { + type: { equals: "Cat" }, + children: { + edges: { + some: { + node: { + type: { equals: "Dog" }, + parents: { + edges: { + some: { + node: { + type: { equals: "Bird" }, }, }, }, @@ -118,25 +116,23 @@ describe("https://github.com/neo4j/graphql/issues/582", () => { const gqlResult = await testHelper.executeGraphQL(query, { variableValues: { where: { - edges: { - node: { - type: { equals: "Cat" }, - children: { - edges: { - some: { - node: { - type: { equals: "Dog" }, - parents: { - edges: { - some: { - node: { - type: { equals: "Bird" }, - children: { - edges: { - some: { - node: { - type: { equals: "Fish" }, - }, + node: { + type: { equals: "Cat" }, + children: { + edges: { + some: { + node: { + type: { equals: "Dog" }, + parents: { + edges: { + some: { + node: { + type: { equals: "Bird" }, + children: { + edges: { + some: { + node: { + type: { equals: "Fish" }, }, }, }, 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 index bc654adea5..89886a55bb 100644 --- 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 @@ -72,8 +72,8 @@ describe("Sort with filter", () => { test("filter and sort by ASC order and return filtered properties", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { title: { in: ["The Matrix 2", "The Matrix 4"] } } } }) { - connection(sort: { edges: { node: { title: ASC } } }) { + ${Movie.plural}(where: { node: { title: { in: ["The Matrix 2", "The Matrix 4"] } } }) { + connection(sort: { node: { title: ASC } }) { edges { node { title @@ -109,8 +109,8 @@ describe("Sort with filter", () => { test("filter and sort by DESC order and return filtered properties", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { edges: { node: { title: { in: ["The Matrix 2", "The Matrix 4"] } } } }) { - connection(sort: { edges: { node: { title: DESC } } }) { + ${Movie.plural}(where: { node: { title: { in: ["The Matrix 2", "The Matrix 4"] } } }) { + connection(sort: { node: { title: DESC } }) { edges { node { title From a6820e85e24e8d1981b182e98964854712fad79d Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 11 Jul 2024 15:12:06 +0100 Subject: [PATCH 102/177] add @unique schema test --- .../api-v6/schema/directives/unique.test.ts | 355 ++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 packages/graphql/tests/api-v6/schema/directives/unique.test.ts diff --git a/packages/graphql/tests/api-v6/schema/directives/unique.test.ts b/packages/graphql/tests/api-v6/schema/directives/unique.test.ts new file mode 100644 index 0000000000..5b0849a7c8 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/unique.test.ts @@ -0,0 +1,355 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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("@unique", () => { + test("@unique without parameter", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + dbId: ID! @unique + 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 + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + type Movie { + dbId: ID! + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + edges: [MovieEdgeSort!] + } + + type MovieEdge { + cursor: String + node: Movie + } + + input MovieEdgeSort { + node: MovieSort + } + + input MovieEdgeWhere { + AND: [MovieEdgeWhere!] + NOT: MovieEdgeWhere + OR: [MovieEdgeWhere!] + node: MovieWhere + } + + type MovieOperation { + connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + edges: MovieEdgeWhere + } + + input MovieSort { + dbId: SortDirection + title: SortDirection + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + dbId: IDWhere + title: StringWhere + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean + hasPreviousPage: Boolean + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); + + test("@unique with constraintName", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + dbId: ID! @unique(constraintName: "UniqueMovieDbId") + 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 + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + type Movie { + dbId: ID! + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + edges: [MovieEdgeSort!] + } + + type MovieEdge { + cursor: String + node: Movie + } + + input MovieEdgeSort { + node: MovieSort + } + + input MovieEdgeWhere { + AND: [MovieEdgeWhere!] + NOT: MovieEdgeWhere + OR: [MovieEdgeWhere!] + node: MovieWhere + } + + type MovieOperation { + connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + edges: MovieEdgeWhere + } + + input MovieSort { + dbId: SortDirection + title: SortDirection + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + dbId: IDWhere + title: StringWhere + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean + hasPreviousPage: Boolean + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); + + test("@unique applied multiple times with same constraint name", async () => { + const typeDefs = /* GraphQL */ ` + type Movie @node { + dbId: ID! @unique(constraintName: "UniqueMovieDbId") + title: String @unique(constraintName: "UniqueMovieDbId") + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + } + + input IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + type Movie { + dbId: ID! + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + edges: [MovieEdgeSort!] + } + + type MovieEdge { + cursor: String + node: Movie + } + + input MovieEdgeSort { + node: MovieSort + } + + input MovieEdgeWhere { + AND: [MovieEdgeWhere!] + NOT: MovieEdgeWhere + OR: [MovieEdgeWhere!] + node: MovieWhere + } + + type MovieOperation { + connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection + } + + input MovieOperationWhere { + AND: [MovieOperationWhere!] + NOT: MovieOperationWhere + OR: [MovieOperationWhere!] + edges: MovieEdgeWhere + } + + input MovieSort { + dbId: SortDirection + title: SortDirection + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + dbId: IDWhere + title: StringWhere + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean + hasPreviousPage: Boolean + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); +}); From 2947e7299b4f0d9e5ff6dde9dcd28c55d35940e6 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 11 Jul 2024 15:40:43 +0100 Subject: [PATCH 103/177] WIP move tck tests --- .../types/number/number-equals.test.ts | 79 ++ .../filters/types/number/number-gt.test.ts | 120 +++ .../filters/types/number/number-lt.test.ts | 120 +++ .../types/string/string-contains.test.ts | 76 ++ .../types/string/string-ends-with.test.ts | 76 ++ .../filters/types/string/string-in.test.ts | 79 ++ .../types/string/string-starts-with.test.ts | 76 ++ .../{array => time}/temporals-array.test.ts | 0 .../{ => time}/temporals-filters.test.ts | 4 +- .../api-v6/tck/projection/aliasing.test.ts | 101 +++ .../tests/tck/advanced-filtering.test.ts | 818 ------------------ packages/graphql/tests/tck/alias.test.ts | 97 --- .../graphql/tests/tck/root-connection.test.ts | 221 ----- packages/graphql/tests/tck/simple.test.ts | 115 --- 14 files changed, 729 insertions(+), 1253 deletions(-) create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/number/number-equals.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/number/number-gt.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/number/number-lt.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/string/string-contains.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/string/string-ends-with.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/string/string-in.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/filters/types/string/string-starts-with.test.ts rename packages/graphql/tests/api-v6/tck/filters/types/{array => time}/temporals-array.test.ts (100%) rename packages/graphql/tests/api-v6/tck/filters/types/{ => time}/temporals-filters.test.ts (99%) create mode 100644 packages/graphql/tests/api-v6/tck/projection/aliasing.test.ts delete mode 100644 packages/graphql/tests/tck/alias.test.ts delete mode 100644 packages/graphql/tests/tck/root-connection.test.ts delete mode 100644 packages/graphql/tests/tck/simple.test.ts 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/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/array/temporals-array.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/time/temporals-array.test.ts similarity index 100% rename from packages/graphql/tests/api-v6/tck/filters/types/array/temporals-array.test.ts rename to packages/graphql/tests/api-v6/tck/filters/types/time/temporals-array.test.ts diff --git a/packages/graphql/tests/api-v6/tck/filters/types/temporals-filters.test.ts b/packages/graphql/tests/api-v6/tck/filters/types/time/temporals-filters.test.ts similarity index 99% rename from packages/graphql/tests/api-v6/tck/filters/types/temporals-filters.test.ts rename to packages/graphql/tests/api-v6/tck/filters/types/time/temporals-filters.test.ts index a09c925f04..08ea5432e4 100644 --- a/packages/graphql/tests/api-v6/tck/filters/types/temporals-filters.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/types/time/temporals-filters.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import { Neo4jGraphQL } from "../../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; +import { Neo4jGraphQL } from "../../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../../tck/utils/tck-test-utils"; describe("Temporal types", () => { let typeDefs: string; 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..ee337410f9 --- /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]-(actors:Actor) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + RETURN collect({ properties: { releaseYear: this1.year }, node: { __id: id(actors), __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { movieTitle: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); 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/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\\" - }" - `); - }); -}); From 2d323784d857589dd4362a4ee6d3e43a47ed6b91 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 11 Jul 2024 16:23:10 +0100 Subject: [PATCH 104/177] add bug TCK test --- .../filters/logical-filters/or-filter.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) 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 index eefd3cbd86..47825a88a7 100644 --- 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 @@ -123,4 +123,61 @@ describe("OR filters", () => { }" `); }); + + test.only("bug with OR", 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 ((this0.title = $param0 AND this0.year = $param1) OR NOT (this0.runtime = $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\\": \\"The Matrix\\", + \\"param1\\": { + \\"low\\": 2, + \\"high\\": 0 + }, + \\"param2\\": 2, + \\"param3\\": { + \\"low\\": 100, + \\"high\\": 0 + } + }" + `); + }); }); From 4e499955a109a5797fa00b7aa0009e24a0dbbfb9 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 12 Jul 2024 09:41:08 +0100 Subject: [PATCH 105/177] Fix error with top level or --- .../api-v6/queryIRFactory/FilterFactory.ts | 11 +++-- ...-filter.test.ts => and-filter.int.test.ts} | 0 ...-filter.test.ts => not-filter.int.test.ts} | 0 ...r-filter.test.ts => or-filter.int.test.ts} | 43 +++++++++++++++++++ .../filters/logical-filters/or-filter.test.ts | 4 +- 5 files changed, 53 insertions(+), 5 deletions(-) rename packages/graphql/tests/api-v6/integration/filters/logical/{and-filter.test.ts => and-filter.int.test.ts} (100%) rename packages/graphql/tests/api-v6/integration/filters/logical/{not-filter.test.ts => not-filter.int.test.ts} (100%) rename packages/graphql/tests/api-v6/integration/filters/logical/{or-filter.test.ts => or-filter.int.test.ts} (81%) diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index fc3c58f419..8a8b8d4d7e 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -88,7 +88,12 @@ export class FilterFactory { }); const edgeFilters = this.createNodeFilter({ entity, where: where.node }); - return [...edgeFilters, ...andFilters, ...orFilters, ...notFilters]; + return this.mergeFiltersWithAnd([ + ...this.mergeFiltersWithAnd(edgeFilters), + ...andFilters, + ...orFilters, + ...notFilters, + ]); } private createTopLevelLogicalFilters({ @@ -176,13 +181,13 @@ export class FilterFactory { relationship, }); } - return [ + return this.mergeFiltersWithAnd([ ...this.mergeFiltersWithAnd(nodeFilters), ...edgePropertiesFilters, ...andFilters, ...orFilters, ...notFilters, - ]; + ]); } private createLogicalEdgeFilters( diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/and-filter.int.test.ts similarity index 100% rename from packages/graphql/tests/api-v6/integration/filters/logical/and-filter.test.ts rename to packages/graphql/tests/api-v6/integration/filters/logical/and-filter.int.test.ts diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/not-filter.int.test.ts similarity index 100% rename from packages/graphql/tests/api-v6/integration/filters/logical/not-filter.test.ts rename to packages/graphql/tests/api-v6/integration/filters/logical/not-filter.int.test.ts diff --git a/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.int.test.ts similarity index 81% rename from packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts rename to packages/graphql/tests/api-v6/integration/filters/logical/or-filter.int.test.ts index 988bec3e66..8a784adf47 100644 --- a/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.test.ts +++ b/packages/graphql/tests/api-v6/integration/filters/logical/or-filter.int.test.ts @@ -34,6 +34,7 @@ describe("Filters OR", () => { type ${Movie} @node { title: String year: Int + runtime: Float actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") } type ${Actor} @node { @@ -178,4 +179,46 @@ describe("Filters OR", () => { }, }); }); + + 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/tck/filters/logical-filters/or-filter.test.ts b/packages/graphql/tests/api-v6/tck/filters/logical-filters/or-filter.test.ts index 47825a88a7..d38258dad2 100644 --- 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 @@ -124,7 +124,7 @@ describe("OR filters", () => { `); }); - test.only("bug with OR", async () => { + test("top level OR filter combined with implicit AND and nested not", async () => { const query = /* GraphQL */ ` query { movies( @@ -153,7 +153,7 @@ describe("OR filters", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) - WHERE ((this0.title = $param0 AND this0.year = $param1) OR NOT (this0.runtime = $param2) OR this0.year = $param3) + WHERE (((this0.title = $param0 AND this0.year = $param1) AND NOT (this0.runtime = $param2)) OR this0.year = $param3) WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { From 1ffeda18e9ac3d1675b189df05339ee7e5b9596b Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 12 Jul 2024 09:58:53 +0100 Subject: [PATCH 106/177] Move pagination and connection tck tests to v6 --- .../tests/api-v6/tck/pagination/after.test.ts | 94 +++++ .../api-v6/tck/pagination/first-after.test.ts | 100 +++++ .../tests/tck/connections/alias.test.ts | 156 ------- .../connections/filtering/node/and.test.ts | 146 ------- .../filtering/node/equality.test.ts | 142 ------- .../tck/connections/filtering/node/or.test.ts | 98 ----- .../filtering/node/relationship.test.ts | 91 ---- .../connections/filtering/node/string.test.ts | 395 ------------------ .../filtering/relationship/and.test.ts | 149 ------- .../filtering/relationship/equality.test.ts | 148 ------- .../filtering/relationship/or.test.ts | 129 ------ .../filtering/relationship/string.test.ts | 395 ------------------ packages/graphql/tests/tck/pagination.test.ts | 200 --------- 13 files changed, 194 insertions(+), 2049 deletions(-) create mode 100644 packages/graphql/tests/api-v6/tck/pagination/after.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/pagination/first-after.test.ts delete mode 100644 packages/graphql/tests/tck/connections/alias.test.ts delete mode 100644 packages/graphql/tests/tck/connections/filtering/node/and.test.ts delete mode 100644 packages/graphql/tests/tck/connections/filtering/node/equality.test.ts delete mode 100644 packages/graphql/tests/tck/connections/filtering/node/or.test.ts delete mode 100644 packages/graphql/tests/tck/connections/filtering/node/relationship.test.ts delete mode 100644 packages/graphql/tests/tck/connections/filtering/node/string.test.ts delete mode 100644 packages/graphql/tests/tck/connections/filtering/relationship/and.test.ts delete mode 100644 packages/graphql/tests/tck/connections/filtering/relationship/equality.test.ts delete mode 100644 packages/graphql/tests/tck/connections/filtering/relationship/or.test.ts delete mode 100644 packages/graphql/tests/tck/connections/filtering/relationship/string.test.ts delete mode 100644 packages/graphql/tests/tck/pagination.test.ts 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/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/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 - } - }" - `); - }); -}); From b46cf2dc04b0fbfe2656ecd172eaedc7ae50f66a Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 12 Jul 2024 10:17:10 +0100 Subject: [PATCH 107/177] WIP graphql-tree cleanup --- .../resolve-tree-parser/ResolveTreeParser.ts | 2 +- .../TopLevelResolveTreeParser.ts | 6 ++-- .../resolve-tree-parser/graphql-tree.ts | 36 +++++++++---------- .../{ => utils}/find-field-by-name.ts | 0 4 files changed, 22 insertions(+), 22 deletions(-) rename packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/{ => utils}/find-field-by-name.ts (100%) diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index 567281527a..ea1fce7848 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -24,7 +24,6 @@ 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 { findFieldByName } from "./find-field-by-name"; import type { GraphQLConnectionArgs, GraphQLReadOperationArgs, @@ -41,6 +40,7 @@ import type { GraphQLTreeScalarField, GraphQLTreeSortElement, } from "./graphql-tree"; +import { findFieldByName } from "./utils/find-field-by-name"; export abstract class ResolveTreeParser { protected entity: T; diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts index b732eae73d..e8be6a0883 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts @@ -20,15 +20,15 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { ResolveTreeParser } from "./ResolveTreeParser"; -import { findFieldByName } from "./find-field-by-name"; import type { GraphQLConnectionArgsTopLevel, GraphQLReadOperationArgsTopLevel, GraphQLSortEdgeArgument, + GraphQLTree, GraphQLTreeConnectionTopLevel, GraphQLTreeEdge, - GraphQLTreeReadOperationTopLevel, } from "./graphql-tree"; +import { findFieldByName } from "./utils/find-field-by-name"; export class TopLevelResolveTreeParser extends ResolveTreeParser { protected get targetNode(): ConcreteEntity { @@ -36,7 +36,7 @@ export class TopLevelResolveTreeParser extends ResolveTreeParser } /** Parse a resolveTree into a Neo4j GraphQLTree */ - public parseOperationTopLevel(resolveTree: ResolveTree): GraphQLTreeReadOperationTopLevel { + public parseOperationTopLevel(resolveTree: ResolveTree): GraphQLTree { const connectionResolveTree = findFieldByName( resolveTree, this.entity.typeNames.connectionOperation, diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts index e4b0568125..2ff9a7e9e8 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts @@ -32,6 +32,22 @@ type LogicalOperation = { NOT?: LogicalOperation; } & T; +interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { + name: string; + fields: { + connection?: GraphQLTreeConnectionTopLevel; + }; + args: GraphQLReadOperationArgsTopLevel; +} + +export interface GraphQLTreeReadOperation extends GraphQLTreeElement { + name: string; + fields: { + connection?: GraphQLTreeConnection; + }; + args: GraphQLReadOperationArgs; +} + export type StringFilters = LogicalOperation<{ equals?: string; in?: string[]; @@ -59,22 +75,6 @@ export type RelationshipFilters = { }; }; -export interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { - name: string; - fields: { - connection?: GraphQLTreeConnectionTopLevel; - }; - args: GraphQLReadOperationArgsTopLevel; -} - -export interface GraphQLTreeReadOperation extends GraphQLTreeElement { - name: string; - fields: { - connection?: GraphQLTreeConnection; - }; - args: GraphQLReadOperationArgs; -} - export interface GraphQLReadOperationArgsTopLevel { where?: GraphQLWhereArgsTopLevel; } @@ -141,9 +141,9 @@ export interface GraphQLTreeEdgeProperties extends GraphQLTreeElement { fields: Record; } -export type GraphQLTreeLeafField = GraphQLTreeScalarField | GraphQLTreePoint | GraphQLTreeCartesianPoint; +type GraphQLTreeLeafField = GraphQLTreeScalarField | GraphQLTreePoint | GraphQLTreeCartesianPoint; + export interface GraphQLTreeScalarField extends GraphQLTreeElement { - fields: undefined; name: string; } export interface GraphQLTreePoint extends GraphQLTreeElement { diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/find-field-by-name.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/utils/find-field-by-name.ts similarity index 100% rename from packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/find-field-by-name.ts rename to packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/utils/find-field-by-name.ts From 133d3025a45d006bca9acb83ef389ceab0040575 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 12 Jul 2024 11:21:38 +0100 Subject: [PATCH 108/177] Cleanup graphql tree types --- .../api-v6/queryIRFactory/FilterFactory.ts | 28 ++- .../queryIRFactory/ReadOperationFactory.ts | 21 +- .../GlobalNodeResolveTreeParser.ts | 2 +- .../resolve-tree-parser/ResolveTreeParser.ts | 23 ++- .../TopLevelResolveTreeParser.ts | 16 +- .../resolve-tree-parser/graphql-tree.ts | 182 ------------------ .../graphql-tree/attributes.ts | 48 +++++ .../graphql-tree/graphql-tree.ts | 83 ++++++++ .../resolve-tree-parser/graphql-tree/sort.ts | 31 +++ .../graphql-tree/tree-element.ts | 23 +++ .../resolve-tree-parser/graphql-tree/where.ts | 71 +++++++ .../parse-resolve-info-tree.ts | 2 +- .../translators/translate-read-operation.ts | 2 +- 13 files changed, 299 insertions(+), 233 deletions(-) delete mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/attributes.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/sort.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/tree-element.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/where.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index fc3c58f419..6ddd10dc7f 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -34,13 +34,13 @@ import { fromGlobalId } from "../../utils/global-ids"; import { getFilterOperator, getRelationshipOperator } from "./FilterOperators"; import type { GraphQLAttributeFilters, - GraphQLEdgeWhereArgs, + GraphQLEdgeWhere, GraphQLNodeFilters, - GraphQLNodeWhereArgs, + GraphQLNodeWhere, GraphQLWhereArgs, GraphQLWhereArgsTopLevel, RelationshipFilters, -} from "./resolve-tree-parser/graphql-tree"; +} from "./resolve-tree-parser/graphql-tree/where"; export class FilterFactory { public schemaModel: Neo4jGraphQLSchemaModel; @@ -128,12 +128,12 @@ export class FilterFactory { entity: ConcreteEntity; relationship?: Relationship; operation: LogicalOperators; - where?: GraphQLEdgeWhereArgs[]; + where?: GraphQLEdgeWhere[]; }): [] | [Filter] { if (where.length === 0) { return []; } - const nestedFilters = where.flatMap((orWhere: GraphQLEdgeWhereArgs) => { + const nestedFilters = where.flatMap((orWhere: GraphQLEdgeWhere) => { return this.createFilters({ entity, relationship, where: orWhere }); }); @@ -156,7 +156,7 @@ export class FilterFactory { }: { entity: ConcreteEntity; relationship?: Relationship; - edgeWhere?: GraphQLEdgeWhereArgs; + edgeWhere?: GraphQLEdgeWhere; }): Filter[] { const andFilters = this.createLogicalEdgeFilters(entity, relationship, "AND", edgeWhere.AND); const orFilters = this.createLogicalEdgeFilters(entity, relationship, "OR", edgeWhere.OR); @@ -189,12 +189,12 @@ export class FilterFactory { entity: ConcreteEntity, relationship: Relationship | undefined, operation: LogicalOperators, - where: GraphQLEdgeWhereArgs[] = [] + where: GraphQLEdgeWhere[] = [] ): [] | [Filter] { if (where.length === 0) { return []; } - const nestedFilters = where.flatMap((logicalWhere: GraphQLEdgeWhereArgs) => { + const nestedFilters = where.flatMap((logicalWhere: GraphQLEdgeWhere) => { return this.createEdgeFilters({ entity, relationship, edgeWhere: logicalWhere }); }); @@ -210,13 +210,7 @@ export class FilterFactory { return []; } - private createNodeFilter({ - where = {}, - entity, - }: { - entity: ConcreteEntity; - where?: GraphQLNodeWhereArgs; - }): Filter[] { + private createNodeFilter({ where = {}, entity }: { entity: ConcreteEntity; where?: GraphQLNodeWhere }): Filter[] { const andFilters = this.createLogicalNodeFilters(entity, "AND", where.AND); const orFilters = this.createLogicalNodeFilters(entity, "OR", where.OR); const notFilters = this.createLogicalNodeFilters(entity, "NOT", where.NOT ? [where.NOT] : undefined); @@ -256,7 +250,7 @@ export class FilterFactory { private createLogicalNodeFilters( entity: ConcreteEntity, operation: LogicalOperators, - where: GraphQLNodeWhereArgs[] = [] + where: GraphQLNodeWhere[] = [] ): [] | [Filter] { if (where.length === 0) { return []; @@ -318,7 +312,7 @@ export class FilterFactory { where, relationship, }: { - where: GraphQLEdgeWhereArgs["properties"]; + where: GraphQLEdgeWhere["properties"]; relationship: Relationship; }): Filter[] { if (!where) { diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 5d35e612ff..41ac45a8e3 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -40,18 +40,21 @@ import { PropertySort } from "../../translate/queryAST/ast/sort/PropertySort"; import { filterTruthy } from "../../utils/utils"; import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { FilterFactory } from "./FilterFactory"; + +import type { GraphQLTreePoint } from "./resolve-tree-parser/graphql-tree/attributes"; import type { - GraphQLConnectionArgs, - GraphQLConnectionArgsTopLevel, - GraphQLSortArgument, - GraphQLSortEdgeArgument, GraphQLTree, + GraphQLTreeConnection, + GraphQLTreeConnectionTopLevel, GraphQLTreeEdgeProperties, - GraphQLTreeLeafField, GraphQLTreeNode, GraphQLTreeReadOperation, +} from "./resolve-tree-parser/graphql-tree/graphql-tree"; +import type { + GraphQLSortArgument, + GraphQLSortEdgeArgument, GraphQLTreeSortElement, -} from "./resolve-tree-parser/graphql-tree"; +} from "./resolve-tree-parser/graphql-tree/sort"; export class ReadOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; @@ -163,7 +166,7 @@ export class ReadOperationFactory { } private getPagination( - connectionTreeArgs: GraphQLConnectionArgs | GraphQLConnectionArgsTopLevel, + connectionTreeArgs: GraphQLTreeConnection["args"] | GraphQLTreeConnectionTopLevel["args"], entity: ConcreteEntity ): Pagination | undefined { const firstArgument = connectionTreeArgs.first; @@ -214,7 +217,7 @@ export class ReadOperationFactory { } if (attribute) { - const field = rawField as GraphQLTreeLeafField; + const field = rawField; const attributeAdapter = new AttributeAdapter(attribute); if (attributeAdapter.typeHelper.isDateTime()) { return new DateTimeField({ @@ -226,7 +229,7 @@ export class ReadOperationFactory { return new SpatialAttributeField({ alias: rawField.alias, attribute: attributeAdapter, - crs: Boolean(field?.fields?.crs), + crs: Boolean((field as GraphQLTreePoint)?.fields?.crs), }); } return new AttributeField({ diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts index 14d5e5b926..13571c68d4 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts @@ -20,7 +20,7 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; -import type { GraphQLTree } from "./graphql-tree"; +import type { GraphQLTree } from "./graphql-tree/graphql-tree"; export class GlobalNodeResolveTreeParser extends TopLevelResolveTreeParser { constructor({ entity }: { entity: ConcreteEntity }) { diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index ea1fce7848..adbd2da410 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -24,22 +24,22 @@ 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 { - GraphQLConnectionArgs, - GraphQLReadOperationArgs, - GraphQLSortArgument, - GraphQLSortEdgeArgument, GraphQLTreeCartesianPoint, + GraphQLTreeLeafField, + GraphQLTreePoint, + GraphQLTreeScalarField, +} from "./graphql-tree/attributes"; +import type { + GraphQLTree, GraphQLTreeConnection, GraphQLTreeEdge, GraphQLTreeEdgeProperties, - GraphQLTreeLeafField, GraphQLTreeNode, - GraphQLTreePoint, GraphQLTreeReadOperation, - GraphQLTreeScalarField, - GraphQLTreeSortElement, -} from "./graphql-tree"; +} from "./graphql-tree/graphql-tree"; +import type { GraphQLSortArgument, GraphQLSortEdgeArgument, GraphQLTreeSortElement } from "./graphql-tree/sort"; import { findFieldByName } from "./utils/find-field-by-name"; export abstract class ResolveTreeParser { @@ -69,7 +69,7 @@ export abstract class ResolveTreeParser }; } - private parseOperationArgs(resolveTreeArgs: Record): GraphQLReadOperationArgs { + private parseOperationArgs(resolveTreeArgs: Record): GraphQLTree["args"] { // Not properly parsed, assuming the type is the same return { where: resolveTreeArgs.where, @@ -221,7 +221,7 @@ export abstract class ResolveTreeParser return relationshipTreeParser.parseOperation(resolveTree); } - private parseConnectionArgs(resolveTreeArgs: { [str: string]: any }): GraphQLConnectionArgs { + private parseConnectionArgs(resolveTreeArgs: { [str: string]: any }): GraphQLTreeConnection["args"] { let sortArg: GraphQLSortArgument[] | undefined; if (resolveTreeArgs.sort) { sortArg = resolveTreeArgs.sort.map((sortArg): GraphQLSortArgument => { @@ -341,6 +341,5 @@ function resolveTreeToLeafField(resolveTree: ResolveTree | undefined): GraphQLTr alias: resolveTree.alias, args: resolveTree.args, name: resolveTree.name, - fields: undefined, }; } diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts index e8be6a0883..0e19e53cb5 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts @@ -20,14 +20,8 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { ResolveTreeParser } from "./ResolveTreeParser"; -import type { - GraphQLConnectionArgsTopLevel, - GraphQLReadOperationArgsTopLevel, - GraphQLSortEdgeArgument, - GraphQLTree, - GraphQLTreeConnectionTopLevel, - GraphQLTreeEdge, -} from "./graphql-tree"; +import type { GraphQLTree, GraphQLTreeConnectionTopLevel, GraphQLTreeEdge } from "./graphql-tree/graphql-tree"; +import type { GraphQLSortEdgeArgument } from "./graphql-tree/sort"; import { findFieldByName } from "./utils/find-field-by-name"; export class TopLevelResolveTreeParser extends ResolveTreeParser { @@ -70,7 +64,9 @@ export class TopLevelResolveTreeParser extends ResolveTreeParser }; } - private parseConnectionArgsTopLevel(resolveTreeArgs: { [str: string]: any }): GraphQLConnectionArgsTopLevel { + private parseConnectionArgsTopLevel(resolveTreeArgs: { + [str: string]: any; + }): GraphQLTreeConnectionTopLevel["args"] { let sortArg: GraphQLSortEdgeArgument[] | undefined; if (resolveTreeArgs.sort) { sortArg = resolveTreeArgs.sort.map((sortArg) => { @@ -85,7 +81,7 @@ export class TopLevelResolveTreeParser extends ResolveTreeParser }; } - protected parseOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLReadOperationArgsTopLevel { + protected parseOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLTree["args"] { // Not properly parsed, assuming the type is the same return { where: resolveTreeArgs.where, diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts deleted file mode 100644 index 2ff9a7e9e8..0000000000 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree.ts +++ /dev/null @@ -1,182 +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 { Integer } from "neo4j-driver"; - -export type GraphQLTree = GraphQLTreeReadOperationTopLevel; - -interface GraphQLTreeElement { - alias: string; - args: Record; -} - -type LogicalOperation = { - AND?: Array>; - OR?: Array>; - NOT?: LogicalOperation; -} & T; - -interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { - name: string; - fields: { - connection?: GraphQLTreeConnectionTopLevel; - }; - args: GraphQLReadOperationArgsTopLevel; -} - -export interface GraphQLTreeReadOperation extends GraphQLTreeElement { - name: string; - fields: { - connection?: GraphQLTreeConnection; - }; - args: GraphQLReadOperationArgs; -} - -export type StringFilters = LogicalOperation<{ - equals?: string; - in?: string[]; - matches?: string; - contains?: string; - startsWith?: string; - endsWith?: string; -}>; - -export type NumberFilters = LogicalOperation<{ - equals?: string; - in?: string[]; - lt?: string; - lte?: string; - gt?: string; - gte?: string; -}>; - -export type RelationshipFilters = { - edges?: { - some?: GraphQLEdgeWhereArgs; - single?: GraphQLEdgeWhereArgs; - all?: GraphQLEdgeWhereArgs; - none?: GraphQLEdgeWhereArgs; - }; -}; - -export interface GraphQLReadOperationArgsTopLevel { - where?: GraphQLWhereArgsTopLevel; -} - -export interface GraphQLReadOperationArgs { - where?: GraphQLWhereArgs; -} - -export type GraphQLWhereArgs = LogicalOperation<{ - edges?: GraphQLEdgeWhereArgs; -}>; - -export type GraphQLWhereArgsTopLevel = LogicalOperation<{ - node?: GraphQLNodeWhereArgs; -}>; - -export type GraphQLNodeWhereArgs = LogicalOperation>; - -export type GraphQLEdgeWhereArgs = LogicalOperation<{ - properties?: Record; - node?: GraphQLNodeWhereArgs; -}>; - -export type GraphQLAttributeFilters = StringFilters | NumberFilters; -export type GraphQLNodeFilters = GraphQLAttributeFilters | RelationshipFilters; - -export interface GraphQLTreeConnection extends GraphQLTreeElement { - fields: { - edges?: GraphQLTreeEdge; - }; - args: GraphQLConnectionArgs; -} - -export interface GraphQLTreeConnectionTopLevel extends GraphQLTreeElement { - fields: { - edges?: GraphQLTreeEdge; - }; - args: GraphQLConnectionArgsTopLevel; -} - -export interface GraphQLConnectionArgs { - sort?: GraphQLSortArgument[]; - first?: Integer; - after?: string; -} -export interface GraphQLConnectionArgsTopLevel { - sort?: GraphQLSortEdgeArgument[]; - 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; -} - -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; -} - -export interface GraphQLSortArgument { - edges: GraphQLSortEdgeArgument; -} - -export interface GraphQLSortEdgeArgument { - 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/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..097e7cd1a7 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Integer } from "neo4j-driver"; +import type { GraphQLTreeLeafField } from "./attributes"; +import type { GraphQLSortArgument, GraphQLSortEdgeArgument } from "./sort"; +import type { GraphQLTreeElement } from "./tree-element"; +import type { GraphQLWhereArgs, GraphQLWhereArgsTopLevel } from "./where"; + +export type GraphQLTree = GraphQLTreeReadOperationTopLevel; + +interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { + name: string; + fields: { + connection?: GraphQLTreeConnectionTopLevel; + }; + args: { + where?: GraphQLWhereArgsTopLevel; + }; +} + +export interface GraphQLTreeReadOperation extends GraphQLTreeElement { + name: string; + fields: { + connection?: GraphQLTreeConnection; + }; + args: { + where?: GraphQLWhereArgs; + }; +} + +export interface GraphQLTreeConnection extends GraphQLTreeElement { + fields: { + edges?: GraphQLTreeEdge; + }; + args: { + sort?: GraphQLSortArgument[]; + first?: Integer; + after?: string; + }; +} + +export interface GraphQLTreeConnectionTopLevel extends GraphQLTreeElement { + fields: { + edges?: GraphQLTreeEdge; + }; + args: { + sort?: GraphQLSortEdgeArgument[]; + 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..df513501a4 --- /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 GraphQLSortArgument { + edges: GraphQLSortEdgeArgument; +} + +export interface GraphQLSortEdgeArgument { + 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..421e2f76e4 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/where.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. + */ + +/** Args for `where` in nested connections (with edge -> node) */ +export type GraphQLWhereArgs = LogicalOperation<{ + edges?: GraphQLEdgeWhere; +}>; + +/** Args for `where` in top level connections only (i.e. no edge available) */ +export type GraphQLWhereArgsTopLevel = LogicalOperation<{ + node?: GraphQLNodeWhere; +}>; + +export type GraphQLEdgeWhere = LogicalOperation<{ + properties?: Record; + node?: GraphQLNodeWhere; +}>; + +export type GraphQLNodeWhere = LogicalOperation>; +export type GraphQLNodeFilters = GraphQLAttributeFilters | RelationshipFilters; + +export type GraphQLAttributeFilters = StringFilters | NumberFilters; + +export type StringFilters = LogicalOperation<{ + equals?: string; + in?: string[]; + matches?: string; + contains?: string; + startsWith?: string; + endsWith?: string; +}>; + +export type NumberFilters = LogicalOperation<{ + equals?: string; + in?: string[]; + lt?: string; + lte?: string; + gt?: string; + gte?: string; +}>; + +export type RelationshipFilters = { + edges?: { + some?: GraphQLEdgeWhere; + single?: GraphQLEdgeWhere; + all?: GraphQLEdgeWhere; + none?: GraphQLEdgeWhere; + }; +}; + +type LogicalOperation = { + AND?: Array>; + OR?: Array>; + NOT?: LogicalOperation; +} & T; 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 index 4fa0160a28..e97e6606ac 100644 --- 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 @@ -21,7 +21,7 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { GlobalNodeResolveTreeParser } from "./GlobalNodeResolveTreeParser"; import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; -import type { GraphQLTree } from "./graphql-tree"; +import type { GraphQLTree } from "./graphql-tree/graphql-tree"; export function parseResolveInfoTree({ resolveTree, diff --git a/packages/graphql/src/api-v6/translators/translate-read-operation.ts b/packages/graphql/src/api-v6/translators/translate-read-operation.ts index 067519c1a9..af3dda8e2e 100644 --- a/packages/graphql/src/api-v6/translators/translate-read-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-read-operation.ts @@ -23,7 +23,7 @@ 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 { GraphQLTree } from "../queryIRFactory/resolve-tree-parser/graphql-tree"; +import type { GraphQLTree } from "../queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree"; const debug = Debug(DEBUG_TRANSLATE); From bb345f6fe0899eb027df56b4ffe6484e0b28ac35 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Fri, 12 Jul 2024 13:08:29 +0100 Subject: [PATCH 109/177] Remove args suffix from graphql-tree types --- .../src/api-v6/queryIRFactory/FilterFactory.ts | 12 ++++++------ .../api-v6/queryIRFactory/ReadOperationFactory.ts | 12 ++++-------- .../resolve-tree-parser/ResolveTreeParser.ts | 10 +++++----- .../resolve-tree-parser/TopLevelResolveTreeParser.ts | 4 ++-- .../resolve-tree-parser/graphql-tree/graphql-tree.ts | 12 ++++++------ .../resolve-tree-parser/graphql-tree/sort.ts | 6 +++--- .../resolve-tree-parser/graphql-tree/where.ts | 4 ++-- 7 files changed, 28 insertions(+), 32 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index 6ddd10dc7f..bfa5eac035 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -37,8 +37,8 @@ import type { GraphQLEdgeWhere, GraphQLNodeFilters, GraphQLNodeWhere, - GraphQLWhereArgs, - GraphQLWhereArgsTopLevel, + GraphQLWhere, + GraphQLWhereTopLevel, RelationshipFilters, } from "./resolve-tree-parser/graphql-tree/where"; @@ -56,7 +56,7 @@ export class FilterFactory { }: { entity: ConcreteEntity; relationship?: Relationship; - where?: GraphQLWhereArgs; + where?: GraphQLWhere; }): Filter[] { const andFilters = this.createLogicalFilters({ operation: "AND", entity, relationship, where: where.AND }); const orFilters = this.createLogicalFilters({ operation: "OR", entity, relationship, where: where.OR }); @@ -77,7 +77,7 @@ export class FilterFactory { entity, }: { entity: ConcreteEntity; - where?: GraphQLWhereArgsTopLevel; + where?: GraphQLWhereTopLevel; }): Filter[] { const andFilters = this.createTopLevelLogicalFilters({ operation: "AND", entity, where: where.AND }); const orFilters = this.createTopLevelLogicalFilters({ operation: "OR", entity, where: where.OR }); @@ -98,12 +98,12 @@ export class FilterFactory { }: { entity: ConcreteEntity; operation: LogicalOperators; - where?: GraphQLWhereArgsTopLevel[]; + where?: GraphQLWhereTopLevel[]; }): [] | [Filter] { if (where.length === 0) { return []; } - const nestedFilters = where.flatMap((orWhere: GraphQLWhereArgsTopLevel) => { + const nestedFilters = where.flatMap((orWhere: GraphQLWhereTopLevel) => { return this.createTopLevelFilters({ entity, where: orWhere }); }); diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 41ac45a8e3..4cbd720c04 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -50,11 +50,7 @@ import type { GraphQLTreeNode, GraphQLTreeReadOperation, } from "./resolve-tree-parser/graphql-tree/graphql-tree"; -import type { - GraphQLSortArgument, - GraphQLSortEdgeArgument, - GraphQLTreeSortElement, -} from "./resolve-tree-parser/graphql-tree/sort"; +import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "./resolve-tree-parser/graphql-tree/sort"; export class ReadOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; @@ -271,7 +267,7 @@ export class ReadOperationFactory { }: { entity: ConcreteEntity; relationship?: Relationship; - sortArgument: GraphQLSortArgument[] | undefined; + sortArgument: GraphQLSort[] | undefined; }): Array<{ edge: PropertySort[]; node: PropertySort[] }> { if (!sortArgument) { return []; @@ -291,7 +287,7 @@ export class ReadOperationFactory { }: { entity: ConcreteEntity; relationship?: Relationship; - sortArgument: GraphQLSortEdgeArgument[] | undefined; + sortArgument: GraphQLSortEdge[] | undefined; }): Array<{ edge: PropertySort[]; node: PropertySort[] }> { if (!sortArgument) { return []; @@ -312,7 +308,7 @@ export class ReadOperationFactory { }: { entity: ConcreteEntity; relationship?: Relationship; - edges: GraphQLSortEdgeArgument; + edges: GraphQLSortEdge; }): { edge: PropertySort[]; node: PropertySort[] } { const nodeSortFields = edges.node ? this.getPropertiesSort({ target: entity, sortArgument: edges.node }) : []; const edgeSortFields = diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts index adbd2da410..d72ff02c25 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts @@ -39,7 +39,7 @@ import type { GraphQLTreeNode, GraphQLTreeReadOperation, } from "./graphql-tree/graphql-tree"; -import type { GraphQLSortArgument, GraphQLSortEdgeArgument, GraphQLTreeSortElement } from "./graphql-tree/sort"; +import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "./graphql-tree/sort"; import { findFieldByName } from "./utils/find-field-by-name"; export abstract class ResolveTreeParser { @@ -222,9 +222,9 @@ export abstract class ResolveTreeParser } private parseConnectionArgs(resolveTreeArgs: { [str: string]: any }): GraphQLTreeConnection["args"] { - let sortArg: GraphQLSortArgument[] | undefined; + let sortArg: GraphQLSort[] | undefined; if (resolveTreeArgs.sort) { - sortArg = resolveTreeArgs.sort.map((sortArg): GraphQLSortArgument => { + sortArg = resolveTreeArgs.sort.map((sortArg): GraphQLSort => { return { edges: this.parseSortEdges(sortArg.edges) }; }); } @@ -238,8 +238,8 @@ export abstract class ResolveTreeParser protected parseSortEdges(sortEdges: { node: Record | undefined; properties: Record | undefined; - }): GraphQLSortEdgeArgument { - const sortFields: GraphQLSortEdgeArgument = {}; + }): GraphQLSortEdge { + const sortFields: GraphQLSortEdge = {}; const nodeFields = sortEdges.node; if (nodeFields) { diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts index 0e19e53cb5..5994a2c320 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts @@ -21,7 +21,7 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { ResolveTreeParser } from "./ResolveTreeParser"; import type { GraphQLTree, GraphQLTreeConnectionTopLevel, GraphQLTreeEdge } from "./graphql-tree/graphql-tree"; -import type { GraphQLSortEdgeArgument } from "./graphql-tree/sort"; +import type { GraphQLSortEdge } from "./graphql-tree/sort"; import { findFieldByName } from "./utils/find-field-by-name"; export class TopLevelResolveTreeParser extends ResolveTreeParser { @@ -67,7 +67,7 @@ export class TopLevelResolveTreeParser extends ResolveTreeParser private parseConnectionArgsTopLevel(resolveTreeArgs: { [str: string]: any; }): GraphQLTreeConnectionTopLevel["args"] { - let sortArg: GraphQLSortEdgeArgument[] | undefined; + let sortArg: GraphQLSortEdge[] | undefined; if (resolveTreeArgs.sort) { sortArg = resolveTreeArgs.sort.map((sortArg) => { return this.parseSortEdges(sortArg); 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 index 097e7cd1a7..8d72aad3a0 100644 --- 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 @@ -19,9 +19,9 @@ import type { Integer } from "neo4j-driver"; import type { GraphQLTreeLeafField } from "./attributes"; -import type { GraphQLSortArgument, GraphQLSortEdgeArgument } from "./sort"; +import type { GraphQLSort, GraphQLSortEdge } from "./sort"; import type { GraphQLTreeElement } from "./tree-element"; -import type { GraphQLWhereArgs, GraphQLWhereArgsTopLevel } from "./where"; +import type { GraphQLWhere, GraphQLWhereTopLevel } from "./where"; export type GraphQLTree = GraphQLTreeReadOperationTopLevel; @@ -31,7 +31,7 @@ interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { connection?: GraphQLTreeConnectionTopLevel; }; args: { - where?: GraphQLWhereArgsTopLevel; + where?: GraphQLWhereTopLevel; }; } @@ -41,7 +41,7 @@ export interface GraphQLTreeReadOperation extends GraphQLTreeElement { connection?: GraphQLTreeConnection; }; args: { - where?: GraphQLWhereArgs; + where?: GraphQLWhere; }; } @@ -50,7 +50,7 @@ export interface GraphQLTreeConnection extends GraphQLTreeElement { edges?: GraphQLTreeEdge; }; args: { - sort?: GraphQLSortArgument[]; + sort?: GraphQLSort[]; first?: Integer; after?: string; }; @@ -61,7 +61,7 @@ export interface GraphQLTreeConnectionTopLevel extends GraphQLTreeElement { edges?: GraphQLTreeEdge; }; args: { - sort?: GraphQLSortEdgeArgument[]; + sort?: GraphQLSortEdge[]; first?: Integer; after?: string; }; 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 index df513501a4..d309ca8bd5 100644 --- 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 @@ -17,11 +17,11 @@ * limitations under the License. */ -export interface GraphQLSortArgument { - edges: GraphQLSortEdgeArgument; +export interface GraphQLSort { + edges: GraphQLSortEdge; } -export interface GraphQLSortEdgeArgument { +export interface GraphQLSortEdge { node?: GraphQLTreeSortElement; properties?: GraphQLTreeSortElement; } 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 index 421e2f76e4..6f6c1f7297 100644 --- 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 @@ -18,12 +18,12 @@ */ /** Args for `where` in nested connections (with edge -> node) */ -export type GraphQLWhereArgs = LogicalOperation<{ +export type GraphQLWhere = LogicalOperation<{ edges?: GraphQLEdgeWhere; }>; /** Args for `where` in top level connections only (i.e. no edge available) */ -export type GraphQLWhereArgsTopLevel = LogicalOperation<{ +export type GraphQLWhereTopLevel = LogicalOperation<{ node?: GraphQLNodeWhere; }>; From 2ec60c6cacf3c2c30229566f4364ca348353b4e5 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 15 Jul 2024 10:19:13 +0100 Subject: [PATCH 110/177] add @unique on v6, and make constraint name mandatory --- .../utils/asserts-indexes-and-constraints.ts | 58 +++--- .../graphql/src/graphql/directives/unique.ts | 6 +- .../annotation/UniqueAnnotation.ts | 4 +- .../annotations-parser/unique-annotation.ts | 2 +- .../alias-relayId/alias-relayId.int.test.ts | 6 +- .../alias-unique/alias-unique.int.test.ts | 168 ++++++++++++++++++ .../relayId/global-node-query.int.test.ts | 6 +- .../relayId/relayId-filters.int.test.ts | 6 +- .../relayId/relayId-projection.int.test.ts | 6 +- .../directives/unique/unique.int.test.ts | 166 +++++++++++++++++ .../api-v6/schema/directives/unique.test.ts | 112 +----------- 11 files changed, 380 insertions(+), 160 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/directives/unique/unique.int.test.ts 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 dc0b1ea6b2..735c201c8e 100644 --- a/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts +++ b/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts @@ -262,53 +262,49 @@ 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 + 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 + ); + // Map that holds as key the label name and as value an object with properties -> constraint name + const existingConstraints: Map> = constraintsQueryResult.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]; + .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>()); 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; } + const constraintName = uniqueField.annotations.unique.constraintName; - 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; + const hasUniqueConstraint = [...entity.labels].some((label) => { + const constraintsForLabel = existingConstraints.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, label: entityAdapter.getMainLabel(), diff --git a/packages/graphql/src/graphql/directives/unique.ts b/packages/graphql/src/graphql/directives/unique.ts index 0a0e84b9c1..11f574ee49 100644 --- a/packages/graphql/src/graphql/directives/unique.ts +++ b/packages/graphql/src/graphql/directives/unique.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import { DirectiveLocation, GraphQLDirective, GraphQLString } from "graphql"; +import { DirectiveLocation, GraphQLDirective, GraphQLNonNull, GraphQLString } from "graphql"; export const uniqueDirective = new GraphQLDirective({ name: "unique", @@ -27,8 +27,8 @@ export const uniqueDirective = new GraphQLDirective({ 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, + "The name which should be used for this constraint", + type: new GraphQLNonNull(GraphQLString), }, }, }); diff --git a/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts b/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts index 367f69ff21..1cbb10b027 100644 --- a/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts @@ -21,9 +21,9 @@ import type { Annotation } from "./Annotation"; export class UniqueAnnotation implements Annotation { readonly name = "unique"; - public readonly constraintName?: string; + public readonly constraintName: string; - constructor({ constraintName }: { constraintName?: string }) { + constructor({ constraintName }: { constraintName: string }) { this.constraintName = constraintName; } } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts index a180e40050..7935d94170 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts @@ -22,7 +22,7 @@ import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; import { parseArguments } from "../parse-arguments"; export function parseUniqueAnnotation(directive: DirectiveNode): UniqueAnnotation { - const { constraintName } = parseArguments<{ constraintName?: string }>(uniqueDirective, directive); + const { constraintName } = parseArguments<{ constraintName: string }>(uniqueDirective, directive); return new UniqueAnnotation({ constraintName, 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 index 1894937ca4..a020d189c4 100644 --- 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 @@ -33,19 +33,19 @@ describe("RelayId projection with alias directive", () => { beforeAll(async () => { const typeDefs = /* GraphQL */ ` type ${Movie} @node { - dbId: ID! @id @unique @relayId @alias(property: "serverId") + dbId: ID! @id @unique(constraintName: "FIELD_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 @relayId + dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId name: String! } type ${Actor} @node { - dbId: ID! @id @unique @relayId @alias(property: "serverId") + dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId @alias(property: "serverId") name: String! } `; diff --git a/packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts b/packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts new file mode 100644 index 0000000000..dd750a1123 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.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 { 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/alias-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(); + }); +}); + +// @alias(property: "internationalStandardBookNumber") 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 index 5aa5e5b4ef..e1e2b2971e 100644 --- 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 @@ -33,19 +33,19 @@ describe("Global node query", () => { beforeAll(async () => { const typeDefs = /* GraphQL */ ` type ${Movie} @node { - dbId: ID! @id @unique @relayId + dbId: ID! @id @unique(constraintName: "FIELD_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 @relayId + dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId name: String! } type ${Actor} @node { - dbId: ID! @id @unique @relayId + dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId name: String! } `; 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 index 5e5bb8d45e..be7e043788 100644 --- 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 @@ -33,19 +33,19 @@ describe("RelayId projection with filters", () => { beforeAll(async () => { const typeDefs = /* GraphQL */ ` type ${Movie} @node { - dbId: ID! @id @unique @relayId + dbId: ID! @id @unique(constraintName: "FIELD_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 @relayId + dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId name: String! } type ${Actor} @node { - dbId: ID! @id @unique @relayId + dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId name: String! } `; 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 index 207c53f651..9a05ee7ad9 100644 --- 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 @@ -33,19 +33,19 @@ describe("RelayId projection", () => { beforeAll(async () => { const typeDefs = /* GraphQL */ ` type ${Movie} @node { - dbId: ID! @id @unique @relayId + dbId: ID! @id @unique(constraintName: "FIELD_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 @relayId + dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId name: String! } type ${Actor} @node { - dbId: ID! @id @unique @relayId + dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId name: String! } `; diff --git a/packages/graphql/tests/api-v6/integration/directives/unique/unique.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/unique/unique.int.test.ts new file mode 100644 index 0000000000..3c92999324 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/unique/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/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/schema/directives/unique.test.ts b/packages/graphql/tests/api-v6/schema/directives/unique.test.ts index 5b0849a7c8..b6f8361577 100644 --- a/packages/graphql/tests/api-v6/schema/directives/unique.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/unique.test.ts @@ -23,117 +23,7 @@ import { Neo4jGraphQL } from "../../../../src"; import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; describe("@unique", () => { - test("@unique without parameter", async () => { - const typeDefs = /* GraphQL */ ` - type Movie @node { - dbId: ID! @unique - 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 - } - - input IDWhere { - AND: [IDWhere!] - NOT: IDWhere - OR: [IDWhere!] - contains: ID - endsWith: ID - equals: ID - in: [ID!] - startsWith: ID - } - - type Movie { - dbId: ID! - title: String - } - - type MovieConnection { - edges: [MovieEdge] - pageInfo: PageInfo - } - - input MovieConnectionSort { - edges: [MovieEdgeSort!] - } - - type MovieEdge { - cursor: String - node: Movie - } - - input MovieEdgeSort { - node: MovieSort - } - - input MovieEdgeWhere { - AND: [MovieEdgeWhere!] - NOT: MovieEdgeWhere - OR: [MovieEdgeWhere!] - node: MovieWhere - } - - type MovieOperation { - connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection - } - - input MovieOperationWhere { - AND: [MovieOperationWhere!] - NOT: MovieOperationWhere - OR: [MovieOperationWhere!] - edges: MovieEdgeWhere - } - - input MovieSort { - dbId: SortDirection - title: SortDirection - } - - input MovieWhere { - AND: [MovieWhere!] - NOT: MovieWhere - OR: [MovieWhere!] - dbId: IDWhere - title: StringWhere - } - - type PageInfo { - endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean - startCursor: String - } - - type Query { - movies(where: MovieOperationWhere): MovieOperation - } - - enum SortDirection { - ASC - DESC - } - - input StringWhere { - AND: [StringWhere!] - NOT: StringWhere - OR: [StringWhere!] - contains: String - endsWith: String - equals: String - in: [String!] - startsWith: String - }" - `); - }); - - test("@unique with constraintName", async () => { + test("@unique", async () => { const typeDefs = /* GraphQL */ ` type Movie @node { dbId: ID! @unique(constraintName: "UniqueMovieDbId") From 0301919135ff7e0fd4da4c8fe3a00fbee9ed863a Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 15 Jul 2024 11:58:47 +0100 Subject: [PATCH 111/177] rename cypher integration file to cypher.int.test --- .../directives/cypher/cypher.int.test.ts | 899 ++++++++++++++++++ 1 file changed, 899 insertions(+) create mode 100644 packages/graphql/tests/integration/directives/cypher/cypher.int.test.ts 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}`, + }, + ], + }, + ], + }); + }); + }); + }); +}); From 3fa449e4463fa4fc4e891b13a0714c50f47881d7 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 15 Jul 2024 13:28:02 +0100 Subject: [PATCH 112/177] remove useless schema test, remove migrated unique tests, bring back constraintName as optional parameter --- .../graphql/src/graphql/directives/unique.ts | 7 +- .../api-v6/schema/directives/unique.test.ts | 245 ----- .../integration/directives/unique.int.test.ts | 873 ------------------ 3 files changed, 3 insertions(+), 1122 deletions(-) delete mode 100644 packages/graphql/tests/api-v6/schema/directives/unique.test.ts delete mode 100644 packages/graphql/tests/integration/directives/unique.int.test.ts diff --git a/packages/graphql/src/graphql/directives/unique.ts b/packages/graphql/src/graphql/directives/unique.ts index 11f574ee49..de263158b9 100644 --- a/packages/graphql/src/graphql/directives/unique.ts +++ b/packages/graphql/src/graphql/directives/unique.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import { DirectiveLocation, GraphQLDirective, GraphQLNonNull, GraphQLString } from "graphql"; +import { DirectiveLocation, GraphQLDirective, GraphQLString } from "graphql"; export const uniqueDirective = new GraphQLDirective({ name: "unique", @@ -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", - type: new GraphQLNonNull(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/tests/api-v6/schema/directives/unique.test.ts b/packages/graphql/tests/api-v6/schema/directives/unique.test.ts deleted file mode 100644 index b6f8361577..0000000000 --- a/packages/graphql/tests/api-v6/schema/directives/unique.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { printSchemaWithDirectives } from "@graphql-tools/utils"; -import { lexicographicSortSchema } from "graphql/utilities"; -import { Neo4jGraphQL } from "../../../../src"; -import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; - -describe("@unique", () => { - test("@unique", async () => { - const typeDefs = /* GraphQL */ ` - type Movie @node { - dbId: ID! @unique(constraintName: "UniqueMovieDbId") - 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 - } - - input IDWhere { - AND: [IDWhere!] - NOT: IDWhere - OR: [IDWhere!] - contains: ID - endsWith: ID - equals: ID - in: [ID!] - startsWith: ID - } - - type Movie { - dbId: ID! - title: String - } - - type MovieConnection { - edges: [MovieEdge] - pageInfo: PageInfo - } - - input MovieConnectionSort { - edges: [MovieEdgeSort!] - } - - type MovieEdge { - cursor: String - node: Movie - } - - input MovieEdgeSort { - node: MovieSort - } - - input MovieEdgeWhere { - AND: [MovieEdgeWhere!] - NOT: MovieEdgeWhere - OR: [MovieEdgeWhere!] - node: MovieWhere - } - - type MovieOperation { - connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection - } - - input MovieOperationWhere { - AND: [MovieOperationWhere!] - NOT: MovieOperationWhere - OR: [MovieOperationWhere!] - edges: MovieEdgeWhere - } - - input MovieSort { - dbId: SortDirection - title: SortDirection - } - - input MovieWhere { - AND: [MovieWhere!] - NOT: MovieWhere - OR: [MovieWhere!] - dbId: IDWhere - title: StringWhere - } - - type PageInfo { - endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean - startCursor: String - } - - type Query { - movies(where: MovieOperationWhere): MovieOperation - } - - enum SortDirection { - ASC - DESC - } - - input StringWhere { - AND: [StringWhere!] - NOT: StringWhere - OR: [StringWhere!] - contains: String - endsWith: String - equals: String - in: [String!] - startsWith: String - }" - `); - }); - - test("@unique applied multiple times with same constraint name", async () => { - const typeDefs = /* GraphQL */ ` - type Movie @node { - dbId: ID! @unique(constraintName: "UniqueMovieDbId") - title: String @unique(constraintName: "UniqueMovieDbId") - } - `; - const neoSchema = new Neo4jGraphQL({ typeDefs }); - const schema = await neoSchema.getAuraSchema(); - raiseOnInvalidSchema(schema); - const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(schema)); - expect(printedSchema).toMatchInlineSnapshot(` - "schema { - query: Query - } - - input IDWhere { - AND: [IDWhere!] - NOT: IDWhere - OR: [IDWhere!] - contains: ID - endsWith: ID - equals: ID - in: [ID!] - startsWith: ID - } - - type Movie { - dbId: ID! - title: String - } - - type MovieConnection { - edges: [MovieEdge] - pageInfo: PageInfo - } - - input MovieConnectionSort { - edges: [MovieEdgeSort!] - } - - type MovieEdge { - cursor: String - node: Movie - } - - input MovieEdgeSort { - node: MovieSort - } - - input MovieEdgeWhere { - AND: [MovieEdgeWhere!] - NOT: MovieEdgeWhere - OR: [MovieEdgeWhere!] - node: MovieWhere - } - - type MovieOperation { - connection(after: String, first: Int, sort: MovieConnectionSort): MovieConnection - } - - input MovieOperationWhere { - AND: [MovieOperationWhere!] - NOT: MovieOperationWhere - OR: [MovieOperationWhere!] - edges: MovieEdgeWhere - } - - input MovieSort { - dbId: SortDirection - title: SortDirection - } - - input MovieWhere { - AND: [MovieWhere!] - NOT: MovieWhere - OR: [MovieWhere!] - dbId: IDWhere - title: StringWhere - } - - type PageInfo { - endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean - startCursor: String - } - - type Query { - movies(where: MovieOperationWhere): MovieOperation - } - - enum SortDirection { - ASC - DESC - } - - input StringWhere { - AND: [StringWhere!] - NOT: StringWhere - OR: [StringWhere!] - contains: String - endsWith: String - equals: String - in: [String!] - startsWith: String - }" - `); - }); -}); 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); - }); - }); -}); From e5ecb8b0a11802cedbbce9615731b393f246c15c Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 15 Jul 2024 14:10:56 +0100 Subject: [PATCH 113/177] autogenerate constraintName while waiting to migrate v5 tests --- .../src/classes/utils/asserts-indexes-and-constraints.ts | 2 +- .../src/schema-model/annotation/UniqueAnnotation.ts | 4 ++-- .../parser/annotations-parser/unique-annotation.ts | 2 +- .../combinations/alias-relayId/alias-relayId.int.test.ts | 6 +++--- .../combinations/alias-unique/alias-unique.int.test.ts | 2 -- .../directives/relayId/global-node-query.int.test.ts | 8 ++++---- .../directives/relayId/relayId-filters.int.test.ts | 6 +++--- .../directives/relayId/relayId-projection.int.test.ts | 6 +++--- 8 files changed, 17 insertions(+), 19 deletions(-) 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 735c201c8e..356674d0f3 100644 --- a/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts +++ b/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts @@ -306,7 +306,7 @@ async function getMissingConstraints({ if (!hasUniqueConstraint) { missingConstraints.push({ - constraintName, + constraintName: constraintName ?? `${entity.name}_${uniqueField.databaseName}`, // TODO: remove default value once the constraintName argument is required label: entityAdapter.getMainLabel(), property: uniqueField.databaseName, }); diff --git a/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts b/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts index 1cbb10b027..367f69ff21 100644 --- a/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/UniqueAnnotation.ts @@ -21,9 +21,9 @@ import type { Annotation } from "./Annotation"; export class UniqueAnnotation implements Annotation { readonly name = "unique"; - public readonly constraintName: string; + public readonly constraintName?: string; - constructor({ constraintName }: { constraintName: string }) { + constructor({ constraintName }: { constraintName?: string }) { this.constraintName = constraintName; } } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts index 7935d94170..a180e40050 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/unique-annotation.ts @@ -22,7 +22,7 @@ import { UniqueAnnotation } from "../../annotation/UniqueAnnotation"; import { parseArguments } from "../parse-arguments"; export function parseUniqueAnnotation(directive: DirectiveNode): UniqueAnnotation { - const { constraintName } = parseArguments<{ constraintName: string }>(uniqueDirective, directive); + const { constraintName } = parseArguments<{ constraintName?: string }>(uniqueDirective, directive); return new UniqueAnnotation({ constraintName, 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 index a020d189c4..ef6f004dac 100644 --- 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 @@ -33,19 +33,19 @@ describe("RelayId projection with alias directive", () => { beforeAll(async () => { const typeDefs = /* GraphQL */ ` type ${Movie} @node { - dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId @alias(property: "serverId") + 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: "FIELD_UNIQUE") @relayId + dbId: ID! @id @unique(constraintName: "GENRE_ID_UNIQUE") @relayId name: String! } type ${Actor} @node { - dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId @alias(property: "serverId") + dbId: ID! @id @unique(constraintName: "ACTOR_ID_UNIQUE") @relayId @alias(property: "serverId") name: String! } `; diff --git a/packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts b/packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts index dd750a1123..f0dd34bf6a 100644 --- a/packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts @@ -164,5 +164,3 @@ describe("assertIndexesAndConstraints/alias-unique", () => { ).resolves.not.toThrow(); }); }); - -// @alias(property: "internationalStandardBookNumber") 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 index e1e2b2971e..961fd3233d 100644 --- 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 @@ -32,20 +32,20 @@ describe("Global node query", () => { beforeAll(async () => { const typeDefs = /* GraphQL */ ` - type ${Movie} @node { - dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId + 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: "FIELD_UNIQUE") @relayId + dbId: ID! @id @unique(constraintName: "GENRE_ID_UNIQUE") @relayId name: String! } type ${Actor} @node { - dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId + dbId: ID! @id @unique(constraintName: "ACTOR_ID_UNIQUE") @relayId name: String! } `; 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 index be7e043788..98d09c59e6 100644 --- 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 @@ -33,19 +33,19 @@ describe("RelayId projection with filters", () => { beforeAll(async () => { const typeDefs = /* GraphQL */ ` type ${Movie} @node { - dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId + 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: "FIELD_UNIQUE") @relayId + dbId: ID! @id @unique(constraintName: "GENRE_ID_UNIQUE") @relayId name: String! } type ${Actor} @node { - dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId + dbId: ID! @id @unique(constraintName: "ACTOR_ID_UNIQUE") @relayId name: String! } `; 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 index 9a05ee7ad9..6697e9687c 100644 --- 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 @@ -33,19 +33,19 @@ describe("RelayId projection", () => { beforeAll(async () => { const typeDefs = /* GraphQL */ ` type ${Movie} @node { - dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId + 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: "FIELD_UNIQUE") @relayId + dbId: ID! @id @unique(constraintName: "GENRE_ID_UNIQUE") @relayId name: String! } type ${Actor} @node { - dbId: ID! @id @unique(constraintName: "FIELD_UNIQUE") @relayId + dbId: ID! @id @unique(constraintName: "ACTOR_ID_UNIQUE") @relayId name: String! } `; From e74c2f4fe3cd134372920948c05b8d82e8def0ae Mon Sep 17 00:00:00 2001 From: angrykoala Date: Mon, 15 Jul 2024 11:04:06 +0100 Subject: [PATCH 114/177] improvements on resolve-tree-parser --- .../GlobalNodeResolveTreeParser.ts | 66 ---- .../resolve-tree-parser/ResolveTreeParser.ts | 345 ------------------ .../TopLevelResolveTreeParser.ts | 107 ------ .../resolve-tree-parser/parse-args.ts | 120 ++++++ .../parse-attribute-fields.ts | 129 +++++++ .../resolve-tree-parser/parse-edges.ts | 87 +++++ .../parse-global-resolve-info-tree.ts | 66 ++++ .../resolve-tree-parser/parse-node.ts | 55 +++ .../parse-resolve-info-tree.ts | 95 ++++- .../api-v6/resolvers/global-node-resolver.ts | 2 +- .../tests/tck/directives/alias.test.ts | 75 ---- 11 files changed, 539 insertions(+), 608 deletions(-) delete mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts delete mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts delete mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-args.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-attribute-fields.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-edges.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-global-resolve-info-tree.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-node.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts deleted file mode 100644 index 13571c68d4..0000000000 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/GlobalNodeResolveTreeParser.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ResolveTree } from "graphql-parse-resolve-info"; -import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; -import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; -import type { GraphQLTree } from "./graphql-tree/graphql-tree"; - -export class GlobalNodeResolveTreeParser extends TopLevelResolveTreeParser { - constructor({ entity }: { entity: ConcreteEntity }) { - super({ entity: entity }); - } - - /** Parse a resolveTree into a Neo4j GraphQLTree */ - public parseTopLevelOperation(resolveTree: ResolveTree): GraphQLTree { - const entityTypes = this.targetNode.typeNames; - resolveTree.fieldsByTypeName[entityTypes.node] = { - ...resolveTree.fieldsByTypeName["Node"], - ...resolveTree.fieldsByTypeName[entityTypes.node], - }; - const node = resolveTree ? this.parseNode(resolveTree) : 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/ResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts deleted file mode 100644 index d72ff02c25..0000000000 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/ResolveTreeParser.ts +++ /dev/null @@ -1,345 +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 { ResolveTree } from "graphql-parse-resolve-info"; -import { CartesianPoint } from "../../../graphql/objects/CartesianPoint"; -import { Point } from "../../../graphql/objects/Point"; -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 type { - GraphQLTree, - GraphQLTreeConnection, - GraphQLTreeEdge, - GraphQLTreeEdgeProperties, - GraphQLTreeNode, - GraphQLTreeReadOperation, -} from "./graphql-tree/graphql-tree"; -import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "./graphql-tree/sort"; -import { findFieldByName } from "./utils/find-field-by-name"; - -export abstract class ResolveTreeParser { - protected entity: T; - - constructor({ entity }: { entity: T }) { - this.entity = entity; - } - - /** Parse a resolveTree into a Neo4j GraphQLTree */ - public parseOperation(resolveTree: ResolveTree): GraphQLTreeReadOperation { - const connectionResolveTree = findFieldByName( - resolveTree, - this.entity.typeNames.connectionOperation, - "connection" - ); - - const connection = connectionResolveTree ? this.parseConnection(connectionResolveTree) : undefined; - const connectionOperationArgs = this.parseOperationArgs(resolveTree.args); - return { - alias: resolveTree.alias, - args: connectionOperationArgs, - name: resolveTree.name, - fields: { - connection, - }, - }; - } - - private parseOperationArgs(resolveTreeArgs: Record): GraphQLTree["args"] { - // Not properly parsed, assuming the type is the same - return { - where: resolveTreeArgs.where, - }; - } - - protected abstract get targetNode(): ConcreteEntity; - protected abstract parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge; - - protected parseAttributeField( - resolveTree: ResolveTree, - entity: ConcreteEntity | Relationship - ): GraphQLTreeLeafField | GraphQLTreePoint | undefined { - const globalIdField = this.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 this.parsePointField(resolveTree); - } - if (wrappedTypeName === "CartesianPoint") { - return this.parseCartesianPointField(resolveTree); - } - return { - alias: resolveTree.alias, - args: resolveTree.args, - name: resolveTree.name, - fields: undefined, - }; - } - } - - private 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, - }; - } - } - } - - private 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), - }, - }; - } - - private 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), - }, - }; - } - - protected parseConnection(resolveTree: ResolveTree): GraphQLTreeConnection { - const entityTypes = this.entity.typeNames; - const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges"); - const edgeResolveTree = edgesResolveTree ? this.parseEdges(edgesResolveTree) : undefined; - const connectionArgs = this.parseConnectionArgs(resolveTree.args); - return { - alias: resolveTree.alias, - args: connectionArgs, - fields: { - edges: edgeResolveTree, - }, - }; - } - - protected parseNode(resolveTree: ResolveTree): GraphQLTreeNode { - const entityTypes = this.targetNode.typeNames; - const fieldsResolveTree = resolveTree.fieldsByTypeName[entityTypes.node] ?? {}; - - const fields = this.getNodeFields(fieldsResolveTree); - - return { - alias: resolveTree.alias, - args: resolveTree.args, - fields: fields, - }; - } - - private getNodeFields( - fields: Record - ): Record { - const propertyFields: Record = {}; - for (const [key, fieldResolveTree] of Object.entries(fields)) { - const fieldName = fieldResolveTree.name; - const field = - this.parseRelationshipField(fieldResolveTree, this.targetNode) ?? - this.parseAttributeField(fieldResolveTree, this.targetNode); - if (!field) { - throw new ResolveTreeParserError(`${fieldName} is not a field of node`); - } - propertyFields[key] = field; - } - return propertyFields; - } - - private parseRelationshipField( - resolveTree: ResolveTree, - entity: ConcreteEntity - ): GraphQLTreeReadOperation | undefined { - const relationship = entity.findRelationship(resolveTree.name); - if (!relationship) { - return; - } - const relationshipTreeParser = new RelationshipResolveTreeParser({ entity: relationship }); - return relationshipTreeParser.parseOperation(resolveTree); - } - - private parseConnectionArgs(resolveTreeArgs: { [str: string]: any }): GraphQLTreeConnection["args"] { - let sortArg: GraphQLSort[] | undefined; - if (resolveTreeArgs.sort) { - sortArg = resolveTreeArgs.sort.map((sortArg): GraphQLSort => { - return { edges: this.parseSortEdges(sortArg.edges) }; - }); - } - return { - sort: sortArg, - first: resolveTreeArgs.first, - after: resolveTreeArgs.after, - }; - } - - protected parseSortEdges(sortEdges: { - node: Record | undefined; - properties: Record | undefined; - }): GraphQLSortEdge { - const sortFields: GraphQLSortEdge = {}; - const nodeFields = sortEdges.node; - - if (nodeFields) { - const fields = this.parseSort(this.targetNode, nodeFields); - sortFields.node = fields; - } - const edgeProperties = sortEdges.properties; - - if (edgeProperties) { - const fields = this.parseSort(this.entity, edgeProperties); - sortFields.properties = fields; - } - return sortFields; - } - - private parseSort( - target: Relationship | ConcreteEntity, - sortObject: Record - ): GraphQLTreeSortElement { - return Object.fromEntries( - Object.entries(sortObject).map(([fieldName, resolveTreeDirection]) => { - if (target.hasAttribute(fieldName)) { - const direction = this.parseDirection(resolveTreeDirection); - return [fieldName, direction]; - } - throw new ResolveTreeParserError(`Invalid sort field: ${fieldName}`); - }) - ); - } - - private parseDirection(direction: string): "ASC" | "DESC" { - if (direction === "ASC" || direction === "DESC") { - return direction; - } - throw new ResolveTreeParserError(`Invalid sort direction: ${direction}`); - } -} - -export class ResolveTreeParserError extends Error {} - -export class RelationshipResolveTreeParser extends ResolveTreeParser { - protected get targetNode(): ConcreteEntity { - return this.entity.target as ConcreteEntity; - } - - protected parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { - const edgeType = this.entity.typeNames.edge; - - const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); - const resolveTreeProperties = findFieldByName(resolveTree, edgeType, "properties"); - - const node = nodeResolveTree ? this.parseNode(nodeResolveTree) : undefined; - const properties = resolveTreeProperties ? this.parseEdgeProperties(resolveTreeProperties) : undefined; - - return { - alias: resolveTree.alias, - args: resolveTree.args, - fields: { - node: node, - properties: properties, - }, - }; - } - - private parseEdgeProperties(resolveTree: ResolveTree): GraphQLTreeEdgeProperties | undefined { - if (!this.entity.typeNames.properties) { - return; - } - const fieldsResolveTree = resolveTree.fieldsByTypeName[this.entity.typeNames.properties] ?? {}; - - const fields = this.getEdgePropertyFields(fieldsResolveTree); - - return { - alias: resolveTree.alias, - args: resolveTree.args, - fields: fields, - }; - } - - private getEdgePropertyFields(fields: Record): Record { - const propertyFields: Record = {}; - for (const [key, fieldResolveTree] of Object.entries(fields)) { - const fieldName = fieldResolveTree.name; - const field = this.parseAttributeField(fieldResolveTree, this.entity); - if (!field) { - throw new ResolveTreeParserError(`${fieldName} is not an attribute of edge`); - } - propertyFields[key] = field; - } - return propertyFields; - } -} - -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/TopLevelResolveTreeParser.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts deleted file mode 100644 index 5994a2c320..0000000000 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/TopLevelResolveTreeParser.ts +++ /dev/null @@ -1,107 +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 { ResolveTree } from "graphql-parse-resolve-info"; -import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; -import { ResolveTreeParser } from "./ResolveTreeParser"; -import type { GraphQLTree, GraphQLTreeConnectionTopLevel, GraphQLTreeEdge } from "./graphql-tree/graphql-tree"; -import type { GraphQLSortEdge } from "./graphql-tree/sort"; -import { findFieldByName } from "./utils/find-field-by-name"; - -export class TopLevelResolveTreeParser extends ResolveTreeParser { - protected get targetNode(): ConcreteEntity { - return this.entity; - } - - /** Parse a resolveTree into a Neo4j GraphQLTree */ - public parseOperationTopLevel(resolveTree: ResolveTree): GraphQLTree { - const connectionResolveTree = findFieldByName( - resolveTree, - this.entity.typeNames.connectionOperation, - "connection" - ); - - const connection = connectionResolveTree ? this.parseTopLevelConnection(connectionResolveTree) : undefined; - const connectionOperationArgs = this.parseOperationArgsTopLevel(resolveTree.args); - return { - alias: resolveTree.alias, - args: connectionOperationArgs, - name: resolveTree.name, - fields: { - connection, - }, - }; - } - - private parseTopLevelConnection(resolveTree: ResolveTree): GraphQLTreeConnectionTopLevel { - const entityTypes = this.entity.typeNames; - const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges"); - const edgeResolveTree = edgesResolveTree ? this.parseEdges(edgesResolveTree) : undefined; - const connectionArgs = this.parseConnectionArgsTopLevel(resolveTree.args); - - return { - alias: resolveTree.alias, - args: connectionArgs, - fields: { - edges: edgeResolveTree, - }, - }; - } - - private parseConnectionArgsTopLevel(resolveTreeArgs: { - [str: string]: any; - }): GraphQLTreeConnectionTopLevel["args"] { - let sortArg: GraphQLSortEdge[] | undefined; - if (resolveTreeArgs.sort) { - sortArg = resolveTreeArgs.sort.map((sortArg) => { - return this.parseSortEdges(sortArg); - }); - } - - return { - sort: sortArg, - first: resolveTreeArgs.first, - after: resolveTreeArgs.after, - }; - } - - protected parseOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLTree["args"] { - // Not properly parsed, assuming the type is the same - return { - where: resolveTreeArgs.where, - }; - } - - protected parseEdges(resolveTree: ResolveTree): GraphQLTreeEdge { - const edgeType = this.entity.typeNames.edge; - - const nodeResolveTree = findFieldByName(resolveTree, edgeType, "node"); - - const node = nodeResolveTree ? this.parseNode(nodeResolveTree) : undefined; - - return { - alias: resolveTree.alias, - args: resolveTree.args, - fields: { - node: node, - properties: undefined, - }, - }; - } -} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-args.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-args.ts new file mode 100644 index 0000000000..a9c6f65a8e --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-args.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 type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../schema-model/relationship/Relationship"; +import type { GraphQLTree, GraphQLTreeConnection, GraphQLTreeConnectionTopLevel } from "./graphql-tree/graphql-tree"; +import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "./graphql-tree/sort"; +import { ResolveTreeParserError } from "./parse-resolve-info-tree"; + +export function parseOperationArgs(resolveTreeArgs: Record): GraphQLTree["args"] { + // Not properly parsed, assuming the type is the same + return { + where: resolveTreeArgs.where, + }; +} + +export function parseOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLTree["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/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..cad517913b --- /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 "./parse-resolve-info-tree"; +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..274ef3574d --- /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 { GraphQLTree } 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; +}): GraphQLTree { + 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..ca73132d06 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-node.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { ResolveTreeParserError, parseRelationshipField } from "./parse-resolve-info-tree"; + +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, + }; +} + +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 index e97e6606ac..433e0ab050 100644 --- 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 @@ -19,9 +19,21 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; -import { GlobalNodeResolveTreeParser } from "./GlobalNodeResolveTreeParser"; -import { TopLevelResolveTreeParser } from "./TopLevelResolveTreeParser"; -import type { GraphQLTree } from "./graphql-tree/graphql-tree"; +import type { Relationship } from "../../../schema-model/relationship/Relationship"; +import type { + GraphQLTree, + GraphQLTreeConnection, + GraphQLTreeConnectionTopLevel, + GraphQLTreeReadOperation, +} from "./graphql-tree/graphql-tree"; +import { + parseConnectionArgs, + parseConnectionArgsTopLevel, + parseOperationArgs, + parseOperationArgsTopLevel, +} from "./parse-args"; +import { parseEdges } from "./parse-edges"; +import { findFieldByName } from "./utils/find-field-by-name"; export function parseResolveInfoTree({ resolveTree, @@ -30,17 +42,72 @@ export function parseResolveInfoTree({ resolveTree: ResolveTree; entity: ConcreteEntity; }): GraphQLTree { - const parser = new TopLevelResolveTreeParser({ entity }); - return parser.parseOperationTopLevel(resolveTree); + 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 parseGlobalNodeResolveInfoTree({ - resolveTree, - entity, -}: { - resolveTree: ResolveTree; - entity: ConcreteEntity; -}): GraphQLTree { - const parser = new GlobalNodeResolveTreeParser({ entity }); - return parser.parseTopLevelOperation(resolveTree); +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, + }, + }; +} + +export class ResolveTreeParserError extends Error {} diff --git a/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts b/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts index 38eb1f2a34..02e9c9b20b 100644 --- a/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/global-node-resolver.ts @@ -24,7 +24,7 @@ import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-t 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-resolve-info-tree"; +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 */ 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 { From 931997aaed63de5605ecf0411032cc2d49746f24 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 15 Jul 2024 15:02:08 +0100 Subject: [PATCH 115/177] maintain functionality to check existing constraint using the default name for now --- .../src/classes/utils/asserts-indexes-and-constraints.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 356674d0f3..cfc4b60a17 100644 --- a/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts +++ b/packages/graphql/src/classes/utils/asserts-indexes-and-constraints.ts @@ -301,7 +301,11 @@ async function getMissingConstraints({ if (!constraintsForLabel) { return false; } - return constraintsForLabel.get(uniqueField.databaseName) === constraintName; + // TODO: remove default value once the constraintName argument is required + return ( + constraintsForLabel.get(uniqueField.databaseName) === + (constraintName ?? `${entity.name}_${uniqueField.databaseName}`) + ); }); if (!hasUniqueConstraint) { From b918320c010b12c710743235fb7225a11da51890 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 16 Jul 2024 11:55:10 +0100 Subject: [PATCH 116/177] WIP: Improve FilterFactory --- .../api-v6/queryIRFactory/FilterFactory.ts | 279 +++++------------- .../queryIRFactory/ReadOperationFactory.ts | 18 +- .../resolve-tree-parser/graphql-tree/where.ts | 20 +- .../filters/logical-filters/or-filter.test.ts | 8 +- 4 files changed, 103 insertions(+), 222 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index 303856e4f3..63d4a4c64a 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -31,6 +31,7 @@ import { DurationFilter } from "../../translate/queryAST/ast/filters/property-fi 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, @@ -56,176 +57,85 @@ export class FilterFactory { }: { entity: ConcreteEntity; relationship?: Relationship; - where?: GraphQLWhere; - }): Filter[] { - const andFilters = this.createLogicalFilters({ operation: "AND", entity, relationship, where: where.AND }); - const orFilters = this.createLogicalFilters({ operation: "OR", entity, relationship, where: where.OR }); - const notFilters = this.createLogicalFilters({ - operation: "NOT", - entity, - relationship, - where: where.NOT ? [where.NOT] : undefined, - }); - - const edgeFilters = this.createEdgeFilters({ entity, relationship, edgeWhere: where.edges }); - - return [...this.mergeFiltersWithAnd(edgeFilters), ...andFilters, ...orFilters, ...notFilters]; - } - - public createTopLevelFilters({ - where = {}, - entity, - }: { - entity: ConcreteEntity; - where?: GraphQLWhereTopLevel; - }): Filter[] { - const andFilters = this.createTopLevelLogicalFilters({ operation: "AND", entity, where: where.AND }); - const orFilters = this.createTopLevelLogicalFilters({ operation: "OR", entity, where: where.OR }); - const notFilters = this.createTopLevelLogicalFilters({ - operation: "NOT", - entity, - where: where.NOT ? [where.NOT] : undefined, - }); - - const edgeFilters = this.createNodeFilter({ entity, where: where.node }); - return this.mergeFiltersWithAnd([ - ...this.mergeFiltersWithAnd(edgeFilters), - ...andFilters, - ...orFilters, - ...notFilters, - ]); - } - - private createTopLevelLogicalFilters({ - where = [], - operation, - entity, - }: { - entity: ConcreteEntity; - operation: LogicalOperators; - where?: GraphQLWhereTopLevel[]; - }): [] | [Filter] { - if (where.length === 0) { - return []; - } - const nestedFilters = where.flatMap((orWhere: GraphQLWhereTopLevel) => { - return this.createTopLevelFilters({ entity, where: orWhere }); - }); + 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) { + const edgePropertiesFilters = this.createEdgePropertiesFilters({ + where: value, + relationship, + }); + + return new LogicalFilter({ + operation: "AND", + filters: filterTruthy(edgePropertiesFilters), + }); + } + }) + ); - if (nestedFilters.length > 0) { - return [ - new LogicalFilter({ - operation, - filters: nestedFilters, - }), - ]; + if (filters.length <= 1) { + return filters[0]; } - return []; - } - - private createLogicalFilters({ - where = [], - relationship, - operation, - entity, - }: { - entity: ConcreteEntity; - relationship?: Relationship; - operation: LogicalOperators; - where?: GraphQLEdgeWhere[]; - }): [] | [Filter] { - if (where.length === 0) { - return []; - } - const nestedFilters = where.flatMap((orWhere: GraphQLEdgeWhere) => { - return this.createFilters({ entity, relationship, where: orWhere }); + // Implicit AND + return new LogicalFilter({ + operation: "AND", + filters: filters, }); - - if (nestedFilters.length > 0) { - return [ - new LogicalFilter({ - operation, - filters: nestedFilters, - }), - ]; - } - - return []; } - private createEdgeFilters({ - edgeWhere = {}, - relationship, + private createNodeFilter({ + where = {}, entity, }: { entity: ConcreteEntity; - relationship?: Relationship; - edgeWhere?: GraphQLEdgeWhere; - }): Filter[] { - const andFilters = this.createLogicalEdgeFilters(entity, relationship, "AND", edgeWhere.AND); - const orFilters = this.createLogicalEdgeFilters(entity, relationship, "OR", edgeWhere.OR); - const notFilters = this.createLogicalEdgeFilters( - entity, - relationship, - "NOT", - edgeWhere.NOT ? [edgeWhere.NOT] : undefined - ); - - const nodeFilters = this.createNodeFilter({ where: edgeWhere.node, entity }); - - let edgePropertiesFilters: Filter[] = []; - if (relationship) { - edgePropertiesFilters = this.createEdgePropertiesFilters({ - where: edgeWhere.properties, - relationship, - }); - } - return this.mergeFiltersWithAnd([ - ...this.mergeFiltersWithAnd(nodeFilters), - ...edgePropertiesFilters, - ...andFilters, - ...orFilters, - ...notFilters, - ]); - } - - private createLogicalEdgeFilters( - entity: ConcreteEntity, - relationship: Relationship | undefined, - operation: LogicalOperators, - where: GraphQLEdgeWhere[] = [] - ): [] | [Filter] { - if (where.length === 0) { - return []; - } - const nestedFilters = where.flatMap((logicalWhere: GraphQLEdgeWhere) => { - return this.createEdgeFilters({ entity, relationship, edgeWhere: logicalWhere }); - }); - - if (nestedFilters.length > 0) { - return [ - new LogicalFilter({ - operation, - filters: nestedFilters, - }), - ]; - } - - return []; - } - - private createNodeFilter({ where = {}, entity }: { entity: ConcreteEntity; where?: GraphQLNodeWhere }): Filter[] { - const andFilters = this.createLogicalNodeFilters(entity, "AND", where.AND); - const orFilters = this.createLogicalNodeFilters(entity, "OR", where.OR); - const notFilters = this.createLogicalNodeFilters(entity, "NOT", where.NOT ? [where.NOT] : undefined); + 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, + }); + }); - const nodePropertiesFilters = Object.entries(where).flatMap(([fieldName, filtersWithLogical]) => { - if (["AND", "OR", "NOT"].includes(fieldName)) { - return []; + return new LogicalFilter({ + operation: fieldName, + filters: filterTruthy(nestedFilters), + }); } - let filters = filtersWithLogical as GraphQLNodeFilters; + let filters = nestedWhere as GraphQLNodeFilters; let attribute: Attribute | undefined; if (fieldName === "id" && entity.globalIdField) { @@ -249,31 +159,10 @@ export class FilterFactory { return []; }); - return [...andFilters, ...orFilters, ...notFilters, ...this.mergeFiltersWithAnd(nodePropertiesFilters)]; - } - - private createLogicalNodeFilters( - entity: ConcreteEntity, - operation: LogicalOperators, - where: GraphQLNodeWhere[] = [] - ): [] | [Filter] { - if (where.length === 0) { - return []; - } - const nestedFilters = where.flatMap((logicalWhere) => { - return this.createNodeFilter({ entity, where: logicalWhere }); + return new LogicalFilter({ + operation: "AND", + filters: filterTruthy(nodePropertiesFilters), }); - - if (nestedFilters.length > 0) { - return [ - new LogicalFilter({ - operation, - filters: nestedFilters, - }), - ]; - } - - return []; } /** Transforms globalId filters into normal property filters */ @@ -297,8 +186,8 @@ export class FilterFactory { const edgeFilters = filters.edges ?? {}; return Object.entries(edgeFilters).map(([rawOperator, filter]) => { - const relatedNodeFilters = this.createEdgeFilters({ - edgeWhere: filter, + const relatedNodeFilters = this.createFilters({ + where: filter, relationship: relationship, entity: relationship.target as ConcreteEntity, }); @@ -308,23 +197,21 @@ export class FilterFactory { target, operator, }); - relationshipFilter.addFilters(relatedNodeFilters); + if (relatedNodeFilters) { + relationshipFilter.addFilters([relatedNodeFilters]); + } return relationshipFilter; }); } private createEdgePropertiesFilters({ - where, + where = {}, relationship, }: { where: GraphQLEdgeWhere["properties"]; relationship: Relationship; }): Filter[] { - if (!where) { - return []; - } return Object.entries(where).flatMap(([fieldName, filters]) => { - // TODO: Logical filters here const attribute = relationship.findAttribute(fieldName); if (!attribute) return []; const attributeAdapter = new AttributeAdapter(attribute); @@ -378,16 +265,4 @@ export class FilterFactory { }); }); } - - private mergeFiltersWithAnd(filters: Filter[]): Filter[] { - if (filters.length > 1) { - return [ - new LogicalFilter({ - operation: "AND", - filters: filters, - }), - ]; - } - return filters; - } } diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 4cbd720c04..02a6ca3693 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -95,6 +95,8 @@ export class ReadOperationFactory { entity, sortArgument, }); + + const filters = filterTruthy([this.filterFactory.createFilters({ entity, where: graphQLTree.args.where })]); return new V6ReadOperation({ target, selection, @@ -104,7 +106,7 @@ export class ReadOperationFactory { }, pagination, sortFields: sortInputFields, - filters: this.filterFactory.createTopLevelFilters({ entity, where: graphQLTree.args.where }), + filters, }); } @@ -144,6 +146,14 @@ export class ReadOperationFactory { 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, @@ -152,11 +162,7 @@ export class ReadOperationFactory { node: nodeFields, }, sortFields: sortInputFields, - filters: this.filterFactory.createFilters({ - entity: relationshipAdapter.target.entity, - relationship, - where: parsedTree.args.where, - }), + filters: filters, pagination, }); } 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 index 6f6c1f7297..4fbb16fc9f 100644 --- 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 @@ -18,26 +18,26 @@ */ /** Args for `where` in nested connections (with edge -> node) */ -export type GraphQLWhere = LogicalOperation<{ +export type GraphQLWhere = WithLogicalOperations<{ edges?: GraphQLEdgeWhere; }>; /** Args for `where` in top level connections only (i.e. no edge available) */ -export type GraphQLWhereTopLevel = LogicalOperation<{ +export type GraphQLWhereTopLevel = WithLogicalOperations<{ node?: GraphQLNodeWhere; }>; -export type GraphQLEdgeWhere = LogicalOperation<{ +export type GraphQLEdgeWhere = WithLogicalOperations<{ properties?: Record; node?: GraphQLNodeWhere; }>; -export type GraphQLNodeWhere = LogicalOperation>; +export type GraphQLNodeWhere = WithLogicalOperations>; export type GraphQLNodeFilters = GraphQLAttributeFilters | RelationshipFilters; export type GraphQLAttributeFilters = StringFilters | NumberFilters; -export type StringFilters = LogicalOperation<{ +export type StringFilters = WithLogicalOperations<{ equals?: string; in?: string[]; matches?: string; @@ -46,7 +46,7 @@ export type StringFilters = LogicalOperation<{ endsWith?: string; }>; -export type NumberFilters = LogicalOperation<{ +export type NumberFilters = WithLogicalOperations<{ equals?: string; in?: string[]; lt?: string; @@ -64,8 +64,8 @@ export type RelationshipFilters = { }; }; -type LogicalOperation = { - AND?: Array>; - OR?: Array>; - NOT?: LogicalOperation; +type WithLogicalOperations = { + AND?: Array>; + OR?: Array>; + NOT?: WithLogicalOperations; } & T; 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 index d38258dad2..8c1125c566 100644 --- 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 @@ -153,7 +153,7 @@ describe("OR filters", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) - WHERE (((this0.title = $param0 AND this0.year = $param1) AND NOT (this0.runtime = $param2)) OR this0.year = $param3) + 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 { @@ -167,12 +167,12 @@ describe("OR filters", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"param0\\": \\"The Matrix\\", - \\"param1\\": { + \\"param0\\": 2, + \\"param1\\": \\"The Matrix\\", + \\"param2\\": { \\"low\\": 2, \\"high\\": 0 }, - \\"param2\\": 2, \\"param3\\": { \\"low\\": 100, \\"high\\": 0 From 7491d46eeb1279f4d37fd2b1b1ce421a81107677 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 16 Jul 2024 15:29:52 +0100 Subject: [PATCH 117/177] apply PR suggestion --- .../utils/asserts-indexes-and-constraints.ts | 66 ++++++++++--------- .../alias-unique.int.test.ts | 8 +-- .../unique.int.test.ts | 8 +-- 3 files changed, 44 insertions(+), 38 deletions(-) rename packages/graphql/tests/api-v6/integration/{combinations/alias-unique => assertIndexesAndConstraints}/alias-unique.int.test.ts (95%) rename packages/graphql/tests/api-v6/integration/{directives/unique => assertIndexesAndConstraints}/unique.int.test.ts (95%) 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 cfc4b60a17..e2e566bec2 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 { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; @@ -262,27 +262,7 @@ async function getMissingConstraints({ schemaModel: Neo4jGraphQLSchemaModel; 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 - ); - // Map that holds as key the label name and as value an object with properties -> constraint name - const existingConstraints: Map> = 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>()); - + const existingUniqueConstraints = await getExistingUniqueConstraints(session); const missingConstraints: MissingConstraint[] = []; for (const entity of schemaModel.concreteEntities) { @@ -294,23 +274,22 @@ async function getMissingConstraints({ if (!uniqueField.annotations.unique) { continue; } - const constraintName = uniqueField.annotations.unique.constraintName; + // 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 = existingConstraints.get(label); + const constraintsForLabel = existingUniqueConstraints.get(label); if (!constraintsForLabel) { return false; } - // TODO: remove default value once the constraintName argument is required - return ( - constraintsForLabel.get(uniqueField.databaseName) === - (constraintName ?? `${entity.name}_${uniqueField.databaseName}`) - ); + + return constraintsForLabel.get(uniqueField.databaseName) === constraintName; }); if (!hasUniqueConstraint) { missingConstraints.push({ - constraintName: constraintName ?? `${entity.name}_${uniqueField.databaseName}`, // TODO: remove default value once the constraintName argument is required + constraintName: constraintName, label: entityAdapter.getMainLabel(), property: uniqueField.databaseName, }); @@ -320,3 +299,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/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts b/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/alias-unique.int.test.ts similarity index 95% rename from packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts rename to packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/alias-unique.int.test.ts index f0dd34bf6a..7096c4e48b 100644 --- a/packages/graphql/tests/api-v6/integration/combinations/alias-unique/alias-unique.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/alias-unique.int.test.ts @@ -18,11 +18,11 @@ */ 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("assertIndexesAndConstraints/alias-unique", () => { +describe("assertIndexesAndConstraints with @alias and @unique", () => { const testHelper = new TestHelper({ v6Api: true }); let databaseName: string; let IS_MULTI_DB_SUPPORTED = true; diff --git a/packages/graphql/tests/api-v6/integration/directives/unique/unique.int.test.ts b/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/unique.int.test.ts similarity index 95% rename from packages/graphql/tests/api-v6/integration/directives/unique/unique.int.test.ts rename to packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/unique.int.test.ts index 3c92999324..07e4e8dbc6 100644 --- a/packages/graphql/tests/api-v6/integration/directives/unique/unique.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/assertIndexesAndConstraints/unique.int.test.ts @@ -18,11 +18,11 @@ */ 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("assertIndexesAndConstraints/unique", () => { +describe("assertIndexesAndConstraints with @unique", () => { const testHelper = new TestHelper({ v6Api: true }); let databaseName: string; let IS_MULTI_DB_SUPPORTED = true; From f248657e76bc31057f542470f9770bd014c44cc1 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 17 Jul 2024 09:52:57 +0100 Subject: [PATCH 118/177] Fix logical filters in edges --- .../api-v6/queryIRFactory/FilterFactory.ts | 52 ++++--- .../relationship-not.int.test.ts | 137 ++++++++++++++++++ .../relationship.int.test.ts | 6 +- .../filters-on-relationships.test.ts | 4 +- .../relationships/relationship-not.test.ts | 109 ++++++++++++++ 5 files changed, 286 insertions(+), 22 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/filters/relationships/relationship-not.int.test.ts rename packages/graphql/tests/api-v6/integration/filters/{ => relationships}/relationship.int.test.ts (96%) rename packages/graphql/tests/api-v6/tck/filters/{ => relationships}/filters-on-relationships.test.ts (99%) create mode 100644 packages/graphql/tests/api-v6/tck/filters/relationships/relationship-not.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index 63d4a4c64a..11f89f0704 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -88,28 +88,15 @@ export class FilterFactory { } if (key === "properties" && relationship) { - const edgePropertiesFilters = this.createEdgePropertiesFilters({ + return this.createEdgePropertiesFilters({ where: value, relationship, }); - - return new LogicalFilter({ - operation: "AND", - filters: filterTruthy(edgePropertiesFilters), - }); } }) ); - if (filters.length <= 1) { - return filters[0]; - } - - // Implicit AND - return new LogicalFilter({ - operation: "AND", - filters: filters, - }); + return this.mergeFilters(filters); } private createNodeFilter({ @@ -210,13 +197,30 @@ export class FilterFactory { }: { where: GraphQLEdgeWhere["properties"]; relationship: Relationship; - }): Filter[] { - return Object.entries(where).flatMap(([fieldName, filters]) => { + }): 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 @@ -265,4 +269,18 @@ export class FilterFactory { }); }); } + + 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/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/relationship.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/relationships/relationship.int.test.ts similarity index 96% rename from packages/graphql/tests/api-v6/integration/filters/relationship.int.test.ts rename to packages/graphql/tests/api-v6/integration/filters/relationships/relationship.int.test.ts index ffc519da34..b7ceca9996 100644 --- a/packages/graphql/tests/api-v6/integration/filters/relationship.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/filters/relationships/relationship.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Relationship filters", () => { const testHelper = new TestHelper({ v6Api: true }); @@ -56,7 +56,7 @@ describe("Relationship filters", () => { await testHelper.close(); }); - test("filter by nested node", async () => { + test("NOT operator on nested node", async () => { const query = /* GraphQL */ ` query { ${Actor.plural} { diff --git a/packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts b/packages/graphql/tests/api-v6/tck/filters/relationships/filters-on-relationships.test.ts similarity index 99% rename from packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts rename to packages/graphql/tests/api-v6/tck/filters/relationships/filters-on-relationships.test.ts index f8140e80ec..03c97d103f 100644 --- a/packages/graphql/tests/api-v6/tck/filters/filters-on-relationships.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/relationships/filters-on-relationships.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import { Neo4jGraphQL } from "../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; +import { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; describe("Relationship", () => { let typeDefs: string; 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..00bd7cd74c --- /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]-(actors:Actor) + WHERE NOT (this1.year = $param0) + WITH collect({ node: actors, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS actors, edge.relationship AS this1 + RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + } + RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + } + RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + } + RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 1999, + \\"high\\": 0 + } + }" + `); + }); +}); From 2492ae7c804fb023e72b3c4cd3c9dcdf9ae17b39 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 17 Jul 2024 15:08:10 +0100 Subject: [PATCH 119/177] make hasNextPage and hasPreviousPage not nullable --- .../schema-types/StaticSchemaTypes.ts | 4 ++-- .../tests/api-v6/schema/directives/relayId.test.ts | 4 ++-- .../graphql/tests/api-v6/schema/relationship.test.ts | 8 ++++---- packages/graphql/tests/api-v6/schema/simple.test.ts | 12 ++++++------ .../graphql/tests/api-v6/schema/types/array.test.ts | 4 ++-- .../tests/api-v6/schema/types/scalars.test.ts | 4 ++-- .../tests/api-v6/schema/types/spatial.test.ts | 4 ++-- .../tests/api-v6/schema/types/temporals.test.ts | 4 ++-- 8 files changed, 22 insertions(+), 22 deletions(-) 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 index b327db5073..556ed0b6b6 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -66,8 +66,8 @@ export class StaticSchemaTypes { return this.schemaBuilder.getOrCreateObjectType("PageInfo", () => { return { fields: { - hasNextPage: this.schemaBuilder.types.boolean, - hasPreviousPage: this.schemaBuilder.types.boolean, + hasNextPage: this.schemaBuilder.types.boolean.NonNull, + hasPreviousPage: this.schemaBuilder.types.boolean.NonNull, startCursor: this.schemaBuilder.types.string, endCursor: this.schemaBuilder.types.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 index 76f4954286..8d520180f3 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -105,8 +105,8 @@ describe("RelayId", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: String } diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 12a2d9b8de..d71c0de33b 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -236,8 +236,8 @@ describe("Relationships", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: String } @@ -514,8 +514,8 @@ describe("Relationships", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: String } diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index d081708719..7f558fdf70 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -81,8 +81,8 @@ describe("Simple Aura-API", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: String } @@ -209,8 +209,8 @@ describe("Simple Aura-API", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: String } @@ -298,8 +298,8 @@ describe("Simple Aura-API", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: 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 index 9eec46258c..cab31a006a 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -348,8 +348,8 @@ describe("Scalars", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: String } diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index 90be897df2..de9654a80b 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -274,8 +274,8 @@ describe("Scalars", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: 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 index 0968c85d6d..9f79e0b323 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -155,8 +155,8 @@ describe("Spatial Types", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: String } diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index afd4afc8fe..08c12be4ff 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -256,8 +256,8 @@ describe("Temporals", () => { type PageInfo { endCursor: String - hasNextPage: Boolean - hasPreviousPage: Boolean + hasNextPage: Boolean! + hasPreviousPage: Boolean! startCursor: String } From 0a417dbc68cb6a9b3d625fa22f60dcc0a6511d00 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 25 Jul 2024 15:04:16 +0100 Subject: [PATCH 120/177] Remove inheritance in schema types --- .../schema-types/EntitySchemaTypes.ts | 105 ------------------ .../schema-types/RelatedEntitySchemaTypes.ts | 76 +++++++++++-- .../schema-types/TopLevelEntitySchemaTypes.ts | 66 +++++++++-- 3 files changed, 120 insertions(+), 127 deletions(-) delete mode 100644 packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts diff --git a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts deleted file mode 100644 index d1935a8304..0000000000 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/EntitySchemaTypes.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { - InputTypeComposer, - ListComposer, - NonNullComposer, - ObjectTypeComposer, - ScalarTypeComposer, -} from "graphql-compose"; -import { connectionOperationResolver } from "../../resolvers/connection-operation-resolver"; -import type { EntityTypeNames } from "../../schema-model/graphql-type-names/EntityTypeNames"; -import type { SchemaBuilder } from "../SchemaBuilder"; -import type { SchemaTypes } from "./SchemaTypes"; - -/** This class defines the GraphQL types for an entity */ -export abstract class EntitySchemaTypes { - protected schemaBuilder: SchemaBuilder; - protected entityTypeNames: T; - protected schemaTypes: SchemaTypes; - - constructor({ - schemaBuilder, - entityTypeNames, - schemaTypes, - }: { - schemaBuilder: SchemaBuilder; - schemaTypes: SchemaTypes; - entityTypeNames: T; - }) { - 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, - }, - }, - }; - }); - } - - protected get connection(): ObjectTypeComposer { - return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.connection, () => { - return { - fields: { - pageInfo: this.schemaTypes.staticTypes.pageInfo, - edges: this.edge.List, - }, - }; - }); - } - - protected get connectionSort(): InputTypeComposer { - return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { - return { - fields: { - edges: this.edgeSort, - }, - }; - }); - } - - protected abstract get edgeSort(): InputTypeComposer; - protected abstract get edge(): ObjectTypeComposer; - - public abstract get nodeType(): ObjectTypeComposer; - public abstract get nodeSort(): InputTypeComposer; - - public abstract isSortable(): boolean; -} 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 index 7ca07b791e..c52940d803 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/RelatedEntitySchemaTypes.ts @@ -17,7 +17,14 @@ * limitations under the License. */ -import type { EnumTypeComposer, InputTypeComposer, ObjectTypeComposer } from "graphql-compose"; +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 { @@ -29,16 +36,19 @@ import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters 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 { EntitySchemaTypes } from "./EntitySchemaTypes"; import type { SchemaTypes } from "./SchemaTypes"; import type { TopLevelEntitySchemaTypes } from "./TopLevelEntitySchemaTypes"; import { RelatedEntityFilterSchemaTypes } from "./filter-schema-types/RelatedEntityFilterSchemaTypes"; -export class RelatedEntitySchemaTypes extends EntitySchemaTypes { - private relationship: Relationship; +export class RelatedEntitySchemaTypes { public filterSchemaTypes: RelatedEntityFilterSchemaTypes; + private relationship: Relationship; + private schemaBuilder: SchemaBuilder; + private entityTypeNames: RelatedEntityTypeNames; + private schemaTypes: SchemaTypes; constructor({ relationship, @@ -51,20 +61,64 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { + 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, + }, + }; + }); } - protected get edge(): ObjectTypeComposer { + private get edge(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.edge, () => { const properties = this.getEdgeProperties(); const fields = { @@ -81,7 +135,7 @@ export class RelatedEntitySchemaTypes extends EntitySchemaTypes { const edgeSortFields = {}; const properties = this.getEdgeSortProperties(); 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 index 3e8e2ce3be..c713b3876a 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -18,7 +18,14 @@ */ import { type GraphQLResolveInfo } from "graphql"; -import type { InputTypeComposer, InterfaceTypeComposer, ObjectTypeComposer } from "graphql-compose"; +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"; @@ -33,17 +40,20 @@ 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 { EntitySchemaTypes } from "./EntitySchemaTypes"; import { RelatedEntitySchemaTypes } from "./RelatedEntitySchemaTypes"; import type { SchemaTypes } from "./SchemaTypes"; import { TopLevelFilterSchemaTypes } from "./filter-schema-types/TopLevelFilterSchemaTypes"; -export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { +export class TopLevelEntitySchemaTypes { private entity: ConcreteEntity; private filterSchemaTypes: TopLevelFilterSchemaTypes; + private schemaBuilder: SchemaBuilder; + private entityTypeNames: TopLevelEntityTypeNames; + private schemaTypes: SchemaTypes; constructor({ entity, @@ -54,13 +64,11 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { + 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: { @@ -91,7 +135,7 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { return { fields: { @@ -102,7 +146,7 @@ export class TopLevelEntitySchemaTypes extends EntitySchemaTypes { return { fields: { From 914e1b8d963a8a16269124de07d2f80fdcb3f988 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 25 Jul 2024 15:18:12 +0100 Subject: [PATCH 121/177] Update schema on nested filters --- .../RelatedEntityFilterSchemaTypes.ts | 10 ++--- .../tests/api-v6/schema/relationship.test.ts | 40 +++++++++---------- .../tests/api-v6/schema/types/array.test.ts | 10 ++--- .../tests/api-v6/schema/types/scalars.test.ts | 10 ++--- .../tests/api-v6/schema/types/spatial.test.ts | 10 ++--- .../api-v6/schema/types/temporals.test.ts | 10 ++--- 6 files changed, 45 insertions(+), 45 deletions(-) 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 index cec5897b17..819cafb41d 100644 --- 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 @@ -64,7 +64,10 @@ export class RelatedEntityFilterSchemaTypes extends FilterSchemaTypes { AND: [ActorMoviesEdgeListWhere!] NOT: ActorMoviesEdgeListWhere OR: [ActorMoviesEdgeListWhere!] - all: ActorMoviesEdgeWhere - none: ActorMoviesEdgeWhere - single: ActorMoviesEdgeWhere - some: ActorMoviesEdgeWhere + edges: ActorMoviesEdgeWhere } input ActorMoviesEdgeSort { @@ -102,7 +99,10 @@ describe("Relationships", () => { AND: [ActorMoviesNestedOperationWhere!] NOT: ActorMoviesNestedOperationWhere OR: [ActorMoviesNestedOperationWhere!] - edges: ActorMoviesEdgeListWhere + all: ActorMoviesEdgeListWhere + none: ActorMoviesEdgeListWhere + single: ActorMoviesEdgeListWhere + some: ActorMoviesEdgeListWhere } type ActorMoviesOperation { @@ -162,10 +162,7 @@ describe("Relationships", () => { AND: [MovieActorsEdgeListWhere!] NOT: MovieActorsEdgeListWhere OR: [MovieActorsEdgeListWhere!] - all: MovieActorsEdgeWhere - none: MovieActorsEdgeWhere - single: MovieActorsEdgeWhere - some: MovieActorsEdgeWhere + edges: MovieActorsEdgeWhere } input MovieActorsEdgeSort { @@ -183,7 +180,10 @@ describe("Relationships", () => { AND: [MovieActorsNestedOperationWhere!] NOT: MovieActorsNestedOperationWhere OR: [MovieActorsNestedOperationWhere!] - edges: MovieActorsEdgeListWhere + all: MovieActorsEdgeListWhere + none: MovieActorsEdgeListWhere + single: MovieActorsEdgeListWhere + some: MovieActorsEdgeListWhere } type MovieActorsOperation { @@ -342,10 +342,7 @@ describe("Relationships", () => { AND: [ActorMoviesEdgeListWhere!] NOT: ActorMoviesEdgeListWhere OR: [ActorMoviesEdgeListWhere!] - all: ActorMoviesEdgeWhere - none: ActorMoviesEdgeWhere - single: ActorMoviesEdgeWhere - some: ActorMoviesEdgeWhere + edges: ActorMoviesEdgeWhere } input ActorMoviesEdgeSort { @@ -365,7 +362,10 @@ describe("Relationships", () => { AND: [ActorMoviesNestedOperationWhere!] NOT: ActorMoviesNestedOperationWhere OR: [ActorMoviesNestedOperationWhere!] - edges: ActorMoviesEdgeListWhere + all: ActorMoviesEdgeListWhere + none: ActorMoviesEdgeListWhere + single: ActorMoviesEdgeListWhere + some: ActorMoviesEdgeListWhere } type ActorMoviesOperation { @@ -438,10 +438,7 @@ describe("Relationships", () => { AND: [MovieActorsEdgeListWhere!] NOT: MovieActorsEdgeListWhere OR: [MovieActorsEdgeListWhere!] - all: MovieActorsEdgeWhere - none: MovieActorsEdgeWhere - single: MovieActorsEdgeWhere - some: MovieActorsEdgeWhere + edges: MovieActorsEdgeWhere } input MovieActorsEdgeSort { @@ -461,7 +458,10 @@ describe("Relationships", () => { AND: [MovieActorsNestedOperationWhere!] NOT: MovieActorsNestedOperationWhere OR: [MovieActorsNestedOperationWhere!] - edges: MovieActorsEdgeListWhere + all: MovieActorsEdgeListWhere + none: MovieActorsEdgeListWhere + single: MovieActorsEdgeListWhere + some: MovieActorsEdgeListWhere } type MovieActorsOperation { diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index cab31a006a..d3d83216ec 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -283,10 +283,7 @@ describe("Scalars", () => { AND: [NodeTypeRelatedNodeEdgeListWhere!] NOT: NodeTypeRelatedNodeEdgeListWhere OR: [NodeTypeRelatedNodeEdgeListWhere!] - all: NodeTypeRelatedNodeEdgeWhere - none: NodeTypeRelatedNodeEdgeWhere - single: NodeTypeRelatedNodeEdgeWhere - some: NodeTypeRelatedNodeEdgeWhere + edges: NodeTypeRelatedNodeEdgeWhere } input NodeTypeRelatedNodeEdgeWhere { @@ -301,7 +298,10 @@ describe("Scalars", () => { AND: [NodeTypeRelatedNodeNestedOperationWhere!] NOT: NodeTypeRelatedNodeNestedOperationWhere OR: [NodeTypeRelatedNodeNestedOperationWhere!] - edges: NodeTypeRelatedNodeEdgeListWhere + all: NodeTypeRelatedNodeEdgeListWhere + none: NodeTypeRelatedNodeEdgeListWhere + single: NodeTypeRelatedNodeEdgeListWhere + some: NodeTypeRelatedNodeEdgeListWhere } type NodeTypeRelatedNodeOperation { diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index de9654a80b..2c49fd3688 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -201,10 +201,7 @@ describe("Scalars", () => { AND: [NodeTypeRelatedNodeEdgeListWhere!] NOT: NodeTypeRelatedNodeEdgeListWhere OR: [NodeTypeRelatedNodeEdgeListWhere!] - all: NodeTypeRelatedNodeEdgeWhere - none: NodeTypeRelatedNodeEdgeWhere - single: NodeTypeRelatedNodeEdgeWhere - some: NodeTypeRelatedNodeEdgeWhere + edges: NodeTypeRelatedNodeEdgeWhere } input NodeTypeRelatedNodeEdgeSort { @@ -224,7 +221,10 @@ describe("Scalars", () => { AND: [NodeTypeRelatedNodeNestedOperationWhere!] NOT: NodeTypeRelatedNodeNestedOperationWhere OR: [NodeTypeRelatedNodeNestedOperationWhere!] - edges: NodeTypeRelatedNodeEdgeListWhere + all: NodeTypeRelatedNodeEdgeListWhere + none: NodeTypeRelatedNodeEdgeListWhere + single: NodeTypeRelatedNodeEdgeListWhere + some: NodeTypeRelatedNodeEdgeListWhere } type NodeTypeRelatedNodeOperation { diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 9f79e0b323..317846b20f 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -114,10 +114,7 @@ describe("Spatial Types", () => { AND: [NodeTypeRelatedNodeEdgeListWhere!] NOT: NodeTypeRelatedNodeEdgeListWhere OR: [NodeTypeRelatedNodeEdgeListWhere!] - all: NodeTypeRelatedNodeEdgeWhere - none: NodeTypeRelatedNodeEdgeWhere - single: NodeTypeRelatedNodeEdgeWhere - some: NodeTypeRelatedNodeEdgeWhere + edges: NodeTypeRelatedNodeEdgeWhere } input NodeTypeRelatedNodeEdgeWhere { @@ -132,7 +129,10 @@ describe("Spatial Types", () => { AND: [NodeTypeRelatedNodeNestedOperationWhere!] NOT: NodeTypeRelatedNodeNestedOperationWhere OR: [NodeTypeRelatedNodeNestedOperationWhere!] - edges: NodeTypeRelatedNodeEdgeListWhere + all: NodeTypeRelatedNodeEdgeListWhere + none: NodeTypeRelatedNodeEdgeListWhere + single: NodeTypeRelatedNodeEdgeListWhere + some: NodeTypeRelatedNodeEdgeListWhere } type NodeTypeRelatedNodeOperation { diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 08c12be4ff..ac54d189ee 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -195,10 +195,7 @@ describe("Temporals", () => { AND: [NodeTypeRelatedNodeEdgeListWhere!] NOT: NodeTypeRelatedNodeEdgeListWhere OR: [NodeTypeRelatedNodeEdgeListWhere!] - all: NodeTypeRelatedNodeEdgeWhere - none: NodeTypeRelatedNodeEdgeWhere - single: NodeTypeRelatedNodeEdgeWhere - some: NodeTypeRelatedNodeEdgeWhere + edges: NodeTypeRelatedNodeEdgeWhere } input NodeTypeRelatedNodeEdgeSort { @@ -218,7 +215,10 @@ describe("Temporals", () => { AND: [NodeTypeRelatedNodeNestedOperationWhere!] NOT: NodeTypeRelatedNodeNestedOperationWhere OR: [NodeTypeRelatedNodeNestedOperationWhere!] - edges: NodeTypeRelatedNodeEdgeListWhere + all: NodeTypeRelatedNodeEdgeListWhere + none: NodeTypeRelatedNodeEdgeListWhere + single: NodeTypeRelatedNodeEdgeListWhere + some: NodeTypeRelatedNodeEdgeListWhere } type NodeTypeRelatedNodeOperation { From 3333c13c4f2d560818d3a1b7efc3f0f5243b6208 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 25 Jul 2024 16:02:51 +0100 Subject: [PATCH 122/177] Update relationship filters order --- .../api-v6/queryIRFactory/FilterFactory.ts | 4 ++-- .../resolve-tree-parser/graphql-tree/where.ts | 14 +++++++------ .../filters/nested/all.int.test.ts | 8 ++++---- .../filters/nested/none.int.test.ts | 8 ++++---- .../filters/nested/single.int.test.ts | 8 ++++---- .../filters/nested/some.int.test.ts | 8 ++++---- .../api-v6/integration/issues/190.int.test.ts | 6 +++--- .../api-v6/integration/issues/582.int.test.ts | 20 +++++++++---------- .../api-v6/tck/filters/nested/all.test.ts | 8 ++++---- .../api-v6/tck/filters/nested/none.test.ts | 8 ++++---- .../api-v6/tck/filters/nested/single.test.ts | 8 ++++---- .../api-v6/tck/filters/nested/some.test.ts | 8 ++++---- 12 files changed, 55 insertions(+), 53 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts index 11f89f0704..2d6314b9d9 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/FilterFactory.ts @@ -170,9 +170,9 @@ export class FilterFactory { const relationshipAdapter = new RelationshipAdapter(relationship); const target = relationshipAdapter.target as ConcreteEntityAdapter; - const edgeFilters = filters.edges ?? {}; - return Object.entries(edgeFilters).map(([rawOperator, filter]) => { + return Object.entries(filters).map(([rawOperator, edgeFilter]) => { + const filter = edgeFilter.edges; const relatedNodeFilters = this.createFilters({ where: filter, relationship: relationship, 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 index 4fbb16fc9f..766c5def17 100644 --- 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 @@ -56,12 +56,14 @@ export type NumberFilters = WithLogicalOperations<{ }>; export type RelationshipFilters = { - edges?: { - some?: GraphQLEdgeWhere; - single?: GraphQLEdgeWhere; - all?: GraphQLEdgeWhere; - none?: GraphQLEdgeWhere; - }; + some?: RelationshipEdgeWhere; + single?: RelationshipEdgeWhere; + all?: RelationshipEdgeWhere; + none?: RelationshipEdgeWhere; +}; + +type RelationshipEdgeWhere = { + edges: GraphQLEdgeWhere; }; type WithLogicalOperations = { 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 index 39bc92ba84..53a528cde9 100644 --- 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 @@ -65,7 +65,7 @@ describe("Relationship filters with all", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { all: { node: { name: { equals: "Keanu" } } } } } } } + where: { node: { actors: { all: { edges: { node: { name: { equals: "Keanu" } } } } } } } ) { connection { edges { @@ -99,7 +99,7 @@ describe("Relationship filters with all", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { all: { properties: { year: { equals: 1999 } } } } } } } + where: { node: { actors: { all: { edges: { properties: { year: { equals: 1999 } } } } } } } ) { connection { edges { @@ -133,7 +133,7 @@ describe("Relationship filters with all", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { all: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } + where: { node: { actors: { all: { edges: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } ) { connection { edges { @@ -172,7 +172,7 @@ describe("Relationship filters with all", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { all: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } + where: { node: { actors: { all: { edges: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } ) { connection { edges { 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 index 1e48882d40..2ce8aa30cf 100644 --- 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 @@ -65,7 +65,7 @@ describe("Relationship filters with none", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { none: { node: { name: { equals: "Keanu" } } } } } } } + where: { node: { actors: { none: { edges: { node: { name: { equals: "Keanu" } } } } } } } ) { connection { edges { @@ -104,7 +104,7 @@ describe("Relationship filters with none", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { none: { properties: { year: { equals: 1999 } } } } } } } + where: { node: { actors: { none: { edges: { properties: { year: { equals: 1999 } } } } } } } ) { connection { edges { @@ -143,7 +143,7 @@ describe("Relationship filters with none", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { none: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } + where: { node: { actors: { none: { edges: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } ) { connection { edges { @@ -177,7 +177,7 @@ describe("Relationship filters with none", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { none: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } + where: { node: { actors: { none: { edges: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } ) { connection { edges { 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 index b3affd3d00..4014963853 100644 --- 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 @@ -67,7 +67,7 @@ describe("Relationship filters with single", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { single: { node: { name: { equals: "Keanu" } } } } } } } + where: { node: { actors: { single: { edges: { node: { name: { equals: "Keanu" } } } } } } } ) { connection { edges { @@ -101,7 +101,7 @@ describe("Relationship filters with single", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { single: { properties: { year: { equals: 1999 } } } } } } } + where: { node: { actors: { single: { edges: { properties: { year: { equals: 1999 } } } } } } } ) { connection { edges { @@ -135,7 +135,7 @@ describe("Relationship filters with single", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { single: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } + where: { node: { actors: { single: { edges: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } ) { connection { edges { @@ -174,7 +174,7 @@ describe("Relationship filters with single", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { single: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } + where: { node: { actors: { single: { edges: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } ) { connection { edges { 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 index f7baa3a696..6c9d7e2985 100644 --- 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 @@ -65,7 +65,7 @@ describe("Relationship filters with some", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { some: { node: { name: { equals: "Keanu" } } } } } } } + where: { node: { actors: { some: { edges: { node: { name: { equals: "Keanu" } } } } } } } ) { connection { edges { @@ -104,7 +104,7 @@ describe("Relationship filters with some", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { some: { properties: { year: { equals: 1999 } } } } } } } + where: { node: { actors: { some: { edges: { properties: { year: { equals: 1999 } } } } } } } ) { connection { edges { @@ -143,7 +143,7 @@ describe("Relationship filters with some", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { some: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } + where: { node: { actors: { some: { edges: { OR: [{ properties: { year: { equals: 1999 } } }, { node: { name: { equals: "Keanu" } } }] } } } } } ) { connection { edges { @@ -187,7 +187,7 @@ describe("Relationship filters with some", () => { const query = /* GraphQL */ ` query { ${Movie.plural}( - where: { node: { actors: { edges: { some: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } + where: { node: { actors: { some: { edges: { NOT: { node: { name: { equals: "Keanu" } } } } } } } } ) { connection { edges { 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 index 145cc44777..81b1d4c5da 100644 --- a/packages/graphql/tests/api-v6/integration/issues/190.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/issues/190.int.test.ts @@ -67,7 +67,7 @@ describe("https://github.com/neo4j/graphql/issues/190", () => { test("Example 1", async () => { const query = /* GraphQL */ ` query { - ${User.plural}(where: { node: { demographics: { edges: { some: {node: { type: { equals: "Gender" }, value: { equals: "Female" } } } } } } }) { + ${User.plural}(where: { node: { demographics: { some: { edges: {node: { type: { equals: "Gender" }, value: { equals: "Female" } } } } } } }) { connection { edges { node { @@ -141,8 +141,8 @@ describe("https://github.com/neo4j/graphql/issues/190", () => { where: { node: { demographics: { - edges: { - some: { + some: { + edges: { node: { OR: [{ type: {equals: "Gender"}, value:{equals: "Female"} }, { type: {equals: "State"} }, { type: {equals: "Age"} }] } 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 index 6841500e9b..b1d717783a 100644 --- a/packages/graphql/tests/api-v6/integration/issues/582.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/issues/582.int.test.ts @@ -74,13 +74,13 @@ describe("https://github.com/neo4j/graphql/issues/582", () => { node: { type: { equals: "Cat" }, children: { - edges: { - some: { + some: { + edges: { node: { type: { equals: "Dog" }, parents: { - edges: { - some: { + some: { + edges: { node: { type: { equals: "Bird" }, }, @@ -119,18 +119,18 @@ describe("https://github.com/neo4j/graphql/issues/582", () => { node: { type: { equals: "Cat" }, children: { - edges: { - some: { + some: { + edges: { node: { type: { equals: "Dog" }, parents: { - edges: { - some: { + some: { + edges: { node: { type: { equals: "Bird" }, children: { - edges: { - some: { + some: { + edges: { node: { type: { equals: "Fish" }, }, 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 index c197c6fdf7..5a8602e91c 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/all.test.ts @@ -47,7 +47,7 @@ describe("Nested Filters with all", () => { test("query nested relationship with all filter", async () => { const query = /* GraphQL */ ` query { - movies(where: { node: { actors: { edges: { all: { node: { name: { equals: "Keanu" } } } } } } }) { + movies(where: { node: { actors: { all: { edges: { node: { name: { equals: "Keanu" } } } } } } }) { connection { edges { node { @@ -91,7 +91,7 @@ describe("Nested Filters with all", () => { test("query nested relationship properties with all filter", async () => { const query = /* GraphQL */ ` query { - movies(where: { node: { actors: { edges: { all: { properties: { year: { equals: 1999 } } } } } } }) { + movies(where: { node: { actors: { all: { edges: { properties: { year: { equals: 1999 } } } } } } }) { connection { edges { node { @@ -142,8 +142,8 @@ describe("Nested Filters with all", () => { where: { node: { actors: { - edges: { - all: { + all: { + edges: { OR: [ { node: { name: { equals: "Keanu" } } } { node: { name: { endsWith: "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 index 2efc518a18..23810c3244 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/none.test.ts @@ -47,7 +47,7 @@ describe("Nested Filters with none", () => { test("query nested relationship with none filter", async () => { const query = /* GraphQL */ ` query { - movies(where: { node: { actors: { edges: { none: { node: { name: { equals: "Keanu" } } } } } } }) { + movies(where: { node: { actors: { none: { edges: { node: { name: { equals: "Keanu" } } } } } } }) { connection { edges { node { @@ -88,7 +88,7 @@ describe("Nested Filters with none", () => { test("query nested relationship properties with none filter", async () => { const query = /* GraphQL */ ` query { - movies(where: { node: { actors: { edges: { none: { properties: { year: { equals: 1999 } } } } } } }) { + movies(where: { node: { actors: { none: { edges: { properties: { year: { equals: 1999 } } } } } } }) { connection { edges { node { @@ -136,8 +136,8 @@ describe("Nested Filters with none", () => { where: { node: { actors: { - edges: { - some: { + some: { + edges: { OR: [ { node: { name: { equals: "Keanu" } } } { node: { name: { endsWith: "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 index 4045b3b826..2aff3b39c5 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/single.test.ts @@ -47,7 +47,7 @@ describe("Nested Filters with single", () => { test("query nested relationship with single filter", async () => { const query = /* GraphQL */ ` query { - movies(where: { node: { actors: { edges: { single: { node: { name: { equals: "Keanu" } } } } } } }) { + movies(where: { node: { actors: { single: { edges: { node: { name: { equals: "Keanu" } } } } } } }) { connection { edges { node { @@ -85,7 +85,7 @@ describe("Nested Filters with single", () => { test("query nested relationship properties with single filter", async () => { const query = /* GraphQL */ ` query { - movies(where: { node: { actors: { edges: { single: { properties: { year: { equals: 1999 } } } } } } }) { + movies(where: { node: { actors: { single: { edges: { properties: { year: { equals: 1999 } } } } } } }) { connection { edges { node { @@ -130,8 +130,8 @@ describe("Nested Filters with single", () => { where: { node: { actors: { - edges: { - single: { + single: { + edges: { OR: [ { node: { name: { equals: "Keanu" } } } { node: { name: { endsWith: "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 index 7721c13a47..c88db78416 100644 --- a/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts +++ b/packages/graphql/tests/api-v6/tck/filters/nested/some.test.ts @@ -47,7 +47,7 @@ describe("Nested Filters with some", () => { test("query nested relationship with some filter", async () => { const query = /* GraphQL */ ` query { - movies(where: { node: { actors: { edges: { some: { node: { name: { equals: "Keanu" } } } } } } }) { + movies(where: { node: { actors: { some: { edges: { node: { name: { equals: "Keanu" } } } } } } }) { connection { edges { node { @@ -88,7 +88,7 @@ describe("Nested Filters with some", () => { test("query nested relationship properties with some filter", async () => { const query = /* GraphQL */ ` query { - movies(where: { node: { actors: { edges: { some: { properties: { year: { equals: 1999 } } } } } } }) { + movies(where: { node: { actors: { some: { edges: { properties: { year: { equals: 1999 } } } } } } }) { connection { edges { node { @@ -136,8 +136,8 @@ describe("Nested Filters with some", () => { where: { node: { actors: { - edges: { - some: { + some: { + edges: { OR: [ { node: { name: { equals: "Keanu" } } } { node: { name: { endsWith: "eeves" } } } From b2617fdbb7a307507f8bf29c8648f6447eb34205 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 26 Jul 2024 09:39:58 +0100 Subject: [PATCH 123/177] initial create implementation --- .../queryIRFactory/CreateOperationFactory.ts | 153 ++++++++++++++++++ .../parse-args.ts | 10 +- .../argument-parser/parse-create-args.ts | 37 +++++ .../graphql-tree/graphql-tree.ts | 14 ++ .../resolve-tree-parser/parse-edges.ts | 2 +- .../resolve-tree-parser/parse-node.ts | 3 +- .../parse-resolve-info-tree.ts | 39 +++-- .../resolve-tree-parser-error.ts | 20 +++ .../resolvers/translate-create-resolver.ts | 54 +++++++ .../api-v6/schema-generation/SchemaBuilder.ts | 26 ++- .../schema-generation/SchemaGenerator.ts | 11 ++ .../schema-types/StaticSchemaTypes.ts | 6 +- .../schema-types/TopLevelEntitySchemaTypes.ts | 48 +++++- .../TopLevelCreateSchemaTypes.ts | 115 +++++++++++++ .../TopLevelEntityTypeNames.ts | 22 +++ .../translators/translate-create-operation.ts | 44 +++++ .../integration/create/create.int.test.ts | 76 +++++++++ .../api-v6/schema/directives/relayId.test.ts | 25 +++ .../tests/api-v6/schema/relationship.test.ts | 88 ++++++++++ .../tests/api-v6/schema/simple.test.ts | 92 +++++++++++ .../tests/api-v6/schema/types/array.test.ts | 42 +++++ .../tests/api-v6/schema/types/scalars.test.ts | 62 +++++++ .../tests/api-v6/schema/types/spatial.test.ts | 42 +++++ .../api-v6/schema/types/temporals.test.ts | 42 +++++ .../tests/api-v6/tck/create/create.test.ts | 88 ++++++++++ 25 files changed, 1142 insertions(+), 19 deletions(-) create mode 100644 packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts rename packages/graphql/src/api-v6/queryIRFactory/{resolve-tree-parser => argument-parser}/parse-args.ts (92%) create mode 100644 packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/resolve-tree-parser-error.ts create mode 100644 packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts create mode 100644 packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts create mode 100644 packages/graphql/src/api-v6/translators/translate-create-operation.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/create.int.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/create/create.test.ts 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..9534129711 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.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 Cypher from "@neo4j/cypher-builder"; +import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; +import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; +import { QueryAST } from "../../translate/queryAST/ast/QueryAST"; + +import type { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; +import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import { PropertyInputField } from "../../translate/queryAST/ast/input-fields/PropertyInputField"; +import { UnwindCreateOperation } from "../../translate/queryAST/ast/operations/UnwindCreateOperation"; +import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "./resolve-tree-parser/graphql-tree/graphql-tree"; + +export class CreateOperationFactory { + public schemaModel: Neo4jGraphQLSchemaModel; + + constructor(schemaModel: Neo4jGraphQLSchemaModel) { + this.schemaModel = 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; + }): UnwindCreateOperation { + const topLevelCreateInput = graphQLTreeCreate.args.input; + const targetAdapter = new ConcreteEntityAdapter(entity); + const unwindCreate = this.parseTopLevelCreate({ + target: targetAdapter, + createInput: topLevelCreateInput, + argumentToUnwind: new Cypher.Param(topLevelCreateInput), + }); + + unwindCreate.addProjectionOperations([]); + return unwindCreate; + } + + private parseTopLevelCreate({ + target, + createInput, + argumentToUnwind, + }: { + target: ConcreteEntityAdapter; + createInput: GraphQLTreeCreateInput[]; + argumentToUnwind: Cypher.Property | Cypher.Param; + }): UnwindCreateOperation { + const unwindCreate = new UnwindCreateOperation({ + target, + argumentToUnwind, + }); + + this.hydrateUnwindCreateOperation({ + target, + createInput: createInput, + unwindCreate, + }); + + return unwindCreate; + } + + private hydrateUnwindCreateOperation({ + target, + createInput, + unwindCreate, + }: { + target: ConcreteEntityAdapter; + createInput: GraphQLTreeCreateInput[]; + unwindCreate: UnwindCreateOperation; + }) { + // TODO: Add autogenerated fields + createInput.forEach((inputItem) => { + for (const key of Object.keys(inputItem)) { + const attribute = getAttribute(target, key); + + const attachedTo = "node"; + const inputField = this.parseAttributeInputField({ + attribute, + unwindCreate, + attachedTo, + }); + if (!inputField) { + continue; + } + unwindCreate.addField(inputField, attachedTo); + } + }); + } + + private parseAttributeInputField({ + attribute, + unwindCreate, + attachedTo, + }: { + attribute: AttributeAdapter; + unwindCreate: UnwindCreateOperation; + attachedTo: "node" | "relationship"; + }): PropertyInputField | undefined { + if (unwindCreate.getField(attribute.name, attachedTo)) { + return; + } + + return new PropertyInputField({ + attribute, + attachedTo, + }); + } +} + +/** + * Get the attribute from the entity, in case it doesn't exist throw an error + **/ +function getAttribute(entity: ConcreteEntityAdapter, key: string): AttributeAdapter { + const attribute = entity.attributes.get(key); + if (!attribute) { + throw new Error(`Transpile Error: Input field ${key} not found in entity ${entity.name}`); + } + return attribute; +} + +export class QueryParseError extends Error {} diff --git a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-args.ts b/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-args.ts similarity index 92% rename from packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-args.ts rename to packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-args.ts index a9c6f65a8e..52504dc105 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/parse-args.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-args.ts @@ -19,9 +19,13 @@ import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; -import type { GraphQLTree, GraphQLTreeConnection, GraphQLTreeConnectionTopLevel } from "./graphql-tree/graphql-tree"; -import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "./graphql-tree/sort"; -import { ResolveTreeParserError } from "./parse-resolve-info-tree"; +import type { + GraphQLTree, + GraphQLTreeConnection, + GraphQLTreeConnectionTopLevel, +} from "../resolve-tree-parser/graphql-tree/graphql-tree"; +import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "../resolve-tree-parser/graphql-tree/sort"; +import { ResolveTreeParserError } from "../resolve-tree-parser/resolve-tree-parser-error"; export function parseOperationArgs(resolveTreeArgs: Record): GraphQLTree["args"] { // Not properly parsed, assuming the type is the same diff --git a/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts b/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts new file mode 100644 index 0000000000..f36a7a1124 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "../resolve-tree-parser/graphql-tree/graphql-tree"; +import { ResolveTreeParserError } from "../resolve-tree-parser/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/graphql-tree/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree.ts index 8d72aad3a0..07f7ebc2d1 100644 --- 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 @@ -24,6 +24,20 @@ import type { GraphQLTreeElement } from "./tree-element"; import type { GraphQLWhere, GraphQLWhereTopLevel } from "./where"; export type GraphQLTree = GraphQLTreeReadOperationTopLevel; +export type GraphQLTreeCreate = GraphQLTreeCreateOperationTopLevel; + +// TODO: Add support for other built-in primitive types as Cartesian Point, Point Date, BigInt etc. +type PrimitiveType = string | number | boolean; + +// TODO GraphQLTreeCreateInput should be a union of PrimitiveTypes and relationship fields +export type GraphQLTreeCreateInput = Record; +interface GraphQLTreeCreateOperationTopLevel extends GraphQLTreeElement { + name: string; + fields: Record; + args: { + input: GraphQLTreeCreateInput[]; + }; +} interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { name: string; 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 index cad517913b..afe70c662d 100644 --- 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 @@ -24,7 +24,7 @@ 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 "./parse-resolve-info-tree"; +import { ResolveTreeParserError } from "./resolve-tree-parser-error"; import { findFieldByName } from "./utils/find-field-by-name"; export function parseEdges(resolveTree: ResolveTree, entity: Relationship | ConcreteEntity): GraphQLTreeEdge { 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 index ca73132d06..e4420ae7aa 100644 --- 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 @@ -22,7 +22,8 @@ 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 { ResolveTreeParserError, parseRelationshipField } from "./parse-resolve-info-tree"; +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; 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 index 433e0ab050..e15eb34c7c 100644 --- 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 @@ -20,18 +20,20 @@ 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 type { GraphQLTree, GraphQLTreeConnection, GraphQLTreeConnectionTopLevel, + GraphQLTreeCreate, GraphQLTreeReadOperation, } from "./graphql-tree/graphql-tree"; -import { - parseConnectionArgs, - parseConnectionArgsTopLevel, - parseOperationArgs, - parseOperationArgsTopLevel, -} from "./parse-args"; import { parseEdges } from "./parse-edges"; import { findFieldByName } from "./utils/find-field-by-name"; @@ -43,9 +45,9 @@ export function parseResolveInfoTree({ entity: ConcreteEntity; }): GraphQLTree { 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, @@ -56,6 +58,27 @@ export function parseResolveInfoTree({ }; } +export function parseResolveInfoTreeCreate({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTreeCreate { + const createResponse = findFieldByName(resolveTree, entity.typeNames.createResponse, entity.typeNames.queryField); + const createArgs = parseCreateOperationArgsTopLevel(resolveTree.args); + console.log("createResponse", createResponse); + console.log("createInput", createArgs); + return { + alias: resolveTree.alias, + name: resolveTree.name, + fields: { + // TODO: add tree for selection + }, + args: createArgs, + }; +} + export function parseConnection(resolveTree: ResolveTree, entity: Relationship): GraphQLTreeConnection { const entityTypes = entity.typeNames; const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges"); @@ -109,5 +132,3 @@ function parseTopLevelConnection(resolveTree: ResolveTree, entity: ConcreteEntit }, }; } - -export class ResolveTreeParserError extends Error {} 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/resolvers/translate-create-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts new file mode 100644 index 0000000000..b1bbec73ee --- /dev/null +++ b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts @@ -0,0 +1,54 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { translateCreateResolver } 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 }); + console.log("graphQLTree", JSON.stringify(graphQLTreeCreate, null, 2)); + const { cypher, params } = translateCreateResolver({ + context: context, + graphQLTreeCreate, + entity, + }); + const executeResult = await execute({ + cypher, + params, + defaultAccessMode: "WRITE", + context, + info, + }); + + return executeResult.records[0]?.this; + }; +} diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index aff40f80b8..8615b735b3 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -38,7 +38,8 @@ type ListOrNullComposer | ListComposer> | NonNullComposer - | NonNullComposer>; + | NonNullComposer> + | NonNullComposer>>; type WrappedComposer = T | ListOrNullComposer; @@ -186,6 +187,29 @@ export class SchemaBuilder { }); } + 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/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index 0e535f76e8..9172d067d3 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -26,6 +26,7 @@ import { SchemaBuilder } from "./SchemaBuilder"; import { SchemaTypes } from "./schema-types/SchemaTypes"; import { StaticSchemaTypes } from "./schema-types/StaticSchemaTypes"; import { TopLevelEntitySchemaTypes } from "./schema-types/TopLevelEntitySchemaTypes"; +import { generateCreateResolver } from "../resolvers/translate-create-resolver"; export class SchemaGenerator { private schemaBuilder: SchemaBuilder; @@ -39,6 +40,7 @@ export class SchemaGenerator { public generate(schemaModel: Neo4jGraphQLSchemaModel): GraphQLSchema { const entityTypesMap = this.generateEntityTypes(schemaModel); this.generateTopLevelQueryFields(entityTypesMap); + this.generateTopLevelCreateFields(entityTypesMap); this.generateGlobalNodeQueryField(schemaModel); @@ -75,6 +77,15 @@ export class SchemaGenerator { } } + private generateTopLevelCreateFields(entityTypesMap: Map): void { + for (const [entity, entitySchemaTypes] of entityTypesMap.entries()) { + const resolver = generateCreateResolver({ + entity, + }); + entitySchemaTypes.addTopLevelCreateField(resolver); + } + } + private generateGlobalNodeQueryField(schemaModel: Neo4jGraphQLSchemaModel): void { const globalEntities = schemaModel.concreteEntities.filter((e) => e.globalIdField); 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 index 556ed0b6b6..f0a9a4a293 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -18,7 +18,7 @@ */ import type { GraphQLInputType } from "graphql"; -import { GraphQLBoolean, GraphQLID } from "graphql"; +import { GraphQLBoolean } from "graphql"; import type { EnumTypeComposer, InputTypeComposer, @@ -352,7 +352,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("IDListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLID), + equals: this.schemaBuilder.types.id.List, }, }; }); @@ -361,7 +361,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("IDListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLID)), + equals: this.schemaBuilder.types.id.NonNull.List, }, }; }); 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 index c713b3876a..87d6ba15d8 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -47,6 +47,7 @@ import type { FieldDefinition, GraphQLResolver, SchemaBuilder } from "../SchemaB import { RelatedEntitySchemaTypes } from "./RelatedEntitySchemaTypes"; import type { SchemaTypes } from "./SchemaTypes"; import { TopLevelFilterSchemaTypes } from "./filter-schema-types/TopLevelFilterSchemaTypes"; +import { TopLevelCreateSchemaTypes } from "./mutation-schema-types/TopLevelCreateSchemaTypes"; export class TopLevelEntitySchemaTypes { private entity: ConcreteEntity; @@ -54,6 +55,7 @@ export class TopLevelEntitySchemaTypes { private schemaBuilder: SchemaBuilder; private entityTypeNames: TopLevelEntityTypeNames; private schemaTypes: SchemaTypes; + private createSchemaTypes: TopLevelCreateSchemaTypes; constructor({ entity, @@ -69,6 +71,7 @@ export class TopLevelEntitySchemaTypes { this.schemaBuilder = schemaBuilder; this.entityTypeNames = entity.typeNames; this.schemaTypes = schemaTypes; + this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity, schemaTypes }); } public addTopLevelQueryField( @@ -125,7 +128,25 @@ export class TopLevelEntitySchemaTypes { }); } - private get connectionSort(): InputTypeComposer { + 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, + }); + } + + protected get connectionSort(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { return { fields: { @@ -278,6 +299,31 @@ export class TopLevelEntitySchemaTypes { }) ); } + + public get createType(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.createResponse, () => { + const nodeType = this.nodeType; + const info = this.createInfo; + + 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, + }, + }; + }); + } } function typeToResolver(type: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType): GraphQLResolver | undefined { 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..e8a883a0bb --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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, ScalarTypeComposer } from "graphql-compose"; +import type { Attribute } from "../../../../schema-model/attribute/Attribute"; +import { GraphQLBuiltInScalarType, 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 TopLevelCreateSchemaTypes { + 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 createInput(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.createInput, (_itc: InputTypeComposer) => { + return { + fields: { + node: this.createNode, + }, + }; + }); + } + + public get createNode(): InputTypeComposer { + return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.createNode, (_itc: InputTypeComposer) => { + return { + fields: { + ...this.getInputFields([...this.entity.attributes.values()]), + _emptyInput: this.schemaBuilder.types.boolean, + }, + }; + }); + } + + private getInputFields(attributes: Attribute[]): Record { + const inputFields: Array<[string, InputTypeComposer | GraphQLScalarType] | []> = filterTruthy( + attributes.map((attribute) => { + const inputField = this.attributeToInputField(attribute); + if (inputField) { + return [attribute.name, inputField]; + } + }) + ); + return Object.fromEntries(inputFields); + } + + private attributeToInputField(attribute: Attribute): any { + if (attribute.type instanceof ScalarType) { + return this.createBuiltInFieldInput(attribute.type); + } + /* const isList = attribute.type instanceof ListType; + const wrappedType = isList ? attribute.type.ofType : attribute.type; + if (wrappedType instanceof ScalarType) { + return this.createScalarType(wrappedType, isList); + } */ + } + + private createBuiltInFieldInput(type: ScalarType): ScalarTypeComposer | undefined { + // TODO: add required sign and other types. + switch (type.name) { + case GraphQLBuiltInScalarType.Boolean: { + return this.schemaBuilder.types.boolean; + } + case GraphQLBuiltInScalarType.String: { + return this.schemaBuilder.types.string; + } + case GraphQLBuiltInScalarType.ID: { + return this.schemaBuilder.types.id; + } + case GraphQLBuiltInScalarType.Int: { + return this.schemaBuilder.types.int; + } + case GraphQLBuiltInScalarType.Float: { + return this.schemaBuilder.types.float; + } + } + } +} 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 index a7dad76b2d..411fb45b23 100644 --- 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 @@ -18,6 +18,7 @@ */ import { plural } from "../../../schema-model/utils/string-manipulation"; +import { upperFirst } from "../../../utils/upper-first"; import { EntityTypeNames } from "./EntityTypeNames"; /** Top level node typenames */ @@ -27,6 +28,10 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { return plural(this.entityName); } + public get createNode(): string { + return `${upperFirst(this.entityName)}CreateNode`; + } + public get connectionOperation(): string { return `${this.entityName}Operation`; } @@ -66,4 +71,21 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { public get node(): string { return this.entityName; } + + /** Top Level Create field */ + public get createField(): string { + return `create${upperFirst(plural(this.entityName))}`; + } + // TODO: do we need to memoize the upperFirst/plural calls? + 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`; + } } 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..b8d2fcc9fa --- /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 translateCreateResolver({ + 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/tests/api-v6/integration/create/create.int.test.ts b/packages/graphql/tests/api-v6/integration/create/create.int.test.ts new file mode 100644 index 0000000000..db8427d9fa --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/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/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index 8d520180f3..c2d70becad 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -37,6 +37,7 @@ describe("RelayId", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } input GlobalIdWhere { @@ -69,6 +70,26 @@ describe("RelayId", () => { node: MovieSort } + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode + } + + input MovieCreateNode { + _emptyInput: Boolean + dbId: ID + title: String + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + type MovieEdge { cursor: String node: Movie @@ -99,6 +120,10 @@ describe("RelayId", () => { title: StringWhere } + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + interface Node { id: ID! } diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 54bba9c5bc..4515e0831c 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -42,6 +42,7 @@ describe("Relationships", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } type Actor { @@ -58,6 +59,25 @@ describe("Relationships", () => { node: ActorSort } + type ActorCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input ActorCreateInput { + node: ActorCreateNode + } + + input ActorCreateNode { + _emptyInput: Boolean + name: String + } + + type ActorCreateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + type ActorEdge { cursor: String node: Actor @@ -206,6 +226,25 @@ describe("Relationships", () => { node: MovieSort } + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode + } + + input MovieCreateNode { + _emptyInput: Boolean + title: String + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + type MovieEdge { cursor: String node: Movie @@ -234,6 +273,11 @@ describe("Relationships", () => { title: StringWhere } + type Mutation { + createActors(input: [ActorCreateInput!]!): ActorCreateResponse + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + type PageInfo { endCursor: String hasNextPage: Boolean! @@ -287,6 +331,7 @@ describe("Relationships", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } type ActedIn { @@ -318,6 +363,25 @@ describe("Relationships", () => { node: ActorSort } + type ActorCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input ActorCreateInput { + node: ActorCreateNode + } + + input ActorCreateNode { + _emptyInput: Boolean + name: String + } + + type ActorCreateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + type ActorEdge { cursor: String node: Actor @@ -484,6 +548,25 @@ describe("Relationships", () => { node: MovieSort } + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode + } + + input MovieCreateNode { + _emptyInput: Boolean + title: String + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + type MovieEdge { cursor: String node: Movie @@ -512,6 +595,11 @@ describe("Relationships", () => { title: StringWhere } + type Mutation { + createActors(input: [ActorCreateInput!]!): ActorCreateResponse + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + type PageInfo { endCursor: String hasNextPage: Boolean! diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 7f558fdf70..6f89f3d5e6 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -37,6 +37,7 @@ describe("Simple Aura-API", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } type Movie { @@ -52,6 +53,25 @@ describe("Simple Aura-API", () => { node: MovieSort } + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode + } + + input MovieCreateNode { + _emptyInput: Boolean + title: String + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + type MovieEdge { cursor: String node: Movie @@ -79,6 +99,10 @@ describe("Simple Aura-API", () => { title: StringWhere } + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + type PageInfo { endCursor: String hasNextPage: Boolean! @@ -125,6 +149,7 @@ describe("Simple Aura-API", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } type Actor { @@ -140,6 +165,25 @@ describe("Simple Aura-API", () => { node: ActorSort } + type ActorCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input ActorCreateInput { + node: ActorCreateNode + } + + input ActorCreateNode { + _emptyInput: Boolean + name: String + } + + type ActorCreateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + type ActorEdge { cursor: String node: Actor @@ -180,6 +224,25 @@ describe("Simple Aura-API", () => { node: MovieSort } + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode + } + + input MovieCreateNode { + _emptyInput: Boolean + title: String + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + type MovieEdge { cursor: String node: Movie @@ -207,6 +270,11 @@ describe("Simple Aura-API", () => { title: StringWhere } + type Mutation { + createActors(input: [ActorCreateInput!]!): ActorCreateResponse + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + type PageInfo { endCursor: String hasNextPage: Boolean! @@ -254,6 +322,7 @@ describe("Simple Aura-API", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } type Movie { @@ -269,6 +338,25 @@ describe("Simple Aura-API", () => { node: MovieSort } + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode + } + + input MovieCreateNode { + _emptyInput: Boolean + title: String + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + type MovieEdge { cursor: String node: Movie @@ -296,6 +384,10 @@ describe("Simple Aura-API", () => { title: StringWhere } + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + type PageInfo { endCursor: String hasNextPage: Boolean! diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index d3d83216ec..dfe4eaabf9 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -116,6 +116,7 @@ describe("Scalars", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } \\"\\"\\" @@ -219,6 +220,11 @@ describe("Scalars", () => { equals: [LocalTime] } + type Mutation { + createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse + createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + } + type NodeType { bigIntList: [BigInt!] bigIntListNullable: [BigInt] @@ -252,6 +258,24 @@ describe("Scalars", () => { pageInfo: PageInfo } + type NodeTypeCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input NodeTypeCreateInput { + node: NodeTypeCreateNode + } + + input NodeTypeCreateNode { + _emptyInput: Boolean + } + + type NodeTypeCreateResponse { + info: NodeTypeCreateInfo + nodeTypes: [NodeType!]! + } + type NodeTypeEdge { cursor: String node: NodeType @@ -390,6 +414,24 @@ describe("Scalars", () => { pageInfo: PageInfo } + type RelatedNodeCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input RelatedNodeCreateInput { + node: RelatedNodeCreateNode + } + + input RelatedNodeCreateNode { + _emptyInput: Boolean + } + + type RelatedNodeCreateResponse { + info: RelatedNodeCreateInfo + relatedNodes: [RelatedNode!]! + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index 2c49fd3688..aa5b957e98 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -80,6 +80,7 @@ describe("Scalars", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } \\"\\"\\" @@ -141,6 +142,11 @@ describe("Scalars", () => { lte: Int } + type Mutation { + createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse + createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + } + type NodeType { bigInt: BigInt! bigIntNullable: BigInt @@ -166,6 +172,34 @@ describe("Scalars", () => { node: NodeTypeSort } + type NodeTypeCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input NodeTypeCreateInput { + node: NodeTypeCreateNode + } + + input NodeTypeCreateNode { + _emptyInput: Boolean + boolean: Boolean + booleanNullable: Boolean + float: Float + floatNullable: Float + id: ID + idNullable: ID + int: Int + intNullable: Int + string: String + stringNullable: String + } + + type NodeTypeCreateResponse { + info: NodeTypeCreateInfo + nodeTypes: [NodeType!]! + } + type NodeTypeEdge { cursor: String node: NodeType @@ -308,6 +342,34 @@ describe("Scalars", () => { node: RelatedNodeSort } + type RelatedNodeCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input RelatedNodeCreateInput { + node: RelatedNodeCreateNode + } + + input RelatedNodeCreateNode { + _emptyInput: Boolean + boolean: Boolean + booleanNullable: Boolean + float: Float + floatNullable: Float + id: ID + idNullable: ID + int: Int + intNullable: Int + string: String + stringNullable: String + } + + type RelatedNodeCreateResponse { + info: RelatedNodeCreateInfo + relatedNodes: [RelatedNode!]! + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 317846b20f..ef13f32248 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -57,6 +57,7 @@ describe("Spatial Types", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } \\"\\"\\" @@ -70,6 +71,11 @@ describe("Spatial Types", () => { z: Float } + type Mutation { + createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse + createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + } + type NodeType { cartesianPoint: CartesianPoint! cartesianPointNullable: CartesianPoint @@ -83,6 +89,24 @@ describe("Spatial Types", () => { pageInfo: PageInfo } + type NodeTypeCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input NodeTypeCreateInput { + node: NodeTypeCreateNode + } + + input NodeTypeCreateNode { + _emptyInput: Boolean + } + + type NodeTypeCreateResponse { + info: NodeTypeCreateInfo + nodeTypes: [NodeType!]! + } + type NodeTypeEdge { cursor: String node: NodeType @@ -188,6 +212,24 @@ describe("Spatial Types", () => { pageInfo: PageInfo } + type RelatedNodeCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input RelatedNodeCreateInput { + node: RelatedNodeCreateNode + } + + input RelatedNodeCreateNode { + _emptyInput: Boolean + } + + type RelatedNodeCreateResponse { + info: RelatedNodeCreateInfo + relatedNodes: [RelatedNode!]! + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index ac54d189ee..3686e7e380 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -62,6 +62,7 @@ describe("Temporals", () => { expect(printedSchema).toMatchInlineSnapshot(` "schema { query: Query + mutation: Mutation } \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" @@ -141,6 +142,11 @@ describe("Temporals", () => { lte: LocalTime } + type Mutation { + createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse + createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + } + type NodeType { date: Date dateTime: DateTime @@ -160,6 +166,24 @@ describe("Temporals", () => { node: NodeTypeSort } + type NodeTypeCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input NodeTypeCreateInput { + node: NodeTypeCreateNode + } + + input NodeTypeCreateNode { + _emptyInput: Boolean + } + + type NodeTypeCreateResponse { + info: NodeTypeCreateInfo + nodeTypes: [NodeType!]! + } + type NodeTypeEdge { cursor: String node: NodeType @@ -284,6 +308,24 @@ describe("Temporals", () => { node: RelatedNodeSort } + type RelatedNodeCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input RelatedNodeCreateInput { + node: RelatedNodeCreateNode + } + + input RelatedNodeCreateNode { + _emptyInput: Boolean + } + + type RelatedNodeCreateResponse { + info: RelatedNodeCreateInfo + relatedNodes: [RelatedNode!]! + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/tck/create/create.test.ts b/packages/graphql/tests/api-v6/tck/create/create.test.ts new file mode 100644 index 0000000000..5793e471d6 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/create/create.test.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 + } + RETURN \\"Query cannot conclude with CALL\\"" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": [ + { + \\"title\\": \\"The Matrix\\" + }, + { + \\"title\\": \\"The Matrix Reloaded\\", + \\"released\\": { + \\"low\\": 2001, + \\"high\\": 0 + } + } + ] + }" + `); + }); +}); From 704e6efbc4cf1b6bce7c5dcdb9c681bcd07f9326 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 26 Jul 2024 10:42:38 +0100 Subject: [PATCH 124/177] export directly GraphQLTree --- .../argument-parser/parse-create-args.ts | 4 +--- .../graphql-tree/graphql-tree.ts | 13 ++++--------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts b/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts index f36a7a1124..8741a52901 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts @@ -30,8 +30,6 @@ function parseCreateOperationInput(resolveTreeCreateInput: any): GraphQLTreeCrea throw new ResolveTreeParserError(`Invalid create input field: ${resolveTreeCreateInput}`); } return resolveTreeCreateInput.map((input) => { - return { - ...input.node, - }; + return input.node; }); } 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 index 07f7ebc2d1..5ebededfdf 100644 --- 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 @@ -23,15 +23,10 @@ import type { GraphQLSort, GraphQLSortEdge } from "./sort"; import type { GraphQLTreeElement } from "./tree-element"; import type { GraphQLWhere, GraphQLWhereTopLevel } from "./where"; -export type GraphQLTree = GraphQLTreeReadOperationTopLevel; -export type GraphQLTreeCreate = GraphQLTreeCreateOperationTopLevel; - -// TODO: Add support for other built-in primitive types as Cartesian Point, Point Date, BigInt etc. -type PrimitiveType = string | number | boolean; - // TODO GraphQLTreeCreateInput should be a union of PrimitiveTypes and relationship fields -export type GraphQLTreeCreateInput = Record; -interface GraphQLTreeCreateOperationTopLevel extends GraphQLTreeElement { +export type GraphQLTreeCreateInput = Record; + +export interface GraphQLTreeCreate extends GraphQLTreeElement { name: string; fields: Record; args: { @@ -39,7 +34,7 @@ interface GraphQLTreeCreateOperationTopLevel extends GraphQLTreeElement { }; } -interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { +export interface GraphQLTree extends GraphQLTreeElement { name: string; fields: { connection?: GraphQLTreeConnectionTopLevel; From 1f3fc46a21415afd5d22ecbd7abda5b8332c21d4 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 26 Jul 2024 14:28:12 +0100 Subject: [PATCH 125/177] move unwind-create to v6 folder under name of CreateOperation --- .../src/api-v6/queryIR/CreateOperation.ts | 257 ++++++++++++++++++ .../queryIRFactory/CreateOperationFactory.ts | 12 +- .../src/translate/queryAST/ast/QueryAST.ts | 22 +- 3 files changed, 272 insertions(+), 19 deletions(-) create mode 100644 packages/graphql/src/api-v6/queryIR/CreateOperation.ts 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..a132b9fe14 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts @@ -0,0 +1,257 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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 { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { checkEntityAuthentication } from "../../translate/authorization/check-authentication"; +import { createRelationshipValidationClauses } from "../../translate/create-relationship-validation-clauses"; +import { QueryASTContext } from "../../translate/queryAST/ast/QueryASTContext"; +import type { QueryASTNode } from "../../translate/queryAST/ast/QueryASTNode"; +import type { AuthorizationFilters } from "../../translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters"; +import type { InputField } from "../../translate/queryAST/ast/input-fields/InputField"; +import { PropertyInputField } from "../../translate/queryAST/ast/input-fields/PropertyInputField"; +import type { ReadOperation } from "../../translate/queryAST/ast/operations/ReadOperation"; +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 { assertIsConcreteEntity } from "../../translate/queryAST/utils/is-concrete-entity"; + +export class V6CreateOperation extends MutationOperation { + public readonly inputFields: Map; + public readonly target: ConcreteEntityAdapter | RelationshipAdapter; + public readonly projectionOperations: ReadOperation[] = []; + protected readonly authFilters: AuthorizationFilters[] = []; + private readonly argumentToUnwind: Cypher.Param | Cypher.Property; + private readonly unwindVariable: Cypher.Variable; + private isNested: boolean; + + constructor({ + target, + argumentToUnwind, + }: { + target: ConcreteEntityAdapter | RelationshipAdapter; + argumentToUnwind: Cypher.Param | Cypher.Property; + }) { + super(); + this.target = target; + this.inputFields = new Map(); + this.argumentToUnwind = argumentToUnwind; + this.unwindVariable = new Cypher.Variable(); + this.isNested = target instanceof RelationshipAdapter; + } + public getChildren(): QueryASTNode[] { + return [...this.inputFields.values(), ...this.authFilters, ...this.projectionOperations]; + } + + public addAuthFilters(...filter: AuthorizationFilters[]) { + this.authFilters.push(...filter); + } + /** + * Get and set field methods are utilities to remove duplicate fields between separate inputs + * TODO: This logic should be handled in the factory. + */ + public getField(key: string, attachedTo: "node" | "relationship") { + return this.inputFields.get(`${attachedTo}_${key}`); + } + + public addField(field: InputField, attachedTo: "node" | "relationship") { + if (!this.inputFields.has(field.name)) { + this.inputFields.set(`${attachedTo}_${field.name}`, field); + } + } + + public getUnwindVariable(): Cypher.Variable { + return this.unwindVariable; + } + + public addProjectionOperations(operations: ReadOperation[]) { + this.projectionOperations.push(...operations); + } + + public transpile(context: QueryASTContext): OperationTranspileResult { + const nestedContext = this.getNestedContext(context); + nestedContext.env.topLevelOperationName = "CREATE"; + + if (!nestedContext.hasTarget()) { + throw new Error("No parent node found!"); + } + const target = this.getTarget(); + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: target.entity, + targetOperations: ["CREATE"], + }); + this.inputFields.forEach((field) => { + if (field.attachedTo === "node" && field instanceof PropertyInputField) + checkEntityAuthentication({ + context: nestedContext.neo4jGraphQLContext, + entity: target.entity, + targetOperations: ["CREATE"], + field: field.name, + }); + }); + const unwindClause = new Cypher.Unwind([this.argumentToUnwind, this.unwindVariable]); + + const createClause = new Cypher.Create( + new Cypher.Pattern(nestedContext.target, { labels: getEntityLabels(target, context.neo4jGraphQLContext) }) + ); + const setSubqueries: Cypher.Clause[] = []; + const mergeClause: Cypher.Merge | undefined = this.getMergeClause(nestedContext); + for (const field of this.inputFields.values()) { + if (field.attachedTo === "relationship" && mergeClause) { + mergeClause.set(...field.getSetParams(nestedContext, this.unwindVariable)); + } else if (field.attachedTo === "node") { + createClause.set(...field.getSetParams(nestedContext, this.unwindVariable)); + setSubqueries.push(...field.getSubqueries(nestedContext)); + } + } + + const nestedSubqueries = setSubqueries.flatMap((clause) => [ + new Cypher.With(nestedContext.target, this.unwindVariable), + new Cypher.Call(clause).importWith(nestedContext.target, this.unwindVariable), + ]); + + const authorizationClauses = this.getAuthorizationClauses(nestedContext); + const cardinalityClauses = createRelationshipValidationClauses({ + entity: target, + context: nestedContext.neo4jGraphQLContext, + varName: nestedContext.target, + }); + const unwindCreateClauses = Cypher.concat( + createClause, + mergeClause, + ...nestedSubqueries, + ...authorizationClauses, + ...(cardinalityClauses.length ? [new Cypher.With(nestedContext.target), ...cardinalityClauses] : []) + ); + + let subQueryClause: Cypher.Clause; + if (this.isNested) { + subQueryClause = Cypher.concat( + unwindCreateClauses, + new Cypher.Return([Cypher.collect(Cypher.Null), new Cypher.Variable()]) + ); + } else { + subQueryClause = new Cypher.Call( + Cypher.concat(unwindCreateClauses, new Cypher.Return(nestedContext.target)) + ).importWith(this.unwindVariable); + } + const projectionContext = new QueryASTContext({ + ...nestedContext, + target: nestedContext.target, + returnVariable: new Cypher.NamedVariable("data"), + shouldCollect: true, + }); + const clauses = Cypher.concat(unwindClause, subQueryClause, ...this.getProjectionClause(projectionContext)); + return { projectionExpr: nestedContext.returnVariable, clauses: [clauses] }; + } + + private getMergeClause(context: QueryASTContext): Cypher.Merge | undefined { + if (this.isNested) { + if (!context.source || !context.relationship) { + throw new Error("Transpile error: No source or relationship found!"); + } + if (!(this.target instanceof RelationshipAdapter)) { + throw new Error("Transpile error: Invalid target"); + } + + return new Cypher.Merge( + new Cypher.Pattern(context.source) + .related(context.relationship, { + type: this.target.type, + direction: this.target.cypherDirectionFromRelDirection(), + }) + .to(context.target) + ); + } + } + + private getTarget(): ConcreteEntityAdapter { + if (this.target instanceof RelationshipAdapter) { + const targetAdapter = this.target.target; + assertIsConcreteEntity(targetAdapter); + return targetAdapter; + } + return this.target; + } + + protected getNestedContext(context: QueryASTContext): QueryASTContext { + if (this.target instanceof RelationshipAdapter) { + const target = new Cypher.Node(); + const relationship = new Cypher.Relationship(); + const nestedContext = context.push({ + target, + relationship, + }); + + return nestedContext; + } + + return context; + } + + private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { + const { selections, subqueries, predicates } = this.getAuthFilters(context); + const lastSelection = selections[selections.length - 1]; + if (lastSelection) { + lastSelection.where(Cypher.and(...predicates)); + return [...subqueries, new Cypher.With("*"), ...selections]; + } + if (predicates.length) { + return [...subqueries, new Cypher.With("*").where(Cypher.and(...predicates))]; + } + return [...subqueries]; + } + + private getAuthFilters(context: QueryASTContext): { + selections: (Cypher.With | Cypher.Match)[]; + subqueries: Cypher.Clause[]; + predicates: Cypher.Predicate[]; + } { + const selections: (Cypher.With | Cypher.Match)[] = []; + const subqueries: Cypher.Clause[] = []; + const predicates: Cypher.Predicate[] = []; + for (const authFilter of this.authFilters) { + const extraSelections = authFilter.getSelection(context); + const authSubqueries = authFilter.getSubqueries(context); + const authPredicate = authFilter.getPredicate(context); + if (extraSelections) { + selections.push(...extraSelections); + } + if (authSubqueries) { + subqueries.push(...authSubqueries); + } + if (authPredicate) { + predicates.push(authPredicate); + } + } + return { selections, subqueries, predicates }; + } + + private getProjectionClause(context: QueryASTContext): Cypher.Clause[] { + if (this.projectionOperations.length === 0 && !this.isNested) { + const emptyProjection = new Cypher.Literal("Query cannot conclude with CALL"); + return [new Cypher.Return(emptyProjection)]; + } + return this.projectionOperations.map((operationField) => { + return Cypher.concat(...operationField.transpile(context).clauses); + }); + } +} diff --git a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts index 9534129711..addadb53fa 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts @@ -25,7 +25,7 @@ import { QueryAST } from "../../translate/queryAST/ast/QueryAST"; import type { AttributeAdapter } from "../../schema-model/attribute/model-adapters/AttributeAdapter"; import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { PropertyInputField } from "../../translate/queryAST/ast/input-fields/PropertyInputField"; -import { UnwindCreateOperation } from "../../translate/queryAST/ast/operations/UnwindCreateOperation"; +import { V6CreateOperation } from "../queryIR/CreateOperation"; import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "./resolve-tree-parser/graphql-tree/graphql-tree"; export class CreateOperationFactory { @@ -55,7 +55,7 @@ export class CreateOperationFactory { }: { graphQLTreeCreate: GraphQLTreeCreate; entity: ConcreteEntity; - }): UnwindCreateOperation { + }): V6CreateOperation { const topLevelCreateInput = graphQLTreeCreate.args.input; const targetAdapter = new ConcreteEntityAdapter(entity); const unwindCreate = this.parseTopLevelCreate({ @@ -76,8 +76,8 @@ export class CreateOperationFactory { target: ConcreteEntityAdapter; createInput: GraphQLTreeCreateInput[]; argumentToUnwind: Cypher.Property | Cypher.Param; - }): UnwindCreateOperation { - const unwindCreate = new UnwindCreateOperation({ + }): V6CreateOperation { + const unwindCreate = new V6CreateOperation({ target, argumentToUnwind, }); @@ -98,7 +98,7 @@ export class CreateOperationFactory { }: { target: ConcreteEntityAdapter; createInput: GraphQLTreeCreateInput[]; - unwindCreate: UnwindCreateOperation; + unwindCreate: V6CreateOperation; }) { // TODO: Add autogenerated fields createInput.forEach((inputItem) => { @@ -125,7 +125,7 @@ export class CreateOperationFactory { attachedTo, }: { attribute: AttributeAdapter; - unwindCreate: UnwindCreateOperation; + unwindCreate: V6CreateOperation; attachedTo: "node" | "relationship"; }): PropertyInputField | undefined { if (unwindCreate.getField(attribute.name, attachedTo)) { diff --git a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts index b11129ee5b..a5c8dd3f59 100644 --- a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts +++ b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts @@ -18,16 +18,13 @@ */ import Cypher from "@neo4j/cypher-builder"; -import { V6ReadOperation } from "../../../api-v6/queryIR/ConnectionReadOperation"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; 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 { @@ -82,15 +79,14 @@ export class QueryAST { private getTargetFromOperation(varName?: string): Cypher.Node | undefined { if ( - this.operation instanceof ReadOperation || - this.operation instanceof ConnectionReadOperation || - this.operation instanceof V6ReadOperation || - 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 { From 5d4fccce90ce0b84a35eb39c41a15feb0d1f1993 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 29 Jul 2024 10:00:06 +0100 Subject: [PATCH 126/177] add create with additional labels tests, add projection for create --- .../src/api-v6/queryIR/CreateOperation.ts | 72 ++------------ .../queryIRFactory/CreateOperationFactory.ts | 12 ++- .../queryIRFactory/ReadOperationFactory.ts | 24 +++++ .../graphql-tree/graphql-tree.ts | 3 +- .../resolve-tree-parser/parse-node.ts | 2 +- .../parse-resolve-info-tree.ts | 20 ++-- .../resolvers/translate-create-resolver.ts | 8 +- .../TopLevelEntityTypeNames.ts | 9 +- .../queryAST/ast/input-fields/InputField.ts | 4 +- .../ast/selection/WithWildCardsSelection.ts | 43 ++++++++ .../create-node/create-node.int.test.ts | 81 +++++++++++++++ .../projection/create/create.int.test.ts | 69 +++++++++++++ .../create-node/create-node.test.ts | 98 +++++++++++++++++++ .../tck/projection/create/create.test.ts | 98 +++++++++++++++++++ 14 files changed, 459 insertions(+), 84 deletions(-) create mode 100644 packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.ts create mode 100644 packages/graphql/tests/api-v6/integration/combinations/create-node/create-node.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/projection/create/create.int.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/combinations/create-node/create-node.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/projection/create/create.test.ts diff --git a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts index a132b9fe14..eb1c07f7f9 100644 --- a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts @@ -20,24 +20,20 @@ import Cypher from "@neo4j/cypher-builder"; import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; -import { checkEntityAuthentication } from "../../translate/authorization/check-authentication"; import { createRelationshipValidationClauses } from "../../translate/create-relationship-validation-clauses"; import { QueryASTContext } from "../../translate/queryAST/ast/QueryASTContext"; import type { QueryASTNode } from "../../translate/queryAST/ast/QueryASTNode"; -import type { AuthorizationFilters } from "../../translate/queryAST/ast/filters/authorization-filters/AuthorizationFilters"; import type { InputField } from "../../translate/queryAST/ast/input-fields/InputField"; -import { PropertyInputField } from "../../translate/queryAST/ast/input-fields/PropertyInputField"; -import type { ReadOperation } from "../../translate/queryAST/ast/operations/ReadOperation"; 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 { assertIsConcreteEntity } from "../../translate/queryAST/utils/is-concrete-entity"; +import type { V6ReadOperation } from "./ConnectionReadOperation"; export class V6CreateOperation extends MutationOperation { public readonly inputFields: Map; public readonly target: ConcreteEntityAdapter | RelationshipAdapter; - public readonly projectionOperations: ReadOperation[] = []; - protected readonly authFilters: AuthorizationFilters[] = []; + public readonly projectionOperations: V6ReadOperation[] = []; private readonly argumentToUnwind: Cypher.Param | Cypher.Property; private readonly unwindVariable: Cypher.Variable; private isNested: boolean; @@ -56,13 +52,11 @@ export class V6CreateOperation extends MutationOperation { this.unwindVariable = new Cypher.Variable(); this.isNested = target instanceof RelationshipAdapter; } + public getChildren(): QueryASTNode[] { - return [...this.inputFields.values(), ...this.authFilters, ...this.projectionOperations]; + return [...this.inputFields.values(), ...this.projectionOperations]; } - public addAuthFilters(...filter: AuthorizationFilters[]) { - this.authFilters.push(...filter); - } /** * Get and set field methods are utilities to remove duplicate fields between separate inputs * TODO: This logic should be handled in the factory. @@ -81,7 +75,7 @@ export class V6CreateOperation extends MutationOperation { return this.unwindVariable; } - public addProjectionOperations(operations: ReadOperation[]) { + public addProjectionOperations(operations: V6ReadOperation[]) { this.projectionOperations.push(...operations); } @@ -93,20 +87,7 @@ export class V6CreateOperation extends MutationOperation { throw new Error("No parent node found!"); } const target = this.getTarget(); - checkEntityAuthentication({ - context: nestedContext.neo4jGraphQLContext, - entity: target.entity, - targetOperations: ["CREATE"], - }); - this.inputFields.forEach((field) => { - if (field.attachedTo === "node" && field instanceof PropertyInputField) - checkEntityAuthentication({ - context: nestedContext.neo4jGraphQLContext, - entity: target.entity, - targetOperations: ["CREATE"], - field: field.name, - }); - }); + const unwindClause = new Cypher.Unwind([this.argumentToUnwind, this.unwindVariable]); const createClause = new Cypher.Create( @@ -127,8 +108,6 @@ export class V6CreateOperation extends MutationOperation { new Cypher.With(nestedContext.target, this.unwindVariable), new Cypher.Call(clause).importWith(nestedContext.target, this.unwindVariable), ]); - - const authorizationClauses = this.getAuthorizationClauses(nestedContext); const cardinalityClauses = createRelationshipValidationClauses({ entity: target, context: nestedContext.neo4jGraphQLContext, @@ -138,7 +117,6 @@ export class V6CreateOperation extends MutationOperation { createClause, mergeClause, ...nestedSubqueries, - ...authorizationClauses, ...(cardinalityClauses.length ? [new Cypher.With(nestedContext.target), ...cardinalityClauses] : []) ); @@ -207,44 +185,6 @@ export class V6CreateOperation extends MutationOperation { return context; } - private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { - const { selections, subqueries, predicates } = this.getAuthFilters(context); - const lastSelection = selections[selections.length - 1]; - if (lastSelection) { - lastSelection.where(Cypher.and(...predicates)); - return [...subqueries, new Cypher.With("*"), ...selections]; - } - if (predicates.length) { - return [...subqueries, new Cypher.With("*").where(Cypher.and(...predicates))]; - } - return [...subqueries]; - } - - private getAuthFilters(context: QueryASTContext): { - selections: (Cypher.With | Cypher.Match)[]; - subqueries: Cypher.Clause[]; - predicates: Cypher.Predicate[]; - } { - const selections: (Cypher.With | Cypher.Match)[] = []; - const subqueries: Cypher.Clause[] = []; - const predicates: Cypher.Predicate[] = []; - for (const authFilter of this.authFilters) { - const extraSelections = authFilter.getSelection(context); - const authSubqueries = authFilter.getSubqueries(context); - const authPredicate = authFilter.getPredicate(context); - if (extraSelections) { - selections.push(...extraSelections); - } - if (authSubqueries) { - subqueries.push(...authSubqueries); - } - if (authPredicate) { - predicates.push(authPredicate); - } - } - return { selections, subqueries, predicates }; - } - private getProjectionClause(context: QueryASTContext): Cypher.Clause[] { if (this.projectionOperations.length === 0 && !this.isNested) { const emptyProjection = new Cypher.Literal("Query cannot conclude with CALL"); diff --git a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts index addadb53fa..29cd2c8343 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts @@ -26,13 +26,16 @@ import type { AttributeAdapter } from "../../schema-model/attribute/model-adapte import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { PropertyInputField } from "../../translate/queryAST/ast/input-fields/PropertyInputField"; import { V6CreateOperation } from "../queryIR/CreateOperation"; +import { ReadOperationFactory } from "./ReadOperationFactory"; import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "./resolve-tree-parser/graphql-tree/graphql-tree"; export class CreateOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; + private readFactory: ReadOperationFactory; constructor(schemaModel: Neo4jGraphQLSchemaModel) { this.schemaModel = schemaModel; + this.readFactory = new ReadOperationFactory(schemaModel); } public createAST({ @@ -63,8 +66,13 @@ export class CreateOperationFactory { createInput: topLevelCreateInput, argumentToUnwind: new Cypher.Param(topLevelCreateInput), }); - - unwindCreate.addProjectionOperations([]); + if (graphQLTreeCreate.fields) { + const projection = this.readFactory.generateMutationOperation({ + graphQLTreeNode: graphQLTreeCreate, + entity, + }); + unwindCreate.addProjectionOperations([projection]); + } return unwindCreate; } diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 02a6ca3693..0366997dd1 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -41,6 +41,7 @@ import { filterTruthy } from "../../utils/utils"; import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { FilterFactory } from "./FilterFactory"; +import { WithWildCardsSelection } from "../../translate/queryAST/ast/selection/WithWildCardsSelection"; import type { GraphQLTreePoint } from "./resolve-tree-parser/graphql-tree/attributes"; import type { GraphQLTree, @@ -69,6 +70,29 @@ export class ReadOperationFactory { return new QueryAST(operation); } + public generateMutationOperation({ + 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, 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 index 5ebededfdf..7cac692a85 100644 --- 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 @@ -26,9 +26,8 @@ import type { GraphQLWhere, GraphQLWhereTopLevel } from "./where"; // TODO GraphQLTreeCreateInput should be a union of PrimitiveTypes and relationship fields export type GraphQLTreeCreateInput = Record; -export interface GraphQLTreeCreate extends GraphQLTreeElement { +export interface GraphQLTreeCreate extends GraphQLTreeNode { name: string; - fields: Record; args: { input: GraphQLTreeCreateInput[]; }; 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 index e4420ae7aa..d1b0c61e9e 100644 --- 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 @@ -38,7 +38,7 @@ export function parseNode(resolveTree: ResolveTree, targetNode: ConcreteEntity): }; } -function getNodeFields( +export function getNodeFields( fields: Record, targetNode: ConcreteEntity ): Record { 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 index e15eb34c7c..d07bb99e02 100644 --- 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 @@ -35,6 +35,7 @@ import type { GraphQLTreeReadOperation, } 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({ @@ -65,17 +66,24 @@ export function parseResolveInfoTreeCreate({ resolveTree: ResolveTree; entity: ConcreteEntity; }): GraphQLTreeCreate { - const createResponse = findFieldByName(resolveTree, entity.typeNames.createResponse, entity.typeNames.queryField); + const entityTypes = entity.typeNames; + const createResponse = findFieldByName(resolveTree, entityTypes.createResponse, entityTypes.queryField); const createArgs = parseCreateOperationArgsTopLevel(resolveTree.args); - console.log("createResponse", createResponse); - console.log("createInput", createArgs); + 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, - fields: { - // TODO: add tree for selection - }, args: createArgs, + fields, }; } diff --git a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts index b1bbec73ee..7e35380bc2 100644 --- a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts @@ -48,7 +48,11 @@ export function generateCreateResolver({ entity }: { entity: ConcreteEntity }) { context, info, }); - - return executeResult.records[0]?.this; + // TODO: AVOID MAPPING here + return { + [entity.typeNames.queryField]: executeResult.records[0]?.data.connection.edges.map( + (edge: any) => edge.node + ), + }; }; } 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 index 411fb45b23..77a11fcde8 100644 --- 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 @@ -28,10 +28,6 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { return plural(this.entityName); } - public get createNode(): string { - return `${upperFirst(this.entityName)}CreateNode`; - } - public get connectionOperation(): string { return `${this.entityName}Operation`; } @@ -76,6 +72,11 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { public get createField(): string { return `create${upperFirst(plural(this.entityName))}`; } + + public get createNode(): string { + return `${upperFirst(this.entityName)}CreateNode`; + } + // TODO: do we need to memoize the upperFirst/plural calls? public get createInput(): string { return `${upperFirst(this.entityName)}CreateInput`; 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/selection/WithWildCardsSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.ts new file mode 100644 index 0000000000..cb7fb63b6c --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.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 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, + + /* new QueryASTContext({ + target: new Cypher.Node(), // This is a dummy node, it will be replaced by the actual node in the next step + neo4jGraphQLContext: context.neo4jGraphQLContext, + }) */ + }; + } +} diff --git a/packages/graphql/tests/api-v6/integration/combinations/create-node/create-node.int.test.ts b/packages/graphql/tests/api-v6/integration/combinations/create-node/create-node.int.test.ts new file mode 100644 index 0000000000..d879b5d887 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/combinations/create-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("Top-Level Create with labels", () => { + 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/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..2b72152a62 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/projection/create/create.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 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 }, + ]), + }, + }); + }); +}); 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/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 + } + } + ] + }" + `); + }); +}); From 1d4ca2424a5f4ba3c38a6b9f44184862949a941d Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 29 Jul 2024 11:28:59 +0100 Subject: [PATCH 127/177] add create info resolver and @alias tests --- .../resolvers/translate-create-resolver.ts | 5 +- .../directives/alias/create-alias.int.test.ts | 69 +++++++++++++ .../projection/create/create.int.test.ts | 24 +++++ .../tck/directives/alias/create-alias.test.ts | 98 +++++++++++++++++++ 4 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/directives/alias/create-alias.int.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/directives/alias/create-alias.test.ts diff --git a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts index 7e35380bc2..fb3bba770f 100644 --- a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts @@ -35,7 +35,6 @@ export function generateCreateResolver({ entity }: { entity: ConcreteEntity }) { const resolveTree = getNeo4jResolveTree(info, { args }); context.resolveTree = resolveTree; const graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity }); - console.log("graphQLTree", JSON.stringify(graphQLTreeCreate, null, 2)); const { cypher, params } = translateCreateResolver({ context: context, graphQLTreeCreate, @@ -48,11 +47,13 @@ export function generateCreateResolver({ entity }: { entity: ConcreteEntity }) { context, info, }); - // TODO: AVOID MAPPING here return { [entity.typeNames.queryField]: executeResult.records[0]?.data.connection.edges.map( (edge: any) => edge.node ), + info: { + ...executeResult.statistics, + }, }; }; } 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..fe3952705e --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/alias/create-alias.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 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 { + 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} { + 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 }, + ]), + }, + }); + }); +}); 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 index 2b72152a62..a4d0d91564 100644 --- 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 @@ -66,4 +66,28 @@ describe("Top-Level Create projection", () => { }, }); }); + + 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/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 + } + } + ] + }" + `); + }); +}); From dfb573869869f167e1fb15bb1de29d6e3c177d55 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 29 Jul 2024 16:26:24 +0100 Subject: [PATCH 128/177] support BigInt on create, create BigInt static type --- .../src/api-v6/queryIR/CreateOperation.ts | 2 +- .../schema-generation/SchemaBuilderTypes.ts | 5 + .../schema-types/StaticSchemaTypes.ts | 9 +- .../TopLevelCreateSchemaTypes.ts | 15 +- .../types/array/number-array.int.test.ts | 102 ++++++++++++++ .../cartesian-point-2d.int.test.ts | 112 +++++++++++++++ .../cartesian-point-3d.int.test.ts | 113 +++++++++++++++ .../create/types/datetime.int.test.ts | 131 ++++++++++++++++++ .../create/types/number.int.test.ts | 73 ++++++++++ .../create/types/point/point-2d.int.test.ts | 112 +++++++++++++++ .../create/types/point/point-3d.int.test.ts | 112 +++++++++++++++ .../api-v6/schema/directives/relayId.test.ts | 2 +- .../tests/api-v6/schema/relationship.test.ts | 8 +- .../tests/api-v6/schema/simple.test.ts | 8 +- .../tests/api-v6/tck/create/create.test.ts | 11 +- 15 files changed, 795 insertions(+), 20 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-2d.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-3d.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/datetime.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/number.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/point/point-2d.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/point/point-3d.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts index eb1c07f7f9..81a15c754e 100644 --- a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts @@ -170,7 +170,7 @@ export class V6CreateOperation extends MutationOperation { return this.target; } - protected getNestedContext(context: QueryASTContext): QueryASTContext { + private getNestedContext(context: QueryASTContext): QueryASTContext { if (this.target instanceof RelationshipAdapter) { const target = new Cypher.Node(); const relationship = new Cypher.Relationship(); diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts index cf40e00391..59cc74fe87 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts @@ -21,6 +21,7 @@ import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } fr import type { SchemaComposer } from "graphql-compose"; import { ScalarTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; +import { GraphQLBigInt } from "../../graphql/scalars"; export class SchemaBuilderTypes { private composer: SchemaComposer; @@ -43,6 +44,10 @@ export class SchemaBuilderTypes { 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); } 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 index f0a9a4a293..d6e2fa33e8 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -32,7 +32,6 @@ import { CartesianPoint } from "../../../graphql/objects/CartesianPoint"; import { Point } from "../../../graphql/objects/Point"; import * as Scalars from "../../../graphql/scalars"; import { - GraphQLBigInt, GraphQLDate, GraphQLDateTime, GraphQLDuration, @@ -415,7 +414,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("BigIntListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLBigInt), + equals: this.schemaBuilder.types.bigInt.List, }, }; }); @@ -424,7 +423,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("BigIntListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLBigInt)), + equals: this.schemaBuilder.types.bigInt.NonNull.List, }, }; }); @@ -435,8 +434,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createNumericOperators(GraphQLBigInt), - in: toGraphQLList(toGraphQLNonNull(GraphQLBigInt)), + ...this.createNumericOperators(this.schemaBuilder.types.bigInt), + in: this.schemaBuilder.types.bigInt.NonNull.List, }, }; }); 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 index e8a883a0bb..79f46a8e6b 100644 --- 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 @@ -20,7 +20,11 @@ import type { GraphQLScalarType } from "graphql"; import type { InputTypeComposer, ScalarTypeComposer } from "graphql-compose"; import type { Attribute } from "../../../../schema-model/attribute/Attribute"; -import { GraphQLBuiltInScalarType, ScalarType } from "../../../../schema-model/attribute/AttributeType"; +import { + GraphQLBuiltInScalarType, + Neo4jGraphQLNumberType, + 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"; @@ -47,12 +51,12 @@ export class TopLevelCreateSchemaTypes { this.schemaBuilder = schemaBuilder; this.schemaTypes = schemaTypes; } - + public get createInput(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.createInput, (_itc: InputTypeComposer) => { return { fields: { - node: this.createNode, + node: this.createNode.NonNull, }, }; }); @@ -63,7 +67,7 @@ export class TopLevelCreateSchemaTypes { return { fields: { ...this.getInputFields([...this.entity.attributes.values()]), - _emptyInput: this.schemaBuilder.types.boolean, + _emptyInput: this.schemaBuilder.types.boolean, // TODO: discuss if we want handle empty input in a different way. }, }; }); @@ -110,6 +114,9 @@ export class TopLevelCreateSchemaTypes { case GraphQLBuiltInScalarType.Float: { return this.schemaBuilder.types.float; } + case Neo4jGraphQLNumberType.BigInt: { + return this.schemaBuilder.types.bigInt; + } } } } diff --git a/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts new file mode 100644 index 0000000000..433495b34b --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/types/array/number-array.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 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!]! + yearNullable: [Int]! + ratingNullable: [Float]! + viewingsNullable: [BigInt]! + + } + `; + await testHelper.initNeo4jGraphQL({ typeDefs }); + + await testHelper.executeCypher(` + CREATE (movie:${Movie} { + yearNullable: [1999], + ratingNullable: [4.0], + viewingsNullable: [4294967297], + 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 + yearNullable + viewings + viewingsNullable + rating + ratingNullable + } + } + + } + } + } + `; + + const gqlResult = await testHelper.executeGraphQL(query); + expect(gqlResult.errors).toBeFalsy(); + expect(gqlResult.data).toEqual({ + [Movie.plural]: { + connection: { + edges: [ + { + node: { + year: [1999], + yearNullable: [1999], + rating: [4.0], + ratingNullable: [4.0], + viewings: ["4294967297"], + viewingsNullable: ["4294967297"], + }, + }, + ], + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-2d.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-2d.int.test.ts new file mode 100644 index 0000000000..6ea0a622e0 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/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 }; + const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; + + 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(); + }); + // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-2d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + y + x + z + crs + # srid + } + } + } + } + + } + } + `; + + const equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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/create/types/cartesian-point/cartesian-point-3d.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-3d.int.test.ts new file mode 100644 index 0000000000..761203a565 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/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(); + }); + // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-3d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + y + x + z + crs + # srid + } + } + } + } + + } + } + `; + + const equalsResult = await testHelper.executeGraphQL(query); + + expect(equalsResult.errors).toBeFalsy(); + expect(equalsResult.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/create/types/datetime.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/datetime.int.test.ts new file mode 100644 index 0000000000..590ff22e25 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/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/create/types/number.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/number.int.test.ts new file mode 100644 index 0000000000..bd152bf92f --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/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("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 int and float 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/create/types/point/point-2d.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/point/point-2d.int.test.ts new file mode 100644 index 0000000000..3298b206b2 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/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 }; + const Rome = { longitude: 12.496365, latitude: 41.902782 }; + + 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(); + }); + // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-2d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + # srid + } + } + } + } + + } + } + `; + + 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", + // 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/create/types/point/point-3d.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/point/point-3d.int.test.ts new file mode 100644 index 0000000000..1ce749c425 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/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(); + }); + // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-3d point", async () => { + const query = /* GraphQL */ ` + query { + ${Location.plural} { + connection { + edges { + node { + id + value { + latitude + longitude + height + crs + # srid + } + } + } + } + + } + } + `; + + 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: London.height, + crs: "wgs-84-3d", + // srid: 4326, + }, + }, + }, + { + node: { + id: "2", + value: { + latitude: Rome.latitude, + longitude: Rome.longitude, + height: Rome.height, + crs: "wgs-84-3d", + //srid: 4326, + }, + }, + }, + ]), + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index c2d70becad..42719b5beb 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -76,7 +76,7 @@ describe("RelayId", () => { } input MovieCreateInput { - node: MovieCreateNode + node: MovieCreateNode! } input MovieCreateNode { diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 4515e0831c..05c4258b32 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -65,7 +65,7 @@ describe("Relationships", () => { } input ActorCreateInput { - node: ActorCreateNode + node: ActorCreateNode! } input ActorCreateNode { @@ -232,7 +232,7 @@ describe("Relationships", () => { } input MovieCreateInput { - node: MovieCreateNode + node: MovieCreateNode! } input MovieCreateNode { @@ -369,7 +369,7 @@ describe("Relationships", () => { } input ActorCreateInput { - node: ActorCreateNode + node: ActorCreateNode! } input ActorCreateNode { @@ -554,7 +554,7 @@ describe("Relationships", () => { } input MovieCreateInput { - node: MovieCreateNode + node: MovieCreateNode! } input MovieCreateNode { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 6f89f3d5e6..f3c465ad05 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -59,7 +59,7 @@ describe("Simple Aura-API", () => { } input MovieCreateInput { - node: MovieCreateNode + node: MovieCreateNode! } input MovieCreateNode { @@ -171,7 +171,7 @@ describe("Simple Aura-API", () => { } input ActorCreateInput { - node: ActorCreateNode + node: ActorCreateNode! } input ActorCreateNode { @@ -230,7 +230,7 @@ describe("Simple Aura-API", () => { } input MovieCreateInput { - node: MovieCreateNode + node: MovieCreateNode! } input MovieCreateNode { @@ -344,7 +344,7 @@ describe("Simple Aura-API", () => { } input MovieCreateInput { - node: MovieCreateNode + node: MovieCreateNode! } input MovieCreateNode { diff --git a/packages/graphql/tests/api-v6/tck/create/create.test.ts b/packages/graphql/tests/api-v6/tck/create/create.test.ts index 5793e471d6..d2c0fa2fce 100644 --- a/packages/graphql/tests/api-v6/tck/create/create.test.ts +++ b/packages/graphql/tests/api-v6/tck/create/create.test.ts @@ -65,7 +65,16 @@ describe("Top-Level Create", () => { this1.released = var0.released RETURN this1 } - RETURN \\"Query cannot conclude with CALL\\"" + 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(` From eb4051e8e922d021c4bb53ea4fd64925e892a2cc Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 30 Jul 2024 14:21:57 +0100 Subject: [PATCH 129/177] add temporal types to the create input, improve temporal types handling --- .../schema-generation/SchemaBuilderTypes.ts | 35 +++++++- .../schema-types/StaticSchemaTypes.ts | 58 +++++------- .../schema-types/TopLevelEntitySchemaTypes.ts | 8 +- .../filter-schema-types/FilterSchemaTypes.ts | 8 ++ .../TopLevelCreateSchemaTypes.ts | 88 ++++++++++++++++--- .../utils/to-graphql-list.ts | 25 ------ .../utils/to-graphql-non-null.ts | 25 ------ .../schema-model/attribute/AttributeType.ts | 27 ++++-- .../attribute/AttributeTypeHelper.ts | 30 +++---- .../schema-model/parser/parse-attribute.ts | 7 +- .../create/types/datetime.int.test.ts | 70 ++++++--------- .../api-v6/schema/directives/relayId.test.ts | 2 +- .../tests/api-v6/schema/types/array.test.ts | 52 ++++++++++- .../tests/api-v6/schema/types/scalars.test.ts | 28 +++--- .../tests/api-v6/schema/types/spatial.test.ts | 4 +- .../api-v6/schema/types/temporals.test.ts | 16 +++- 16 files changed, 292 insertions(+), 191 deletions(-) delete mode 100644 packages/graphql/src/api-v6/schema-generation/utils/to-graphql-list.ts delete mode 100644 packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts index 59cc74fe87..df1d6b4f1d 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts @@ -21,7 +21,15 @@ import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } fr import type { SchemaComposer } from "graphql-compose"; import { ScalarTypeComposer } from "graphql-compose"; import { Memoize } from "typescript-memoize"; -import { GraphQLBigInt } from "../../graphql/scalars"; +import { + GraphQLBigInt, + GraphQLDate, + GraphQLDateTime, + GraphQLDuration, + GraphQLLocalDateTime, + GraphQLLocalTime, + GraphQLTime, +} from "../../graphql/scalars"; export class SchemaBuilderTypes { private composer: SchemaComposer; @@ -34,7 +42,6 @@ export class SchemaBuilderTypes { public get id(): ScalarTypeComposer { return new ScalarTypeComposer(GraphQLID, this.composer); } - @Memoize() public get int(): ScalarTypeComposer { return new ScalarTypeComposer(GraphQLInt, this.composer); @@ -55,4 +62,28 @@ export class SchemaBuilderTypes { 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); + } } 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 index d6e2fa33e8..b7dde3a654 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -31,17 +31,7 @@ import { Memoize } from "typescript-memoize"; import { CartesianPoint } from "../../../graphql/objects/CartesianPoint"; import { Point } from "../../../graphql/objects/Point"; import * as Scalars from "../../../graphql/scalars"; -import { - GraphQLDate, - GraphQLDateTime, - GraphQLDuration, - GraphQLLocalDateTime, - GraphQLLocalTime, - GraphQLTime, -} from "../../../graphql/scalars"; import type { SchemaBuilder } from "../SchemaBuilder"; -import { toGraphQLList } from "../utils/to-graphql-list"; -import { toGraphQLNonNull } from "../utils/to-graphql-non-null"; export class StaticSchemaTypes { private schemaBuilder: SchemaBuilder; @@ -147,8 +137,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - in: toGraphQLList(toGraphQLNonNull(GraphQLDate)), - ...this.createNumericOperators(GraphQLDate), + in: this.schemaBuilder.types.date.NonNull.List, + ...this.createNumericOperators(this.schemaBuilder.types.date), }, }; }); @@ -158,7 +148,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("DateListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLDate), + equals: this.schemaBuilder.types.date.List, }, }; }); @@ -167,7 +157,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("DateListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLDate)), + equals: this.schemaBuilder.types.date.NonNull.List, }, }; }); @@ -178,8 +168,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - in: toGraphQLList(toGraphQLNonNull(GraphQLDateTime)), - ...this.createNumericOperators(GraphQLDateTime), + in: this.schemaBuilder.types.dateTime.NonNull.List, + ...this.createNumericOperators(this.schemaBuilder.types.dateTime), }, }; }); @@ -190,7 +180,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("DateTimeListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLDateTime), + equals: this.schemaBuilder.types.dateTime.List, }, }; }); @@ -199,7 +189,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("DateTimeListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLDateTime)), + equals: this.schemaBuilder.types.dateTime.NonNull.List, }, }; }); @@ -210,8 +200,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createNumericOperators(GraphQLLocalDateTime), - in: toGraphQLList(toGraphQLNonNull(GraphQLLocalDateTime)), + ...this.createNumericOperators(this.schemaBuilder.types.localDateTime), + in: this.schemaBuilder.types.localDateTime.NonNull.List, }, }; }); @@ -222,7 +212,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("LocalDateTimeListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLLocalDateTime), + equals: this.schemaBuilder.types.localDateTime.List, }, }; }); @@ -231,7 +221,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("LocalDateTimeListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLLocalDateTime)), + equals: this.schemaBuilder.types.localDateTime.NonNull.List, }, }; }); @@ -242,8 +232,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createNumericOperators(GraphQLDuration), - in: toGraphQLList(toGraphQLNonNull(GraphQLDuration)), + ...this.createNumericOperators(this.schemaBuilder.types.duration), + in: this.schemaBuilder.types.duration.NonNull.List, }, }; }); @@ -254,7 +244,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("DurationListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLDuration), + equals: this.schemaBuilder.types.duration.List, }, }; }); @@ -263,7 +253,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("DurationListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLDuration)), + equals: this.schemaBuilder.types.duration.NonNull.List, }, }; }); @@ -274,8 +264,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createNumericOperators(GraphQLTime), - in: toGraphQLList(toGraphQLNonNull(GraphQLTime)), + ...this.createNumericOperators(this.schemaBuilder.types.time), + in: this.schemaBuilder.types.time.NonNull.List, }, }; }); @@ -286,7 +276,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("TimeListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLTime), + equals: this.schemaBuilder.types.time.List, }, }; }); @@ -295,7 +285,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("TimeListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLTime)), + equals: this.schemaBuilder.types.time.NonNull.List, }, }; }); @@ -306,8 +296,8 @@ class StaticFilterTypes { return { fields: { ...this.createBooleanOperators(itc), - ...this.createNumericOperators(GraphQLLocalTime), - in: toGraphQLList(toGraphQLNonNull(GraphQLLocalTime)), + ...this.createNumericOperators(this.schemaBuilder.types.localTime), + in: this.schemaBuilder.types.localTime.NonNull.List, }, }; }); @@ -318,7 +308,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("LocalTimeListWhereNullable", () => { return { fields: { - equals: toGraphQLList(GraphQLLocalTime), + equals: this.schemaBuilder.types.localTime.List, }, }; }); @@ -327,7 +317,7 @@ class StaticFilterTypes { return this.schemaBuilder.getOrCreateInputType("LocalTimeListWhere", () => { return { fields: { - equals: toGraphQLList(toGraphQLNonNull(GraphQLLocalTime)), + equals: this.schemaBuilder.types.localTime.NonNull.List, }, }; }); 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 index 87d6ba15d8..7a7a433237 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -227,9 +227,9 @@ export class TopLevelEntitySchemaTypes { private getSortableFields(): Attribute[] { return this.getFields().filter( (field) => - field.type.name === GraphQLBuiltInScalarType[GraphQLBuiltInScalarType[field.type.name]] || - field.type.name === Neo4jGraphQLNumberType[Neo4jGraphQLNumberType[field.type.name]] || - field.type.name === Neo4jGraphQLTemporalType[Neo4jGraphQLTemporalType[field.type.name]] + field.type.name in GraphQLBuiltInScalarType || + field.type.name in Neo4jGraphQLNumberType || + field.type.name in Neo4jGraphQLTemporalType ); } @@ -326,7 +326,7 @@ export class TopLevelEntitySchemaTypes { } } -function typeToResolver(type: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType): GraphQLResolver | undefined { +function typeToResolver(type: Neo4jGraphQLScalarType): GraphQLResolver | undefined { switch (type) { case GraphQLBuiltInScalarType.Int: case GraphQLBuiltInScalarType.Float: 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 index c5d895ef6b..74c29551bb 100644 --- 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 @@ -25,6 +25,7 @@ import { ListType, Neo4jGraphQLNumberType, Neo4jGraphQLTemporalType, + Neo4jTemporalType, ScalarType, } from "../../../../schema-model/attribute/AttributeType"; import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; @@ -113,6 +114,9 @@ export abstract class FilterSchemaTypes { const inputFields: Array<[string, InputTypeComposer | GraphQLScalarType] | []> = filterTruthy( attributes.map((attribute) => { - const inputField = this.attributeToInputField(attribute); + const inputField = this.attributeToInputField(attribute.type); if (inputField) { return [attribute.name, inputField]; } @@ -85,9 +89,15 @@ export class TopLevelCreateSchemaTypes { return Object.fromEntries(inputFields); } - private attributeToInputField(attribute: Attribute): any { - if (attribute.type instanceof ScalarType) { - return this.createBuiltInFieldInput(attribute.type); + private attributeToInputField(type: AttributeType): any { + if (type instanceof ListType) { + return this.attributeToInputField(type.ofType).List; + } + if (type instanceof ScalarType) { + return this.createBuiltInFieldInput(type); + } + if (type instanceof Neo4jTemporalType) { + return this.createTemporalFieldInput(type); } /* const isList = attribute.type instanceof ListType; const wrappedType = isList ? attribute.type.ofType : attribute.type; @@ -96,27 +106,79 @@ export class TopLevelCreateSchemaTypes { } */ } - private createBuiltInFieldInput(type: ScalarType): ScalarTypeComposer | undefined { - // TODO: add required sign and other types. + private createBuiltInFieldInput(type: ScalarType): ScalarTypeComposer | NonNullComposer { + let builtInType: ScalarTypeComposer; switch (type.name) { case GraphQLBuiltInScalarType.Boolean: { - return this.schemaBuilder.types.boolean; + builtInType = this.schemaBuilder.types.boolean; + break; } case GraphQLBuiltInScalarType.String: { - return this.schemaBuilder.types.string; + builtInType = this.schemaBuilder.types.string; + break; } case GraphQLBuiltInScalarType.ID: { - return this.schemaBuilder.types.id; + builtInType = this.schemaBuilder.types.id; + break; } case GraphQLBuiltInScalarType.Int: { - return this.schemaBuilder.types.int; + builtInType = this.schemaBuilder.types.int; + break; } case GraphQLBuiltInScalarType.Float: { - return this.schemaBuilder.types.float; + builtInType = this.schemaBuilder.types.float; + break; } case Neo4jGraphQLNumberType.BigInt: { - return this.schemaBuilder.types.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 + ): ScalarTypeComposer | NonNullComposer { + 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; } } diff --git a/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-list.ts b/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-list.ts deleted file mode 100644 index f0faa50b05..0000000000 --- a/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-list.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { GraphQLType } from "graphql"; -import { GraphQLList } from "graphql"; - -export function toGraphQLList(type: T): GraphQLList { - return new GraphQLList(type); -} diff --git a/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts b/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts deleted file mode 100644 index d1c54a0408..0000000000 --- a/packages/graphql/src/api-v6/schema-generation/utils/to-graphql-non-null.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { GraphQLType } from "graphql"; -import { GraphQLNonNull } from "graphql"; - -export function toGraphQLNonNull(type: T): GraphQLNonNull { - return new GraphQLNonNull(type); -} diff --git a/packages/graphql/src/schema-model/attribute/AttributeType.ts b/packages/graphql/src/schema-model/attribute/AttributeType.ts index a8e2882c57..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,13 +45,20 @@ 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: Neo4jGraphQLScalarType, isRequired: boolean) { + this.name = name; + this.isRequired = isRequired; + } +} + +export class Neo4jTemporalType { + public readonly name: Neo4jGraphQLTemporalType; public readonly isRequired: boolean; - constructor(name: GraphQLBuiltInScalarType | Neo4jGraphQLScalarType, isRequired: boolean) { + constructor(name: Neo4jGraphQLTemporalType, isRequired: boolean) { this.name = name; this.isRequired = isRequired; } @@ -150,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 d63fb5641f..f0c42efa6b 100644 --- a/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts +++ b/packages/graphql/src/schema-model/attribute/AttributeTypeHelper.ts @@ -26,9 +26,7 @@ import { Neo4jGraphQLNumberType, Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, - Neo4jSpatialType, ObjectType, - ScalarType, UnionType, UserScalarType, } from "./AttributeType"; @@ -58,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 Neo4jSpatialType && type.name === Neo4jGraphQLSpatialType.CartesianPoint; + return Neo4jGraphQLSpatialType[type.name] === Neo4jGraphQLSpatialType.CartesianPoint; } public isPoint(options = this.assertionOptions): boolean { const type = this.getTypeForAssertion(options.includeLists); - return type instanceof Neo4jSpatialType && type.name === Neo4jGraphQLSpatialType.Point; + 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/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 13c5f0b91a..4a355cfe2f 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -33,6 +33,7 @@ import { Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, Neo4jSpatialType, + Neo4jTemporalType, ObjectType, ScalarType, UnionType, @@ -98,6 +99,8 @@ 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 Neo4jSpatialType(typeNode.name.value, isRequired); } else if (isCartesianPoint(typeNode.name.value)) { @@ -164,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/tests/api-v6/integration/create/types/datetime.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/datetime.int.test.ts index 590ff22e25..bf5a891a05 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/datetime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/datetime.int.test.ts @@ -17,69 +17,53 @@ * limitations under the License. */ -import neo4jDriver from "neo4j-driver"; import type { UniqueType } from "../../../../utils/graphql-types"; import { TestHelper } from "../../../../utils/tests-helper"; -describe("DateTime", () => { +describe.skip("DateTime", () => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; - beforeEach(() => { + 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 return a movie created with a datetime parameter", async () => { - const typeDefs = /* GraphQL */ ` - type ${Movie.name} @node { - datetime: DateTime - } - `; - - const date = new Date(1716904582368); + test.only("should return a movie created with a datetime parameter", async () => { + const date1 = new Date(1716904582368); + const date2 = new Date(1796904582368); - 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 { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { node: { datetime: "${date1.toISOString()}" } } + { node: { datetime: "${date2.toISOString()}" } } + ]) { ${Movie.plural} { - connection { - edges { - node { - datetime - } - } - } + dateTime } } - `; + } + `; - const gqlResult = await testHelper.executeGraphQL(query); + const gqlResult = await testHelper.executeGraphQL(mutation); expect(gqlResult.errors).toBeFalsy(); - expect((gqlResult.data as any)[Movie.plural]).toEqual({ - connection: { - edges: [ - { - node: { - datetime: date.toISOString(), - }, - }, - ], + expect(gqlResult.data).toEqual({ + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { dateTime: date1.toISOString() }, + { dateTime: date1.toISOString() }, + ]), }, }); }); diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index 42719b5beb..f37fe5a752 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -81,7 +81,7 @@ describe("RelayId", () => { input MovieCreateNode { _emptyInput: Boolean - dbId: ID + dbId: ID! title: 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 index dfe4eaabf9..964a03366d 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -264,11 +264,35 @@ describe("Scalars", () => { } input NodeTypeCreateInput { - node: NodeTypeCreateNode + node: NodeTypeCreateNode! } input NodeTypeCreateNode { _emptyInput: Boolean + bigIntList: [BigInt!] + bigIntListNullable: [BigInt] + booleanList: [Boolean!] + booleanListNullable: [Boolean] + dateList: [Date!] + dateListNullable: [Date] + dateTimeList: [DateTime!] + dateTimeListNullable: [DateTime] + durationList: [Duration!] + durationListNullable: [Duration] + floatList: [Float!] + floatListNullable: [Float] + idList: [ID!] + idListNullable: [ID] + intList: [Int!] + intListNullable: [Int] + localDateTimeList: [LocalDateTime!] + localDateTimeListNullable: [LocalDateTime] + localTimeList: [LocalTime!] + localTimeListNullable: [LocalTime] + stringList: [String!] + stringListNullable: [String] + timeList: [Time!] + timeListNullable: [Time] } type NodeTypeCreateResponse { @@ -420,11 +444,35 @@ describe("Scalars", () => { } input RelatedNodeCreateInput { - node: RelatedNodeCreateNode + node: RelatedNodeCreateNode! } input RelatedNodeCreateNode { _emptyInput: Boolean + bigIntList: [BigInt!] + bigIntListNullable: [BigInt] + booleanList: [Boolean!] + booleanListNullable: [Boolean] + dateList: [Date!] + dateListNullable: [Date] + dateTimeList: [DateTime!] + dateTimeListNullable: [DateTime] + durationList: [Duration!] + durationListNullable: [Duration] + floatList: [Float!] + floatListNullable: [Float] + idList: [ID!] + idListNullable: [ID] + intList: [Int!] + intListNullable: [Int] + localDateTimeList: [LocalDateTime!] + localDateTimeListNullable: [LocalDateTime] + localTimeList: [LocalTime!] + localTimeListNullable: [LocalTime] + stringList: [String!] + stringListNullable: [String] + timeList: [Time!] + timeListNullable: [Time] } type RelatedNodeCreateResponse { diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index aa5b957e98..283c94b6af 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -178,20 +178,22 @@ describe("Scalars", () => { } input NodeTypeCreateInput { - node: NodeTypeCreateNode + node: NodeTypeCreateNode! } input NodeTypeCreateNode { _emptyInput: Boolean - boolean: Boolean + bigInt: BigInt! + bigIntNullable: BigInt + boolean: Boolean! booleanNullable: Boolean - float: Float + float: Float! floatNullable: Float - id: ID + id: ID! idNullable: ID - int: Int + int: Int! intNullable: Int - string: String + string: String! stringNullable: String } @@ -348,20 +350,22 @@ describe("Scalars", () => { } input RelatedNodeCreateInput { - node: RelatedNodeCreateNode + node: RelatedNodeCreateNode! } input RelatedNodeCreateNode { _emptyInput: Boolean - boolean: Boolean + bigInt: BigInt! + bigIntNullable: BigInt + boolean: Boolean! booleanNullable: Boolean - float: Float + float: Float! floatNullable: Float - id: ID + id: ID! idNullable: ID - int: Int + int: Int! intNullable: Int - string: String + string: String! stringNullable: 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 index ef13f32248..8cbd67be4c 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -95,7 +95,7 @@ describe("Spatial Types", () => { } input NodeTypeCreateInput { - node: NodeTypeCreateNode + node: NodeTypeCreateNode! } input NodeTypeCreateNode { @@ -218,7 +218,7 @@ describe("Spatial Types", () => { } input RelatedNodeCreateInput { - node: RelatedNodeCreateNode + node: RelatedNodeCreateNode! } input RelatedNodeCreateNode { diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 3686e7e380..72f4ff7abd 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -172,11 +172,17 @@ describe("Temporals", () => { } input NodeTypeCreateInput { - node: NodeTypeCreateNode + node: NodeTypeCreateNode! } input NodeTypeCreateNode { _emptyInput: Boolean + date: Date + dateTime: DateTime + duration: Duration + localDateTime: LocalDateTime + localTime: LocalTime + time: Time } type NodeTypeCreateResponse { @@ -314,11 +320,17 @@ describe("Temporals", () => { } input RelatedNodeCreateInput { - node: RelatedNodeCreateNode + node: RelatedNodeCreateNode! } input RelatedNodeCreateNode { _emptyInput: Boolean + date: Date + dateTime: DateTime + duration: Duration + localDateTime: LocalDateTime + localTime: LocalTime + time: Time } type RelatedNodeCreateResponse { From 2d19de48a8e6f78721c255c7780a45c69d600f90 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 30 Jul 2024 16:58:37 +0100 Subject: [PATCH 130/177] Add temporal types tests --- .../TopLevelCreateSchemaTypes.ts | 16 +- .../types/array/number-array.int.test.ts | 84 +++++------ .../types/array/temporal-array.int.test.ts | 137 ++++++++++++++++++ .../create/types/date/date.int.test.ts | 73 ++++++++++ .../dateTime.int.test.ts} | 59 +------- .../types/duration/duration.int.test.ts | 70 +++++++++ .../localDateTiime/localDateTime.int.test.ts | 74 ++++++++++ .../types/localTime/localTime.int.test.ts | 74 ++++++++++ .../create/types/number.int.test.ts | 4 +- .../create/types/time/time.int.test.ts | 74 ++++++++++ .../types/date/date-equals.int.test.ts | 10 +- .../api-v6/schema/directives/relayId.test.ts | 1 - .../tests/api-v6/schema/relationship.test.ts | 4 - .../tests/api-v6/schema/simple.test.ts | 4 - .../tests/api-v6/schema/types/array.test.ts | 2 - .../tests/api-v6/schema/types/scalars.test.ts | 2 - .../api-v6/schema/types/temporals.test.ts | 2 - 17 files changed, 560 insertions(+), 130 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/date/date.int.test.ts rename packages/graphql/tests/api-v6/integration/create/types/{datetime.int.test.ts => dateTime/dateTime.int.test.ts} (51%) create mode 100644 packages/graphql/tests/api-v6/integration/create/types/duration/duration.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/localDateTiime/localDateTime.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/localTime/localTime.int.test.ts create mode 100644 packages/graphql/tests/api-v6/integration/create/types/time/time.int.test.ts 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 index 6576db3eae..955c0c83fe 100644 --- 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 @@ -68,11 +68,11 @@ export class TopLevelCreateSchemaTypes { public get createNode(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.createNode, (_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: { - ...this.getInputFields([...this.entity.attributes.values()]), - _emptyInput: this.schemaBuilder.types.boolean, // TODO: discuss if we want handle empty input in a different way. - }, + fields, }; }); } @@ -91,6 +91,9 @@ export class TopLevelCreateSchemaTypes { 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) { @@ -99,11 +102,6 @@ export class TopLevelCreateSchemaTypes { if (type instanceof Neo4jTemporalType) { return this.createTemporalFieldInput(type); } - /* const isList = attribute.type instanceof ListType; - const wrappedType = isList ? attribute.type.ofType : attribute.type; - if (wrappedType instanceof ScalarType) { - return this.createScalarType(wrappedType, isList); - } */ } private createBuiltInFieldInput(type: ScalarType): ScalarTypeComposer | NonNullComposer { diff --git a/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts index 433495b34b..5eabf0d540 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts @@ -20,7 +20,7 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Numeric array fields", () => { +describe("Create Nodes with Numeric array fields", () => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; @@ -33,69 +33,59 @@ describe("Numeric array fields", () => { year: [Int!]! rating: [Float!]! viewings: [BigInt!]! - yearNullable: [Int]! - ratingNullable: [Float]! - viewingsNullable: [BigInt]! - } `; await testHelper.initNeo4jGraphQL({ typeDefs }); - - await testHelper.executeCypher(` - CREATE (movie:${Movie} { - yearNullable: [1999], - ratingNullable: [4.0], - viewingsNullable: [4294967297], - 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 - yearNullable - viewings - viewingsNullable - rating - ratingNullable - } + test("should be able to create nodes with Numeric fields", async () => { + const mutation = /* GraphQL */ ` + mutation { + ${Movie.operations.create}(input: [ + { + node: { + year: [1999], + rating: [4.0], + viewings: ["4294967297"], } - + } + { + node: { + year: [2001], + rating: [4.2], + viewings: ["194967297"], + } + } + ]) { + ${Movie.plural} { + year + rating + viewings } } } `; - const gqlResult = await testHelper.executeGraphQL(query); + const gqlResult = await testHelper.executeGraphQL(mutation); expect(gqlResult.errors).toBeFalsy(); expect(gqlResult.data).toEqual({ - [Movie.plural]: { - connection: { - edges: [ - { - node: { - year: [1999], - yearNullable: [1999], - rating: [4.0], - ratingNullable: [4.0], - viewings: ["4294967297"], - viewingsNullable: ["4294967297"], - }, - }, - ], - }, + [Movie.operations.create]: { + [Movie.plural]: expect.toIncludeSameMembers([ + { + year: [1999], + rating: [4.0], + viewings: ["4294967297"], + }, + { + year: [2001], + rating: [4.2], + viewings: ["194967297"], + }, + ]), }, }); }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts new file mode 100644 index 0000000000..ecad5862e6 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/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()}"], + dateTime: ["${neoDateTime1.toString()}"] + localTime: ["${neoLocalTime1.toString()}"] + localDateTime: ["${neoLocalDateTime1.toString()}"] + time: ["${neoTime1.toString()}"], + duration: ["${duration1.toString()}"] + } + } + { + node: { + date: ["${neoDate2.toString()}"], + dateTime: ["${neoDateTime2.toString()}"] + localTime: ["${neoLocalTime2.toString()}"] + localDateTime: ["${neoLocalDateTime2.toString()}"] + time: ["${neoTime2.toString()}"], + duration: ["${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()], + dateTime: [neoDateTime1.toString()], + localTime: [neoLocalTime1.toString()], + localDateTime: [neoLocalDateTime1.toString()], + time: [neoTime1.toString()], + duration: [duration1.toString()], + }, + { + date: [neoDate2.toString()], + dateTime: [neoDateTime2.toString()], + localTime: [neoLocalTime2.toString()], + localDateTime: [neoLocalDateTime2.toString()], + time: [neoTime2.toString()], + duration: [duration2.toString()], + }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/integration/create/types/date/date.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/date/date.int.test.ts new file mode 100644 index 0000000000..7938bfde50 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/types/date/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/create/types/datetime.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/dateTime/dateTime.int.test.ts similarity index 51% rename from packages/graphql/tests/api-v6/integration/create/types/datetime.int.test.ts rename to packages/graphql/tests/api-v6/integration/create/types/dateTime/dateTime.int.test.ts index bf5a891a05..8488a9b0e6 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/datetime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/dateTime/dateTime.int.test.ts @@ -17,10 +17,10 @@ * limitations under the License. */ -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.skip("DateTime", () => { +describe("Create Nodes with DateTime fields", () => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; @@ -38,7 +38,7 @@ describe.skip("DateTime", () => { await testHelper.close(); }); - test.only("should return a movie created with a datetime parameter", async () => { + test("should be able to create nodes with DateTime fields", async () => { const date1 = new Date(1716904582368); const date2 = new Date(1796904582368); @@ -49,7 +49,7 @@ describe.skip("DateTime", () => { { node: { datetime: "${date2.toISOString()}" } } ]) { ${Movie.plural} { - dateTime + datetime } } } @@ -61,55 +61,10 @@ describe.skip("DateTime", () => { expect(gqlResult.data).toEqual({ [Movie.operations.create]: { [Movie.plural]: expect.toIncludeSameMembers([ - { dateTime: date1.toISOString() }, - { dateTime: date1.toISOString() }, + { datetime: date1.toISOString() }, + { datetime: date2.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/create/types/duration/duration.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/duration/duration.int.test.ts new file mode 100644 index 0000000000..fb192393a0 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/types/duration/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/create/types/localDateTiime/localDateTime.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/localDateTiime/localDateTime.int.test.ts new file mode 100644 index 0000000000..2b84e26301 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/types/localDateTiime/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/create/types/localTime/localTime.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/localTime/localTime.int.test.ts new file mode 100644 index 0000000000..d55965cc16 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/types/localTime/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/create/types/number.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/number.int.test.ts index bd152bf92f..fcdb860d10 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/number.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/number.int.test.ts @@ -20,7 +20,7 @@ import type { UniqueType } from "../../../../utils/graphql-types"; import { TestHelper } from "../../../../utils/tests-helper"; -describe("Numeric fields", () => { +describe("Create Nodes with Numeric fields", () => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; @@ -42,7 +42,7 @@ describe("Numeric fields", () => { await testHelper.close(); }); - test("should be able to create int and float fields", async () => { + test("should be able to create nodes with numeric fields", async () => { const mutation = /* GraphQL */ ` mutation { ${Movie.operations.create}(input: [ diff --git a/packages/graphql/tests/api-v6/integration/create/types/time/time.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/time/time.int.test.ts new file mode 100644 index 0000000000..e3e622fd15 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/create/types/time/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/filters/types/date/date-equals.int.test.ts b/packages/graphql/tests/api-v6/integration/filters/types/date/date-equals.int.test.ts index 6dffaf6410..e1722ae302 100644 --- 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 @@ -43,22 +43,22 @@ describe("Date - Equals", () => { const date1 = new Date(1716904582368); const date2 = new Date(1736900000000); - const datetime1 = neo4jDriver.types.Date.fromStandardDate(date1); - const datetime2 = neo4jDriver.types.Date.fromStandardDate(date2); + 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, datetime2 } + { datetime1: neoDate1, datetime2: neoDate2 } ); await testHelper.initNeo4jGraphQL({ typeDefs }); const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { node: { date: { equals: "${datetime1.toString()}" }} }) { + ${Movie.plural}(where: { node: { date: { equals: "${neoDate1.toString()}" }} }) { connection{ edges { node { @@ -81,7 +81,7 @@ describe("Date - Equals", () => { { node: { title: "The Matrix", - date: datetime1.toString(), + date: neoDate1.toString(), }, }, ], diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index f37fe5a752..3db3146d78 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -80,7 +80,6 @@ describe("RelayId", () => { } input MovieCreateNode { - _emptyInput: Boolean dbId: ID! title: String } diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 05c4258b32..bc71b78b59 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -69,7 +69,6 @@ describe("Relationships", () => { } input ActorCreateNode { - _emptyInput: Boolean name: String } @@ -236,7 +235,6 @@ describe("Relationships", () => { } input MovieCreateNode { - _emptyInput: Boolean title: String } @@ -373,7 +371,6 @@ describe("Relationships", () => { } input ActorCreateNode { - _emptyInput: Boolean name: String } @@ -558,7 +555,6 @@ describe("Relationships", () => { } input MovieCreateNode { - _emptyInput: Boolean title: String } diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index f3c465ad05..a04b45590c 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -63,7 +63,6 @@ describe("Simple Aura-API", () => { } input MovieCreateNode { - _emptyInput: Boolean title: String } @@ -175,7 +174,6 @@ describe("Simple Aura-API", () => { } input ActorCreateNode { - _emptyInput: Boolean name: String } @@ -234,7 +232,6 @@ describe("Simple Aura-API", () => { } input MovieCreateNode { - _emptyInput: Boolean title: String } @@ -348,7 +345,6 @@ describe("Simple Aura-API", () => { } input MovieCreateNode { - _emptyInput: Boolean title: 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 index 964a03366d..6c897f668e 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -268,7 +268,6 @@ describe("Scalars", () => { } input NodeTypeCreateNode { - _emptyInput: Boolean bigIntList: [BigInt!] bigIntListNullable: [BigInt] booleanList: [Boolean!] @@ -448,7 +447,6 @@ describe("Scalars", () => { } input RelatedNodeCreateNode { - _emptyInput: Boolean bigIntList: [BigInt!] bigIntListNullable: [BigInt] booleanList: [Boolean!] diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index 283c94b6af..0d902c5bdd 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -182,7 +182,6 @@ describe("Scalars", () => { } input NodeTypeCreateNode { - _emptyInput: Boolean bigInt: BigInt! bigIntNullable: BigInt boolean: Boolean! @@ -354,7 +353,6 @@ describe("Scalars", () => { } input RelatedNodeCreateNode { - _emptyInput: Boolean bigInt: BigInt! bigIntNullable: BigInt boolean: Boolean! diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 72f4ff7abd..03432895cc 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -176,7 +176,6 @@ describe("Temporals", () => { } input NodeTypeCreateNode { - _emptyInput: Boolean date: Date dateTime: DateTime duration: Duration @@ -324,7 +323,6 @@ describe("Temporals", () => { } input RelatedNodeCreateNode { - _emptyInput: Boolean date: Date dateTime: DateTime duration: Duration From c7ceb772ab811cfc445c1edd6a68dde74595c227 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 30 Jul 2024 17:03:05 +0100 Subject: [PATCH 131/177] remove extra folder from temporal types tests --- .../integration/create/types/{date => }/date.int.test.ts | 4 ++-- .../create/types/{dateTime => }/dateTime.int.test.ts | 4 ++-- .../create/types/{duration => }/duration.int.test.ts | 4 ++-- .../types/{localDateTiime => }/localDateTime.int.test.ts | 4 ++-- .../create/types/{localTime => }/localTime.int.test.ts | 4 ++-- .../integration/create/types/{time => }/time.int.test.ts | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) rename packages/graphql/tests/api-v6/integration/create/types/{date => }/date.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/create/types/{dateTime => }/dateTime.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/create/types/{duration => }/duration.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/create/types/{localDateTiime => }/localDateTime.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/create/types/{localTime => }/localTime.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/create/types/{time => }/time.int.test.ts (94%) diff --git a/packages/graphql/tests/api-v6/integration/create/types/date/date.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/date.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/date/date.int.test.ts rename to packages/graphql/tests/api-v6/integration/create/types/date.int.test.ts index 7938bfde50..493db1382a 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/date/date.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/date.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Nodes with Date fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/dateTime/dateTime.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/dateTime.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/dateTime/dateTime.int.test.ts rename to packages/graphql/tests/api-v6/integration/create/types/dateTime.int.test.ts index 8488a9b0e6..cd02dd7dea 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/dateTime/dateTime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/dateTime.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Create Nodes with DateTime fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/duration/duration.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/duration.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/duration/duration.int.test.ts rename to packages/graphql/tests/api-v6/integration/create/types/duration.int.test.ts index fb192393a0..5008fc3285 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/duration/duration.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/duration.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Nodes with Duration fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/localDateTiime/localDateTime.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/localDateTime.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/localDateTiime/localDateTime.int.test.ts rename to packages/graphql/tests/api-v6/integration/create/types/localDateTime.int.test.ts index 2b84e26301..5897ae1901 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/localDateTiime/localDateTime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/localDateTime.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Nodes with LocalDateTime fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/localTime/localTime.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/localTime.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/localTime/localTime.int.test.ts rename to packages/graphql/tests/api-v6/integration/create/types/localTime.int.test.ts index d55965cc16..885fb9982e 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/localTime/localTime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/localTime.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Nodes with LocalTime fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/time/time.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/time.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/time/time.int.test.ts rename to packages/graphql/tests/api-v6/integration/create/types/time.int.test.ts index e3e622fd15..2b2e9263ad 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/time/time.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/time.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Node with Time", () => { const testHelper = new TestHelper({ v6Api: true }); From 367ee0b5e19afcc5bc4f28acaa3bbf14ff683637 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Wed, 31 Jul 2024 17:56:38 +0100 Subject: [PATCH 132/177] add support for create with Spatial types, improve tests around Spatial types --- .../schema-generation/SchemaBuilderTypes.ts | 22 +++- .../TopLevelCreateSchemaTypes.ts | 26 +++++ .../TopLevelEntityTypeNames.ts | 1 - .../cartesian-point-2d.int.test.ts | 108 ++++++++--------- .../cartesian-point-3d.int.test.ts | 110 ++++++++---------- .../integration/create/types/date.int.test.ts | 6 +- .../create/types/point/point-2d.int.test.ts | 103 +++++++--------- .../create/types/point/point-3d.int.test.ts | 104 ++++++++--------- .../cartesian-point-2d-equals.int.test.ts | 12 +- .../cartesian-point-3d-equals.int.test.ts | 12 +- .../cartesian-point-2d-equals.int.test.ts | 18 +-- .../cartesian-point-2d-gt.int.test.ts | 18 +-- .../cartesian-point-2d-in.int.test.ts | 18 +-- .../cartesian-point-2d-lt.int.test.ts | 18 +-- .../cartesian-point-3d-equals.int.test.ts | 18 +-- .../cartesian-point-3d-gt.int.test.ts | 18 +-- .../cartesian-point-3d-lt.int.test.ts | 18 +-- .../point/array/point-2d-equals.int.test.ts | 12 +- .../point/array/point-3d-equals.int.test.ts | 12 +- .../types/point/point-2d-equals.int.test.ts | 6 +- .../types/point/point-2d-gt.int.test.ts | 18 +-- .../types/point/point-2d-in.int.test.ts | 18 +-- .../types/point/point-2d-lt.int.test.ts | 18 +-- .../types/point/point-3d-equals.int.test.ts | 18 +-- .../types/point/point-3d-gt.int.test.ts | 18 +-- .../types/point/point-3d-lt.int.test.ts | 18 +-- .../cartesian-point-2d.int.test.ts | 18 +-- .../cartesian-point-3d.int.test.ts | 14 +-- .../types/point/point-2d.int.test.ts | 18 +-- .../types/point/point-3d.int.test.ts | 14 +-- .../tests/api-v6/schema/types/spatial.test.ts | 24 +++- 31 files changed, 435 insertions(+), 421 deletions(-) diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts index df1d6b4f1d..c1eb6ff8a7 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilderTypes.ts @@ -19,8 +19,12 @@ import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString } from "graphql"; import type { SchemaComposer } from "graphql-compose"; -import { ScalarTypeComposer } 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, @@ -86,4 +90,20 @@ export class SchemaBuilderTypes { 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/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelCreateSchemaTypes.ts index 955c0c83fe..41f99984b2 100644 --- 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 @@ -25,7 +25,9 @@ import { GraphQLBuiltInScalarType, ListType, Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, + Neo4jSpatialType, Neo4jTemporalType, ScalarType, } from "../../../../schema-model/attribute/AttributeType"; @@ -102,6 +104,9 @@ export class TopLevelCreateSchemaTypes { if (type instanceof Neo4jTemporalType) { return this.createTemporalFieldInput(type); } + if (type instanceof Neo4jSpatialType) { + return this.createSpatialFieldInput(type); + } } private createBuiltInFieldInput(type: ScalarType): ScalarTypeComposer | NonNullComposer { @@ -179,4 +184,25 @@ export class TopLevelCreateSchemaTypes { } return builtInType; } + + private createSpatialFieldInput(type: Neo4jSpatialType): InputTypeComposer | NonNullComposer { + 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-model/graphql-type-names/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/TopLevelEntityTypeNames.ts index 77a11fcde8..fdf93a7d18 100644 --- 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 @@ -76,7 +76,6 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { public get createNode(): string { return `${upperFirst(this.entityName)}CreateNode`; } - // TODO: do we need to memoize the upperFirst/plural calls? public get createInput(): string { return `${upperFirst(this.entityName)}CreateInput`; diff --git a/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-2d.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-2d.int.test.ts index 6ea0a622e0..87ad8a1d7a 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-2d.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-2d.int.test.ts @@ -20,12 +20,13 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 2d", () => { +describe("Create Nodes with CartesianPoint 2d", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { x: -14221.955504767046, y: 6711533.711877272 }; - const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; + + 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"); @@ -36,76 +37,61 @@ describe("CartesianPoint 2d", () => { 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(); }); - // srid commented as results of https://github.com/neo4j/graphql/issues/5223 - test("wgs-84-2d point", async () => { - const query = /* GraphQL */ ` - query { - ${Location.plural} { - connection { - edges { - node { - id - value { - y - x - z - crs - # srid - } - } + + 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 equalsResult = await testHelper.executeGraphQL(query); + + + } + `; - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ - [Location.plural]: { - connection: { - edges: expect.toIncludeSameMembers([ - { - node: { - id: "1", - value: { - y: London.y, - x: London.x, - z: null, - crs: "cartesian", - // srid: 7203, - }, - }, + 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, }, - { - node: { - id: "2", - value: { - y: Rome.y, - x: Rome.x, - z: null, - 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/create/types/cartesian-point/cartesian-point-3d.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-3d.int.test.ts index 761203a565..5fa68e9882 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-3d.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-3d.int.test.ts @@ -20,13 +20,13 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("CartesianPoint 3d", () => { +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 }; - const Rome = { x: 1391088.9885668862, y: 5146427.7652232265, z: 0 }; + 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"); @@ -37,76 +37,64 @@ describe("CartesianPoint 3d", () => { 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(); }); - // srid commented as results of https://github.com/neo4j/graphql/issues/5223 - test("wgs-84-3d point", async () => { - const query = /* GraphQL */ ` - query { - ${Location.plural} { - connection { - edges { - node { - id - value { - y - x - z - crs - # srid - } - } + + 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 equalsResult = await testHelper.executeGraphQL(query); + + + } + `; - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.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, - }, - }, + 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, }, - { - node: { - id: "2", - value: { - y: Rome.y, - x: Rome.x, - z: Rome.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/create/types/date.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/date.int.test.ts index 493db1382a..e8a9cd95ef 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/date.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/date.int.test.ts @@ -51,10 +51,10 @@ describe("Create Nodes with Date fields", () => { { node: { date: "${neoDate1.toString()}" } } { node: { date: "${neoDate2.toString()}" } } ]) { - ${Movie.plural} { - date + ${Movie.plural} { + date + } } - } } `; diff --git a/packages/graphql/tests/api-v6/integration/create/types/point/point-2d.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/point/point-2d.int.test.ts index 3298b206b2..c961c8ba06 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/point/point-2d.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/point/point-2d.int.test.ts @@ -20,12 +20,12 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 2d", () => { +describe("Create Nodes with Point 2d", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { longitude: -0.127758, latitude: 51.507351 }; - const Rome = { longitude: 12.496365, latitude: 41.902782 }; + 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"); @@ -36,76 +36,63 @@ describe("Point 2d", () => { 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(); }); - // srid commented as results of https://github.com/neo4j/graphql/issues/5223 - test("wgs-84-2d point", async () => { - const query = /* GraphQL */ ` - query { - ${Location.plural} { - connection { - edges { - node { - id - value { - latitude - longitude - height - crs - # srid - } - } + + 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 equalsResult = await testHelper.executeGraphQL(query); + const mutationResult = await testHelper.executeGraphQL(mutation); - 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", - // srid: 4326, - }, - }, + 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, }, - { - node: { - id: "2", - value: { - latitude: Rome.latitude, - longitude: Rome.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/create/types/point/point-3d.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/point/point-3d.int.test.ts index 1ce749c425..cd4e5b4a90 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/point/point-3d.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/point/point-3d.int.test.ts @@ -20,12 +20,12 @@ import type { UniqueType } from "../../../../../utils/graphql-types"; import { TestHelper } from "../../../../../utils/tests-helper"; -describe("Point 3d", () => { +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 }; - const Rome = { longitude: 12.496365, latitude: 41.902782, height: 35 }; + 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"); @@ -36,76 +36,64 @@ describe("Point 3d", () => { 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(); }); - // srid commented as results of https://github.com/neo4j/graphql/issues/5223 - test("wgs-84-3d point", async () => { - const query = /* GraphQL */ ` - query { - ${Location.plural} { - connection { - edges { - node { - id - value { - latitude - longitude - height - crs - # srid - } + + 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 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: London.height, - crs: "wgs-84-3d", - // srid: 4326, - }, - }, + 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, }, - { - node: { - id: "2", - value: { - latitude: Rome.latitude, - longitude: Rome.longitude, - height: Rome.height, - crs: "wgs-84-3d", - //srid: 4326, - }, - }, + }, + { + 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/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 index c08cbc9a80..ac8cb8f506 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("CartesianPoint 2d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { x: -14221.955504767046, y: 6711533.711877272 }; - const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; - const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + 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"); @@ -74,10 +74,10 @@ describe.skip("CartesianPoint 2d array EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index 3631acbb22..a194ced96a 100644 --- 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 @@ -26,9 +26,9 @@ 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 }; - const Rome = { x: 12.496365, y: 41.902782, z: 0 }; - const Paris = { x: 2.352222, y: 48.856613, z: 0 }; + 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"); @@ -74,10 +74,10 @@ describe.skip("CartesianPoint 3d array EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index ef76334bc0..bc893f7faf 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("CartesianPoint 2d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { x: -14221.955504767046, y: 6711533.711877272 }; - const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; - const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + 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"); @@ -75,10 +75,10 @@ describe.skip("CartesianPoint 2d EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -121,10 +121,10 @@ describe.skip("CartesianPoint 2d EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: expect.toIncludeSameMembers([ 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 index 4521acbca4..107934c922 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("CartesianPoint 2d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { x: -14221.955504767046, y: 6711533.711877272 }; - const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; - const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + 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"); @@ -77,10 +77,10 @@ describe.skip("CartesianPoint 2d GT", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -125,9 +125,9 @@ describe.skip("CartesianPoint 2d GT", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + const queryResult = await testHelper.executeGraphQL(query); + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index fd6eef4e7a..51c6addc59 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("CartesianPoint 2d IN", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { x: -14221.955504767046, y: 6711533.711877272 }; - const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; - const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + 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"); @@ -74,12 +74,12 @@ describe.skip("CartesianPoint 2d IN", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: {}, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -121,12 +121,12 @@ describe.skip("CartesianPoint 2d IN", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: {}, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index cc1a81e71c..5c06c59a5a 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("CartesianPoint 2d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { x: -14221.955504767046, y: 6711533.711877272 }; - const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; - const Paris = { x: 261848.15527273554, y: 6250566.54904563 }; + 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"); @@ -76,12 +76,12 @@ describe.skip("CartesianPoint 2d LT", () => { `; // distance is in meters const distance = 1000 * 1000; // 1000 km - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: { x: Paris.x, y: Paris.y, distance }, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -125,12 +125,12 @@ describe.skip("CartesianPoint 2d LT", () => { `; // distance is in meters const distance = 1000 * 1000; // 1000 km - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: { x: Paris.x, y: Paris.y, distance }, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index 195bbfc09e..746bb37732 100644 --- 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 @@ -27,9 +27,9 @@ describe.skip("CartesianPoint 3d EQ", () => { let Location: UniqueType; - const London = { x: -14221.955504767046, y: 6711533.711877272, z: 0 }; - const Rome = { x: 1391088.9885668862, y: 5146427.7652232265, z: 0 }; - const Paris = { x: 261848.15527273554, y: 6250566.54904563, z: 0 }; + 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"); @@ -76,10 +76,10 @@ describe.skip("CartesianPoint 3d EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -122,10 +122,10 @@ describe.skip("CartesianPoint 3d EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: expect.toIncludeSameMembers([ 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 index 4263eaa60b..4e7035f41c 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("CartesianPoint 3d GT", () => { 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 }; - const Paris = { x: 261848.15527273554, y: 6250566.54904563, z: 0 }; + 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"); @@ -75,10 +75,10 @@ describe.skip("CartesianPoint 3d GT", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -122,10 +122,10 @@ describe.skip("CartesianPoint 3d GT", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index d684515d75..dea40f7e62 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("CartesianPoint 3d LT", () => { 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 }; - const Paris = { x: 261848.15527273554, y: 6250566.54904563, z: 0 }; + 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"); @@ -76,10 +76,10 @@ describe.skip("CartesianPoint 3d LT", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -123,10 +123,10 @@ describe.skip("CartesianPoint 3d LT", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index c5e41f8bb4..d4a56aff56 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("Point 2d array EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { longitude: -0.127758, latitude: 51.507351 }; - const Rome = { longitude: 12.496365, latitude: 41.902782 }; - const Paris = { longitude: 2.352222, latitude: 48.856613 }; + 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"); @@ -75,10 +75,10 @@ describe.skip("Point 2d array EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index c3f6a8d621..d433fab32d 100644 --- 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 @@ -26,9 +26,9 @@ 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 }; - const Rome = { longitude: 12.496365, latitude: 41.902782, height: 35 }; - const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 }; + 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"); @@ -74,10 +74,10 @@ describe.skip("Point 3d array EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index 13374e26c0..49827c7c26 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("Point 2d EQ", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { longitude: -0.127758, latitude: 51.507351 }; - const Rome = { longitude: 12.496365, latitude: 41.902782 }; - const Paris = { longitude: 2.352222, latitude: 48.856613 }; + 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"); 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 index f9d3292ceb..0d1ab22839 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("Point 2d GT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { longitude: -0.127758, latitude: 51.507351 }; - const Rome = { longitude: 12.496365, latitude: 41.902782 }; - const Paris = { longitude: 2.352222, latitude: 48.856613 }; + 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"); @@ -76,12 +76,12 @@ describe.skip("Point 2d GT", () => { `; // distance is in meters const distance = 1000 * 1000; // 1000 km - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -125,12 +125,12 @@ describe.skip("Point 2d GT", () => { `; // distance is in meters const distance = 1000 * 1000; // 1000 km - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index fe01db7ca2..47a97ce27c 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("Point 2d IN", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { longitude: -0.127758, latitude: 51.507351 }; - const Rome = { longitude: 12.496365, latitude: 41.902782 }; - const Paris = { longitude: 2.352222, latitude: 48.856613 }; + 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"); @@ -74,12 +74,12 @@ describe.skip("Point 2d IN", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: {}, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -121,12 +121,12 @@ describe.skip("Point 2d IN", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: {}, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index 08208cc54d..9285007e48 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("Point 2d LT", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { longitude: -0.127758, latitude: 51.507351 }; - const Rome = { longitude: 12.496365, latitude: 41.902782 }; - const Paris = { longitude: 2.352222, latitude: 48.856613 }; + 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"); @@ -76,12 +76,12 @@ describe.skip("Point 2d LT", () => { `; // distance is in meters const distance = 1000 * 1000; // 1000 km - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -125,12 +125,12 @@ describe.skip("Point 2d LT", () => { `; // distance is in meters const distance = 1000 * 1000; // 1000 km - const equalsResult = await testHelper.executeGraphQL(query, { + const queryResult = await testHelper.executeGraphQL(query, { variableValues: { longitude: Paris.longitude, latitude: Paris.latitude, distance }, }); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index 4a662684d5..70ec7bbd7e 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("Point 3d EQ", () => { 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 }; - const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 }; + 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"); @@ -75,10 +75,10 @@ describe.skip("Point 3d EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -121,10 +121,10 @@ describe.skip("Point 3d EQ", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: expect.toIncludeSameMembers([ 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 index 615df873cd..7084ac6a00 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("Point 3d GT", () => { 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 }; - const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 }; + 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"); @@ -75,10 +75,10 @@ describe.skip("Point 3d GT", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -122,10 +122,10 @@ describe.skip("Point 3d GT", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index 8430c7f284..da1f19a3c6 100644 --- 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 @@ -26,9 +26,9 @@ describe.skip("Point 3d LT", () => { 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 }; - const Paris = { longitude: 2.352222, latitude: 48.856613, height: 21 }; + 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"); @@ -75,10 +75,10 @@ describe.skip("Point 3d LT", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ @@ -122,10 +122,10 @@ describe.skip("Point 3d LT", () => { } } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: [ 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 index 6ea0a622e0..36101dcbe2 100644 --- 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 @@ -24,8 +24,8 @@ describe("CartesianPoint 2d", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { x: -14221.955504767046, y: 6711533.711877272 }; - const Rome = { x: 1391088.9885668862, y: 5146427.7652232265 }; + 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"); @@ -49,7 +49,7 @@ describe("CartesianPoint 2d", () => { afterEach(async () => { await testHelper.close(); }); - // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-2d point", async () => { const query = /* GraphQL */ ` query { @@ -63,7 +63,7 @@ describe("CartesianPoint 2d", () => { x z crs - # srid + srid } } } @@ -73,10 +73,10 @@ describe("CartesianPoint 2d", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: expect.toIncludeSameMembers([ @@ -88,7 +88,7 @@ describe("CartesianPoint 2d", () => { x: London.x, z: null, crs: "cartesian", - // srid: 7203, + srid: 7203, }, }, }, @@ -100,7 +100,7 @@ describe("CartesianPoint 2d", () => { x: Rome.x, z: null, crs: "cartesian", - //srid: 7203, + 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 index 761203a565..dc9f1a21ee 100644 --- 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 @@ -50,7 +50,7 @@ describe("CartesianPoint 3d", () => { afterEach(async () => { await testHelper.close(); }); - // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-3d point", async () => { const query = /* GraphQL */ ` query { @@ -64,7 +64,7 @@ describe("CartesianPoint 3d", () => { x z crs - # srid + srid } } } @@ -74,10 +74,10 @@ describe("CartesianPoint 3d", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: expect.toIncludeSameMembers([ @@ -89,7 +89,7 @@ describe("CartesianPoint 3d", () => { x: London.x, z: London.z, crs: "cartesian-3d", - // srid: 9157, + srid: 9157, }, }, }, @@ -101,7 +101,7 @@ describe("CartesianPoint 3d", () => { x: Rome.x, z: Rome.z, crs: "cartesian-3d", - // srid: 9157, + srid: 9157, }, }, }, 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 index 3298b206b2..f4e7d3feda 100644 --- 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 @@ -24,8 +24,8 @@ describe("Point 2d", () => { const testHelper = new TestHelper({ v6Api: true }); let Location: UniqueType; - const London = { longitude: -0.127758, latitude: 51.507351 }; - const Rome = { longitude: 12.496365, latitude: 41.902782 }; + 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"); @@ -49,7 +49,7 @@ describe("Point 2d", () => { afterEach(async () => { await testHelper.close(); }); - // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-2d point", async () => { const query = /* GraphQL */ ` query { @@ -63,7 +63,7 @@ describe("Point 2d", () => { longitude height crs - # srid + srid } } } @@ -73,10 +73,10 @@ describe("Point 2d", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: expect.toIncludeSameMembers([ @@ -88,7 +88,7 @@ describe("Point 2d", () => { longitude: London.longitude, height: null, crs: "wgs-84", - // srid: 4326, + srid: 4326, }, }, }, @@ -100,7 +100,7 @@ describe("Point 2d", () => { longitude: Rome.longitude, height: null, crs: "wgs-84", - //srid: 4326, + 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 index 1ce749c425..149a5c44e7 100644 --- 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 @@ -49,7 +49,7 @@ describe("Point 3d", () => { afterEach(async () => { await testHelper.close(); }); - // srid commented as results of https://github.com/neo4j/graphql/issues/5223 + test("wgs-84-3d point", async () => { const query = /* GraphQL */ ` query { @@ -63,7 +63,7 @@ describe("Point 3d", () => { longitude height crs - # srid + srid } } } @@ -73,10 +73,10 @@ describe("Point 3d", () => { } `; - const equalsResult = await testHelper.executeGraphQL(query); + const queryResult = await testHelper.executeGraphQL(query); - expect(equalsResult.errors).toBeFalsy(); - expect(equalsResult.data).toEqual({ + expect(queryResult.errors).toBeFalsy(); + expect(queryResult.data).toEqual({ [Location.plural]: { connection: { edges: expect.toIncludeSameMembers([ @@ -88,7 +88,7 @@ describe("Point 3d", () => { longitude: London.longitude, height: London.height, crs: "wgs-84-3d", - // srid: 4326, + srid: 4979, }, }, }, @@ -100,7 +100,7 @@ describe("Point 3d", () => { longitude: Rome.longitude, height: Rome.height, crs: "wgs-84-3d", - //srid: 4326, + srid: 4979, }, }, }, diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 8cbd67be4c..c02909f82e 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -71,6 +71,13 @@ describe("Spatial Types", () => { z: Float } + \\"\\"\\"Input type for a cartesian point\\"\\"\\" + input CartesianPointInput { + x: Float! + y: Float! + z: Float + } + type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse @@ -99,7 +106,10 @@ describe("Spatial Types", () => { } input NodeTypeCreateNode { - _emptyInput: Boolean + cartesianPoint: CartesianPointInput! + cartesianPointNullable: CartesianPointInput + point: PointInput! + pointNullable: PointInput } type NodeTypeCreateResponse { @@ -195,6 +205,13 @@ describe("Spatial Types", () => { srid: Int! } + \\"\\"\\"Input type for a point\\"\\"\\" + input PointInput { + height: Float + latitude: Float! + longitude: Float! + } + type Query { nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation @@ -222,7 +239,10 @@ describe("Spatial Types", () => { } input RelatedNodeCreateNode { - _emptyInput: Boolean + cartesianPoint: CartesianPointInput! + cartesianPointNullable: CartesianPointInput + point: PointInput! + pointNullable: PointInput } type RelatedNodeCreateResponse { From 110e5ad30c7b5ac6b0592e92c070a0238868ebf2 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 1 Aug 2024 12:14:00 +0100 Subject: [PATCH 133/177] simplify Create operation by removing unnecessary logic --- .../src/api-v6/queryIR/CreateOperation.ts | 115 +++--------------- .../translate/queryAST/ast/QueryASTContext.ts | 2 +- 2 files changed, 18 insertions(+), 99 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts index 81a15c754e..89cb4a71ed 100644 --- a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts @@ -19,30 +19,26 @@ import Cypher from "@neo4j/cypher-builder"; import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; -import { createRelationshipValidationClauses } from "../../translate/create-relationship-validation-clauses"; -import { QueryASTContext } from "../../translate/queryAST/ast/QueryASTContext"; +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 { assertIsConcreteEntity } from "../../translate/queryAST/utils/is-concrete-entity"; import type { V6ReadOperation } from "./ConnectionReadOperation"; export class V6CreateOperation extends MutationOperation { public readonly inputFields: Map; - public readonly target: ConcreteEntityAdapter | RelationshipAdapter; + public readonly target: ConcreteEntityAdapter; public readonly projectionOperations: V6ReadOperation[] = []; private readonly argumentToUnwind: Cypher.Param | Cypher.Property; private readonly unwindVariable: Cypher.Variable; - private isNested: boolean; constructor({ target, argumentToUnwind, }: { - target: ConcreteEntityAdapter | RelationshipAdapter; + target: ConcreteEntityAdapter; argumentToUnwind: Cypher.Param | Cypher.Property; }) { super(); @@ -50,7 +46,6 @@ export class V6CreateOperation extends MutationOperation { this.inputFields = new Map(); this.argumentToUnwind = argumentToUnwind; this.unwindVariable = new Cypher.Variable(); - this.isNested = target instanceof RelationshipAdapter; } public getChildren(): QueryASTNode[] { @@ -80,116 +75,40 @@ export class V6CreateOperation extends MutationOperation { } public transpile(context: QueryASTContext): OperationTranspileResult { - const nestedContext = this.getNestedContext(context); - nestedContext.env.topLevelOperationName = "CREATE"; - - if (!nestedContext.hasTarget()) { + if (!context.hasTarget()) { throw new Error("No parent node found!"); } - const target = this.getTarget(); const unwindClause = new Cypher.Unwind([this.argumentToUnwind, this.unwindVariable]); const createClause = new Cypher.Create( - new Cypher.Pattern(nestedContext.target, { labels: getEntityLabels(target, context.neo4jGraphQLContext) }) + new Cypher.Pattern(context.target, { labels: getEntityLabels(this.target, context.neo4jGraphQLContext) }) ); const setSubqueries: Cypher.Clause[] = []; - const mergeClause: Cypher.Merge | undefined = this.getMergeClause(nestedContext); for (const field of this.inputFields.values()) { - if (field.attachedTo === "relationship" && mergeClause) { - mergeClause.set(...field.getSetParams(nestedContext, this.unwindVariable)); - } else if (field.attachedTo === "node") { - createClause.set(...field.getSetParams(nestedContext, this.unwindVariable)); - setSubqueries.push(...field.getSubqueries(nestedContext)); + if (field.attachedTo === "node") { + createClause.set(...field.getSetParams(context, this.unwindVariable)); + setSubqueries.push(...field.getSubqueries(context)); } } const nestedSubqueries = setSubqueries.flatMap((clause) => [ - new Cypher.With(nestedContext.target, this.unwindVariable), - new Cypher.Call(clause).importWith(nestedContext.target, this.unwindVariable), + new Cypher.With(context.target, this.unwindVariable), + new Cypher.Call(clause).importWith(context.target, this.unwindVariable), ]); - const cardinalityClauses = createRelationshipValidationClauses({ - entity: target, - context: nestedContext.neo4jGraphQLContext, - varName: nestedContext.target, - }); - const unwindCreateClauses = Cypher.concat( - createClause, - mergeClause, - ...nestedSubqueries, - ...(cardinalityClauses.length ? [new Cypher.With(nestedContext.target), ...cardinalityClauses] : []) - ); - - let subQueryClause: Cypher.Clause; - if (this.isNested) { - subQueryClause = Cypher.concat( - unwindCreateClauses, - new Cypher.Return([Cypher.collect(Cypher.Null), new Cypher.Variable()]) - ); - } else { - subQueryClause = new Cypher.Call( - Cypher.concat(unwindCreateClauses, new Cypher.Return(nestedContext.target)) - ).importWith(this.unwindVariable); - } - const projectionContext = new QueryASTContext({ - ...nestedContext, - target: nestedContext.target, - returnVariable: new Cypher.NamedVariable("data"), - shouldCollect: true, - }); - const clauses = Cypher.concat(unwindClause, subQueryClause, ...this.getProjectionClause(projectionContext)); - return { projectionExpr: nestedContext.returnVariable, clauses: [clauses] }; - } - - private getMergeClause(context: QueryASTContext): Cypher.Merge | undefined { - if (this.isNested) { - if (!context.source || !context.relationship) { - throw new Error("Transpile error: No source or relationship found!"); - } - if (!(this.target instanceof RelationshipAdapter)) { - throw new Error("Transpile error: Invalid target"); - } - - return new Cypher.Merge( - new Cypher.Pattern(context.source) - .related(context.relationship, { - type: this.target.type, - direction: this.target.cypherDirectionFromRelDirection(), - }) - .to(context.target) - ); - } - } - - private getTarget(): ConcreteEntityAdapter { - if (this.target instanceof RelationshipAdapter) { - const targetAdapter = this.target.target; - assertIsConcreteEntity(targetAdapter); - return targetAdapter; - } - return this.target; - } - private getNestedContext(context: QueryASTContext): QueryASTContext { - if (this.target instanceof RelationshipAdapter) { - const target = new Cypher.Node(); - const relationship = new Cypher.Relationship(); - const nestedContext = context.push({ - target, - relationship, - }); + const unwindCreateClauses = Cypher.concat(createClause, ...nestedSubqueries); - return nestedContext; - } + const subQueryClause: Cypher.Clause = new Cypher.Call( + Cypher.concat(unwindCreateClauses, new Cypher.Return(context.target)) + ).importWith(this.unwindVariable); - return context; + 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.projectionOperations.length === 0 && !this.isNested) { - const emptyProjection = new Cypher.Literal("Query cannot conclude with CALL"); - return [new Cypher.Return(emptyProjection)]; - } return this.projectionOperations.map((operationField) => { return Cypher.concat(...operationField.transpile(context).clauses); }); 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, From 3ef47853fa83029a5199305d661e11b775c35b58 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 1 Aug 2024 16:07:03 +0100 Subject: [PATCH 134/177] improve CreateFactory --- .../src/api-v6/errors/QueryParseError.ts | 20 ++++ .../src/api-v6/queryIR/CreateOperation.ts | 45 +++++---- .../queryIRFactory/CreateOperationFactory.ts | 93 ++++++------------- .../queryIRFactory/ReadOperationFactory.ts | 5 +- 4 files changed, 73 insertions(+), 90 deletions(-) create mode 100644 packages/graphql/src/api-v6/errors/QueryParseError.ts diff --git a/packages/graphql/src/api-v6/errors/QueryParseError.ts b/packages/graphql/src/api-v6/errors/QueryParseError.ts new file mode 100644 index 0000000000..0a7b1a702e --- /dev/null +++ b/packages/graphql/src/api-v6/errors/QueryParseError.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 QueryParseError extends Error {} diff --git a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts index 89cb4a71ed..7992bc6701 100644 --- a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts @@ -25,38 +25,44 @@ import type { InputField } from "../../translate/queryAST/ast/input-fields/Input 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 inputFields: Map; public readonly target: ConcreteEntityAdapter; - public readonly projectionOperations: V6ReadOperation[] = []; - private readonly argumentToUnwind: Cypher.Param | Cypher.Property; + private readonly inputFields: InputField[]; + private readonly createInputParam: Cypher.Param | Cypher.Property; private readonly unwindVariable: Cypher.Variable; + private readonly projection: V6ReadOperation | undefined; constructor({ target, - argumentToUnwind, + createInputParam, + inputFields, + projection, }: { target: ConcreteEntityAdapter; - argumentToUnwind: Cypher.Param | Cypher.Property; + createInputParam: Cypher.Param | Cypher.Property; + inputFields: InputField[]; + projection?: V6ReadOperation; }) { super(); this.target = target; - this.inputFields = new Map(); - this.argumentToUnwind = argumentToUnwind; + this.inputFields = inputFields; + this.createInputParam = createInputParam; this.unwindVariable = new Cypher.Variable(); + this.projection = projection; } public getChildren(): QueryASTNode[] { - return [...this.inputFields.values(), ...this.projectionOperations]; + return filterTruthy([...this.inputFields.values(), this.projection]); } /** * Get and set field methods are utilities to remove duplicate fields between separate inputs * TODO: This logic should be handled in the factory. */ - public getField(key: string, attachedTo: "node" | "relationship") { + /* public getField(key: string, attachedTo: "node" | "relationship") { return this.inputFields.get(`${attachedTo}_${key}`); } @@ -64,28 +70,20 @@ export class V6CreateOperation extends MutationOperation { if (!this.inputFields.has(field.name)) { this.inputFields.set(`${attachedTo}_${field.name}`, field); } - } - - public getUnwindVariable(): Cypher.Variable { - return this.unwindVariable; - } - - public addProjectionOperations(operations: V6ReadOperation[]) { - this.projectionOperations.push(...operations); - } + } */ public transpile(context: QueryASTContext): OperationTranspileResult { if (!context.hasTarget()) { throw new Error("No parent node found!"); } - const unwindClause = new Cypher.Unwind([this.argumentToUnwind, this.unwindVariable]); + 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.values()) { + for (const field of this.inputFields) { if (field.attachedTo === "node") { createClause.set(...field.getSetParams(context, this.unwindVariable)); setSubqueries.push(...field.getSubqueries(context)); @@ -109,8 +107,9 @@ export class V6CreateOperation extends MutationOperation { } private getProjectionClause(context: QueryASTContext): Cypher.Clause[] { - return this.projectionOperations.map((operationField) => { - return Cypher.concat(...operationField.transpile(context).clauses); - }); + 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 index 29cd2c8343..96540f3893 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts @@ -19,12 +19,13 @@ import Cypher from "@neo4j/cypher-builder"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; -import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; -import { QueryAST } from "../../translate/queryAST/ast/QueryAST"; - 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 { PropertyInputField } from "../../translate/queryAST/ast/input-fields/PropertyInputField"; +import { QueryParseError } from "../errors/QueryParseError"; +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"; @@ -61,89 +62,55 @@ export class CreateOperationFactory { }): V6CreateOperation { const topLevelCreateInput = graphQLTreeCreate.args.input; const targetAdapter = new ConcreteEntityAdapter(entity); - const unwindCreate = this.parseTopLevelCreate({ - target: targetAdapter, - createInput: topLevelCreateInput, - argumentToUnwind: new Cypher.Param(topLevelCreateInput), - }); + let projection: V6ReadOperation | undefined; if (graphQLTreeCreate.fields) { - const projection = this.readFactory.generateMutationOperation({ + projection = this.readFactory.generateMutationProjection({ graphQLTreeNode: graphQLTreeCreate, entity, }); - unwindCreate.addProjectionOperations([projection]); } - return unwindCreate; - } - - private parseTopLevelCreate({ - target, - createInput, - argumentToUnwind, - }: { - target: ConcreteEntityAdapter; - createInput: GraphQLTreeCreateInput[]; - argumentToUnwind: Cypher.Property | Cypher.Param; - }): V6CreateOperation { - const unwindCreate = new V6CreateOperation({ - target, - argumentToUnwind, + const inputFields = this.getInputFields({ + target: targetAdapter, + createInput: topLevelCreateInput, }); - - this.hydrateUnwindCreateOperation({ - target, - createInput: createInput, - unwindCreate, + const createOP = new V6CreateOperation({ + target: targetAdapter, + createInputParam: new Cypher.Param(topLevelCreateInput), + projection, + inputFields, }); - return unwindCreate; + return createOP; } - private hydrateUnwindCreateOperation({ + private getInputFields({ target, createInput, - unwindCreate, }: { target: ConcreteEntityAdapter; createInput: GraphQLTreeCreateInput[]; - unwindCreate: V6CreateOperation; - }) { + }): PropertyInputField[] { + const inputFieldsExistence = new Set(); + const inputFields: PropertyInputField[] = []; // TODO: Add autogenerated fields - createInput.forEach((inputItem) => { + + for (const inputItem of createInput) { for (const key of Object.keys(inputItem)) { const attribute = getAttribute(target, key); const attachedTo = "node"; - const inputField = this.parseAttributeInputField({ + if (inputFieldsExistence.has(attribute.name)) { + continue; + } + inputFieldsExistence.add(attribute.name); + const propertyInputField = new PropertyInputField({ attribute, - unwindCreate, attachedTo, }); - if (!inputField) { - continue; - } - unwindCreate.addField(inputField, attachedTo); + inputFields.push(propertyInputField); } - }); - } - - private parseAttributeInputField({ - attribute, - unwindCreate, - attachedTo, - }: { - attribute: AttributeAdapter; - unwindCreate: V6CreateOperation; - attachedTo: "node" | "relationship"; - }): PropertyInputField | undefined { - if (unwindCreate.getField(attribute.name, attachedTo)) { - return; } - - return new PropertyInputField({ - attribute, - attachedTo, - }); + return inputFields; } } @@ -153,9 +120,7 @@ export class CreateOperationFactory { function getAttribute(entity: ConcreteEntityAdapter, key: string): AttributeAdapter { const attribute = entity.attributes.get(key); if (!attribute) { - throw new Error(`Transpile Error: Input field ${key} not found in entity ${entity.name}`); + throw new QueryParseError(`Transpile Error: Input field ${key} not found in entity ${entity.name}`); } return attribute; } - -export class QueryParseError extends Error {} diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 0366997dd1..de3e55d157 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -42,6 +42,7 @@ import { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { FilterFactory } from "./FilterFactory"; import { WithWildCardsSelection } from "../../translate/queryAST/ast/selection/WithWildCardsSelection"; +import { QueryParseError } from "../errors/QueryParseError"; import type { GraphQLTreePoint } from "./resolve-tree-parser/graphql-tree/attributes"; import type { GraphQLTree, @@ -70,7 +71,7 @@ export class ReadOperationFactory { return new QueryAST(operation); } - public generateMutationOperation({ + public generateMutationProjection({ graphQLTreeNode, entity, }: { @@ -385,5 +386,3 @@ export class ReadOperationFactory { }); } } - -export class QueryParseError extends Error {} From 92a6ce482823e43dffec742f1ce0d44aa4f0a2a5 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 1 Aug 2024 17:23:34 +0100 Subject: [PATCH 135/177] clean-up Top-Level Create PR --- .../graphql/src/api-v6/queryIR/CreateOperation.ts | 14 -------------- .../queryIRFactory/CreateOperationFactory.ts | 4 ++-- .../api-v6/queryIRFactory/ReadOperationFactory.ts | 7 +++---- .../factory-parse-error.ts} | 3 +-- .../ast/selection/WithWildCardsSelection.ts | 5 ----- 5 files changed, 6 insertions(+), 27 deletions(-) rename packages/graphql/src/api-v6/{errors/QueryParseError.ts => queryIRFactory/factory-parse-error.ts} (93%) diff --git a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts index 7992bc6701..289e305098 100644 --- a/packages/graphql/src/api-v6/queryIR/CreateOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/CreateOperation.ts @@ -58,20 +58,6 @@ export class V6CreateOperation extends MutationOperation { return filterTruthy([...this.inputFields.values(), this.projection]); } - /** - * Get and set field methods are utilities to remove duplicate fields between separate inputs - * TODO: This logic should be handled in the factory. - */ - /* public getField(key: string, attachedTo: "node" | "relationship") { - return this.inputFields.get(`${attachedTo}_${key}`); - } - - public addField(field: InputField, attachedTo: "node" | "relationship") { - if (!this.inputFields.has(field.name)) { - this.inputFields.set(`${attachedTo}_${field.name}`, field); - } - } */ - public transpile(context: QueryASTContext): OperationTranspileResult { if (!context.hasTarget()) { throw new Error("No parent node found!"); diff --git a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts index 96540f3893..cc4ba5a6c8 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts @@ -24,10 +24,10 @@ 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 { PropertyInputField } from "../../translate/queryAST/ast/input-fields/PropertyInputField"; -import { QueryParseError } from "../errors/QueryParseError"; import type { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { V6CreateOperation } from "../queryIR/CreateOperation"; import { ReadOperationFactory } from "./ReadOperationFactory"; +import { FactoryParseError } from "./factory-parse-error"; import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "./resolve-tree-parser/graphql-tree/graphql-tree"; export class CreateOperationFactory { @@ -120,7 +120,7 @@ export class CreateOperationFactory { function getAttribute(entity: ConcreteEntityAdapter, key: string): AttributeAdapter { const attribute = entity.attributes.get(key); if (!attribute) { - throw new QueryParseError(`Transpile Error: Input field ${key} not found in entity ${entity.name}`); + 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/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index de3e55d157..c86e8062c0 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -36,13 +36,12 @@ import { SpatialAttributeField } from "../../translate/queryAST/ast/fields/attri 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 { WithWildCardsSelection } from "../../translate/queryAST/ast/selection/WithWildCardsSelection"; -import { QueryParseError } from "../errors/QueryParseError"; +import { FactoryParseError } from "./factory-parse-error"; import type { GraphQLTreePoint } from "./resolve-tree-parser/graphql-tree/attributes"; import type { GraphQLTree, @@ -149,7 +148,7 @@ export class ReadOperationFactory { const relationshipAdapter = new RelationshipAdapter(relationship); if (!(relationshipAdapter.target instanceof ConcreteEntityAdapter)) { - throw new QueryParseError("Interfaces not supported"); + throw new FactoryParseError("Interfaces not supported"); } // Selection diff --git a/packages/graphql/src/api-v6/errors/QueryParseError.ts b/packages/graphql/src/api-v6/queryIRFactory/factory-parse-error.ts similarity index 93% rename from packages/graphql/src/api-v6/errors/QueryParseError.ts rename to packages/graphql/src/api-v6/queryIRFactory/factory-parse-error.ts index 0a7b1a702e..d7dbc644d3 100644 --- a/packages/graphql/src/api-v6/errors/QueryParseError.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/factory-parse-error.ts @@ -16,5 +16,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export class QueryParseError extends Error {} +export class FactoryParseError extends Error {} diff --git a/packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.ts index cb7fb63b6c..0a2e0cf804 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/WithWildCardsSelection.ts @@ -33,11 +33,6 @@ export class WithWildCardsSelection extends EntitySelection { return { selection: new Cypher.With("*"), nestedContext: context, - - /* new QueryASTContext({ - target: new Cypher.Node(), // This is a dummy node, it will be replaced by the actual node in the next step - neo4jGraphQLContext: context.neo4jGraphQLContext, - }) */ }; } } From 94dae173c61aaa89c78014432ff82f8327046d2e Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 1 Aug 2024 17:38:12 +0100 Subject: [PATCH 136/177] fix unit test --- .../model-adapters/AttributeAdapter.test.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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 f3c9fcfb48..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 @@ -29,6 +29,7 @@ import { Neo4jGraphQLSpatialType, Neo4jGraphQLTemporalType, Neo4jSpatialType, + Neo4jTemporalType, ObjectType, ScalarType, UnionType, @@ -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: [], }) ); @@ -443,7 +444,7 @@ describe("Attribute", () => { new Attribute({ name: "test", annotations: {}, - type: new ScalarType(Neo4jGraphQLTemporalType.Date, true), + type: new Neo4jTemporalType(Neo4jGraphQLTemporalType.Date, true), args: [], }) ); From a143fdbc01affb5931218e84d8597d63fcb87c18 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 2 Aug 2024 14:41:00 +0100 Subject: [PATCH 137/177] add extra entries on create array tests --- .../types/array/number-array.int.test.ts | 24 +++++----- .../types/array/temporal-array.int.test.ts | 48 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts index 5eabf0d540..44b9d61984 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts @@ -48,16 +48,16 @@ describe("Create Nodes with Numeric array fields", () => { ${Movie.operations.create}(input: [ { node: { - year: [1999], - rating: [4.0], - viewings: ["4294967297"], + year: [1999, 2000], + rating: [4.0, 5.0], + viewings: ["4294967297", "5294967297"], } } { node: { - year: [2001], - rating: [4.2], - viewings: ["194967297"], + year: [2001, 2002], + rating: [4.2, 5.2], + viewings: ["194967297", "194967292"], } } ]) { @@ -76,14 +76,14 @@ describe("Create Nodes with Numeric array fields", () => { [Movie.operations.create]: { [Movie.plural]: expect.toIncludeSameMembers([ { - year: [1999], - rating: [4.0], - viewings: ["4294967297"], + year: [1999, 2000], + rating: [4.0, 5.0], + viewings: ["4294967297", "5294967297"], }, { - year: [2001], - rating: [4.2], - viewings: ["194967297"], + year: [2001, 2002], + rating: [4.2, 5.2], + viewings: ["194967297", "194967292"], }, ]), }, diff --git a/packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts b/packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts index ecad5862e6..22fade43cc 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts @@ -78,22 +78,22 @@ describe("Create Nodes with Temporal array fields", () => { ${Movie.operations.create}(input: [ { node: { - date: ["${neoDate1.toString()}"], - dateTime: ["${neoDateTime1.toString()}"] - localTime: ["${neoLocalTime1.toString()}"] - localDateTime: ["${neoLocalDateTime1.toString()}"] - time: ["${neoTime1.toString()}"], - duration: ["${duration1.toString()}"] + 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()}"], - dateTime: ["${neoDateTime2.toString()}"] - localTime: ["${neoLocalTime2.toString()}"] - localDateTime: ["${neoLocalDateTime2.toString()}"] - time: ["${neoTime2.toString()}"], - duration: ["${duration2.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()}"] } } ]) { @@ -115,20 +115,20 @@ describe("Create Nodes with Temporal array fields", () => { [Movie.operations.create]: { [Movie.plural]: expect.toIncludeSameMembers([ { - date: [neoDate1.toString()], - dateTime: [neoDateTime1.toString()], - localTime: [neoLocalTime1.toString()], - localDateTime: [neoLocalDateTime1.toString()], - time: [neoTime1.toString()], - duration: [duration1.toString()], + 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()], - dateTime: [neoDateTime2.toString()], - localTime: [neoLocalTime2.toString()], - localDateTime: [neoLocalDateTime2.toString()], - time: [neoTime2.toString()], - duration: [duration2.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()], }, ]), }, From a86e570f9829f466bff909173fb5cd7e880351b9 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 2 Aug 2024 14:43:14 +0100 Subject: [PATCH 138/177] move node-create tests from combinations folder to directives/node folder --- .../schema-model/graphql-type-names/TopLevelEntityTypeNames.ts | 2 +- .../create-node => directives/node}/create-node.int.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/graphql/tests/api-v6/integration/{combinations/create-node => directives/node}/create-node.int.test.ts (98%) 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 index fdf93a7d18..f7bbb94eaa 100644 --- 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 @@ -76,7 +76,7 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { public get createNode(): string { return `${upperFirst(this.entityName)}CreateNode`; } - // TODO: do we need to memoize the upperFirst/plural calls? + public get createInput(): string { return `${upperFirst(this.entityName)}CreateInput`; } diff --git a/packages/graphql/tests/api-v6/integration/combinations/create-node/create-node.int.test.ts b/packages/graphql/tests/api-v6/integration/directives/node/create-node.int.test.ts similarity index 98% rename from packages/graphql/tests/api-v6/integration/combinations/create-node/create-node.int.test.ts rename to packages/graphql/tests/api-v6/integration/directives/node/create-node.int.test.ts index d879b5d887..4c20a43641 100644 --- a/packages/graphql/tests/api-v6/integration/combinations/create-node/create-node.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/directives/node/create-node.int.test.ts @@ -21,7 +21,7 @@ import { Integer } from "neo4j-driver"; import type { UniqueType } from "../../../../utils/graphql-types"; import { TestHelper } from "../../../../utils/tests-helper"; -describe("Top-Level Create with labels", () => { +describe("@node with Top-Level Create", () => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; From 441fd8bd285e3b482c3450a23cdcbde7dccec844 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 2 Aug 2024 11:09:20 +0100 Subject: [PATCH 139/177] Remove unused parameter typeNames from TopLevelCreateSchemaTypes --- .../schema-types/TopLevelEntitySchemaTypes.ts | 2 +- .../TopLevelCreateSchemaTypes.ts | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) 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 index 7a7a433237..186e3cf2b4 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -71,7 +71,7 @@ export class TopLevelEntitySchemaTypes { this.schemaBuilder = schemaBuilder; this.entityTypeNames = entity.typeNames; this.schemaTypes = schemaTypes; - this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity, schemaTypes }); + this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity }); } public addTopLevelQueryField( 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 index 41f99984b2..d31f4e47ee 100644 --- 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 @@ -35,27 +35,16 @@ import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEnt 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 TopLevelCreateSchemaTypes { private entityTypeNames: TopLevelEntityTypeNames; - private schemaTypes: SchemaTypes; private schemaBuilder: SchemaBuilder; private entity: ConcreteEntity; - constructor({ - entity, - schemaBuilder, - schemaTypes, - }: { - entity: ConcreteEntity; - schemaBuilder: SchemaBuilder; - schemaTypes: SchemaTypes; - }) { + constructor({ entity, schemaBuilder }: { entity: ConcreteEntity; schemaBuilder: SchemaBuilder }) { this.entity = entity; this.entityTypeNames = entity.typeNames; this.schemaBuilder = schemaBuilder; - this.schemaTypes = schemaTypes; } public get createInput(): InputTypeComposer { From 35c510a259b9b269c774466b5b1a868fadeb3fbd Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 2 Aug 2024 14:34:05 +0100 Subject: [PATCH 140/177] initial implementation @default directive --- .../api-v6/schema-generation/SchemaBuilder.ts | 22 ++- .../TopLevelCreateSchemaTypes.ts | 38 ++-- .../api-v6/schema/directives/default.test.ts | 165 ++++++++++++++++++ 3 files changed, 205 insertions(+), 20 deletions(-) create mode 100644 packages/graphql/tests/api-v6/schema/directives/default.test.ts diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index 8615b735b3..51098e4f7e 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -31,6 +31,7 @@ import { SchemaComposer } from "graphql-compose"; import { SchemaBuilderTypes } from "./SchemaBuilderTypes"; export type TypeDefinition = string | WrappedComposer; +export type InputTypeDefinition = string | WrappedComposer; type ObjectOrInputTypeComposer = ObjectTypeComposer | InputTypeComposer; @@ -41,7 +42,7 @@ type ListOrNullComposer> | NonNullComposer>>; -type WrappedComposer = T | ListOrNullComposer; +export type WrappedComposer = T | ListOrNullComposer; export type GraphQLResolver = (...args) => any; @@ -53,6 +54,14 @@ export type FieldDefinition = { 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; @@ -108,7 +117,6 @@ export class SchemaBuilder { if (description) { tc.setDescription(description); } - // This is used for global node, not sure if needed for other interfaces tc.setResolveType((obj) => { return obj.__resolveType; @@ -125,6 +133,7 @@ export class SchemaBuilder { | GraphQLInputType | GraphQLNonNull | WrappedComposer + | InputFieldDefinition >; description?: string; } @@ -142,7 +151,14 @@ export class SchemaBuilder { public createInputObjectType( name: string, - fields: Record>, + fields: Record< + string, + | EnumTypeComposer + | GraphQLInputType + | GraphQLNonNull + | WrappedComposer + | InputFieldDefinition + >, description?: string ): InputTypeComposer { return this.composer.createInputTC({ 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 index d31f4e47ee..0affbe825d 100644 --- 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 @@ -17,8 +17,7 @@ * limitations under the License. */ -import type { GraphQLScalarType } from "graphql"; -import type { InputTypeComposer, NonNullComposer, ScalarTypeComposer } from "graphql-compose"; +import type { InputTypeComposer, ScalarTypeComposer } from "graphql-compose"; import type { Attribute } from "../../../../schema-model/attribute/Attribute"; import type { AttributeType } from "../../../../schema-model/attribute/AttributeType"; import { @@ -34,7 +33,7 @@ import { 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 { InputFieldDefinition, SchemaBuilder, WrappedComposer } from "../../SchemaBuilder"; export class TopLevelCreateSchemaTypes { private entityTypeNames: TopLevelEntityTypeNames; @@ -68,37 +67,41 @@ export class TopLevelCreateSchemaTypes { }); } - private getInputFields(attributes: Attribute[]): Record { - const inputFields: Array<[string, InputTypeComposer | GraphQLScalarType] | []> = filterTruthy( + private getInputFields(attributes: Attribute[]): Record { + const inputFields: Array<[string, InputFieldDefinition] | []> = filterTruthy( attributes.map((attribute) => { - const inputField = this.attributeToInputField(attribute.type); + const inputField = this.attributeToInputField(attribute.type, attribute); + const fieldDefinition: InputFieldDefinition = { + type: inputField, + defaultValue: attribute.annotations.default?.value, + }; if (inputField) { - return [attribute.name, inputField]; + return [attribute.name, fieldDefinition]; } }) ); return Object.fromEntries(inputFields); } - private attributeToInputField(type: AttributeType): any { + private attributeToInputField(type: AttributeType, attribute: Attribute): any { if (type instanceof ListType) { if (type.isRequired) { - return this.attributeToInputField(type.ofType).List.NonNull; + return this.attributeToInputField(type.ofType, attribute).List.NonNull; } - return this.attributeToInputField(type.ofType).List; + return this.attributeToInputField(type.ofType, attribute).List; } if (type instanceof ScalarType) { - return this.createBuiltInFieldInput(type); + return this.createBuiltInFieldInput(type, attribute); } if (type instanceof Neo4jTemporalType) { - return this.createTemporalFieldInput(type); + return this.createTemporalFieldInput(type, attribute); } if (type instanceof Neo4jSpatialType) { - return this.createSpatialFieldInput(type); + return this.createSpatialFieldInput(type, attribute); } } - private createBuiltInFieldInput(type: ScalarType): ScalarTypeComposer | NonNullComposer { + private createBuiltInFieldInput(type: ScalarType, attribute: Attribute): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case GraphQLBuiltInScalarType.Boolean: { @@ -136,8 +139,9 @@ export class TopLevelCreateSchemaTypes { } private createTemporalFieldInput( - type: Neo4jTemporalType - ): ScalarTypeComposer | NonNullComposer { + type: Neo4jTemporalType, + attribute: Attribute + ): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case Neo4jGraphQLTemporalType.Date: { @@ -174,7 +178,7 @@ export class TopLevelCreateSchemaTypes { return builtInType; } - private createSpatialFieldInput(type: Neo4jSpatialType): InputTypeComposer | NonNullComposer { + private createSpatialFieldInput(type: Neo4jSpatialType, attribute: Attribute): WrappedComposer { let builtInType: InputTypeComposer; switch (type.name) { case Neo4jGraphQLSpatialType.CartesianPoint: { 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..8bc59a047b --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/default.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 { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("@default", () => { + 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) + # isReleased: Boolean @default(value: true) + # playedCounter: BigInt @default(value: "100") + # duration: Duration @default(value: "PT190M") + # releasedDate: Date @default(value: "2021-01-01") + # releasedTime: Time @default(value: "00:00:00+0100") + # releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") + # releasedLocalTime: LocalTime @default(value: "00:00:00") + # releasedLocalDateTime: LocalDateTime @default(value: "2021-01-01T00:00:00+0100") + # premiereLocation: Point @default(value: { longitude: 1, latitude: 2 }) + # locations: [Point] @default(value: [{ longitude: 1, latitude: 2 }]) + # premiereGeoLocation: [CartesianPoint] @default(value: { x: 1, y: 2 }) + # geoLocations: [CartesianPoint] @default(value: [{ x: 1, y: 2 }]) + } + `; + 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 IDWhere { + AND: [IDWhere!] + NOT: IDWhere + OR: [IDWhere!] + contains: ID + endsWith: ID + equals: ID + in: [ID!] + startsWith: ID + } + + type Movie { + id: ID! + title: String + } + + type MovieConnection { + edges: [MovieEdge] + pageInfo: PageInfo + } + + input MovieConnectionSort { + node: MovieSort + } + + type MovieCreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + id: ID! = \\"id\\" + title: String = \\"title\\" + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + 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 MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + id: IDWhere + title: StringWhere + } + + type Mutation { + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + enum SortDirection { + ASC + DESC + } + + input StringWhere { + AND: [StringWhere!] + NOT: StringWhere + OR: [StringWhere!] + contains: String + endsWith: String + equals: String + in: [String!] + startsWith: String + }" + `); + }); + test.todo("@default directive with invalid values"); + test.todo("@default directive with relationship properties"); + test.todo("@default directive with user defined scalars"); +}); From 663e8ca4c7afcabe795ab4bba3a6ad1068057073 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 2 Aug 2024 16:20:26 +0100 Subject: [PATCH 141/177] add @default test with create --- .../default/create-default.int.test.ts | 71 +++++ .../api-v6/schema/directives/default.test.ts | 247 +++++++++++++++++- 2 files changed, 304 insertions(+), 14 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/directives/default/create-default.int.test.ts 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..57e28e07c5 --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/directives/default/create-default.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 @default", () => { + 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 @default(value: 2001) + ratings: [Int!] @default(value: [1, 2, 3]) + } + `; + 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"} } + ]) { + ${Movie.plural} { + title + released + ratings + } + } + } + `; + + 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: 2001, ratings: [1, 2, 3] }, + { title: "The Matrix 2", released: 2001, ratings: [1, 2, 3] }, + ]), + }, + }); + }); +}); diff --git a/packages/graphql/tests/api-v6/schema/directives/default.test.ts b/packages/graphql/tests/api-v6/schema/directives/default.test.ts index 8bc59a047b..6f208faa6d 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -28,20 +28,18 @@ describe("@default", () => { type Movie @node { id: ID! @default(value: "id") title: String @default(value: "title") - # year: Int @default(value: 2021) - # length: Float @default(value: 120.5) - # isReleased: Boolean @default(value: true) - # playedCounter: BigInt @default(value: "100") - # duration: Duration @default(value: "PT190M") - # releasedDate: Date @default(value: "2021-01-01") - # releasedTime: Time @default(value: "00:00:00+0100") - # releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") - # releasedLocalTime: LocalTime @default(value: "00:00:00") - # releasedLocalDateTime: LocalDateTime @default(value: "2021-01-01T00:00:00+0100") - # premiereLocation: Point @default(value: { longitude: 1, latitude: 2 }) - # locations: [Point] @default(value: [{ longitude: 1, latitude: 2 }]) - # premiereGeoLocation: [CartesianPoint] @default(value: { x: 1, y: 2 }) - # geoLocations: [CartesianPoint] @default(value: [{ x: 1, y: 2 }]) + year: Int @default(value: 2021) + length: Float @default(value: 120.5) + isReleased: Boolean @default(value: true) + playedCounter: BigInt @default(value: "100") + duration: Duration @default(value: "PT190M") + releasedDate: Date @default(value: "2021-01-01") + releasedTime: Time @default(value: "00:00:00") + releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") + releasedLocalTime: LocalTime @default(value: "00:00:00") + releasedLocalDateTime: LocalDateTime @default(value: "2015-07-04T19:32:24") + premiereLocation: Point @default(value: { longitude: 1, latitude: 2 }) + premiereGeoLocation: CartesianPoint @default(value: { x: 1, y: 2 }) } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); @@ -54,6 +52,105 @@ describe("@default", () => { 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 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 + } + + \\"\\"\\" + 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 + } + + \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" + scalar Date + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeWhere { + AND: [DateTimeWhere!] + NOT: DateTimeWhere + OR: [DateTimeWhere!] + equals: DateTime + gt: DateTime + gte: DateTime + in: [DateTime!] + lt: DateTime + lte: DateTime + } + + input DateWhere { + AND: [DateWhere!] + NOT: DateWhere + OR: [DateWhere!] + equals: Date + gt: Date + gte: Date + in: [Date!] + lt: Date + lte: Date + } + + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" + scalar Duration + + input DurationWhere { + AND: [DurationWhere!] + NOT: DurationWhere + OR: [DurationWhere!] + equals: Duration + gt: Duration + gte: Duration + in: [Duration!] + lt: Duration + lte: Duration + } + + input FloatWhere { + AND: [FloatWhere!] + NOT: FloatWhere + OR: [FloatWhere!] + equals: Float + gt: Float + gte: Float + in: [Float!] + lt: Float + lte: Float + } + input IDWhere { AND: [IDWhere!] NOT: IDWhere @@ -65,9 +162,65 @@ describe("@default", () => { startsWith: ID } + input IntWhere { + AND: [IntWhere!] + NOT: IntWhere + OR: [IntWhere!] + equals: Int + gt: Int + gte: Int + in: [Int!] + lt: Int + lte: Int + } + + \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" + scalar 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 LocalTimeWhere { + AND: [LocalTimeWhere!] + NOT: LocalTimeWhere + OR: [LocalTimeWhere!] + equals: LocalTime + gt: LocalTime + gte: LocalTime + in: [LocalTime!] + lt: LocalTime + lte: LocalTime + } + type Movie { + duration: Duration id: ID! + isReleased: Boolean + length: Float + playedCounter: BigInt + premiereGeoLocation: CartesianPoint + premiereLocation: Point + releasedDate: Date + releasedDateTime: DateTime + releasedLocalDateTime: LocalDateTime + releasedLocalTime: LocalTime + releasedTime: Time title: String + year: Int } type MovieConnection { @@ -89,8 +242,20 @@ describe("@default", () => { } input MovieCreateNode { + duration: Duration = \\"P0M0DT11400S\\" id: ID! = \\"id\\" + isReleased: Boolean = true + length: Float = 120.5 + playedCounter: BigInt = \\"100\\" + premiereGeoLocation: CartesianPointInput = {x: 1, y: 2} + premiereLocation: PointInput = {latitude: 2, longitude: 1} + releasedDate: Date = \\"2021-01-01\\" + releasedDateTime: DateTime = \\"2021-01-01T00:00:00.000Z\\" + releasedLocalDateTime: LocalDateTime = \\"2015-07-04T19:32:24\\" + releasedLocalTime: LocalTime = \\"00:00:00\\" + releasedTime: Time = \\"00:00:00Z\\" title: String = \\"title\\" + year: Int = 2021 } type MovieCreateResponse { @@ -115,16 +280,36 @@ describe("@default", () => { } input MovieSort { + duration: SortDirection id: SortDirection + isReleased: SortDirection + length: SortDirection + playedCounter: SortDirection + releasedDate: SortDirection + releasedDateTime: SortDirection + releasedLocalDateTime: SortDirection + releasedLocalTime: SortDirection + releasedTime: SortDirection title: SortDirection + year: SortDirection } input MovieWhere { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + duration: DurationWhere id: IDWhere + isReleased: BooleanWhere + length: FloatWhere + playedCounter: BigIntWhere + releasedDate: DateWhere + releasedDateTime: DateTimeWhere + releasedLocalDateTime: LocalDateTimeWhere + releasedLocalTime: LocalTimeWhere + releasedTime: TimeWhere title: StringWhere + year: IntWhere } type Mutation { @@ -138,6 +323,24 @@ describe("@default", () => { 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! + } + type Query { movies(where: MovieOperationWhere): MovieOperation } @@ -156,10 +359,26 @@ describe("@default", () => { equals: String in: [String!] startsWith: String + } + + \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" + scalar Time + + input TimeWhere { + AND: [TimeWhere!] + NOT: TimeWhere + OR: [TimeWhere!] + equals: Time + gt: Time + gte: Time + in: [Time!] + lt: Time + lte: Time }" `); }); test.todo("@default directive with invalid values"); + test.todo("@default directive with list properties"); test.todo("@default directive with relationship properties"); test.todo("@default directive with user defined scalars"); }); From 015cdf339e392dbbebbe760553c13bd13a076c0d Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 5 Aug 2024 17:21:14 +0100 Subject: [PATCH 142/177] add validation for @default, tests, and clean-up @default implementation --- .../TopLevelCreateSchemaTypes.ts | 23 +-- .../api-v6/validation/rules/valid-default.ts | 101 ++++++++++ .../api-v6/validation/validate-v6-document.ts | 2 + .../utils/same-type-argument-as-field.ts | 6 +- .../validation/validate-document.test.ts | 34 ---- .../default/create-default.int.test.ts | 30 ++- .../schema/directives/default-array.test.ts | 160 ++++++++++++++++ .../api-v6/schema/directives/default.test.ts | 181 +----------------- .../invalid-default-values.test.ts | 116 +++++++++++ 9 files changed, 413 insertions(+), 240 deletions(-) create mode 100644 packages/graphql/src/api-v6/validation/rules/valid-default.ts create mode 100644 packages/graphql/tests/api-v6/schema/directives/default-array.test.ts create mode 100644 packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts 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 index 0affbe825d..e5efa704be 100644 --- 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 @@ -70,7 +70,7 @@ export class TopLevelCreateSchemaTypes { private getInputFields(attributes: Attribute[]): Record { const inputFields: Array<[string, InputFieldDefinition] | []> = filterTruthy( attributes.map((attribute) => { - const inputField = this.attributeToInputField(attribute.type, attribute); + const inputField = this.attributeToInputField(attribute.type); const fieldDefinition: InputFieldDefinition = { type: inputField, defaultValue: attribute.annotations.default?.value, @@ -83,25 +83,25 @@ export class TopLevelCreateSchemaTypes { return Object.fromEntries(inputFields); } - private attributeToInputField(type: AttributeType, attribute: Attribute): any { + private attributeToInputField(type: AttributeType): any { if (type instanceof ListType) { if (type.isRequired) { - return this.attributeToInputField(type.ofType, attribute).List.NonNull; + return this.attributeToInputField(type.ofType).List.NonNull; } - return this.attributeToInputField(type.ofType, attribute).List; + return this.attributeToInputField(type.ofType).List; } if (type instanceof ScalarType) { - return this.createBuiltInFieldInput(type, attribute); + return this.createBuiltInFieldInput(type); } if (type instanceof Neo4jTemporalType) { - return this.createTemporalFieldInput(type, attribute); + return this.createTemporalFieldInput(type); } if (type instanceof Neo4jSpatialType) { - return this.createSpatialFieldInput(type, attribute); + return this.createSpatialFieldInput(type); } } - private createBuiltInFieldInput(type: ScalarType, attribute: Attribute): WrappedComposer { + private createBuiltInFieldInput(type: ScalarType): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case GraphQLBuiltInScalarType.Boolean: { @@ -138,10 +138,7 @@ export class TopLevelCreateSchemaTypes { return builtInType; } - private createTemporalFieldInput( - type: Neo4jTemporalType, - attribute: Attribute - ): WrappedComposer { + private createTemporalFieldInput(type: Neo4jTemporalType): WrappedComposer { let builtInType: ScalarTypeComposer; switch (type.name) { case Neo4jGraphQLTemporalType.Date: { @@ -178,7 +175,7 @@ export class TopLevelCreateSchemaTypes { return builtInType; } - private createSpatialFieldInput(type: Neo4jSpatialType, attribute: Attribute): WrappedComposer { + private createSpatialFieldInput(type: Neo4jSpatialType): WrappedComposer { let builtInType: InputTypeComposer; switch (type.name) { case Neo4jGraphQLSpatialType.CartesianPoint: { 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..ced97d40fb --- /dev/null +++ b/packages/graphql/src/api-v6/validation/rules/valid-default.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 { ASTVisitor, FieldDefinitionNode, StringValueNode } from "graphql"; +import type { SDLValidationContext } from "graphql/validation/ValidationContext"; +import { isSpatial, isTemporal } from "../../../constants"; +import { defaultDirective } from "../../../graphql/directives"; +import { + GraphQLBuiltInScalarType, + Neo4jGraphQLNumberType, + Neo4jGraphQLSpatialType, + Neo4jGraphQLTemporalType, +} from "../../../schema-model/attribute/AttributeType"; +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"; + +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 supported + // !isTypeABuiltInType(expectedType) && !userEnums.some((x) => x.name.value === expectedType) + 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, + }) + ); + } + }, + }; +} + +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/validate-v6-document.ts b/packages/graphql/src/api-v6/validation/validate-v6-document.ts index b3d1c0f0fe..d8c6981ca9 100644 --- a/packages/graphql/src/api-v6/validation/validate-v6-document.ts +++ b/packages/graphql/src/api-v6/validation/validate-v6-document.ts @@ -44,6 +44,7 @@ import { DirectiveCombinationValid } from "../../schema/validation/custom-rules/ 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 { ValidLimit } from "./rules/valid-limit"; import { ValidRelationship } from "./rules/valid-relationship"; @@ -66,6 +67,7 @@ function runNeo4jGraphQLValidationRules({ ...specifiedSDLRules, ValidRelationship, ValidLimit, + ValidDefault, DirectiveCombinationValid, ValidRelationshipProperties, ReservedTypeNames, 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..cbfd2b9a26 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` 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 index 57e28e07c5..5ff485814e 100644 --- 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 @@ -17,31 +17,30 @@ * limitations under the License. */ -import type { UniqueType } from "../../../../utils/graphql-types"; import { TestHelper } from "../../../../utils/tests-helper"; describe("Create with @default", () => { const testHelper = new TestHelper({ v6Api: true }); - let Movie: UniqueType; - beforeAll(async () => { - Movie = testHelper.createUniqueType("Movie"); + 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! - released: Int @default(value: 2001) - ratings: [Int!] @default(value: [1, 2, 3]) + testField: ${dataType} @default(value: ${typeof value === "string" ? `"${value}"` : value}) } `; 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: [ @@ -50,8 +49,7 @@ describe("Create with @default", () => { ]) { ${Movie.plural} { title - released - ratings + testField } } } @@ -62,8 +60,8 @@ describe("Create with @default", () => { expect(gqlResult.data).toEqual({ [Movie.operations.create]: { [Movie.plural]: expect.toIncludeSameMembers([ - { title: "The Matrix", released: 2001, ratings: [1, 2, 3] }, - { title: "The Matrix 2", released: 2001, ratings: [1, 2, 3] }, + { title: "The Matrix", testField: value }, + { title: "The Matrix 2", testField: value }, ]), }, }); 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..5d1cdc80f0 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/default-array.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 { 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 + } + + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" + scalar DateTime + + input DateTimeListWhere { + equals: [DateTime!] + } + + input FloatListWhere { + equals: [Float!] + } + + input IDListWhere { + equals: [ID!] + } + + input IntListWhere { + equals: [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! + relationshipsCreated: 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: MovieCreateInfo + movies: [Movie!]! + } + + 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 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 + } + + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + type Query { + movies(where: MovieOperationWhere): MovieOperation + } + + input StringListWhere { + equals: [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 index 6f208faa6d..6c09889c99 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -22,7 +22,7 @@ import { lexicographicSortSchema } from "graphql/utilities"; import { Neo4jGraphQL } from "../../../../src"; import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; -describe("@default", () => { +describe("@default on fields", () => { test("@default should add a default value in mutation inputs", async () => { const typeDefs = /* GraphQL */ ` type Movie @node { @@ -30,16 +30,8 @@ describe("@default", () => { title: String @default(value: "title") year: Int @default(value: 2021) length: Float @default(value: 120.5) - isReleased: Boolean @default(value: true) - playedCounter: BigInt @default(value: "100") - duration: Duration @default(value: "PT190M") - releasedDate: Date @default(value: "2021-01-01") - releasedTime: Time @default(value: "00:00:00") + flag: Boolean @default(value: true) releasedDateTime: DateTime @default(value: "2021-01-01T00:00:00") - releasedLocalTime: LocalTime @default(value: "00:00:00") - releasedLocalDateTime: LocalDateTime @default(value: "2015-07-04T19:32:24") - premiereLocation: Point @default(value: { longitude: 1, latitude: 2 }) - premiereGeoLocation: CartesianPoint @default(value: { x: 1, y: 2 }) } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); @@ -52,23 +44,6 @@ describe("@default", () => { 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 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 @@ -76,27 +51,6 @@ describe("@default", () => { equals: Boolean } - \\"\\"\\" - 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 - } - - \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" - scalar Date - \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" scalar DateTime @@ -112,33 +66,6 @@ describe("@default", () => { lte: DateTime } - input DateWhere { - AND: [DateWhere!] - NOT: DateWhere - OR: [DateWhere!] - equals: Date - gt: Date - gte: Date - in: [Date!] - lt: Date - lte: Date - } - - \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" - scalar Duration - - input DurationWhere { - AND: [DurationWhere!] - NOT: DurationWhere - OR: [DurationWhere!] - equals: Duration - gt: Duration - gte: Duration - in: [Duration!] - lt: Duration - lte: Duration - } - input FloatWhere { AND: [FloatWhere!] NOT: FloatWhere @@ -174,51 +101,11 @@ describe("@default", () => { lte: Int } - \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" - scalar 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 LocalTimeWhere { - AND: [LocalTimeWhere!] - NOT: LocalTimeWhere - OR: [LocalTimeWhere!] - equals: LocalTime - gt: LocalTime - gte: LocalTime - in: [LocalTime!] - lt: LocalTime - lte: LocalTime - } - type Movie { - duration: Duration + flag: Boolean id: ID! - isReleased: Boolean length: Float - playedCounter: BigInt - premiereGeoLocation: CartesianPoint - premiereLocation: Point - releasedDate: Date releasedDateTime: DateTime - releasedLocalDateTime: LocalDateTime - releasedLocalTime: LocalTime - releasedTime: Time title: String year: Int } @@ -242,18 +129,10 @@ describe("@default", () => { } input MovieCreateNode { - duration: Duration = \\"P0M0DT11400S\\" + flag: Boolean = true id: ID! = \\"id\\" - isReleased: Boolean = true length: Float = 120.5 - playedCounter: BigInt = \\"100\\" - premiereGeoLocation: CartesianPointInput = {x: 1, y: 2} - premiereLocation: PointInput = {latitude: 2, longitude: 1} - releasedDate: Date = \\"2021-01-01\\" releasedDateTime: DateTime = \\"2021-01-01T00:00:00.000Z\\" - releasedLocalDateTime: LocalDateTime = \\"2015-07-04T19:32:24\\" - releasedLocalTime: LocalTime = \\"00:00:00\\" - releasedTime: Time = \\"00:00:00Z\\" title: String = \\"title\\" year: Int = 2021 } @@ -280,16 +159,10 @@ describe("@default", () => { } input MovieSort { - duration: SortDirection + flag: SortDirection id: SortDirection - isReleased: SortDirection length: SortDirection - playedCounter: SortDirection - releasedDate: SortDirection releasedDateTime: SortDirection - releasedLocalDateTime: SortDirection - releasedLocalTime: SortDirection - releasedTime: SortDirection title: SortDirection year: SortDirection } @@ -298,16 +171,10 @@ describe("@default", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] - duration: DurationWhere + flag: BooleanWhere id: IDWhere - isReleased: BooleanWhere length: FloatWhere - playedCounter: BigIntWhere - releasedDate: DateWhere releasedDateTime: DateTimeWhere - releasedLocalDateTime: LocalDateTimeWhere - releasedLocalTime: LocalTimeWhere - releasedTime: TimeWhere title: StringWhere year: IntWhere } @@ -323,24 +190,6 @@ describe("@default", () => { 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! - } - type Query { movies(where: MovieOperationWhere): MovieOperation } @@ -359,26 +208,10 @@ describe("@default", () => { equals: String in: [String!] startsWith: String - } - - \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" - scalar Time - - input TimeWhere { - AND: [TimeWhere!] - NOT: TimeWhere - OR: [TimeWhere!] - equals: Time - gt: Time - gte: Time - in: [Time!] - lt: Time - lte: Time }" `); }); - test.todo("@default directive with invalid values"); - test.todo("@default directive with list properties"); + test.todo("@default directive with relationship properties"); test.todo("@default directive with user defined scalars"); }); diff --git a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts new file mode 100644 index 0000000000..2cbd507f52 --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.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 { GraphQLError } from "graphql"; +import { Neo4jGraphQL } from "../../../../src"; +import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; + +describe("invalid @default usage", () => { + test("@default should fail without define a 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", + invalidValue: 1.2, + errorMsg: "@default.value on ID fields must be of type ID", + }, + { + dataType: "String", + invalidValue: 1.2, + errorMsg: "@default.value on String fields must be of type String", + }, + { + dataType: "Boolean", + invalidValue: 1.2, + errorMsg: "@default.value on Boolean fields must be of type Boolean", + }, + { dataType: "Int", invalidValue: 1.2, errorMsg: "@default.value on Int fields must be of type Int" }, + { + dataType: "Float", + invalidValue: "stuff", + errorMsg: "@default.value on Float fields must be of type Float", + }, + { dataType: "DateTime", invalidValue: "dummy", errorMsg: "@default.value is not a valid DateTime" }, + ] as const)( + "@default should fail with an invalid $dataType value", + async ({ dataType, invalidValue, errorMsg }) => { + const fn = async () => { + const typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${ + typeof invalidValue === "string" ? `"${invalidValue}"` : invalidValue + })} + `; + const neoSchema = new Neo4jGraphQL({ typeDefs }); + const schema = await neoSchema.getAuraSchema(); + raiseOnInvalidSchema(schema); + }; + await expect(fn()).rejects.toEqual([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 typeDefs = /* GraphQL */ ` + type User @node { + name: ${dataType} @default(value: ${typeof value === "string" ? `"${value}"` : value})} + `; + 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"); +}); From 078d9bf96807dcf850eef2840136c456c6a7795f Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 5 Aug 2024 18:22:25 +0100 Subject: [PATCH 143/177] move @default tests --- .../validation/validate-document.test.ts | 459 ------------------ ...valid-default-usage-on-list-fields.test.ts | 130 +++++ ....test.ts => invalid-default-usage.test.ts} | 40 +- .../directives/default.int.test.ts | 66 --- .../graphql/tests/schema/issues/200.test.ts | 4 +- 5 files changed, 156 insertions(+), 543 deletions(-) create mode 100644 packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage-on-list-fields.test.ts rename packages/graphql/tests/api-v6/schema/invalid-schema/{invalid-default-values.test.ts => invalid-default-usage.test.ts} (75%) diff --git a/packages/graphql/src/schema/validation/validate-document.test.ts b/packages/graphql/src/schema/validation/validate-document.test.ts index cbfd2b9a26..f3b7a46781 100644 --- a/packages/graphql/src/schema/validation/validate-document.test.ts +++ b/packages/graphql/src/schema/validation/validate-document.test.ts @@ -964,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 { @@ -1180,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 { 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..e504d29a7e --- /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 define a 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-values.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts similarity index 75% rename from packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts rename to packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts index 2cbd507f52..ee27592c2f 100644 --- a/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-values.test.ts +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts @@ -43,41 +43,44 @@ describe("invalid @default usage", () => { test.each([ { dataType: "ID", - invalidValue: 1.2, + value: 1.2, errorMsg: "@default.value on ID fields must be of type ID", }, { dataType: "String", - invalidValue: 1.2, + value: 1.2, errorMsg: "@default.value on String fields must be of type String", }, { dataType: "Boolean", - invalidValue: 1.2, + value: 1.2, errorMsg: "@default.value on Boolean fields must be of type Boolean", }, - { dataType: "Int", invalidValue: 1.2, errorMsg: "@default.value on Int fields must be of type Int" }, + { dataType: "Int", value: 1.2, errorMsg: "@default.value on Int fields must be of type Int" }, { dataType: "Float", - invalidValue: "stuff", + value: "stuff", errorMsg: "@default.value on Float fields must be of type Float", }, - { dataType: "DateTime", invalidValue: "dummy", errorMsg: "@default.value is not a valid DateTime" }, + { dataType: "DateTime", value: "dummy", errorMsg: "@default.value is not a valid DateTime" }, ] as const)( "@default should fail with an invalid $dataType value", - async ({ dataType, invalidValue, errorMsg }) => { + 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: ${ - typeof invalidValue === "string" ? `"${invalidValue}"` : invalidValue - })} - `; + 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)]); + await expect(fn()).rejects.toEqual([new GraphQLError(errorMsg), new GraphQLError(errorMsg)]); } ); @@ -99,10 +102,15 @@ describe("invalid @default usage", () => { { 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: ${typeof value === "string" ? `"${value}"` : value})} - `; + 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); 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/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 { From 98bb6c3ba9c0875c77552859da130ca36720c50a Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 6 Aug 2024 10:27:05 +0100 Subject: [PATCH 144/177] fix default for array tests --- .../src/api-v6/validation/rules/valid-default.ts | 3 +-- .../invalid-default-usage-on-list-fields.test.ts | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/graphql/src/api-v6/validation/rules/valid-default.ts b/packages/graphql/src/api-v6/validation/rules/valid-default.ts index ced97d40fb..27e0f29453 100644 --- a/packages/graphql/src/api-v6/validation/rules/valid-default.ts +++ b/packages/graphql/src/api-v6/validation/rules/valid-default.ts @@ -65,8 +65,7 @@ export function ValidDefault(context: SDLValidationContext): ASTVisitor { ); } } else if (!isTypeABuiltInType(expectedType)) { - //TODO: Add check for user defined enums that are currently not supported - // !isTypeABuiltInType(expectedType) && !userEnums.some((x) => x.name.value === 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`, [] 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 index e504d29a7e..ae3d73a63b 100644 --- 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 @@ -43,28 +43,28 @@ describe("invalid @default usage on List fields", () => { test.each([ { dataType: "[ID]", - value: 1.2, + value: [1.2], errorMsg: "@default.value on ID list fields must be a list of ID values", }, { dataType: "[String]", - value: 1.2, + value: [1.2], errorMsg: "@default.value on String list fields must be a list of String values", }, { dataType: "[Boolean]", - value: 1.2, + 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", + value: ["stuff"], errorMsg: "@default.value on Float list fields must be a list of Float values", }, { dataType: "[DateTime]", - value: "dummy", + value: ["dummy"], errorMsg: "@default.value on DateTime list fields must be a list of DateTime values", }, ] as const)( From b6fee1d9e4720cf694166166c9e61ee3ed66767f Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 6 Aug 2024 11:08:20 +0100 Subject: [PATCH 145/177] Update mutations on version 6 --- .../resolvers/translate-update-resolver.ts | 59 ++++ .../schema-generation/SchemaGenerator.ts | 21 +- .../schema-types/TopLevelEntitySchemaTypes.ts | 47 +++ .../TopLevelUpdateSchemaTypes.ts | 325 ++++++++++++++++++ .../TopLevelEntityTypeNames.ts | 21 ++ .../api-v6/schema/directives/relayId.test.ts | 23 ++ .../tests/api-v6/schema/relationship.test.ts | 64 ++++ .../tests/api-v6/schema/simple.test.ts | 68 ++++ .../tests/api-v6/schema/types/array.test.ts | 118 +++++++ .../tests/api-v6/schema/types/scalars.test.ts | 70 ++++ .../tests/api-v6/schema/types/spatial.test.ts | 42 +++ .../api-v6/schema/types/temporals.test.ts | 62 ++++ 12 files changed, 913 insertions(+), 7 deletions(-) create mode 100644 packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts create mode 100644 packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelUpdateSchemaTypes.ts 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..16fa489527 --- /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 { parseResolveInfoTreeCreate } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; +import { translateCreateResolver } from "../translators/translate-create-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 graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity }); + const { cypher, params } = translateCreateResolver({ + 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/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index 9172d067d3..9757795c17 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -22,11 +22,12 @@ import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSch 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 { 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"; -import { generateCreateResolver } from "../resolvers/translate-create-resolver"; export class SchemaGenerator { private schemaBuilder: SchemaBuilder; @@ -40,7 +41,7 @@ export class SchemaGenerator { public generate(schemaModel: Neo4jGraphQLSchemaModel): GraphQLSchema { const entityTypesMap = this.generateEntityTypes(schemaModel); this.generateTopLevelQueryFields(entityTypesMap); - this.generateTopLevelCreateFields(entityTypesMap); + this.generateTopLevelMutationFields(entityTypesMap); this.generateGlobalNodeQueryField(schemaModel); @@ -77,12 +78,18 @@ export class SchemaGenerator { } } - private generateTopLevelCreateFields(entityTypesMap: Map): void { + private generateTopLevelMutationFields(entityTypesMap: Map): void { for (const [entity, entitySchemaTypes] of entityTypesMap.entries()) { - const resolver = generateCreateResolver({ - entity, - }); - entitySchemaTypes.addTopLevelCreateField(resolver); + entitySchemaTypes.addTopLevelCreateField( + generateCreateResolver({ + entity, + }) + ); + entitySchemaTypes.addTopLevelUpdateField( + generateUpdateResolver({ + entity, + }) + ); } } 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 index 7a7a433237..a1491892f4 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -48,6 +48,7 @@ import { RelatedEntitySchemaTypes } from "./RelatedEntitySchemaTypes"; import type { SchemaTypes } from "./SchemaTypes"; import { TopLevelFilterSchemaTypes } from "./filter-schema-types/TopLevelFilterSchemaTypes"; import { TopLevelCreateSchemaTypes } from "./mutation-schema-types/TopLevelCreateSchemaTypes"; +import { TopLevelUpdateSchemaTypes } from "./mutation-schema-types/TopLevelUpdateSchemaTypes"; export class TopLevelEntitySchemaTypes { private entity: ConcreteEntity; @@ -56,6 +57,7 @@ export class TopLevelEntitySchemaTypes { private entityTypeNames: TopLevelEntityTypeNames; private schemaTypes: SchemaTypes; private createSchemaTypes: TopLevelCreateSchemaTypes; + private updateSchemaTypes: TopLevelUpdateSchemaTypes; constructor({ entity, @@ -72,6 +74,7 @@ export class TopLevelEntitySchemaTypes { this.entityTypeNames = entity.typeNames; this.schemaTypes = schemaTypes; this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity, schemaTypes }); + this.updateSchemaTypes = new TopLevelUpdateSchemaTypes({ schemaBuilder, entity, schemaTypes }); } public addTopLevelQueryField( @@ -145,6 +148,23 @@ export class TopLevelEntitySchemaTypes { 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.List.NonNull, + }, + resolver, + }); + } protected get connectionSort(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { @@ -314,6 +334,20 @@ export class TopLevelEntitySchemaTypes { }); } + 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 { @@ -324,6 +358,19 @@ export class TopLevelEntitySchemaTypes { }; }); } + + 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 { 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/TopLevelEntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/TopLevelEntityTypeNames.ts index f7bbb94eaa..7bf1b2edc7 100644 --- 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 @@ -88,4 +88,25 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { 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`; + } } diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index 3db3146d78..305c38d505 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -44,6 +44,10 @@ describe("RelayId", () => { equals: String } + input IDUpdate { + set: ID + } + input IDWhere { AND: [IDWhere!] NOT: IDWhere @@ -110,6 +114,20 @@ describe("RelayId", () => { title: SortDirection } + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + dbId: IDUpdate + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + input MovieWhere { AND: [MovieWhere!] NOT: MovieWhere @@ -121,6 +139,7 @@ describe("RelayId", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse } interface Node { @@ -145,6 +164,10 @@ describe("RelayId", () => { DESC } + input StringUpdate { + set: String + } + input StringWhere { AND: [StringWhere!] NOT: StringWhere diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index bc71b78b59..f57dfea811 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -150,6 +150,19 @@ describe("Relationships", () => { name: SortDirection } + input ActorUpdateInput { + node: ActorUpdateNode! + } + + input ActorUpdateNode { + name: StringUpdate + } + + type ActorUpdateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + input ActorWhere { AND: [ActorWhere!] NOT: ActorWhere @@ -263,6 +276,19 @@ describe("Relationships", () => { title: SortDirection } + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + input MovieWhere { AND: [MovieWhere!] NOT: MovieWhere @@ -274,6 +300,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + updateActors(input: [ActorUpdateInput!]!): ActorUpdateResponse + updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse } type PageInfo { @@ -293,6 +321,10 @@ describe("Relationships", () => { DESC } + input StringUpdate { + set: String + } + input StringWhere { AND: [StringWhere!] NOT: StringWhere @@ -455,6 +487,19 @@ describe("Relationships", () => { name: SortDirection } + input ActorUpdateInput { + node: ActorUpdateNode! + } + + input ActorUpdateNode { + name: StringUpdate + } + + type ActorUpdateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + input ActorWhere { AND: [ActorWhere!] NOT: ActorWhere @@ -583,6 +628,19 @@ describe("Relationships", () => { title: SortDirection } + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + input MovieWhere { AND: [MovieWhere!] NOT: MovieWhere @@ -594,6 +652,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + updateActors(input: [ActorUpdateInput!]!): ActorUpdateResponse + updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse } type PageInfo { @@ -613,6 +673,10 @@ describe("Relationships", () => { DESC } + input StringUpdate { + set: String + } + input StringWhere { AND: [StringWhere!] NOT: StringWhere diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index a04b45590c..bf045f6c6c 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -91,6 +91,19 @@ describe("Simple Aura-API", () => { title: SortDirection } + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + input MovieWhere { AND: [MovieWhere!] NOT: MovieWhere @@ -100,6 +113,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse } type PageInfo { @@ -118,6 +132,10 @@ describe("Simple Aura-API", () => { DESC } + input StringUpdate { + set: String + } + input StringWhere { AND: [StringWhere!] NOT: StringWhere @@ -202,6 +220,19 @@ describe("Simple Aura-API", () => { name: SortDirection } + input ActorUpdateInput { + node: ActorUpdateNode! + } + + input ActorUpdateNode { + name: StringUpdate + } + + type ActorUpdateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + input ActorWhere { AND: [ActorWhere!] NOT: ActorWhere @@ -260,6 +291,19 @@ describe("Simple Aura-API", () => { title: SortDirection } + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + input MovieWhere { AND: [MovieWhere!] NOT: MovieWhere @@ -270,6 +314,8 @@ describe("Simple Aura-API", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + updateActors(input: [ActorUpdateInput!]!): ActorUpdateResponse + updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse } type PageInfo { @@ -289,6 +335,10 @@ describe("Simple Aura-API", () => { DESC } + input StringUpdate { + set: String + } + input StringWhere { AND: [StringWhere!] NOT: StringWhere @@ -373,6 +423,19 @@ describe("Simple Aura-API", () => { title: SortDirection } + input MovieUpdateInput { + node: MovieUpdateNode! + } + + input MovieUpdateNode { + title: StringUpdate + } + + type MovieUpdateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + input MovieWhere { AND: [MovieWhere!] NOT: MovieWhere @@ -382,6 +445,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse } type PageInfo { @@ -400,6 +464,10 @@ describe("Simple Aura-API", () => { DESC } + input StringUpdate { + set: String + } + input StringWhere { AND: [StringWhere!] NOT: StringWhere diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 6c897f668e..8ef8f7d5d8 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -132,6 +132,10 @@ describe("Scalars", () => { equals: [BigInt] } + input BigIntUpdate { + set: BigInt + } + input BooleanWhere { AND: [BooleanWhere!] NOT: BooleanWhere @@ -161,6 +165,14 @@ describe("Scalars", () => { equals: [DateTime] } + input DateTimeUpdate { + set: DateTime + } + + input DateUpdate { + set: Date + } + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" scalar Duration @@ -172,6 +184,10 @@ describe("Scalars", () => { equals: [Duration] } + input DurationUpdate { + set: Duration + } + input FloatListWhere { equals: [Float!] } @@ -180,6 +196,10 @@ describe("Scalars", () => { equals: [Float] } + input FloatUpdate { + set: Float + } + input IDListWhere { equals: [ID!] } @@ -188,6 +208,10 @@ describe("Scalars", () => { equals: [ID] } + input IDUpdate { + set: ID + } + input IntListWhere { equals: [Int!] } @@ -196,6 +220,10 @@ describe("Scalars", () => { equals: [Int] } + input IntUpdate { + set: Int + } + \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" scalar LocalDateTime @@ -207,6 +235,10 @@ describe("Scalars", () => { equals: [LocalDateTime] } + input LocalDateTimeUpdate { + set: LocalDateTime + } + \\"\\"\\" A local time, represented as a time string without timezone information \\"\\"\\" @@ -220,9 +252,15 @@ describe("Scalars", () => { equals: [LocalTime] } + input LocalTimeUpdate { + set: LocalTime + } + type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + updateNodeTypes(input: [NodeTypeUpdateInput!]!): NodeTypeUpdateResponse + updateRelatedNodes(input: [RelatedNodeUpdateInput!]!): RelatedNodeUpdateResponse } type NodeType { @@ -362,6 +400,42 @@ describe("Scalars", () => { edges: NodeTypeRelatedNodeEdgeWhere } + input NodeTypeUpdateInput { + node: NodeTypeUpdateNode! + } + + input NodeTypeUpdateNode { + bigIntList: BigIntUpdate + bigIntListNullable: BigIntUpdate + booleanList: IntUpdate + booleanListNullable: IntUpdate + dateList: DateUpdate + dateListNullable: DateUpdate + dateTimeList: DateTimeUpdate + dateTimeListNullable: DateTimeUpdate + durationList: DurationUpdate + durationListNullable: DurationUpdate + floatList: FloatUpdate + floatListNullable: FloatUpdate + idList: IDUpdate + idListNullable: IDUpdate + intList: IntUpdate + intListNullable: IntUpdate + localDateTimeList: LocalDateTimeUpdate + localDateTimeListNullable: LocalDateTimeUpdate + localTimeList: LocalTimeUpdate + localTimeListNullable: LocalTimeUpdate + stringList: StringUpdate + stringListNullable: StringUpdate + timeList: TimeUpdate + timeListNullable: TimeUpdate + } + + type NodeTypeUpdateResponse { + info: NodeTypeCreateInfo + nodeTypes: [NodeType!]! + } + input NodeTypeWhere { AND: [NodeTypeWhere!] NOT: NodeTypeWhere @@ -551,6 +625,42 @@ describe("Scalars", () => { timeListNullable: TimeListWhereNullable } + input RelatedNodeUpdateInput { + node: RelatedNodeUpdateNode! + } + + input RelatedNodeUpdateNode { + bigIntList: BigIntUpdate + bigIntListNullable: BigIntUpdate + booleanList: IntUpdate + booleanListNullable: IntUpdate + dateList: DateUpdate + dateListNullable: DateUpdate + dateTimeList: DateTimeUpdate + dateTimeListNullable: DateTimeUpdate + durationList: DurationUpdate + durationListNullable: DurationUpdate + floatList: FloatUpdate + floatListNullable: FloatUpdate + idList: IDUpdate + idListNullable: IDUpdate + intList: IntUpdate + intListNullable: IntUpdate + localDateTimeList: LocalDateTimeUpdate + localDateTimeListNullable: LocalDateTimeUpdate + localTimeList: LocalTimeUpdate + localTimeListNullable: LocalTimeUpdate + stringList: StringUpdate + stringListNullable: StringUpdate + timeList: TimeUpdate + timeListNullable: TimeUpdate + } + + type RelatedNodeUpdateResponse { + info: RelatedNodeCreateInfo + relatedNodes: [RelatedNode!]! + } + input RelatedNodeWhere { AND: [RelatedNodeWhere!] NOT: RelatedNodeWhere @@ -589,6 +699,10 @@ describe("Scalars", () => { equals: [String] } + input StringUpdate { + set: String + } + \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" scalar Time @@ -598,6 +712,10 @@ describe("Scalars", () => { input TimeListWhereNullable { 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 index 0d902c5bdd..ad97c01b68 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -88,6 +88,10 @@ describe("Scalars", () => { \\"\\"\\" scalar BigInt + input BigIntUpdate { + set: BigInt + } + input BigIntWhere { AND: [BigIntWhere!] NOT: BigIntWhere @@ -107,6 +111,10 @@ describe("Scalars", () => { equals: Boolean } + input FloatUpdate { + set: Float + } + input FloatWhere { AND: [FloatWhere!] NOT: FloatWhere @@ -119,6 +127,10 @@ describe("Scalars", () => { lte: Float } + input IDUpdate { + set: ID + } + input IDWhere { AND: [IDWhere!] NOT: IDWhere @@ -130,6 +142,10 @@ describe("Scalars", () => { startsWith: ID } + input IntUpdate { + set: Int + } + input IntWhere { AND: [IntWhere!] NOT: IntWhere @@ -145,6 +161,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + updateNodeTypes(input: [NodeTypeUpdateInput!]!): NodeTypeUpdateResponse + updateRelatedNodes(input: [RelatedNodeUpdateInput!]!): RelatedNodeUpdateResponse } type NodeType { @@ -288,6 +306,30 @@ describe("Scalars", () => { 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 @@ -451,6 +493,30 @@ describe("Scalars", () => { 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 @@ -474,6 +540,10 @@ describe("Scalars", () => { DESC } + input StringUpdate { + set: String + } + input StringWhere { AND: [StringWhere!] NOT: StringWhere diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index c02909f82e..74ff7cbe4d 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -78,9 +78,15 @@ describe("Spatial Types", () => { z: Float } + input CartesianPointInputUpdate { + set: CartesianPointInput + } + type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + updateNodeTypes(input: [NodeTypeUpdateInput!]!): NodeTypeUpdateResponse + updateRelatedNodes(input: [RelatedNodeUpdateInput!]!): RelatedNodeUpdateResponse } type NodeType { @@ -180,6 +186,22 @@ describe("Spatial Types", () => { 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 @@ -212,6 +234,10 @@ describe("Spatial Types", () => { longitude: Float! } + input PointInputUpdate { + set: PointInput + } + type Query { nodeTypes(where: NodeTypeOperationWhere): NodeTypeOperation relatedNodes(where: RelatedNodeOperationWhere): RelatedNodeOperation @@ -279,6 +305,22 @@ describe("Spatial Types", () => { 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 diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 03432895cc..1de1359b0e 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -71,6 +71,10 @@ describe("Temporals", () => { \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" scalar DateTime + input DateTimeUpdate { + set: DateTime + } + input DateTimeWhere { AND: [DateTimeWhere!] NOT: DateTimeWhere @@ -83,6 +87,10 @@ describe("Temporals", () => { lte: DateTime } + input DateUpdate { + set: Date + } + input DateWhere { AND: [DateWhere!] NOT: DateWhere @@ -98,6 +106,10 @@ describe("Temporals", () => { \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" scalar Duration + input DurationUpdate { + set: Duration + } + input DurationWhere { AND: [DurationWhere!] NOT: DurationWhere @@ -113,6 +125,10 @@ describe("Temporals", () => { \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" scalar LocalDateTime + input LocalDateTimeUpdate { + set: LocalDateTime + } + input LocalDateTimeWhere { AND: [LocalDateTimeWhere!] NOT: LocalDateTimeWhere @@ -130,6 +146,10 @@ describe("Temporals", () => { \\"\\"\\" scalar LocalTime + input LocalTimeUpdate { + set: LocalTime + } + input LocalTimeWhere { AND: [LocalTimeWhere!] NOT: LocalTimeWhere @@ -145,6 +165,8 @@ describe("Temporals", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + updateNodeTypes(input: [NodeTypeUpdateInput!]!): NodeTypeUpdateResponse + updateRelatedNodes(input: [RelatedNodeUpdateInput!]!): RelatedNodeUpdateResponse } type NodeType { @@ -270,6 +292,24 @@ describe("Temporals", () => { 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 @@ -391,6 +431,24 @@ describe("Temporals", () => { 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 @@ -411,6 +469,10 @@ describe("Temporals", () => { \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" scalar Time + input TimeUpdate { + set: Time + } + input TimeWhere { AND: [TimeWhere!] NOT: TimeWhere From 5a9364486bf4bfd44436b3816276a8768acc786d Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 6 Aug 2024 11:43:41 +0100 Subject: [PATCH 146/177] Add where to update mutations --- .../schema-types/TopLevelEntitySchemaTypes.ts | 1 + .../tests/api-v6/schema/directives/relayId.test.ts | 2 +- packages/graphql/tests/api-v6/schema/relationship.test.ts | 8 ++++---- packages/graphql/tests/api-v6/schema/simple.test.ts | 8 ++++---- packages/graphql/tests/api-v6/schema/types/array.test.ts | 4 ++-- .../graphql/tests/api-v6/schema/types/scalars.test.ts | 4 ++-- .../graphql/tests/api-v6/schema/types/spatial.test.ts | 4 ++-- .../graphql/tests/api-v6/schema/types/temporals.test.ts | 4 ++-- 8 files changed, 18 insertions(+), 17 deletions(-) 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 index a1491892f4..92cf9ac91d 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -161,6 +161,7 @@ export class TopLevelEntitySchemaTypes { type: this.updateType, args: { input: this.updateSchemaTypes.updateInput.NonNull.List.NonNull, + where: this.filterSchemaTypes.operationWhereTopLevel, }, resolver, }); diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index 305c38d505..bb0675308f 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -139,7 +139,7 @@ describe("RelayId", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse + updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse } interface Node { diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index f57dfea811..aaff71ac57 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -300,8 +300,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateActors(input: [ActorUpdateInput!]!): ActorUpdateResponse - updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse + updateActors(input: [ActorUpdateInput!]!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { @@ -652,8 +652,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateActors(input: [ActorUpdateInput!]!): ActorUpdateResponse - updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse + updateActors(input: [ActorUpdateInput!]!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index bf045f6c6c..3d4f0bb215 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -113,7 +113,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse + updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { @@ -314,8 +314,8 @@ describe("Simple Aura-API", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateActors(input: [ActorUpdateInput!]!): ActorUpdateResponse - updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse + updateActors(input: [ActorUpdateInput!]!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { @@ -445,7 +445,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateMovies(input: [MovieUpdateInput!]!): MovieUpdateResponse + updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 8ef8f7d5d8..0952655dcc 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -259,8 +259,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - updateNodeTypes(input: [NodeTypeUpdateInput!]!): NodeTypeUpdateResponse - updateRelatedNodes(input: [RelatedNodeUpdateInput!]!): RelatedNodeUpdateResponse + updateNodeTypes(input: [NodeTypeUpdateInput!]!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: [RelatedNodeUpdateInput!]!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } type NodeType { diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index ad97c01b68..d1e782dad2 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -161,8 +161,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - updateNodeTypes(input: [NodeTypeUpdateInput!]!): NodeTypeUpdateResponse - updateRelatedNodes(input: [RelatedNodeUpdateInput!]!): RelatedNodeUpdateResponse + updateNodeTypes(input: [NodeTypeUpdateInput!]!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: [RelatedNodeUpdateInput!]!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } type NodeType { diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 74ff7cbe4d..7613bb8f47 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -85,8 +85,8 @@ describe("Spatial Types", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - updateNodeTypes(input: [NodeTypeUpdateInput!]!): NodeTypeUpdateResponse - updateRelatedNodes(input: [RelatedNodeUpdateInput!]!): RelatedNodeUpdateResponse + updateNodeTypes(input: [NodeTypeUpdateInput!]!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: [RelatedNodeUpdateInput!]!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } type NodeType { diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 1de1359b0e..1b1e350732 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -165,8 +165,8 @@ describe("Temporals", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - updateNodeTypes(input: [NodeTypeUpdateInput!]!): NodeTypeUpdateResponse - updateRelatedNodes(input: [RelatedNodeUpdateInput!]!): RelatedNodeUpdateResponse + updateNodeTypes(input: [NodeTypeUpdateInput!]!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: [RelatedNodeUpdateInput!]!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } type NodeType { From f08f22f5db39e638b1db9ba1a3da48e4c112a8ff Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 6 Aug 2024 13:27:54 +0100 Subject: [PATCH 147/177] Update packages/graphql/tests/api-v6/schema/invalid-schema/invalid-default-usage.test.ts Co-authored-by: Michael Webb <28074382+mjfwebb@users.noreply.github.com> --- .../api-v6/schema/invalid-schema/invalid-default-usage.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ee27592c2f..56dd419d9f 100644 --- 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 @@ -22,7 +22,7 @@ import { Neo4jGraphQL } from "../../../../src"; import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; describe("invalid @default usage", () => { - test("@default should fail without define a value", async () => { + test("@default should fail without a defined value", async () => { const fn = async () => { const typeDefs = /* GraphQL */ ` type User @node { From 991eca32a4942ec3b430d31902d5296536c73c4e Mon Sep 17 00:00:00 2001 From: Michael Webb <28074382+mjfwebb@users.noreply.github.com> Date: Tue, 6 Aug 2024 16:42:53 +0200 Subject: [PATCH 148/177] refactor: rename GraphQLTree to GraphQLTreeReadOperationTopLevel (#5439) --- .../api-v6/queryIRFactory/ReadOperationFactory.ts | 12 +++++++++--- .../queryIRFactory/argument-parser/parse-args.ts | 9 ++++++--- .../resolve-tree-parser/graphql-tree/graphql-tree.ts | 2 +- .../parse-global-resolve-info-tree.ts | 4 ++-- .../resolve-tree-parser/parse-resolve-info-tree.ts | 4 ++-- .../api-v6/translators/translate-read-operation.ts | 4 ++-- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index c86e8062c0..3892bb2eb6 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -44,12 +44,12 @@ import { FilterFactory } from "./FilterFactory"; import { FactoryParseError } from "./factory-parse-error"; import type { GraphQLTreePoint } from "./resolve-tree-parser/graphql-tree/attributes"; import type { - GraphQLTree, 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"; @@ -62,7 +62,13 @@ export class ReadOperationFactory { this.filterFactory = new FilterFactory(schemaModel); } - public createAST({ graphQLTree, entity }: { graphQLTree: GraphQLTree; entity: ConcreteEntity }): QueryAST { + public createAST({ + graphQLTree, + entity, + }: { + graphQLTree: GraphQLTreeReadOperationTopLevel; + entity: ConcreteEntity; + }): QueryAST { const operation = this.generateOperation({ graphQLTree, entity, @@ -97,7 +103,7 @@ export class ReadOperationFactory { graphQLTree, entity, }: { - graphQLTree: GraphQLTree; + graphQLTree: GraphQLTreeReadOperationTopLevel; entity: ConcreteEntity; }): V6ReadOperation { const connectionTree = graphQLTree.fields.connection; diff --git a/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-args.ts b/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-args.ts index 52504dc105..7e21bed120 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-args.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-args.ts @@ -20,21 +20,24 @@ import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../schema-model/relationship/Relationship"; import type { - GraphQLTree, GraphQLTreeConnection, GraphQLTreeConnectionTopLevel, + GraphQLTreeReadOperation, + GraphQLTreeReadOperationTopLevel, } from "../resolve-tree-parser/graphql-tree/graphql-tree"; import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "../resolve-tree-parser/graphql-tree/sort"; import { ResolveTreeParserError } from "../resolve-tree-parser/resolve-tree-parser-error"; -export function parseOperationArgs(resolveTreeArgs: Record): GraphQLTree["args"] { +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): GraphQLTree["args"] { +export function parseOperationArgsTopLevel( + resolveTreeArgs: Record +): GraphQLTreeReadOperationTopLevel["args"] { // Not properly parsed, assuming the type is the same return { where: resolveTreeArgs.where, 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 index 7cac692a85..06502dbae7 100644 --- 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 @@ -33,7 +33,7 @@ export interface GraphQLTreeCreate extends GraphQLTreeNode { }; } -export interface GraphQLTree extends GraphQLTreeElement { +export interface GraphQLTreeReadOperationTopLevel extends GraphQLTreeElement { name: string; fields: { connection?: GraphQLTreeConnectionTopLevel; 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 index 274ef3574d..014d64cd1f 100644 --- 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 @@ -19,7 +19,7 @@ import type { ResolveTree } from "graphql-parse-resolve-info"; import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; -import type { GraphQLTree } from "./graphql-tree/graphql-tree"; +import type { GraphQLTreeReadOperationTopLevel } from "./graphql-tree/graphql-tree"; import { parseNode } from "./parse-node"; /** Parses the resolve info tree for a global node query */ @@ -29,7 +29,7 @@ export function parseGlobalNodeResolveInfoTree({ }: { resolveTree: ResolveTree; entity: ConcreteEntity; -}): GraphQLTree { +}): GraphQLTreeReadOperationTopLevel { const entityTypes = entity.typeNames; resolveTree.fieldsByTypeName[entityTypes.node] = { ...resolveTree.fieldsByTypeName["Node"], 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 index d07bb99e02..502ba8c494 100644 --- 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 @@ -28,11 +28,11 @@ import { } from "../argument-parser/parse-args"; import { parseCreateOperationArgsTopLevel } from "../argument-parser/parse-create-args"; import type { - GraphQLTree, GraphQLTreeConnection, GraphQLTreeConnectionTopLevel, GraphQLTreeCreate, GraphQLTreeReadOperation, + GraphQLTreeReadOperationTopLevel, } from "./graphql-tree/graphql-tree"; import { parseEdges } from "./parse-edges"; import { getNodeFields } from "./parse-node"; @@ -44,7 +44,7 @@ export function parseResolveInfoTree({ }: { resolveTree: ResolveTree; entity: ConcreteEntity; -}): GraphQLTree { +}): GraphQLTreeReadOperationTopLevel { const connectionResolveTree = findFieldByName(resolveTree, entity.typeNames.connectionOperation, "connection"); const connection = connectionResolveTree ? parseTopLevelConnection(connectionResolveTree, entity) : undefined; const connectionOperationArgs = parseOperationArgsTopLevel(resolveTree.args); diff --git a/packages/graphql/src/api-v6/translators/translate-read-operation.ts b/packages/graphql/src/api-v6/translators/translate-read-operation.ts index af3dda8e2e..a60c403c31 100644 --- a/packages/graphql/src/api-v6/translators/translate-read-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-read-operation.ts @@ -23,7 +23,7 @@ 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 { GraphQLTree } from "../queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree"; +import type { GraphQLTreeReadOperationTopLevel } from "../queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree"; const debug = Debug(DEBUG_TRANSLATE); @@ -33,7 +33,7 @@ export function translateReadOperation({ graphQLTree, }: { context: Neo4jGraphQLTranslationContext; - graphQLTree: GraphQLTree; + graphQLTree: GraphQLTreeReadOperationTopLevel; entity: ConcreteEntity; }): Cypher.CypherResult { const readFactory = new ReadOperationFactory(context.schemaModel); From ae9ab225692d4fb65e4f53f67b3a5d2cb84f6fe9 Mon Sep 17 00:00:00 2001 From: Michael Webb <28074382+mjfwebb@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:29:35 +0200 Subject: [PATCH 149/177] refactor: move createInfo to staticTypes (#5443) * refactor: move createInfo to staticTypes * test: update snapshots --- .../schema-types/StaticSchemaTypes.ts | 11 +++++ .../schema-types/TopLevelEntitySchemaTypes.ts | 14 +----- .../schema/directives/default-array.test.ts | 12 +++--- .../api-v6/schema/directives/default.test.ts | 12 +++--- .../api-v6/schema/directives/relayId.test.ts | 12 +++--- .../tests/api-v6/schema/relationship.test.ts | 38 ++++++---------- .../tests/api-v6/schema/simple.test.ts | 43 ++++++++----------- .../tests/api-v6/schema/types/array.test.ts | 19 +++----- .../tests/api-v6/schema/types/scalars.test.ts | 19 +++----- .../tests/api-v6/schema/types/spatial.test.ts | 19 +++----- .../api-v6/schema/types/temporals.test.ts | 19 +++----- 11 files changed, 91 insertions(+), 127 deletions(-) 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 index b7dde3a654..9559051d56 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -64,6 +64,17 @@ export class StaticSchemaTypes { }); } + public get createInfo(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType("CreateInfo", () => { + return { + fields: { + nodesCreated: this.schemaBuilder.types.int.NonNull, + relationshipsCreated: this.schemaBuilder.types.int.NonNull, + }, + }; + }); + } + @Memoize() public get sortDirection(): EnumTypeComposer { return this.schemaBuilder.createEnumType("SortDirection", ["ASC", "DESC"]); 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 index 186e3cf2b4..ba097c201b 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -303,23 +303,11 @@ export class TopLevelEntitySchemaTypes { public get createType(): ObjectTypeComposer { return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.createResponse, () => { const nodeType = this.nodeType; - const info = this.createInfo; 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, + info: this.schemaTypes.staticTypes.createInfo, }, }; }); 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 index 5d1cdc80f0..7e68b4d1aa 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts @@ -51,6 +51,11 @@ describe("@default on array fields", () => { equals: Boolean } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" scalar DateTime @@ -84,11 +89,6 @@ describe("@default on array fields", () => { pageInfo: PageInfo } - type MovieCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input MovieCreateInput { node: MovieCreateNode! } @@ -103,7 +103,7 @@ describe("@default on array fields", () => { } type MovieCreateResponse { - info: MovieCreateInfo + info: CreateInfo movies: [Movie!]! } diff --git a/packages/graphql/tests/api-v6/schema/directives/default.test.ts b/packages/graphql/tests/api-v6/schema/directives/default.test.ts index 6c09889c99..644570e3ae 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -51,6 +51,11 @@ describe("@default on fields", () => { equals: Boolean } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" scalar DateTime @@ -119,11 +124,6 @@ describe("@default on fields", () => { node: MovieSort } - type MovieCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input MovieCreateInput { node: MovieCreateNode! } @@ -138,7 +138,7 @@ describe("@default on fields", () => { } type MovieCreateResponse { - info: MovieCreateInfo + info: CreateInfo movies: [Movie!]! } diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index 3db3146d78..326cca0dc6 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -40,6 +40,11 @@ describe("RelayId", () => { mutation: Mutation } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + input GlobalIdWhere { equals: String } @@ -70,11 +75,6 @@ describe("RelayId", () => { node: MovieSort } - type MovieCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input MovieCreateInput { node: MovieCreateNode! } @@ -85,7 +85,7 @@ describe("RelayId", () => { } type MovieCreateResponse { - info: MovieCreateInfo + info: CreateInfo movies: [Movie!]! } diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index bc71b78b59..1b3c1eba31 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -59,11 +59,6 @@ describe("Relationships", () => { node: ActorSort } - type ActorCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input ActorCreateInput { node: ActorCreateNode! } @@ -74,7 +69,7 @@ describe("Relationships", () => { type ActorCreateResponse { actors: [Actor!]! - info: ActorCreateInfo + info: CreateInfo } type ActorEdge { @@ -158,6 +153,11 @@ describe("Relationships", () => { name: StringWhere } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + type Movie { actors(where: MovieActorsOperationWhere): MovieActorsOperation title: String @@ -225,11 +225,6 @@ describe("Relationships", () => { node: MovieSort } - type MovieCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input MovieCreateInput { node: MovieCreateNode! } @@ -239,7 +234,7 @@ describe("Relationships", () => { } type MovieCreateResponse { - info: MovieCreateInfo + info: CreateInfo movies: [Movie!]! } @@ -361,11 +356,6 @@ describe("Relationships", () => { node: ActorSort } - type ActorCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input ActorCreateInput { node: ActorCreateNode! } @@ -376,7 +366,7 @@ describe("Relationships", () => { type ActorCreateResponse { actors: [Actor!]! - info: ActorCreateInfo + info: CreateInfo } type ActorEdge { @@ -463,6 +453,11 @@ describe("Relationships", () => { name: StringWhere } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + input IntWhere { AND: [IntWhere!] NOT: IntWhere @@ -545,11 +540,6 @@ describe("Relationships", () => { node: MovieSort } - type MovieCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input MovieCreateInput { node: MovieCreateNode! } @@ -559,7 +549,7 @@ describe("Relationships", () => { } type MovieCreateResponse { - info: MovieCreateInfo + info: CreateInfo movies: [Movie!]! } diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index a04b45590c..557364c7d6 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -40,6 +40,11 @@ describe("Simple Aura-API", () => { mutation: Mutation } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + type Movie { title: String } @@ -53,11 +58,6 @@ describe("Simple Aura-API", () => { node: MovieSort } - type MovieCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input MovieCreateInput { node: MovieCreateNode! } @@ -67,7 +67,7 @@ describe("Simple Aura-API", () => { } type MovieCreateResponse { - info: MovieCreateInfo + info: CreateInfo movies: [Movie!]! } @@ -164,11 +164,6 @@ describe("Simple Aura-API", () => { node: ActorSort } - type ActorCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input ActorCreateInput { node: ActorCreateNode! } @@ -179,7 +174,7 @@ describe("Simple Aura-API", () => { type ActorCreateResponse { actors: [Actor!]! - info: ActorCreateInfo + info: CreateInfo } type ActorEdge { @@ -209,6 +204,11 @@ describe("Simple Aura-API", () => { name: StringWhere } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + type Movie { title: String } @@ -222,11 +222,6 @@ describe("Simple Aura-API", () => { node: MovieSort } - type MovieCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input MovieCreateInput { node: MovieCreateNode! } @@ -236,7 +231,7 @@ describe("Simple Aura-API", () => { } type MovieCreateResponse { - info: MovieCreateInfo + info: CreateInfo movies: [Movie!]! } @@ -322,6 +317,11 @@ describe("Simple Aura-API", () => { mutation: Mutation } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + type Movie { title: String } @@ -335,11 +335,6 @@ describe("Simple Aura-API", () => { node: MovieSort } - type MovieCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input MovieCreateInput { node: MovieCreateNode! } @@ -349,7 +344,7 @@ describe("Simple Aura-API", () => { } type MovieCreateResponse { - info: MovieCreateInfo + info: CreateInfo movies: [Movie!]! } diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 6c897f668e..0dc856600a 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -139,6 +139,11 @@ describe("Scalars", () => { equals: Boolean } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" scalar Date @@ -258,11 +263,6 @@ describe("Scalars", () => { pageInfo: PageInfo } - type NodeTypeCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input NodeTypeCreateInput { node: NodeTypeCreateNode! } @@ -295,7 +295,7 @@ describe("Scalars", () => { } type NodeTypeCreateResponse { - info: NodeTypeCreateInfo + info: CreateInfo nodeTypes: [NodeType!]! } @@ -437,11 +437,6 @@ describe("Scalars", () => { pageInfo: PageInfo } - type RelatedNodeCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input RelatedNodeCreateInput { node: RelatedNodeCreateNode! } @@ -474,7 +469,7 @@ describe("Scalars", () => { } type RelatedNodeCreateResponse { - info: RelatedNodeCreateInfo + info: CreateInfo relatedNodes: [RelatedNode!]! } diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index 0d902c5bdd..ff389f458d 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -107,6 +107,11 @@ describe("Scalars", () => { equals: Boolean } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + input FloatWhere { AND: [FloatWhere!] NOT: FloatWhere @@ -172,11 +177,6 @@ describe("Scalars", () => { node: NodeTypeSort } - type NodeTypeCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input NodeTypeCreateInput { node: NodeTypeCreateNode! } @@ -197,7 +197,7 @@ describe("Scalars", () => { } type NodeTypeCreateResponse { - info: NodeTypeCreateInfo + info: CreateInfo nodeTypes: [NodeType!]! } @@ -343,11 +343,6 @@ describe("Scalars", () => { node: RelatedNodeSort } - type RelatedNodeCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input RelatedNodeCreateInput { node: RelatedNodeCreateNode! } @@ -368,7 +363,7 @@ describe("Scalars", () => { } type RelatedNodeCreateResponse { - info: RelatedNodeCreateInfo + info: CreateInfo relatedNodes: [RelatedNode!]! } diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index c02909f82e..1b62c4d7e9 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -78,6 +78,11 @@ describe("Spatial Types", () => { z: Float } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse @@ -96,11 +101,6 @@ describe("Spatial Types", () => { pageInfo: PageInfo } - type NodeTypeCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input NodeTypeCreateInput { node: NodeTypeCreateNode! } @@ -113,7 +113,7 @@ describe("Spatial Types", () => { } type NodeTypeCreateResponse { - info: NodeTypeCreateInfo + info: CreateInfo nodeTypes: [NodeType!]! } @@ -229,11 +229,6 @@ describe("Spatial Types", () => { pageInfo: PageInfo } - type RelatedNodeCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input RelatedNodeCreateInput { node: RelatedNodeCreateNode! } @@ -246,7 +241,7 @@ describe("Spatial Types", () => { } type RelatedNodeCreateResponse { - info: RelatedNodeCreateInfo + info: CreateInfo relatedNodes: [RelatedNode!]! } diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 03432895cc..2cd864c906 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -65,6 +65,11 @@ describe("Temporals", () => { mutation: Mutation } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + \\"\\"\\"A date, represented as a 'yyyy-mm-dd' string\\"\\"\\" scalar Date @@ -166,11 +171,6 @@ describe("Temporals", () => { node: NodeTypeSort } - type NodeTypeCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input NodeTypeCreateInput { node: NodeTypeCreateNode! } @@ -185,7 +185,7 @@ describe("Temporals", () => { } type NodeTypeCreateResponse { - info: NodeTypeCreateInfo + info: CreateInfo nodeTypes: [NodeType!]! } @@ -313,11 +313,6 @@ describe("Temporals", () => { node: RelatedNodeSort } - type RelatedNodeCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input RelatedNodeCreateInput { node: RelatedNodeCreateNode! } @@ -332,7 +327,7 @@ describe("Temporals", () => { } type RelatedNodeCreateResponse { - info: RelatedNodeCreateInfo + info: CreateInfo relatedNodes: [RelatedNode!]! } From fda2ffb9df6ae00cbef1fdb47ab382eb9f22cbba Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Wed, 7 Aug 2024 10:40:37 +0200 Subject: [PATCH 150/177] feat: add top level delete to the schema --- .../resolvers/translate-delete-resolver.ts | 57 +++++++++++++++++++ .../schema-generation/SchemaGenerator.ts | 13 ++++- .../schema-types/StaticSchemaTypes.ts | 11 ++++ .../schema-types/TopLevelEntitySchemaTypes.ts | 28 +++++++++ .../TopLevelEntityTypeNames.ts | 13 +++++ 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts 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..a72098ef51 --- /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 { parseResolveInfoTreeCreate } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; +import { translateCreateResolver } from "../translators/translate-create-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 graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity }); + const { cypher, params } = translateCreateResolver({ + context: context, + graphQLTreeCreate, + entity, + }); + const executeResult = await execute({ + cypher, + params, + defaultAccessMode: "WRITE", + context, + info, + }); + return { + info: { + ...executeResult.statistics, + }, + }; + }; +} diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts index 9172d067d3..2504481fc6 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaGenerator.ts @@ -22,11 +22,12 @@ import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSch 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 { SchemaBuilder } from "./SchemaBuilder"; import { SchemaTypes } from "./schema-types/SchemaTypes"; import { StaticSchemaTypes } from "./schema-types/StaticSchemaTypes"; import { TopLevelEntitySchemaTypes } from "./schema-types/TopLevelEntitySchemaTypes"; -import { generateCreateResolver } from "../resolvers/translate-create-resolver"; export class SchemaGenerator { private schemaBuilder: SchemaBuilder; @@ -41,6 +42,7 @@ export class SchemaGenerator { const entityTypesMap = this.generateEntityTypes(schemaModel); this.generateTopLevelQueryFields(entityTypesMap); this.generateTopLevelCreateFields(entityTypesMap); + this.generateTopLevelDeleteFields(entityTypesMap); this.generateGlobalNodeQueryField(schemaModel); @@ -86,6 +88,15 @@ export class SchemaGenerator { } } + private generateTopLevelDeleteFields(entityTypesMap: Map): void { + for (const [entity, entitySchemaTypes] of entityTypesMap.entries()) { + const resolver = generateDeleteResolver({ + entity, + }); + entitySchemaTypes.addTopLevelDeleteField(resolver); + } + } + private generateGlobalNodeQueryField(schemaModel: Neo4jGraphQLSchemaModel): void { const globalEntities = schemaModel.concreteEntities.filter((e) => e.globalIdField); 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 index 9559051d56..84f13af875 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -75,6 +75,17 @@ export class StaticSchemaTypes { }); } + public get deleteInfo(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType("DeleteInfo", () => { + return { + fields: { + nodesDeleted: this.schemaBuilder.types.int.NonNull, + relationshipsDeleted: this.schemaBuilder.types.int.NonNull, + }, + }; + }); + } + @Memoize() public get sortDirection(): EnumTypeComposer { return this.schemaBuilder.createEnumType("SortDirection", ["ASC", "DESC"]); 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 index ba097c201b..76157c22c5 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -146,6 +146,24 @@ export class TopLevelEntitySchemaTypes { }); } + public addTopLevelDeleteField( + resolver: ( + _root: any, + args: any, + context: Neo4jGraphQLTranslationContext, + info: GraphQLResolveInfo + ) => Promise + ) { + this.schemaBuilder.addMutationField({ + name: this.entity.typeNames.deleteField, + type: this.deleteType, + args: { + where: this.filterSchemaTypes.operationWhereTopLevel, + }, + resolver, + }); + } + protected get connectionSort(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.connectionSort, () => { return { @@ -312,6 +330,16 @@ export class TopLevelEntitySchemaTypes { }; }); } + + public get deleteType(): ObjectTypeComposer { + return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.deleteResponse, () => { + return { + fields: { + info: this.schemaTypes.staticTypes.deleteInfo, + }, + }; + }); + } } function typeToResolver(type: Neo4jGraphQLScalarType): GraphQLResolver | undefined { 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 index f7bbb94eaa..8206051f28 100644 --- 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 @@ -88,4 +88,17 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { public get createInfo(): string { return `${upperFirst(this.entityName)}CreateInfo`; } + + /** Top Level Delete field */ + public get deleteField(): string { + return `delete${upperFirst(plural(this.entityName))}`; + } + + public get deleteResponse(): string { + return `${upperFirst(this.entityName)}DeleteResponse`; + } + + public get deleteInfo(): string { + return `${upperFirst(this.entityName)}DeleteInfo`; + } } From a47de442d95c93f5ab638d6e15f2940159eacf39 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Wed, 7 Aug 2024 10:40:48 +0200 Subject: [PATCH 151/177] test: update snapshots --- .../schema/directives/default-array.test.ts | 10 ++++++ .../api-v6/schema/directives/default.test.ts | 10 ++++++ .../api-v6/schema/directives/relayId.test.ts | 10 ++++++ .../tests/api-v6/schema/relationship.test.ts | 30 ++++++++++++++++ .../tests/api-v6/schema/simple.test.ts | 35 +++++++++++++++++++ .../tests/api-v6/schema/types/array.test.ts | 15 ++++++++ .../tests/api-v6/schema/types/scalars.test.ts | 15 ++++++++ .../tests/api-v6/schema/types/spatial.test.ts | 15 ++++++++ .../api-v6/schema/types/temporals.test.ts | 15 ++++++++ 9 files changed, 155 insertions(+) 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 index 7e68b4d1aa..af4de13742 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts @@ -63,6 +63,11 @@ describe("@default on array fields", () => { equals: [DateTime!] } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + input FloatListWhere { equals: [Float!] } @@ -107,6 +112,10 @@ describe("@default on array fields", () => { movies: [Movie!]! } + type MovieDeleteResponse { + info: DeleteInfo + } + type MovieEdge { cursor: String node: Movie @@ -137,6 +146,7 @@ describe("@default on array fields", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(where: MovieOperationWhere): MovieDeleteResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/directives/default.test.ts b/packages/graphql/tests/api-v6/schema/directives/default.test.ts index 644570e3ae..9af6cc762d 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -71,6 +71,11 @@ describe("@default on fields", () => { lte: DateTime } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + input FloatWhere { AND: [FloatWhere!] NOT: FloatWhere @@ -142,6 +147,10 @@ describe("@default on fields", () => { movies: [Movie!]! } + type MovieDeleteResponse { + info: DeleteInfo + } + type MovieEdge { cursor: String node: Movie @@ -181,6 +190,7 @@ describe("@default on fields", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(where: MovieOperationWhere): MovieDeleteResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index 326cca0dc6..fa6a28b332 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -45,6 +45,11 @@ describe("RelayId", () => { relationshipsCreated: Int! } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + input GlobalIdWhere { equals: String } @@ -89,6 +94,10 @@ describe("RelayId", () => { movies: [Movie!]! } + type MovieDeleteResponse { + info: DeleteInfo + } + type MovieEdge { cursor: String node: Movie @@ -121,6 +130,7 @@ describe("RelayId", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(where: MovieOperationWhere): MovieDeleteResponse } interface Node { diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 1b3c1eba31..6e80186b2d 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -72,6 +72,10 @@ describe("Relationships", () => { info: CreateInfo } + type ActorDeleteResponse { + info: DeleteInfo + } + type ActorEdge { cursor: String node: Actor @@ -158,6 +162,11 @@ describe("Relationships", () => { relationshipsCreated: Int! } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + type Movie { actors(where: MovieActorsOperationWhere): MovieActorsOperation title: String @@ -238,6 +247,10 @@ describe("Relationships", () => { movies: [Movie!]! } + type MovieDeleteResponse { + info: DeleteInfo + } + type MovieEdge { cursor: String node: Movie @@ -269,6 +282,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteActors(where: ActorOperationWhere): ActorDeleteResponse + deleteMovies(where: MovieOperationWhere): MovieDeleteResponse } type PageInfo { @@ -369,6 +384,10 @@ describe("Relationships", () => { info: CreateInfo } + type ActorDeleteResponse { + info: DeleteInfo + } + type ActorEdge { cursor: String node: Actor @@ -458,6 +477,11 @@ describe("Relationships", () => { relationshipsCreated: Int! } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + input IntWhere { AND: [IntWhere!] NOT: IntWhere @@ -553,6 +577,10 @@ describe("Relationships", () => { movies: [Movie!]! } + type MovieDeleteResponse { + info: DeleteInfo + } + type MovieEdge { cursor: String node: Movie @@ -584,6 +612,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteActors(where: ActorOperationWhere): ActorDeleteResponse + deleteMovies(where: MovieOperationWhere): MovieDeleteResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 557364c7d6..8263bc4113 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -45,6 +45,11 @@ describe("Simple Aura-API", () => { relationshipsCreated: Int! } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + type Movie { title: String } @@ -71,6 +76,10 @@ describe("Simple Aura-API", () => { movies: [Movie!]! } + type MovieDeleteResponse { + info: DeleteInfo + } + type MovieEdge { cursor: String node: Movie @@ -100,6 +109,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(where: MovieOperationWhere): MovieDeleteResponse } type PageInfo { @@ -177,6 +187,10 @@ describe("Simple Aura-API", () => { info: CreateInfo } + type ActorDeleteResponse { + info: DeleteInfo + } + type ActorEdge { cursor: String node: Actor @@ -209,6 +223,11 @@ describe("Simple Aura-API", () => { relationshipsCreated: Int! } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + type Movie { title: String } @@ -235,6 +254,10 @@ describe("Simple Aura-API", () => { movies: [Movie!]! } + type MovieDeleteResponse { + info: DeleteInfo + } + type MovieEdge { cursor: String node: Movie @@ -265,6 +288,8 @@ describe("Simple Aura-API", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteActors(where: ActorOperationWhere): ActorDeleteResponse + deleteMovies(where: MovieOperationWhere): MovieDeleteResponse } type PageInfo { @@ -322,6 +347,11 @@ describe("Simple Aura-API", () => { relationshipsCreated: Int! } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + type Movie { title: String } @@ -348,6 +378,10 @@ describe("Simple Aura-API", () => { movies: [Movie!]! } + type MovieDeleteResponse { + info: DeleteInfo + } + type MovieEdge { cursor: String node: Movie @@ -377,6 +411,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteMovies(where: MovieOperationWhere): MovieDeleteResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 0dc856600a..938987da80 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -166,6 +166,11 @@ describe("Scalars", () => { equals: [DateTime] } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" scalar Duration @@ -228,6 +233,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + deleteNodeTypes(where: NodeTypeOperationWhere): NodeTypeDeleteResponse + deleteRelatedNodes(where: RelatedNodeOperationWhere): RelatedNodeDeleteResponse } type NodeType { @@ -299,6 +306,10 @@ describe("Scalars", () => { nodeTypes: [NodeType!]! } + type NodeTypeDeleteResponse { + info: DeleteInfo + } + type NodeTypeEdge { cursor: String node: NodeType @@ -473,6 +484,10 @@ describe("Scalars", () => { relatedNodes: [RelatedNode!]! } + type RelatedNodeDeleteResponse { + info: DeleteInfo + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index ff389f458d..c765a38c13 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -112,6 +112,11 @@ describe("Scalars", () => { relationshipsCreated: Int! } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + input FloatWhere { AND: [FloatWhere!] NOT: FloatWhere @@ -150,6 +155,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + deleteNodeTypes(where: NodeTypeOperationWhere): NodeTypeDeleteResponse + deleteRelatedNodes(where: RelatedNodeOperationWhere): RelatedNodeDeleteResponse } type NodeType { @@ -201,6 +208,10 @@ describe("Scalars", () => { nodeTypes: [NodeType!]! } + type NodeTypeDeleteResponse { + info: DeleteInfo + } + type NodeTypeEdge { cursor: String node: NodeType @@ -367,6 +378,10 @@ describe("Scalars", () => { relatedNodes: [RelatedNode!]! } + type RelatedNodeDeleteResponse { + info: DeleteInfo + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 1b62c4d7e9..7a474934f1 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -83,9 +83,16 @@ describe("Spatial Types", () => { relationshipsCreated: Int! } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + deleteNodeTypes(where: NodeTypeOperationWhere): NodeTypeDeleteResponse + deleteRelatedNodes(where: RelatedNodeOperationWhere): RelatedNodeDeleteResponse } type NodeType { @@ -117,6 +124,10 @@ describe("Spatial Types", () => { nodeTypes: [NodeType!]! } + type NodeTypeDeleteResponse { + info: DeleteInfo + } + type NodeTypeEdge { cursor: String node: NodeType @@ -245,6 +256,10 @@ describe("Spatial Types", () => { relatedNodes: [RelatedNode!]! } + type RelatedNodeDeleteResponse { + info: DeleteInfo + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 2cd864c906..ecc42c7737 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -100,6 +100,11 @@ describe("Temporals", () => { lte: Date } + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" scalar Duration @@ -150,6 +155,8 @@ describe("Temporals", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse + deleteNodeTypes(where: NodeTypeOperationWhere): NodeTypeDeleteResponse + deleteRelatedNodes(where: RelatedNodeOperationWhere): RelatedNodeDeleteResponse } type NodeType { @@ -189,6 +196,10 @@ describe("Temporals", () => { nodeTypes: [NodeType!]! } + type NodeTypeDeleteResponse { + info: DeleteInfo + } + type NodeTypeEdge { cursor: String node: NodeType @@ -331,6 +342,10 @@ describe("Temporals", () => { relatedNodes: [RelatedNode!]! } + type RelatedNodeDeleteResponse { + info: DeleteInfo + } + type RelatedNodeEdge { cursor: String node: RelatedNode From 9f9465fd5037a42ae3ebd9e8c68b4b054c020f85 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Thu, 8 Aug 2024 10:13:23 +0200 Subject: [PATCH 152/177] refactor: set DeleteResponse as static type --- .../schema-types/StaticSchemaTypes.ts | 10 ++++++++++ .../schema-types/TopLevelEntitySchemaTypes.ts | 12 +----------- .../graphql-type-names/TopLevelEntityTypeNames.ts | 8 -------- 3 files changed, 11 insertions(+), 19 deletions(-) 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 index 84f13af875..9b939f682b 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -86,6 +86,16 @@ export class StaticSchemaTypes { }); } + 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"]); 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 index 76157c22c5..bfe55350a8 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -156,7 +156,7 @@ export class TopLevelEntitySchemaTypes { ) { this.schemaBuilder.addMutationField({ name: this.entity.typeNames.deleteField, - type: this.deleteType, + type: this.schemaTypes.staticTypes.deleteResponse, args: { where: this.filterSchemaTypes.operationWhereTopLevel, }, @@ -330,16 +330,6 @@ export class TopLevelEntitySchemaTypes { }; }); } - - public get deleteType(): ObjectTypeComposer { - return this.schemaBuilder.getOrCreateObjectType(this.entityTypeNames.deleteResponse, () => { - return { - fields: { - info: this.schemaTypes.staticTypes.deleteInfo, - }, - }; - }); - } } function typeToResolver(type: Neo4jGraphQLScalarType): GraphQLResolver | undefined { 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 index 8206051f28..7fb953c646 100644 --- 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 @@ -93,12 +93,4 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { public get deleteField(): string { return `delete${upperFirst(plural(this.entityName))}`; } - - public get deleteResponse(): string { - return `${upperFirst(this.entityName)}DeleteResponse`; - } - - public get deleteInfo(): string { - return `${upperFirst(this.entityName)}DeleteInfo`; - } } From c707737327320afc21f116a0056017616379c415 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Thu, 8 Aug 2024 10:36:25 +0200 Subject: [PATCH 153/177] test: update snapshots --- .../schema/directives/default-array.test.ts | 10 +++--- .../api-v6/schema/directives/default.test.ts | 10 +++--- .../api-v6/schema/directives/relayId.test.ts | 10 +++--- .../tests/api-v6/schema/relationship.test.ts | 32 +++++++---------- .../tests/api-v6/schema/simple.test.ts | 36 +++++++++---------- .../tests/api-v6/schema/types/array.test.ts | 16 ++++----- .../tests/api-v6/schema/types/scalars.test.ts | 16 ++++----- .../tests/api-v6/schema/types/spatial.test.ts | 16 ++++----- .../api-v6/schema/types/temporals.test.ts | 16 ++++----- 9 files changed, 67 insertions(+), 95 deletions(-) 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 index af4de13742..955faf76fc 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts @@ -68,6 +68,10 @@ describe("@default on array fields", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + input FloatListWhere { equals: [Float!] } @@ -112,10 +116,6 @@ describe("@default on array fields", () => { movies: [Movie!]! } - type MovieDeleteResponse { - info: DeleteInfo - } - type MovieEdge { cursor: String node: Movie @@ -146,7 +146,7 @@ describe("@default on array fields", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): MovieDeleteResponse + deleteMovies(where: MovieOperationWhere): DeleteResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/directives/default.test.ts b/packages/graphql/tests/api-v6/schema/directives/default.test.ts index 9af6cc762d..ce14197c67 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -76,6 +76,10 @@ describe("@default on fields", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + input FloatWhere { AND: [FloatWhere!] NOT: FloatWhere @@ -147,10 +151,6 @@ describe("@default on fields", () => { movies: [Movie!]! } - type MovieDeleteResponse { - info: DeleteInfo - } - type MovieEdge { cursor: String node: Movie @@ -190,7 +190,7 @@ describe("@default on fields", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): MovieDeleteResponse + deleteMovies(where: MovieOperationWhere): DeleteResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index fa6a28b332..f4dd9ba22d 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -50,6 +50,10 @@ describe("RelayId", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + input GlobalIdWhere { equals: String } @@ -94,10 +98,6 @@ describe("RelayId", () => { movies: [Movie!]! } - type MovieDeleteResponse { - info: DeleteInfo - } - type MovieEdge { cursor: String node: Movie @@ -130,7 +130,7 @@ describe("RelayId", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): MovieDeleteResponse + deleteMovies(where: MovieOperationWhere): DeleteResponse } interface Node { diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 6e80186b2d..ad40c12293 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -72,10 +72,6 @@ describe("Relationships", () => { info: CreateInfo } - type ActorDeleteResponse { - info: DeleteInfo - } - type ActorEdge { cursor: String node: Actor @@ -167,6 +163,10 @@ describe("Relationships", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + type Movie { actors(where: MovieActorsOperationWhere): MovieActorsOperation title: String @@ -247,10 +247,6 @@ describe("Relationships", () => { movies: [Movie!]! } - type MovieDeleteResponse { - info: DeleteInfo - } - type MovieEdge { cursor: String node: Movie @@ -282,8 +278,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteActors(where: ActorOperationWhere): ActorDeleteResponse - deleteMovies(where: MovieOperationWhere): MovieDeleteResponse + deleteActors(where: ActorOperationWhere): DeleteResponse + deleteMovies(where: MovieOperationWhere): DeleteResponse } type PageInfo { @@ -384,10 +380,6 @@ describe("Relationships", () => { info: CreateInfo } - type ActorDeleteResponse { - info: DeleteInfo - } - type ActorEdge { cursor: String node: Actor @@ -482,6 +474,10 @@ describe("Relationships", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + input IntWhere { AND: [IntWhere!] NOT: IntWhere @@ -577,10 +573,6 @@ describe("Relationships", () => { movies: [Movie!]! } - type MovieDeleteResponse { - info: DeleteInfo - } - type MovieEdge { cursor: String node: Movie @@ -612,8 +604,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteActors(where: ActorOperationWhere): ActorDeleteResponse - deleteMovies(where: MovieOperationWhere): MovieDeleteResponse + deleteActors(where: ActorOperationWhere): DeleteResponse + deleteMovies(where: MovieOperationWhere): DeleteResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 8263bc4113..04fd537757 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -50,6 +50,10 @@ describe("Simple Aura-API", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + type Movie { title: String } @@ -76,10 +80,6 @@ describe("Simple Aura-API", () => { movies: [Movie!]! } - type MovieDeleteResponse { - info: DeleteInfo - } - type MovieEdge { cursor: String node: Movie @@ -109,7 +109,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): MovieDeleteResponse + deleteMovies(where: MovieOperationWhere): DeleteResponse } type PageInfo { @@ -187,10 +187,6 @@ describe("Simple Aura-API", () => { info: CreateInfo } - type ActorDeleteResponse { - info: DeleteInfo - } - type ActorEdge { cursor: String node: Actor @@ -228,6 +224,10 @@ describe("Simple Aura-API", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + type Movie { title: String } @@ -254,10 +254,6 @@ describe("Simple Aura-API", () => { movies: [Movie!]! } - type MovieDeleteResponse { - info: DeleteInfo - } - type MovieEdge { cursor: String node: Movie @@ -288,8 +284,8 @@ describe("Simple Aura-API", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteActors(where: ActorOperationWhere): ActorDeleteResponse - deleteMovies(where: MovieOperationWhere): MovieDeleteResponse + deleteActors(where: ActorOperationWhere): DeleteResponse + deleteMovies(where: MovieOperationWhere): DeleteResponse } type PageInfo { @@ -352,6 +348,10 @@ describe("Simple Aura-API", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + type Movie { title: String } @@ -378,10 +378,6 @@ describe("Simple Aura-API", () => { movies: [Movie!]! } - type MovieDeleteResponse { - info: DeleteInfo - } - type MovieEdge { cursor: String node: Movie @@ -411,7 +407,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): MovieDeleteResponse + deleteMovies(where: MovieOperationWhere): DeleteResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 938987da80..26d8907c65 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -171,6 +171,10 @@ describe("Scalars", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" scalar Duration @@ -233,8 +237,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - deleteNodeTypes(where: NodeTypeOperationWhere): NodeTypeDeleteResponse - deleteRelatedNodes(where: RelatedNodeOperationWhere): RelatedNodeDeleteResponse + deleteNodeTypes(where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(where: RelatedNodeOperationWhere): DeleteResponse } type NodeType { @@ -306,10 +310,6 @@ describe("Scalars", () => { nodeTypes: [NodeType!]! } - type NodeTypeDeleteResponse { - info: DeleteInfo - } - type NodeTypeEdge { cursor: String node: NodeType @@ -484,10 +484,6 @@ describe("Scalars", () => { relatedNodes: [RelatedNode!]! } - type RelatedNodeDeleteResponse { - info: DeleteInfo - } - type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index c765a38c13..e913ff5f23 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -117,6 +117,10 @@ describe("Scalars", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + input FloatWhere { AND: [FloatWhere!] NOT: FloatWhere @@ -155,8 +159,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - deleteNodeTypes(where: NodeTypeOperationWhere): NodeTypeDeleteResponse - deleteRelatedNodes(where: RelatedNodeOperationWhere): RelatedNodeDeleteResponse + deleteNodeTypes(where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(where: RelatedNodeOperationWhere): DeleteResponse } type NodeType { @@ -208,10 +212,6 @@ describe("Scalars", () => { nodeTypes: [NodeType!]! } - type NodeTypeDeleteResponse { - info: DeleteInfo - } - type NodeTypeEdge { cursor: String node: NodeType @@ -378,10 +378,6 @@ describe("Scalars", () => { relatedNodes: [RelatedNode!]! } - type RelatedNodeDeleteResponse { - info: DeleteInfo - } - type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 7a474934f1..4a61ac0553 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -88,11 +88,15 @@ describe("Spatial Types", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - deleteNodeTypes(where: NodeTypeOperationWhere): NodeTypeDeleteResponse - deleteRelatedNodes(where: RelatedNodeOperationWhere): RelatedNodeDeleteResponse + deleteNodeTypes(where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(where: RelatedNodeOperationWhere): DeleteResponse } type NodeType { @@ -124,10 +128,6 @@ describe("Spatial Types", () => { nodeTypes: [NodeType!]! } - type NodeTypeDeleteResponse { - info: DeleteInfo - } - type NodeTypeEdge { cursor: String node: NodeType @@ -256,10 +256,6 @@ describe("Spatial Types", () => { relatedNodes: [RelatedNode!]! } - type RelatedNodeDeleteResponse { - info: DeleteInfo - } - type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index ecc42c7737..edc0e9169e 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -105,6 +105,10 @@ describe("Temporals", () => { relationshipsDeleted: Int! } + type DeleteResponse { + info: DeleteInfo + } + \\"\\"\\"A duration, represented as an ISO 8601 duration string\\"\\"\\" scalar Duration @@ -155,8 +159,8 @@ describe("Temporals", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - deleteNodeTypes(where: NodeTypeOperationWhere): NodeTypeDeleteResponse - deleteRelatedNodes(where: RelatedNodeOperationWhere): RelatedNodeDeleteResponse + deleteNodeTypes(where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(where: RelatedNodeOperationWhere): DeleteResponse } type NodeType { @@ -196,10 +200,6 @@ describe("Temporals", () => { nodeTypes: [NodeType!]! } - type NodeTypeDeleteResponse { - info: DeleteInfo - } - type NodeTypeEdge { cursor: String node: NodeType @@ -342,10 +342,6 @@ describe("Temporals", () => { relatedNodes: [RelatedNode!]! } - type RelatedNodeDeleteResponse { - info: DeleteInfo - } - type RelatedNodeEdge { cursor: String node: RelatedNode From eeb4d2eddad0fb75e42c5cc1220f220554e44079 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 8 Aug 2024 13:31:03 +0100 Subject: [PATCH 154/177] Set parameters in update --- .../queryIR/MutationInput/UpdateProperty.ts | 83 ++++++++++ .../src/api-v6/queryIR/UpdateOperation.ts | 83 ++++++++++ .../queryIRFactory/UpdateOperationFactory.ts | 150 ++++++++++++++++++ .../argument-parser/parse-args.ts | 14 +- .../argument-parser/parse-create-args.ts | 4 +- .../argument-parser/parse-update-args.ts | 37 +++++ .../graphql-tree/graphql-tree.ts | 12 ++ .../parse-resolve-info-tree.ts | 33 +++- .../resolvers/translate-create-resolver.ts | 4 +- .../resolvers/translate-update-resolver.ts | 10 +- .../translators/translate-create-operation.ts | 2 +- .../translators/translate-update-operation.ts | 44 +++++ .../tck/{ => mutations}/create/create.test.ts | 4 +- .../tck/mutations/update/update.test.ts | 78 +++++++++ 14 files changed, 535 insertions(+), 23 deletions(-) create mode 100644 packages/graphql/src/api-v6/queryIR/MutationInput/UpdateProperty.ts create mode 100644 packages/graphql/src/api-v6/queryIR/UpdateOperation.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts rename packages/graphql/src/api-v6/queryIRFactory/{ => resolve-tree-parser}/argument-parser/parse-args.ts (88%) rename packages/graphql/src/api-v6/queryIRFactory/{ => resolve-tree-parser}/argument-parser/parse-create-args.ts (90%) create mode 100644 packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-update-args.ts create mode 100644 packages/graphql/src/api-v6/translators/translate-update-operation.ts rename packages/graphql/tests/api-v6/tck/{ => mutations}/create/create.test.ts (97%) create mode 100644 packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts 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..d8ddccbef9 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIR/MutationInput/UpdateProperty.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import 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]; + } + + // private coerceReference( + // variable: Cypher.Variable | Cypher.Property + // ): Exclude { + // if (this.attribute.typeHelper.isSpatial()) { + // if (!this.attribute.typeHelper.isList()) { + // return Cypher.point(variable); + // } + // const comprehensionVar = new Cypher.Variable(); + // const mapPoint = Cypher.point(comprehensionVar); + // return new Cypher.ListComprehension(comprehensionVar, variable).map(mapPoint); + // } + // return variable; + // } + + // protected getTarget(queryASTContext: QueryASTContext): Cypher.Node | Cypher.Relationship { + // const target = this.attachedTo === "node" ? queryASTContext.target : queryASTContext.relationship; + // if (!target) { + // throw new Error("No target found"); + // } + // return target; + // } + + // public getSetParams(_queryASTContext: QueryASTContext, _inputVariable?: Cypher.Variable): Cypher.SetParam[] { + // return []; + // } +} 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..9c0dc338ff --- /dev/null +++ b/packages/graphql/src/api-v6/queryIR/UpdateOperation.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import 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 propertySet: UpdateProperty[]; + private readonly projection: V6ReadOperation | undefined; + + protected filters: Filter[] = []; + protected selection: EntitySelection; + + constructor({ + target, + propertySet, + projection, + selection, + }: { + selection: EntitySelection; + target: ConcreteEntityAdapter; + propertySet: UpdateProperty[]; + projection?: V6ReadOperation; + }) { + super(); + this.target = target; + this.propertySet = propertySet; + this.projection = projection; + this.selection = selection; + } + + public getChildren(): QueryASTNode[] { + return filterTruthy([...this.propertySet.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.propertySet.flatMap((p) => p.getSetParams(nestedContext)); + + (selectionClause as Cypher.Match).set(...setParams); + + 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/UpdateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts new file mode 100644 index 0000000000..a5dbead035 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.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 { 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 { ReadOperationFactory } from "./ReadOperationFactory"; +import { FactoryParseError } from "./factory-parse-error"; +import type { + GraphQLTreeUpdate, + GraphQLTreeUpdateField, + GraphQLTreeUpdateInput, +} from "./resolve-tree-parser/graphql-tree/graphql-tree"; + +export class UpdateOperationFactory { + public schemaModel: Neo4jGraphQLSchemaModel; + private readFactory: ReadOperationFactory; + + constructor(schemaModel: Neo4jGraphQLSchemaModel) { + this.schemaModel = schemaModel; + this.readFactory = new ReadOperationFactory(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 createOP = new V6UpdateOperation({ + target: targetAdapter, + projection, + propertySet: inputFields, + selection: new NodeSelection({ + target: targetAdapter, + }), + }); + + return createOP; + } + + private getInputFields({ + target, + updateInput, + }: { + target: ConcreteEntityAdapter; + updateInput: GraphQLTreeUpdateInput[]; + }): UpdateProperty[] { + return updateInput.flatMap((input) => { + return Object.entries(input).flatMap(([attributeName, setOperations]) => { + const attribute = getAttribute(target, attributeName); + return this.getPropertyInputOperations(attribute, setOperations); + }); + }); + + // const inputFieldsExistence = new Set(); + // const inputFields: PropertyInputField[] = []; + // // TODO: Add autogenerated fields + + // for (const inputItem of updateInput) { + // 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 getPropertyInputOperations( + attribute: AttributeAdapter, + operations: GraphQLTreeUpdateField + ): UpdateProperty[] { + return Object.entries(operations).map(([operation, value]) => { + return new UpdateProperty({ + value, + attribute: attribute, + attachedTo: "node", + }); + }); + } +} +/** + * Get the attribute from the entity, in case it doesn't exist throw an error + **/ +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/argument-parser/parse-args.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-args.ts similarity index 88% rename from packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-args.ts rename to packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-args.ts index 52504dc105..c97a74c9f2 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-args.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-args.ts @@ -17,15 +17,11 @@ * limitations under the License. */ -import type { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; -import type { Relationship } from "../../../schema-model/relationship/Relationship"; -import type { - GraphQLTree, - GraphQLTreeConnection, - GraphQLTreeConnectionTopLevel, -} from "../resolve-tree-parser/graphql-tree/graphql-tree"; -import type { GraphQLSort, GraphQLSortEdge, GraphQLTreeSortElement } from "../resolve-tree-parser/graphql-tree/sort"; -import { ResolveTreeParserError } from "../resolve-tree-parser/resolve-tree-parser-error"; +import type { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; +import type { Relationship } from "../../../../schema-model/relationship/Relationship"; +import type { GraphQLTree, GraphQLTreeConnection, GraphQLTreeConnectionTopLevel } 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): GraphQLTree["args"] { // Not properly parsed, assuming the type is the same diff --git a/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-create-args.ts similarity index 90% rename from packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts rename to packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-create-args.ts index 8741a52901..e4d86c5c86 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/argument-parser/parse-create-args.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-create-args.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "../resolve-tree-parser/graphql-tree/graphql-tree"; -import { ResolveTreeParserError } from "../resolve-tree-parser/resolve-tree-parser-error"; +import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "../graphql-tree/graphql-tree"; +import { ResolveTreeParserError } from "../resolve-tree-parser-error"; export function parseCreateOperationArgsTopLevel(resolveTreeArgs: Record): GraphQLTreeCreate["args"] { return { 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..9631ea93a6 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/argument-parser/parse-update-args.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { 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(resolveTreeCreateInput: any): GraphQLTreeUpdateInput[] { + 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/graphql-tree/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree.ts index 7cac692a85..474320ae30 100644 --- 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 @@ -25,6 +25,10 @@ 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 UpdateOperation = "set"; +export type GraphQLTreeUpdateField = Record; export interface GraphQLTreeCreate extends GraphQLTreeNode { name: string; @@ -33,6 +37,14 @@ export interface GraphQLTreeCreate extends GraphQLTreeNode { }; } +export interface GraphQLTreeUpdate extends GraphQLTreeNode { + name: string; + args: { + where: GraphQLWhereTopLevel; + input: GraphQLTreeUpdateInput[]; + }; +} + export interface GraphQLTree extends GraphQLTreeElement { name: string; fields: { 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 index d07bb99e02..f060b58187 100644 --- 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 @@ -25,14 +25,16 @@ import { parseConnectionArgsTopLevel, parseOperationArgs, parseOperationArgsTopLevel, -} from "../argument-parser/parse-args"; -import { parseCreateOperationArgsTopLevel } from "../argument-parser/parse-create-args"; +} from "./argument-parser/parse-args"; +import { parseCreateOperationArgsTopLevel } from "./argument-parser/parse-create-args"; +import { parseUpdateOperationArgsTopLevel } from "./argument-parser/parse-update-args"; import type { GraphQLTree, GraphQLTreeConnection, GraphQLTreeConnectionTopLevel, GraphQLTreeCreate, GraphQLTreeReadOperation, + GraphQLTreeUpdate, } from "./graphql-tree/graphql-tree"; import { parseEdges } from "./parse-edges"; import { getNodeFields } from "./parse-node"; @@ -86,6 +88,33 @@ export function parseResolveInfoTreeCreate({ fields, }; } +export function parseResolveInfoTreeUpdate({ + resolveTree, + entity, +}: { + resolveTree: ResolveTree; + entity: ConcreteEntity; +}): GraphQLTreeUpdate { + const entityTypes = entity.typeNames; + const createResponse = findFieldByName(resolveTree, entityTypes.createResponse, 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 parseConnection(resolveTree: ResolveTree, entity: Relationship): GraphQLTreeConnection { const entityTypes = entity.typeNames; diff --git a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts index fb3bba770f..f5a0403f35 100644 --- a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts @@ -23,7 +23,7 @@ import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-t import { execute } from "../../utils"; import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree"; import { parseResolveInfoTreeCreate } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; -import { translateCreateResolver } from "../translators/translate-create-operation"; +import { translateCreateOperation } from "../translators/translate-create-operation"; export function generateCreateResolver({ entity }: { entity: ConcreteEntity }) { return async function resolve( @@ -35,7 +35,7 @@ export function generateCreateResolver({ entity }: { entity: ConcreteEntity }) { const resolveTree = getNeo4jResolveTree(info, { args }); context.resolveTree = resolveTree; const graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity }); - const { cypher, params } = translateCreateResolver({ + const { cypher, params } = translateCreateOperation({ context: context, graphQLTreeCreate, entity, diff --git a/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts index 16fa489527..97cd4e9e08 100644 --- a/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts @@ -22,8 +22,8 @@ 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 { translateCreateResolver } from "../translators/translate-create-operation"; +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( @@ -34,10 +34,10 @@ export function generateUpdateResolver({ entity }: { entity: ConcreteEntity }) { ) { const resolveTree = getNeo4jResolveTree(info, { args }); context.resolveTree = resolveTree; - const graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity }); - const { cypher, params } = translateCreateResolver({ + const graphQLTreeUpdate = parseResolveInfoTreeUpdate({ resolveTree: context.resolveTree, entity }); + const { cypher, params } = translateUpdateOperation({ context: context, - graphQLTreeCreate, + graphQLTreeUpdate, entity, }); const executeResult = await execute({ diff --git a/packages/graphql/src/api-v6/translators/translate-create-operation.ts b/packages/graphql/src/api-v6/translators/translate-create-operation.ts index b8d2fcc9fa..5477f199fa 100644 --- a/packages/graphql/src/api-v6/translators/translate-create-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-create-operation.ts @@ -27,7 +27,7 @@ import type { GraphQLTreeCreate } from "../queryIRFactory/resolve-tree-parser/gr const debug = Debug(DEBUG_TRANSLATE); -export function translateCreateResolver({ +export function translateCreateOperation({ context, entity, graphQLTreeCreate, 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..dab3d744e3 --- /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 createFactory = new UpdateOperationFactory(context.schemaModel); + const createAST = createFactory.createAST({ graphQLTreeUpdate, entity }); + debug(createAST.print()); + const results = createAST.build(context); + return results.build(); +} diff --git a/packages/graphql/tests/api-v6/tck/create/create.test.ts b/packages/graphql/tests/api-v6/tck/mutations/create/create.test.ts similarity index 97% rename from packages/graphql/tests/api-v6/tck/create/create.test.ts rename to packages/graphql/tests/api-v6/tck/mutations/create/create.test.ts index d2c0fa2fce..9c48f6b30f 100644 --- a/packages/graphql/tests/api-v6/tck/create/create.test.ts +++ b/packages/graphql/tests/api-v6/tck/mutations/create/create.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -import { Neo4jGraphQL } from "../../../../src"; -import { formatCypher, formatParams, translateQuery } from "../../../tck/utils/tck-test-utils"; +import { Neo4jGraphQL } from "../../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../../../tck/utils/tck-test-utils"; describe("Top-Level Create", () => { let typeDefs: string; 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..255c0a5402 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/mutations/update/update.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 { 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) + SET + this0.title = $param0 + 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: { __id: id(this0), __resolveType: \\"Movie\\" } }) AS var1 + } + RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\" + }" + `); + }); +}); From a99cc61e23810abb7bfd8e164cff9c7a525a6916 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 8 Aug 2024 15:18:53 +0100 Subject: [PATCH 155/177] Fix projection on update mutations --- .../resolve-tree-parser/parse-resolve-info-tree.ts | 2 +- .../graphql/src/api-v6/resolvers/translate-create-resolver.ts | 1 + .../graphql/src/api-v6/resolvers/translate-update-resolver.ts | 2 +- .../graphql/tests/api-v6/tck/mutations/update/update.test.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) 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 index f060b58187..fdbb4a5593 100644 --- 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 @@ -96,7 +96,7 @@ export function parseResolveInfoTreeUpdate({ entity: ConcreteEntity; }): GraphQLTreeUpdate { const entityTypes = entity.typeNames; - const createResponse = findFieldByName(resolveTree, entityTypes.createResponse, entityTypes.queryField); + const createResponse = findFieldByName(resolveTree, entityTypes.updateResponse, entityTypes.queryField); const createArgs = parseUpdateOperationArgsTopLevel(resolveTree.args); if (!createResponse) { return { diff --git a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts index f5a0403f35..9491af47e6 100644 --- a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts @@ -47,6 +47,7 @@ export function generateCreateResolver({ entity }: { entity: ConcreteEntity }) { context, info, }); + return { [entity.typeNames.queryField]: executeResult.records[0]?.data.connection.edges.map( (edge: any) => edge.node diff --git a/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts index 97cd4e9e08..e3d63686c1 100644 --- a/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/translate-update-resolver.ts @@ -48,7 +48,7 @@ export function generateUpdateResolver({ entity }: { entity: ConcreteEntity }) { info, }); return { - [entity.typeNames.queryField]: executeResult.records[0]?.data.connection.edges.map( + [entity.typeNames.queryField]: executeResult.records[0]?.this.connection.edges.map( (edge: any) => edge.node ), info: { 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 index 255c0a5402..ce7236dcec 100644 --- a/packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts +++ b/packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts @@ -64,7 +64,7 @@ describe("Top-Level Update", () => { WITH edges UNWIND edges AS edge WITH edge.node AS this0 - RETURN collect({ node: { __id: id(this0), __resolveType: \\"Movie\\" } }) AS var1 + RETURN collect({ node: { title: this0.title, __resolveType: \\"Movie\\" } }) AS var1 } RETURN { connection: { edges: var1, totalCount: totalCount } } AS this" `); From 02d5add5eeb03a93daaf1ba3a5e74591775d93f6 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 8 Aug 2024 15:21:13 +0100 Subject: [PATCH 156/177] Move create tests in mutations --- .../integration/{ => mutations}/create/create.int.test.ts | 4 ++-- .../create/types/array/number-array.int.test.ts | 4 ++-- .../create/types/array/temporal-array.int.test.ts | 4 ++-- .../types/cartesian-point/cartesian-point-2d.int.test.ts | 4 ++-- .../types/cartesian-point/cartesian-point-3d.int.test.ts | 4 ++-- .../{ => mutations}/create/types/date.int.test.ts | 4 ++-- .../{ => mutations}/create/types/dateTime.int.test.ts | 4 ++-- .../{ => mutations}/create/types/duration.int.test.ts | 4 ++-- .../{ => mutations}/create/types/localDateTime.int.test.ts | 6 +++--- .../{ => mutations}/create/types/localTime.int.test.ts | 4 ++-- .../{ => mutations}/create/types/number.int.test.ts | 4 ++-- .../{ => mutations}/create/types/point/point-2d.int.test.ts | 4 ++-- .../{ => mutations}/create/types/point/point-3d.int.test.ts | 4 ++-- .../{ => mutations}/create/types/time.int.test.ts | 4 ++-- 14 files changed, 29 insertions(+), 29 deletions(-) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/create.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/array/number-array.int.test.ts (95%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/array/temporal-array.int.test.ts (97%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/cartesian-point/cartesian-point-2d.int.test.ts (95%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/cartesian-point/cartesian-point-3d.int.test.ts (95%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/date.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/dateTime.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/duration.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/localDateTime.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/localTime.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/number.int.test.ts (94%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/point/point-2d.int.test.ts (95%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/point/point-3d.int.test.ts (96%) rename packages/graphql/tests/api-v6/integration/{ => mutations}/create/types/time.int.test.ts (94%) diff --git a/packages/graphql/tests/api-v6/integration/create/create.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/create.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/create.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/create.int.test.ts index db8427d9fa..270daf0a76 100644 --- a/packages/graphql/tests/api-v6/integration/create/create.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/create.int.test.ts @@ -18,8 +18,8 @@ */ import { Integer } from "neo4j-driver"; -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("Top-Level Create", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/array/number-array.int.test.ts similarity index 95% rename from packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/array/number-array.int.test.ts index 44b9d61984..4f000628ca 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/array/number-array.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/array/number-array.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Create Nodes with Numeric array fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/array/temporal-array.int.test.ts similarity index 97% rename from packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/array/temporal-array.int.test.ts index 22fade43cc..4953b59a1b 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/array/temporal-array.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/array/temporal-array.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Nodes with Temporal array fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/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 similarity index 95% rename from packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-2d.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/cartesian-point/cartesian-point-2d.int.test.ts index 87ad8a1d7a..a8fe3d5884 100644 --- a/packages/graphql/tests/api-v6/integration/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 @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Create Nodes with CartesianPoint 2d", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/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 similarity index 95% rename from packages/graphql/tests/api-v6/integration/create/types/cartesian-point/cartesian-point-3d.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/cartesian-point/cartesian-point-3d.int.test.ts index 5fa68e9882..460efdf29e 100644 --- a/packages/graphql/tests/api-v6/integration/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 @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Create Nodes with CartesianPoint 3d", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/date.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/date.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/date.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/date.int.test.ts index e8a9cd95ef..d0ec2aab34 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/date.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/date.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Nodes with Date fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/dateTime.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/dateTime.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/dateTime.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/dateTime.int.test.ts index cd02dd7dea..8488a9b0e6 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/dateTime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/dateTime.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Create Nodes with DateTime fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/duration.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/duration.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/duration.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/duration.int.test.ts index 5008fc3285..fb192393a0 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/duration.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/duration.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Nodes with Duration fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/localDateTime.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/localDateTime.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/localDateTime.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/localDateTime.int.test.ts index 5897ae1901..90ed66fc60 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/localDateTime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/localDateTime.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Nodes with LocalDateTime fields", () => { const testHelper = new TestHelper({ v6Api: true }); @@ -38,7 +38,7 @@ describe("Create Nodes with LocalDateTime fields", () => { 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"); diff --git a/packages/graphql/tests/api-v6/integration/create/types/localTime.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/localTime.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/localTime.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/localTime.int.test.ts index 885fb9982e..d55965cc16 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/localTime.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/localTime.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Nodes with LocalTime fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/number.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/number.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/number.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/number.int.test.ts index fcdb860d10..944f644541 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/number.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/number.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Create Nodes with Numeric fields", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/point/point-2d.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-2d.int.test.ts similarity index 95% rename from packages/graphql/tests/api-v6/integration/create/types/point/point-2d.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-2d.int.test.ts index c961c8ba06..37db56a6ba 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/point/point-2d.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-2d.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Create Nodes with Point 2d", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/point/point-3d.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-3d.int.test.ts similarity index 96% rename from packages/graphql/tests/api-v6/integration/create/types/point/point-3d.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-3d.int.test.ts index cd4e5b4a90..697863a41a 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/point/point-3d.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/point/point-3d.int.test.ts @@ -17,8 +17,8 @@ * limitations under the License. */ -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("Create Nodes with Point 3d", () => { const testHelper = new TestHelper({ v6Api: true }); diff --git a/packages/graphql/tests/api-v6/integration/create/types/time.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/create/types/time.int.test.ts similarity index 94% rename from packages/graphql/tests/api-v6/integration/create/types/time.int.test.ts rename to packages/graphql/tests/api-v6/integration/mutations/create/types/time.int.test.ts index 2b2e9263ad..e3e622fd15 100644 --- a/packages/graphql/tests/api-v6/integration/create/types/time.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/mutations/create/types/time.int.test.ts @@ -18,8 +18,8 @@ */ import neo4jDriver from "neo4j-driver"; -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("Create Node with Time", () => { const testHelper = new TestHelper({ v6Api: true }); From 7fa8bcab413041f90f9f7e55531ead7ad64b20f2 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Thu, 8 Aug 2024 16:47:23 +0100 Subject: [PATCH 157/177] Add filters on update mutations --- .../queryIR/MutationInput/UpdateProperty.ts | 26 ----- .../src/api-v6/queryIR/UpdateOperation.ts | 29 +++-- .../queryIRFactory/CreateOperationFactory.ts | 14 +-- .../queryIRFactory/UpdateOperationFactory.ts | 52 +++------ .../queryIRFactory/utils/get-attribute.ts | 33 ++++++ .../queryAST/ast/selection/EntitySelection.ts | 4 +- .../queryAST/ast/selection/NodeSelection.ts | 6 +- .../mutations/update/update.int.test.ts | 110 ++++++++++++++++++ .../tck/mutations/update/update.test.ts | 6 +- 9 files changed, 187 insertions(+), 93 deletions(-) create mode 100644 packages/graphql/src/api-v6/queryIRFactory/utils/get-attribute.ts create mode 100644 packages/graphql/tests/api-v6/integration/mutations/update/update.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIR/MutationInput/UpdateProperty.ts b/packages/graphql/src/api-v6/queryIR/MutationInput/UpdateProperty.ts index d8ddccbef9..ecd693875e 100644 --- a/packages/graphql/src/api-v6/queryIR/MutationInput/UpdateProperty.ts +++ b/packages/graphql/src/api-v6/queryIR/MutationInput/UpdateProperty.ts @@ -54,30 +54,4 @@ export class UpdateProperty extends InputField { return [setField]; } - - // private coerceReference( - // variable: Cypher.Variable | Cypher.Property - // ): Exclude { - // if (this.attribute.typeHelper.isSpatial()) { - // if (!this.attribute.typeHelper.isList()) { - // return Cypher.point(variable); - // } - // const comprehensionVar = new Cypher.Variable(); - // const mapPoint = Cypher.point(comprehensionVar); - // return new Cypher.ListComprehension(comprehensionVar, variable).map(mapPoint); - // } - // return variable; - // } - - // protected getTarget(queryASTContext: QueryASTContext): Cypher.Node | Cypher.Relationship { - // const target = this.attachedTo === "node" ? queryASTContext.target : queryASTContext.relationship; - // if (!target) { - // throw new Error("No target found"); - // } - // return target; - // } - - // public getSetParams(_queryASTContext: QueryASTContext, _inputVariable?: Cypher.Variable): Cypher.SetParam[] { - // return []; - // } } diff --git a/packages/graphql/src/api-v6/queryIR/UpdateOperation.ts b/packages/graphql/src/api-v6/queryIR/UpdateOperation.ts index 9c0dc338ff..7e3e8416be 100644 --- a/packages/graphql/src/api-v6/queryIR/UpdateOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/UpdateOperation.ts @@ -31,32 +31,35 @@ import type { UpdateProperty } from "./MutationInput/UpdateProperty"; export class V6UpdateOperation extends MutationOperation { public readonly target: ConcreteEntityAdapter; - private readonly propertySet: UpdateProperty[]; + private readonly inputFields: UpdateProperty[]; private readonly projection: V6ReadOperation | undefined; - protected filters: Filter[] = []; - protected selection: EntitySelection; + protected filters: Filter[]; + protected selection: EntitySelection; constructor({ target, - propertySet, + inputFields, projection, selection, + filters = [], }: { - selection: EntitySelection; + selection: EntitySelection; target: ConcreteEntityAdapter; - propertySet: UpdateProperty[]; + inputFields: UpdateProperty[]; projection?: V6ReadOperation; + filters?: Filter[]; }) { super(); this.target = target; - this.propertySet = propertySet; + this.inputFields = inputFields; this.projection = projection; this.selection = selection; + this.filters = filters; } public getChildren(): QueryASTNode[] { - return filterTruthy([...this.propertySet.values(), ...this.filters, this.selection, this.projection]); + return filterTruthy([...this.inputFields.values(), ...this.filters, this.selection, this.projection]); } public transpile(context: QueryASTContext): OperationTranspileResult { @@ -66,9 +69,15 @@ export class V6UpdateOperation extends MutationOperation { const { selection: selectionClause, nestedContext } = this.selection.apply(context); - const setParams = this.propertySet.flatMap((p) => p.getSetParams(nestedContext)); + const setParams = this.inputFields.flatMap((p) => p.getSetParams(nestedContext)); - (selectionClause as Cypher.Match).set(...setParams); + 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] }; diff --git a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts index cc4ba5a6c8..2a6c3a566b 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts @@ -19,7 +19,6 @@ import Cypher from "@neo4j/cypher-builder"; 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"; @@ -27,8 +26,8 @@ import { PropertyInputField } from "../../translate/queryAST/ast/input-fields/Pr import type { V6ReadOperation } from "../queryIR/ConnectionReadOperation"; import { V6CreateOperation } from "../queryIR/CreateOperation"; import { ReadOperationFactory } from "./ReadOperationFactory"; -import { FactoryParseError } from "./factory-parse-error"; import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "./resolve-tree-parser/graphql-tree/graphql-tree"; +import { getAttribute } from "./utils/get-attribute"; export class CreateOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; @@ -113,14 +112,3 @@ export class CreateOperationFactory { return inputFields; } } - -/** - * Get the attribute from the entity, in case it doesn't exist throw an error - **/ -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/UpdateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts index a5dbead035..ef1e619d7e 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts @@ -26,21 +26,24 @@ import { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelect 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 { FactoryParseError } from "./factory-parse-error"; 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({ @@ -67,6 +70,7 @@ export class UpdateOperationFactory { const topLevelUpdateInput = graphQLTreeUpdate.args.input; const targetAdapter = new ConcreteEntityAdapter(entity); let projection: V6ReadOperation | undefined; + if (graphQLTreeUpdate.fields) { projection = this.readFactory.generateMutationProjection({ graphQLTreeNode: graphQLTreeUpdate, @@ -77,16 +81,21 @@ export class UpdateOperationFactory { target: targetAdapter, updateInput: topLevelUpdateInput, }); - const createOP = new V6UpdateOperation({ + + const filters = this.filterFactory.createFilters({ + entity, + where: graphQLTreeUpdate.args.where, + }); + + return new V6UpdateOperation({ target: targetAdapter, projection, - propertySet: inputFields, + inputFields, selection: new NodeSelection({ target: targetAdapter, }), + filters: filters ? [filters] : [], }); - - return createOP; } private getInputFields({ @@ -102,34 +111,13 @@ export class UpdateOperationFactory { return this.getPropertyInputOperations(attribute, setOperations); }); }); - - // const inputFieldsExistence = new Set(); - // const inputFields: PropertyInputField[] = []; - // // TODO: Add autogenerated fields - - // for (const inputItem of updateInput) { - // 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 getPropertyInputOperations( attribute: AttributeAdapter, operations: GraphQLTreeUpdateField ): UpdateProperty[] { return Object.entries(operations).map(([operation, value]) => { + // TODO: other operations return new UpdateProperty({ value, attribute: attribute, @@ -138,13 +126,3 @@ export class UpdateOperationFactory { }); } } -/** - * Get the attribute from the entity, in case it doesn't exist throw an error - **/ -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/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/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/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..c6f3a0951f --- /dev/null +++ b/packages/graphql/tests/api-v6/integration/mutations/update/update.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"; + +describe("Top-Level Update", () => { + 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 update a movies", 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", + }, + }, + ]) + ); + }); +}); 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 index ce7236dcec..5ef2137c2f 100644 --- a/packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts +++ b/packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts @@ -55,8 +55,9 @@ describe("Top-Level Update", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:Movie) + WHERE this0.title = $param0 SET - this0.title = $param0 + this0.title = $param1 WITH * WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount @@ -71,7 +72,8 @@ describe("Top-Level Update", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"param0\\": \\"The Matrix\\" + \\"param0\\": \\"Matrix\\", + \\"param1\\": \\"The Matrix\\" }" `); }); From 77a10b2f0d59ed4a5c8e33fd77a162ecc76d8e99 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Fri, 9 Aug 2024 12:08:57 +0200 Subject: [PATCH 158/177] feat: remove alias on ReadOperationFactor selection --- .../graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts index 3892bb2eb6..9613c729e1 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/ReadOperationFactory.ts @@ -160,7 +160,6 @@ export class ReadOperationFactory { // Selection const selection = new RelationshipSelection({ relationship: relationshipAdapter, - alias: parsedTree.alias, }); // Fields From 2e4ea6a0d8447c82fc4c08de32fd574eaf527da1 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Fri, 9 Aug 2024 12:09:54 +0200 Subject: [PATCH 159/177] test: update snapshots --- .../tck/directives/alias/query-alias.test.ts | 14 +-- .../tck/directives/limit/query-limit.test.ts | 110 +++++++++--------- .../filters-on-relationships.test.ts | 46 ++++---- .../relationships/relationship-not.test.ts | 14 +-- .../types/time/temporals-array.test.ts | 30 ++--- .../types/time/temporals-filters.test.ts | 30 ++--- .../api-v6/tck/projection/aliasing.test.ts | 14 +-- .../tck/projection/relationship.test.ts | 28 ++--- .../types/array/temporals-array.test.ts | 28 ++--- .../tck/projection/types/temporals.test.ts | 28 ++--- .../tck/sort/sort-relationship-alias.test.ts | 62 +++++----- .../api-v6/tck/sort/sort-relationship.test.ts | 62 +++++----- 12 files changed, 233 insertions(+), 233 deletions(-) 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 index f72193ba7b..f768bce3c8 100644 --- 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 @@ -125,20 +125,20 @@ describe("Alias directive", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:DIRECTED]-(directors:Director) - WITH collect({ node: directors, relationship: this1 }) AS edges + 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 directors, edge.relationship AS this1 - RETURN collect({ properties: { year: this1.year, movieYear: this1.year }, node: { name: directors.name, nameAgain: directors.name, __resolveType: \\"Director\\" } }) AS var2 + 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: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, titleAgain: this0.title, directors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, titleAgain: this0.title, directors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); 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 index ce76dd1246..276dce0efc 100644 --- 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 @@ -203,34 +203,34 @@ describe("@limit directive", () => { 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 + "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 edges - UNWIND edges AS edge - WITH edge.node AS this0 - WITH * - LIMIT $param0 + 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 this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Person) - WITH collect({ node: actors, relationship: this1 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS actors, edge.relationship AS this1 - WITH * - LIMIT $param1 - RETURN collect({ node: { id: actors.id, __resolveType: \\"Person\\" } }) AS var2 - } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + 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 collect({ node: { id: this0.id, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" - `); + 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(` "{ @@ -273,34 +273,34 @@ describe("@limit directive", () => { 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 + "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 edges - UNWIND edges AS edge - WITH edge.node AS this0 - WITH * - LIMIT $param0 + 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 this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Person) - WITH collect({ node: actors, relationship: this1 }) AS edges - WITH edges, size(edges) AS totalCount - CALL { - WITH edges - UNWIND edges AS edge - WITH edge.node AS actors, edge.relationship AS this1 - WITH * - LIMIT $param1 - RETURN collect({ node: { id: actors.id, __resolveType: \\"Person\\" } }) AS var2 - } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + 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 collect({ node: { id: this0.id, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" - `); + 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(` "{ @@ -352,22 +352,22 @@ describe("@limit directive", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:PART_OF]-(shows:Show) - WITH collect({ node: shows, relationship: this1 }) AS edges + 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 shows, edge.relationship AS this1 + WITH edge.node AS this2, edge.relationship AS this1 WITH * LIMIT $param0 - RETURN collect({ node: { id: shows.id, __resolveType: \\"Show\\" } }) AS var2 + RETURN collect({ node: { id: this2.id, __resolveType: \\"Show\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { name: this0.name, shows: var3, __resolveType: \\"Festival\\" } }) AS var4 + RETURN collect({ node: { name: this0.name, shows: var4, __resolveType: \\"Festival\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` 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 index 03c97d103f..a2460147c1 100644 --- 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 @@ -80,21 +80,21 @@ describe("Relationship", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WHERE actors.name = $param0 - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 - RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -140,21 +140,21 @@ describe("Relationship", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) WHERE this1.year = $param0 - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 - RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -209,21 +209,21 @@ describe("Relationship", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WHERE (actors.name = $param0 AND this1.year = $param1) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 - RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` 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 index 00bd7cd74c..05fa0bbf34 100644 --- 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 @@ -80,21 +80,21 @@ describe("NOT filters", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) WHERE NOT (this1.year = $param0) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 - RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` 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 index 7231f9526c..4292f67ddd 100644 --- 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 @@ -302,21 +302,21 @@ describe("Temporal types", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) - WHERE (relatedNode.dateTimeNullable = $param0 AND relatedNode.dateTime = $param1 AND relatedNode.localDateTimeNullable = $param2 AND relatedNode.localDateTime = $param3 AND relatedNode.durationNullable = $param4 AND relatedNode.duration = $param5 AND relatedNode.timeNullable = $param6 AND relatedNode.time = $param7 AND relatedNode.localTimeNullable = $param8 AND relatedNode.localTime = $param9) - WITH collect({ node: relatedNode, relationship: this1 }) AS edges + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) + WHERE (this2.dateTimeNullable = $param0 AND this2.dateTime = $param1 AND this2.localDateTimeNullable = $param2 AND this2.localDateTime = $param3 AND this2.durationNullable = $param4 AND this2.duration = $param5 AND this2.timeNullable = $param6 AND this2.time = $param7 AND this2.localTimeNullable = $param8 AND this2.localTime = $param9) + 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 relatedNode, edge.relationship AS this1 - RETURN collect({ node: { dateTime: [var2 IN relatedNode.dateTime | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: relatedNode.localDateTime, duration: relatedNode.duration, time: relatedNode.time, localTime: relatedNode.localTime, dateTimeNullable: [var3 IN relatedNode.dateTimeNullable | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: relatedNode.localDateTimeNullable, durationNullable: relatedNode.durationNullable, timeNullable: relatedNode.timeNullable, localTimeNullable: relatedNode.localTimeNullable, __resolveType: \\"RelatedNode\\" } }) AS var4 + 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, dateTimeNullable: [var4 IN this2.dateTimeNullable | apoc.date.convertFormat(toString(var4), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this2.localDateTimeNullable, durationNullable: this2.durationNullable, timeNullable: this2.timeNullable, localTimeNullable: this2.localTimeNullable, __resolveType: \\"RelatedNode\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 + RETURN { connection: { edges: var5, totalCount: totalCount } } AS var6 } - RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 + RETURN collect({ node: { relatedNode: var6, __resolveType: \\"TypeNode\\" } }) AS var7 } - RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var7, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -494,21 +494,21 @@ describe("Temporal types", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) + MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) WHERE (this1.dateTimeNullable = $param0 AND this1.dateTime = $param1 AND this1.localDateTimeNullable = $param2 AND this1.localDateTime = $param3 AND this1.durationNullable = $param4 AND this1.duration = $param5 AND this1.timeNullable = $param6 AND this1.time = $param7 AND this1.localTimeNullable = $param8 AND this1.localTime = $param9) - WITH collect({ node: relatedNode, relationship: this1 }) AS edges + 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 relatedNode, edge.relationship AS this1 - RETURN collect({ properties: { dateTime: [var2 IN this1.dateTime | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTime: this1.localDateTime, duration: this1.duration, time: this1.time, localTime: this1.localTime, dateTimeNullable: [var3 IN this1.dateTimeNullable | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this1.localDateTimeNullable, durationNullable: this1.durationNullable, timeNullable: this1.timeNullable, localTimeNullable: this1.localTimeNullable }, node: { __id: id(relatedNode), __resolveType: \\"RelatedNode\\" } }) AS var4 + 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, dateTimeNullable: [var4 IN this1.dateTimeNullable | apoc.date.convertFormat(toString(var4), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this1.localDateTimeNullable, durationNullable: this1.durationNullable, timeNullable: this1.timeNullable, localTimeNullable: this1.localTimeNullable }, node: { __id: id(this2), __resolveType: \\"RelatedNode\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 + RETURN { connection: { edges: var5, totalCount: totalCount } } AS var6 } - RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 + RETURN collect({ node: { relatedNode: var6, __resolveType: \\"TypeNode\\" } }) AS var7 } - RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var7, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` 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 index 08ea5432e4..63a885aacf 100644 --- 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 @@ -203,21 +203,21 @@ describe("Temporal types", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) - WHERE (relatedNode.dateTime = $param0 AND relatedNode.localDateTime > $param1 AND (datetime() + relatedNode.duration) >= (datetime() + $param2) AND relatedNode.time < $param3 AND relatedNode.localTime <= $param4) - WITH collect({ node: relatedNode, relationship: this1 }) AS edges + 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 relatedNode, edge.relationship AS this1 - RETURN collect({ node: { dateTime: apoc.date.convertFormat(toString(relatedNode.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: relatedNode.localDateTime, duration: relatedNode.duration, time: relatedNode.time, localTime: relatedNode.localTime, __resolveType: \\"RelatedNode\\" } }) AS var2 + 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: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { relatedNode: var3, __resolveType: \\"TypeNode\\" } }) AS var4 + RETURN collect({ node: { relatedNode: var4, __resolveType: \\"TypeNode\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -321,21 +321,21 @@ describe("Temporal types", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) + 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: relatedNode, relationship: this1 }) AS edges + 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 relatedNode, 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(relatedNode), __resolveType: \\"RelatedNode\\" } }) AS var2 + 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: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { relatedNode: var3, __resolveType: \\"TypeNode\\" } }) AS var4 + RETURN collect({ node: { relatedNode: var4, __resolveType: \\"TypeNode\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/api-v6/tck/projection/aliasing.test.ts b/packages/graphql/tests/api-v6/tck/projection/aliasing.test.ts index ee337410f9..9830be0662 100644 --- a/packages/graphql/tests/api-v6/tck/projection/aliasing.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/aliasing.test.ts @@ -80,20 +80,20 @@ describe("Aliasing", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 - RETURN collect({ properties: { releaseYear: this1.year }, node: { __id: id(actors), __resolveType: \\"Actor\\" } }) AS var2 + 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: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { movieTitle: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { movieTitle: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); diff --git a/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts b/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts index 87a12b6894..da17c28b22 100644 --- a/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/relationship.test.ts @@ -80,20 +80,20 @@ describe("Relationship", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 - RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -135,20 +135,20 @@ describe("Relationship", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 - RETURN collect({ properties: { year: this1.year }, node: { __id: id(actors), __resolveType: \\"Actor\\" } }) AS var2 + 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: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, 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 index 2ca20c20ac..7b82c4a974 100644 --- 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 @@ -159,20 +159,20 @@ describe("Temporal types", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) - WITH collect({ node: relatedNode, relationship: this1 }) AS edges + 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 relatedNode, edge.relationship AS this1 - RETURN collect({ node: { dateTimeNullable: [var2 IN relatedNode.dateTimeNullable | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var3 IN relatedNode.dateTime | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: relatedNode.localDateTimeNullable, localDateTime: relatedNode.localDateTime, durationNullable: relatedNode.durationNullable, duration: relatedNode.duration, timeNullable: relatedNode.timeNullable, time: relatedNode.time, localTimeNullable: relatedNode.localTimeNullable, localTime: relatedNode.localTime, __resolveType: \\"RelatedNode\\" } }) AS var4 + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { dateTimeNullable: [var3 IN this2.dateTimeNullable | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var4 IN this2.dateTime | apoc.date.convertFormat(toString(var4), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this2.localDateTimeNullable, localDateTime: this2.localDateTime, durationNullable: this2.durationNullable, duration: this2.duration, timeNullable: this2.timeNullable, time: this2.time, localTimeNullable: this2.localTimeNullable, localTime: this2.localTime, __resolveType: \\"RelatedNode\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 + RETURN { connection: { edges: var5, totalCount: totalCount } } AS var6 } - RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 + RETURN collect({ node: { relatedNode: var6, __resolveType: \\"TypeNode\\" } }) AS var7 } - RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var7, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -222,20 +222,20 @@ describe("Temporal types", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) - WITH collect({ node: relatedNode, relationship: this1 }) AS edges + 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 relatedNode, edge.relationship AS this1 - RETURN collect({ properties: { dateTimeNullable: [var2 IN this1.dateTimeNullable | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var3 IN this1.dateTime | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this1.localDateTimeNullable, localDateTime: this1.localDateTime, durationNullable: this1.durationNullable, duration: this1.duration, timeNullable: this1.timeNullable, time: this1.time, localTimeNullable: this1.localTimeNullable, localTime: this1.localTime }, node: { __id: id(relatedNode), __resolveType: \\"RelatedNode\\" } }) AS var4 + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ properties: { dateTimeNullable: [var3 IN this1.dateTimeNullable | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var4 IN this1.dateTime | apoc.date.convertFormat(toString(var4), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this1.localDateTimeNullable, localDateTime: this1.localDateTime, durationNullable: this1.durationNullable, duration: this1.duration, timeNullable: this1.timeNullable, time: this1.time, localTimeNullable: this1.localTimeNullable, localTime: this1.localTime }, node: { __id: id(this2), __resolveType: \\"RelatedNode\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 + RETURN { connection: { edges: var5, totalCount: totalCount } } AS var6 } - RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 + RETURN collect({ node: { relatedNode: var6, __resolveType: \\"TypeNode\\" } }) AS var7 } - RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var7, 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 index 8a8869cf0f..d09f681b47 100644 --- a/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/tck/projection/types/temporals.test.ts @@ -134,20 +134,20 @@ describe("Temporal types", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) - WITH collect({ node: relatedNode, relationship: this1 }) AS edges + 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 relatedNode, edge.relationship AS this1 - RETURN collect({ node: { dateTime: apoc.date.convertFormat(toString(relatedNode.dateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), localDateTime: relatedNode.localDateTime, duration: relatedNode.duration, time: relatedNode.time, localTime: relatedNode.localTime, __resolveType: \\"RelatedNode\\" } }) AS var2 + 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: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { relatedNode: var3, __resolveType: \\"TypeNode\\" } }) AS var4 + RETURN collect({ node: { relatedNode: var4, __resolveType: \\"TypeNode\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -192,20 +192,20 @@ describe("Temporal types", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)-[this1:RELATED_TO]->(relatedNode:RelatedNode) - WITH collect({ node: relatedNode, relationship: this1 }) AS edges + 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 relatedNode, 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(relatedNode), __resolveType: \\"RelatedNode\\" } }) AS var2 + 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: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { relatedNode: var3, __resolveType: \\"TypeNode\\" } }) AS var4 + RETURN collect({ node: { relatedNode: var4, __resolveType: \\"TypeNode\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + 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-alias.test.ts b/packages/graphql/tests/api-v6/tck/sort/sort-relationship-alias.test.ts index e60886c015..05e2818208 100644 --- 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 @@ -85,22 +85,22 @@ describe("Sort relationship with alias", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 + WITH edge.node AS this2, edge.relationship AS this1 WITH * - ORDER BY actors.actorName DESC - RETURN collect({ node: { name: actors.actorName, __resolveType: \\"Actor\\" } }) AS var2 + ORDER BY this2.actorName DESC + RETURN collect({ node: { name: this2.actorName, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -144,22 +144,22 @@ describe("Sort relationship with alias", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 + WITH edge.node AS this2, edge.relationship AS this1 WITH * - ORDER BY actors.actorName DESC, actors.actorAge DESC - RETURN collect({ node: { name: actors.actorName, __resolveType: \\"Actor\\" } }) AS var2 + ORDER BY this2.actorName DESC, this2.actorAge DESC + RETURN collect({ node: { name: this2.actorName, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -201,22 +201,22 @@ describe("Sort relationship with alias", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 + WITH edge.node AS this2, edge.relationship AS this1 WITH * ORDER BY this1.actedInYear DESC - RETURN collect({ node: { name: actors.actorName, __resolveType: \\"Actor\\" } }) AS var2 + RETURN collect({ node: { name: this2.actorName, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -264,22 +264,22 @@ describe("Sort relationship with alias", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 + WITH edge.node AS this2, edge.relationship AS this1 WITH * - ORDER BY this1.actedInYear DESC, actors.actorName ASC, this1.role ASC - RETURN collect({ node: { age: actors.actorAge, __resolveType: \\"Actor\\" } }) AS var2 + ORDER BY this1.actedInYear DESC, this2.actorName ASC, this1.role ASC + RETURN collect({ node: { age: this2.actorAge, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + 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 index e54d0dc77c..7d01b3c208 100644 --- a/packages/graphql/tests/api-v6/tck/sort/sort-relationship.test.ts +++ b/packages/graphql/tests/api-v6/tck/sort/sort-relationship.test.ts @@ -85,22 +85,22 @@ describe("Sort relationship", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 + WITH edge.node AS this2, edge.relationship AS this1 WITH * - ORDER BY actors.name DESC - RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + ORDER BY this2.name DESC + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -144,22 +144,22 @@ describe("Sort relationship", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 + WITH edge.node AS this2, edge.relationship AS this1 WITH * - ORDER BY actors.name DESC, actors.age DESC - RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + ORDER BY this2.name DESC, this2.age DESC + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -201,22 +201,22 @@ describe("Sort relationship", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 + WITH edge.node AS this2, edge.relationship AS this1 WITH * ORDER BY this1.year DESC - RETURN collect({ node: { name: actors.name, __resolveType: \\"Actor\\" } }) AS var2 + RETURN collect({ node: { name: this2.name, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -264,22 +264,22 @@ describe("Sort relationship", () => { WITH edge.node AS this0 CALL { WITH this0 - MATCH (this0)<-[this1:ACTED_IN]-(actors:Actor) - WITH collect({ node: actors, relationship: this1 }) AS edges + 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 actors, edge.relationship AS this1 + WITH edge.node AS this2, edge.relationship AS this1 WITH * - ORDER BY this1.year DESC, actors.name ASC, this1.role ASC - RETURN collect({ node: { age: actors.age, __resolveType: \\"Actor\\" } }) AS var2 + ORDER BY this1.year DESC, this2.name ASC, this1.role ASC + RETURN collect({ node: { age: this2.age, __resolveType: \\"Actor\\" } }) AS var3 } - RETURN { connection: { edges: var2, totalCount: totalCount } } AS var3 + RETURN { connection: { edges: var3, totalCount: totalCount } } AS var4 } - RETURN collect({ node: { title: this0.title, actors: var3, __resolveType: \\"Movie\\" } }) AS var4 + RETURN collect({ node: { title: this0.title, actors: var4, __resolveType: \\"Movie\\" } }) AS var5 } - RETURN { connection: { edges: var4, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var5, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); From 76a58449c6d6e258cafb3974871ab729fae35fd2 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 6 Aug 2024 13:30:50 +0100 Subject: [PATCH 160/177] fix name of default directive test --- .../invalid-schema/invalid-default-usage-on-list-fields.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ae3d73a63b..0456d08d75 100644 --- 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 @@ -22,7 +22,7 @@ import { Neo4jGraphQL } from "../../../../src"; import { raiseOnInvalidSchema } from "../../../utils/raise-on-invalid-schema"; describe("invalid @default usage on List fields", () => { - test("@default should fail without define a value", async () => { + test("@default should fail without a defined value", async () => { const fn = async () => { const typeDefs = /* GraphQL */ ` type User @node { From b1bbcf6f437ec094bcabff103f474fea91bb27ad Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Thu, 8 Aug 2024 14:32:22 +0100 Subject: [PATCH 161/177] add schema for @id --- .../TopLevelCreateSchemaTypes.ts | 5 +- .../src/api-v6/validation/rules/types.ts | 22 ++ .../utils/find-type-path-in-type-paths.ts | 24 ++ .../validation/rules/utils/get-type-path.ts | 28 ++ .../rules/utils/is-type-a-built-in-type.ts | 29 ++ .../api-v6/validation/rules/valid-default.ts | 13 +- .../src/api-v6/validation/rules/valid-id.ts | 69 ++++ .../api-v6/validation/validate-v6-document.ts | 2 + .../validation/custom-rules/directives/id.ts | 12 +- .../validation/validate-document.test.ts | 45 --- .../tests/api-v6/schema/directives/id.test.ts | 368 ++++++++++++++++++ .../schema/invalid-schema/invalid-id.test.ts | 113 ++++++ .../tck/directives/id/create-id.test.ts | 98 +++++ 13 files changed, 759 insertions(+), 69 deletions(-) create mode 100644 packages/graphql/src/api-v6/validation/rules/types.ts create mode 100644 packages/graphql/src/api-v6/validation/rules/utils/find-type-path-in-type-paths.ts create mode 100644 packages/graphql/src/api-v6/validation/rules/utils/get-type-path.ts create mode 100644 packages/graphql/src/api-v6/validation/rules/utils/is-type-a-built-in-type.ts create mode 100644 packages/graphql/src/api-v6/validation/rules/valid-id.ts create mode 100644 packages/graphql/tests/api-v6/schema/directives/id.test.ts create mode 100644 packages/graphql/tests/api-v6/schema/invalid-schema/invalid-id.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/directives/id/create-id.test.ts 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 index e5efa704be..4986ca6063 100644 --- 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 @@ -58,7 +58,10 @@ export class TopLevelCreateSchemaTypes { public get createNode(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType(this.entityTypeNames.createNode, (_itc: InputTypeComposer) => { - const inputFields = this.getInputFields([...this.entity.attributes.values()]); + 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 { 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/valid-default.ts b/packages/graphql/src/api-v6/validation/rules/valid-default.ts index 27e0f29453..fd4e615430 100644 --- a/packages/graphql/src/api-v6/validation/rules/valid-default.ts +++ b/packages/graphql/src/api-v6/validation/rules/valid-default.ts @@ -21,12 +21,6 @@ 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 { - GraphQLBuiltInScalarType, - Neo4jGraphQLNumberType, - Neo4jGraphQLSpatialType, - Neo4jGraphQLTemporalType, -} from "../../../schema-model/attribute/AttributeType"; import { assertValid, createGraphQLError, @@ -35,6 +29,7 @@ import { 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 { @@ -92,9 +87,3 @@ export function ValidDefault(context: SDLValidationContext): ASTVisitor { }, }; } - -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/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/validate-v6-document.ts b/packages/graphql/src/api-v6/validation/validate-v6-document.ts index d8c6981ca9..5104abe913 100644 --- a/packages/graphql/src/api-v6/validation/validate-v6-document.ts +++ b/packages/graphql/src/api-v6/validation/validate-v6-document.ts @@ -45,6 +45,7 @@ import { WarnIfListOfListsFieldDefinition } from "../../schema/validation/custom 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 { ValidRelationship } from "./rules/valid-relationship"; @@ -68,6 +69,7 @@ function runNeo4jGraphQLValidationRules({ ValidRelationship, ValidLimit, ValidDefault, + ValidID, DirectiveCombinationValid, ValidRelationshipProperties, ReservedTypeNames, 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/validate-document.test.ts b/packages/graphql/src/schema/validation/validate-document.test.ts index f3b7a46781..58227301be 100644 --- a/packages/graphql/src/schema/validation/validate-document.test.ts +++ b/packages/graphql/src/schema/validation/validate-document.test.ts @@ -2525,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/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..6731f8cfef --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/directives/id.test.ts @@ -0,0 +1,368 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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! + relationshipsCreated: Int! + } + + input ActorCreateInput { + node: ActorCreateNode! + } + + input ActorCreateNode { + name: String + } + + type ActorCreateResponse { + actors: [Actor!]! + info: ActorCreateInfo + } + + type ActorEdge { + cursor: String + node: Actor + } + + type ActorMoviesConnection { + edges: [ActorMoviesEdge] + pageInfo: PageInfo + } + + input ActorMoviesConnectionSort { + edges: ActorMoviesEdgeSort + } + + 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 ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + id: IDWhere + movies: ActorMoviesNestedOperationWhere + name: StringWhere + } + + 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 + } + + 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! + relationshipsCreated: Int! + } + + input MovieCreateInput { + node: MovieCreateNode! + } + + input MovieCreateNode { + title: String + } + + type MovieCreateResponse { + info: MovieCreateInfo + movies: [Movie!]! + } + + 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 MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + actors: MovieActorsNestedOperationWhere + id: IDWhere + title: StringWhere + } + + type Mutation { + createActors(input: [ActorCreateInput!]!): ActorCreateResponse + createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + } + + 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 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-id.test.ts b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-id.test.ts new file mode 100644 index 0000000000..41a9ad768a --- /dev/null +++ b/packages/graphql/tests/api-v6/schema/invalid-schema/invalid-id.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 { 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." }, + { 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/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..247f64afa3 --- /dev/null +++ b/packages/graphql/tests/api-v6/tck/directives/id/create-id.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("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.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 + } + } + ] + }" + `); + }); +}); From 628a3f4c61e54e1b7c8390041f46308d0cb55d46 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 9 Aug 2024 14:14:25 +0100 Subject: [PATCH 162/177] add @id translation --- .../queryIRFactory/CreateOperationFactory.ts | 24 +++++- .../resolvers/translate-create-resolver.ts | 4 +- .../translators/translate-create-operation.ts | 2 +- .../directives/alias/create-alias.int.test.ts | 6 +- .../directives/id/create-id.int.test.ts | 81 +++++++++++++++++++ .../tck/directives/id/create-id.test.ts | 1 + 6 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 packages/graphql/tests/api-v6/integration/directives/id/create-id.int.test.ts diff --git a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts index cc4ba5a6c8..f4016a2668 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts @@ -23,7 +23,10 @@ import type { AttributeAdapter } from "../../schema-model/attribute/model-adapte 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"; @@ -89,11 +92,10 @@ export class CreateOperationFactory { }: { target: ConcreteEntityAdapter; createInput: GraphQLTreeCreateInput[]; - }): PropertyInputField[] { + }): InputField[] { const inputFieldsExistence = new Set(); - const inputFields: PropertyInputField[] = []; - // TODO: Add autogenerated fields - + const inputFields: InputField[] = []; + inputFields.push(...this.addAutogeneratedFields(target, inputFieldsExistence)); for (const inputItem of createInput) { for (const key of Object.keys(inputItem)) { const attribute = getAttribute(target, key); @@ -112,6 +114,20 @@ export class CreateOperationFactory { } 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/resolvers/translate-create-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts index fb3bba770f..f5a0403f35 100644 --- a/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/translate-create-resolver.ts @@ -23,7 +23,7 @@ import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-t import { execute } from "../../utils"; import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree"; import { parseResolveInfoTreeCreate } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; -import { translateCreateResolver } from "../translators/translate-create-operation"; +import { translateCreateOperation } from "../translators/translate-create-operation"; export function generateCreateResolver({ entity }: { entity: ConcreteEntity }) { return async function resolve( @@ -35,7 +35,7 @@ export function generateCreateResolver({ entity }: { entity: ConcreteEntity }) { const resolveTree = getNeo4jResolveTree(info, { args }); context.resolveTree = resolveTree; const graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity }); - const { cypher, params } = translateCreateResolver({ + const { cypher, params } = translateCreateOperation({ context: context, graphQLTreeCreate, entity, diff --git a/packages/graphql/src/api-v6/translators/translate-create-operation.ts b/packages/graphql/src/api-v6/translators/translate-create-operation.ts index b8d2fcc9fa..5477f199fa 100644 --- a/packages/graphql/src/api-v6/translators/translate-create-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-create-operation.ts @@ -27,7 +27,7 @@ import type { GraphQLTreeCreate } from "../queryIRFactory/resolve-tree-parser/gr const debug = Debug(DEBUG_TRANSLATE); -export function translateCreateResolver({ +export function translateCreateOperation({ context, entity, graphQLTreeCreate, 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 index fe3952705e..ddd77f8f8e 100644 --- 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 @@ -29,6 +29,7 @@ describe("Create with @alias", () => { const typeDefs = /* GraphQL */ ` type ${Movie} @node { + id: ID! @id @alias(property: "serverId") title: String! @alias(property: "name") released: Int @alias(property: "year") } @@ -48,6 +49,7 @@ describe("Create with @alias", () => { { node: { title: "The Matrix 2", released: 2001 } } ]) { ${Movie.plural} { + id title released } @@ -60,8 +62,8 @@ describe("Create with @alias", () => { expect(gqlResult.data).toEqual({ [Movie.operations.create]: { [Movie.plural]: expect.toIncludeSameMembers([ - { title: "The Matrix", released: null }, - { title: "The Matrix 2", released: 2001 }, + { 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/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/tck/directives/id/create-id.test.ts b/packages/graphql/tests/api-v6/tck/directives/id/create-id.test.ts index 247f64afa3..c9a23ed538 100644 --- 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 @@ -62,6 +62,7 @@ describe("Top-Level Create with autogenerated id", () => { WITH var0 CREATE (this1:Movie) SET + this1.id = randomUUID(), this1.title = var0.title, this1.released = var0.released RETURN this1 From 80161637970910e02300be1a2aa4bbce19f26924 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 9 Aug 2024 14:27:53 +0100 Subject: [PATCH 163/177] fix merge conflic on translate-delete-resolver --- .../graphql/src/api-v6/resolvers/translate-delete-resolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts index a72098ef51..3a3707626e 100644 --- a/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts @@ -23,7 +23,7 @@ import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-t import { execute } from "../../utils"; import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree"; import { parseResolveInfoTreeCreate } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree"; -import { translateCreateResolver } from "../translators/translate-create-operation"; +import { translateCreateOperation } from "../translators/translate-create-operation"; export function generateDeleteResolver({ entity }: { entity: ConcreteEntity }) { return async function resolve( @@ -36,7 +36,7 @@ export function generateDeleteResolver({ entity }: { entity: ConcreteEntity }) { context.resolveTree = resolveTree; // TODO: Implement delete resolver const graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity }); - const { cypher, params } = translateCreateResolver({ + const { cypher, params } = translateCreateOperation({ context: context, graphQLTreeCreate, entity, From a3a827a7eff87c638e73ad35b792b74928d94bde Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Fri, 9 Aug 2024 17:21:23 +0100 Subject: [PATCH 164/177] fix schema tests after merge --- .../tests/api-v6/schema/directives/id.test.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/packages/graphql/tests/api-v6/schema/directives/id.test.ts b/packages/graphql/tests/api-v6/schema/directives/id.test.ts index 6731f8cfef..30c0afa0bd 100644 --- a/packages/graphql/tests/api-v6/schema/directives/id.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/id.test.ts @@ -85,11 +85,6 @@ describe("@id", () => { node: ActorSort } - type ActorCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input ActorCreateInput { node: ActorCreateNode! } @@ -100,7 +95,7 @@ describe("@id", () => { type ActorCreateResponse { actors: [Actor!]! - info: ActorCreateInfo + info: CreateInfo } type ActorEdge { @@ -189,6 +184,20 @@ describe("@id", () => { name: StringWhere } + type CreateInfo { + nodesCreated: Int! + relationshipsCreated: Int! + } + + type DeleteInfo { + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type DeleteResponse { + info: DeleteInfo + } + input IDWhere { AND: [IDWhere!] NOT: IDWhere @@ -283,11 +292,6 @@ describe("@id", () => { node: MovieSort } - type MovieCreateInfo { - nodesCreated: Int! - relationshipsCreated: Int! - } - input MovieCreateInput { node: MovieCreateNode! } @@ -297,7 +301,7 @@ describe("@id", () => { } type MovieCreateResponse { - info: MovieCreateInfo + info: CreateInfo movies: [Movie!]! } @@ -334,6 +338,8 @@ describe("@id", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse + deleteActors(where: ActorOperationWhere): DeleteResponse + deleteMovies(where: MovieOperationWhere): DeleteResponse } type PageInfo { From 50c5ddb5d2065857b0a160db8f438c1f64ecddf2 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 12 Aug 2024 11:15:05 +0100 Subject: [PATCH 165/177] add validation rule to list of nullable elements, fix tests --- .../rules/utils/type-node-to-string.ts | 29 ++ .../validation/rules/valid-list-element.ts | 72 +++++ .../api-v6/validation/validate-v6-document.ts | 2 + .../number/array/number-equals.int.test.ts | 15 +- .../types/array/number-array.int.test.ts | 12 - ...valid-default-usage-on-list-fields.test.ts | 26 +- .../schema/invalid-schema/invalid-id.test.ts | 1 - .../invalid-schema/invalid-list.test.ts | 70 +++++ .../tests/api-v6/schema/types/array.test.ts | 176 ------------ .../tck/filters/array/array-filters.test.ts | 2 +- .../types/time/temporals-array.test.ts | 251 ++---------------- .../types/array/temporals-array.test.ts | 50 +--- 12 files changed, 226 insertions(+), 480 deletions(-) create mode 100644 packages/graphql/src/api-v6/validation/rules/utils/type-node-to-string.ts create mode 100644 packages/graphql/src/api-v6/validation/rules/valid-list-element.ts create mode 100644 packages/graphql/tests/api-v6/schema/invalid-schema/invalid-list.test.ts 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-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/validate-v6-document.ts b/packages/graphql/src/api-v6/validation/validate-v6-document.ts index 5104abe913..0f09d50925 100644 --- a/packages/graphql/src/api-v6/validation/validate-v6-document.ts +++ b/packages/graphql/src/api-v6/validation/validate-v6-document.ts @@ -47,6 +47,7 @@ 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({ @@ -67,6 +68,7 @@ function runNeo4jGraphQLValidationRules({ [ ...specifiedSDLRules, ValidRelationship, + ValidListField, ValidLimit, ValidDefault, ValidID, 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 index 96aac3fe34..0e4ff87aae 100644 --- 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 @@ -31,7 +31,6 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering array - 'equals const typeDefs = /* GraphQL */ ` type ${Movie} @node { list: [${type}!]! - listNullable: [${type}]! title: String! } @@ -39,9 +38,9 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering array - 'equals await testHelper.initNeo4jGraphQL({ typeDefs }); await testHelper.executeCypher(` - CREATE (:${Movie} {list: [1999, 2000], listNullable: [1999, 2000], title: "The Matrix"}) - CREATE (:${Movie} {list: [2001, 2000], listNullable: [2001, 2000], title: "The Matrix 2"}) - CREATE (:${Movie} {list: [1999, 2000], listNullable: [1999, 2000], title: "Bill And Ted"}) + 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"}) `); }); @@ -49,10 +48,10 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering array - 'equals await testHelper.close(); }); - test.each(["list", "listNullable"])("%s filter by 'equals'", async (field) => { + test("list filter by 'equals'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { node: { ${field}: { equals: [2001, 2000] } } }) { + ${Movie.plural}(where: { node: { list: { equals: [2001, 2000] } } }) { connection { edges { node { @@ -81,10 +80,10 @@ describe.each(["Float", "Int", "BigInt"] as const)("%s Filtering array - 'equals }); }); - test.each(["list", "listNullable"])("%s filter by NOT 'equals'", async (field) => { + test("List filter by NOT 'equals'", async () => { const query = /* GraphQL */ ` query { - ${Movie.plural}(where: { NOT: { node: { ${field}: { equals: [2001, 2000] } } } }) { + ${Movie.plural}(where: { NOT: { node: { list: { equals: [2001, 2000] } } } }) { connection { edges { node { 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 index 433495b34b..fe5b05c4b5 100644 --- 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 @@ -33,9 +33,6 @@ describe("Numeric array fields", () => { year: [Int!]! rating: [Float!]! viewings: [BigInt!]! - yearNullable: [Int]! - ratingNullable: [Float]! - viewingsNullable: [BigInt]! } `; @@ -43,9 +40,6 @@ describe("Numeric array fields", () => { await testHelper.executeCypher(` CREATE (movie:${Movie} { - yearNullable: [1999], - ratingNullable: [4.0], - viewingsNullable: [4294967297], year: [1999], rating: [4.0], viewings: [4294967297] @@ -65,11 +59,8 @@ describe("Numeric array fields", () => { edges { node { year - yearNullable viewings - viewingsNullable rating - ratingNullable } } @@ -87,11 +78,8 @@ describe("Numeric array fields", () => { { node: { year: [1999], - yearNullable: [1999], rating: [4.0], - ratingNullable: [4.0], viewings: ["4294967297"], - viewingsNullable: ["4294967297"], }, }, ], 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 index 0456d08d75..b26850ef65 100644 --- 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 @@ -26,7 +26,7 @@ describe("invalid @default usage on List fields", () => { const fn = async () => { const typeDefs = /* GraphQL */ ` type User @node { - name: [String] @default + name: [String!] @default } `; const neoSchema = new Neo4jGraphQL({ typeDefs }); @@ -42,28 +42,28 @@ describe("invalid @default usage on List fields", () => { test.each([ { - dataType: "[ID]", + dataType: "[ID!]", value: [1.2], errorMsg: "@default.value on ID list fields must be a list of ID values", }, { - dataType: "[String]", + dataType: "[String!]", value: [1.2], errorMsg: "@default.value on String list fields must be a list of String values", }, { - dataType: "[Boolean]", + 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: "[Int!]", value: 1.2, errorMsg: "@default.value on Int list fields must be a list of Int values" }, { - dataType: "[Float]", + dataType: "[Float!]", value: ["stuff"], errorMsg: "@default.value on Float list fields must be a list of Float values", }, { - dataType: "[DateTime]", + dataType: "[DateTime!]", value: ["dummy"], errorMsg: "@default.value on DateTime list fields must be a list of DateTime values", }, @@ -91,20 +91,20 @@ describe("invalid @default usage on List fields", () => { test.each([ { - dataType: "[ID]", + dataType: "[ID!]", value: ["some-unique-id", "another-unique-id"], }, { - dataType: "[String]", + dataType: "[String!]", value: ["dummyValue", "anotherDummyValue"], }, { - dataType: "[Boolean]", + 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"] }, + { 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(", "); 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 index 41a9ad768a..f7636761dc 100644 --- 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 @@ -56,7 +56,6 @@ describe("@id validation", () => { { dataType: "String!", errorMsg: "Cannot autogenerate a non ID field." }, { dataType: "[String!]", errorMsg: "Cannot autogenerate an array." }, { 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 */ ` 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/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 26d8907c65..703da767e7 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -27,85 +27,49 @@ describe("Scalars", () => { const typeDefs = /* GraphQL */ ` type NodeType @node { stringList: [String!] - stringListNullable: [String] intList: [Int!] - intListNullable: [Int] floatList: [Float!] - floatListNullable: [Float] idList: [ID!] - idListNullable: [ID] booleanList: [Boolean!] - booleanListNullable: [Boolean] dateList: [Date!] - dateListNullable: [Date] dateTimeList: [DateTime!] - dateTimeListNullable: [DateTime] localDateTimeList: [LocalDateTime!] - localDateTimeListNullable: [LocalDateTime] durationList: [Duration!] - durationListNullable: [Duration] timeList: [Time!] - timeListNullable: [Time] localTimeList: [LocalTime!] - localTimeListNullable: [LocalTime] bigIntList: [BigInt!] - bigIntListNullable: [BigInt] relatedNode: [RelatedNode!]! @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") } type RelatedNodeProperties @relationshipProperties { stringList: [String!] - stringListNullable: [String] intList: [Int!] - intListNullable: [Int] floatList: [Float!] - floatListNullable: [Float] idList: [ID!] - idListNullable: [ID] booleanList: [Boolean!] - booleanListNullable: [Boolean] dateList: [Date!] - dateListNullable: [Date] dateTimeList: [DateTime!] - dateTimeListNullable: [DateTime] localDateTimeList: [LocalDateTime!] - localDateTimeListNullable: [LocalDateTime] durationList: [Duration!] - durationListNullable: [Duration] timeList: [Time!] - timeListNullable: [Time] localTimeList: [LocalTime!] - localTimeListNullable: [LocalTime] bigIntList: [BigInt!] - bigIntListNullable: [BigInt] } type RelatedNode @node { stringList: [String!] - stringListNullable: [String] intList: [Int!] - intListNullable: [Int] floatList: [Float!] - floatListNullable: [Float] idList: [ID!] - idListNullable: [ID] booleanList: [Boolean!] - booleanListNullable: [Boolean] dateList: [Date!] - dateListNullable: [Date] dateTimeList: [DateTime!] - dateTimeListNullable: [DateTime] localDateTimeList: [LocalDateTime!] - localDateTimeListNullable: [LocalDateTime] durationList: [Duration!] - durationListNullable: [Duration] timeList: [Time!] - timeListNullable: [Time] localTimeList: [LocalTime!] - localTimeListNullable: [LocalTime] bigIntList: [BigInt!] - bigIntListNullable: [BigInt] } `; @@ -128,10 +92,6 @@ describe("Scalars", () => { equals: [BigInt!] } - input BigIntListWhereNullable { - equals: [BigInt] - } - input BooleanWhere { AND: [BooleanWhere!] NOT: BooleanWhere @@ -151,10 +111,6 @@ describe("Scalars", () => { equals: [Date!] } - input DateListWhereNullable { - equals: [Date] - } - \\"\\"\\"A date and time, represented as an ISO-8601 string\\"\\"\\" scalar DateTime @@ -162,10 +118,6 @@ describe("Scalars", () => { equals: [DateTime!] } - input DateTimeListWhereNullable { - equals: [DateTime] - } - type DeleteInfo { nodesDeleted: Int! relationshipsDeleted: Int! @@ -182,34 +134,18 @@ describe("Scalars", () => { equals: [Duration!] } - input DurationListWhereNullable { - equals: [Duration] - } - input FloatListWhere { equals: [Float!] } - input FloatListWhereNullable { - equals: [Float] - } - input IDListWhere { equals: [ID!] } - input IDListWhereNullable { - equals: [ID] - } - input IntListWhere { equals: [Int!] } - input IntListWhereNullable { - equals: [Int] - } - \\"\\"\\"A local datetime, represented as 'YYYY-MM-DDTHH:MM:SS'\\"\\"\\" scalar LocalDateTime @@ -217,10 +153,6 @@ describe("Scalars", () => { equals: [LocalDateTime!] } - input LocalDateTimeListWhereNullable { - equals: [LocalDateTime] - } - \\"\\"\\" A local time, represented as a time string without timezone information \\"\\"\\" @@ -230,10 +162,6 @@ describe("Scalars", () => { equals: [LocalTime!] } - input LocalTimeListWhereNullable { - equals: [LocalTime] - } - type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse @@ -243,30 +171,18 @@ describe("Scalars", () => { type NodeType { bigIntList: [BigInt!] - bigIntListNullable: [BigInt] booleanList: [Boolean!] - booleanListNullable: [Boolean] dateList: [Date!] - dateListNullable: [Date] dateTimeList: [DateTime!] - dateTimeListNullable: [DateTime] durationList: [Duration!] - durationListNullable: [Duration] floatList: [Float!] - floatListNullable: [Float] idList: [ID!] - idListNullable: [ID] intList: [Int!] - intListNullable: [Int] localDateTimeList: [LocalDateTime!] - localDateTimeListNullable: [LocalDateTime] localTimeList: [LocalTime!] - localTimeListNullable: [LocalTime] relatedNode(where: NodeTypeRelatedNodeOperationWhere): NodeTypeRelatedNodeOperation stringList: [String!] - stringListNullable: [String] timeList: [Time!] - timeListNullable: [Time] } type NodeTypeConnection { @@ -280,29 +196,17 @@ describe("Scalars", () => { input NodeTypeCreateNode { bigIntList: [BigInt!] - bigIntListNullable: [BigInt] booleanList: [Boolean!] - booleanListNullable: [Boolean] dateList: [Date!] - dateListNullable: [Date] dateTimeList: [DateTime!] - dateTimeListNullable: [DateTime] durationList: [Duration!] - durationListNullable: [Duration] floatList: [Float!] - floatListNullable: [Float] idList: [ID!] - idListNullable: [ID] intList: [Int!] - intListNullable: [Int] localDateTimeList: [LocalDateTime!] - localDateTimeListNullable: [LocalDateTime] localTimeList: [LocalTime!] - localTimeListNullable: [LocalTime] stringList: [String!] - stringListNullable: [String] timeList: [Time!] - timeListNullable: [Time] } type NodeTypeCreateResponse { @@ -378,30 +282,18 @@ describe("Scalars", () => { NOT: NodeTypeWhere OR: [NodeTypeWhere!] bigIntList: BigIntListWhere - bigIntListNullable: BigIntListWhereNullable booleanList: BooleanWhere - booleanListNullable: BooleanWhere dateList: DateListWhere - dateListNullable: DateListWhereNullable dateTimeList: DateTimeListWhere - dateTimeListNullable: DateTimeListWhereNullable durationList: DurationListWhere - durationListNullable: DurationListWhereNullable floatList: FloatListWhere - floatListNullable: FloatListWhereNullable idList: IDListWhere - idListNullable: IDListWhereNullable intList: IntListWhere - intListNullable: IntListWhereNullable localDateTimeList: LocalDateTimeListWhere - localDateTimeListNullable: LocalDateTimeListWhereNullable localTimeList: LocalTimeListWhere - localTimeListNullable: LocalTimeListWhereNullable relatedNode: NodeTypeRelatedNodeNestedOperationWhere stringList: StringListWhere - stringListNullable: StringListWhereNullable timeList: TimeListWhere - timeListNullable: TimeListWhereNullable } type PageInfo { @@ -418,29 +310,17 @@ describe("Scalars", () => { type RelatedNode { bigIntList: [BigInt!] - bigIntListNullable: [BigInt] booleanList: [Boolean!] - booleanListNullable: [Boolean] dateList: [Date!] - dateListNullable: [Date] dateTimeList: [DateTime!] - dateTimeListNullable: [DateTime] durationList: [Duration!] - durationListNullable: [Duration] floatList: [Float!] - floatListNullable: [Float] idList: [ID!] - idListNullable: [ID] intList: [Int!] - intListNullable: [Int] localDateTimeList: [LocalDateTime!] - localDateTimeListNullable: [LocalDateTime] localTimeList: [LocalTime!] - localTimeListNullable: [LocalTime] stringList: [String!] - stringListNullable: [String] timeList: [Time!] - timeListNullable: [Time] } type RelatedNodeConnection { @@ -454,29 +334,17 @@ describe("Scalars", () => { input RelatedNodeCreateNode { bigIntList: [BigInt!] - bigIntListNullable: [BigInt] booleanList: [Boolean!] - booleanListNullable: [Boolean] dateList: [Date!] - dateListNullable: [Date] dateTimeList: [DateTime!] - dateTimeListNullable: [DateTime] durationList: [Duration!] - durationListNullable: [Duration] floatList: [Float!] - floatListNullable: [Float] idList: [ID!] - idListNullable: [ID] intList: [Int!] - intListNullable: [Int] localDateTimeList: [LocalDateTime!] - localDateTimeListNullable: [LocalDateTime] localTimeList: [LocalTime!] - localTimeListNullable: [LocalTime] stringList: [String!] - stringListNullable: [String] timeList: [Time!] - timeListNullable: [Time] } type RelatedNodeCreateResponse { @@ -502,29 +370,17 @@ describe("Scalars", () => { type RelatedNodeProperties { bigIntList: [BigInt!] - bigIntListNullable: [BigInt] booleanList: [Boolean!] - booleanListNullable: [Boolean] dateList: [Date!] - dateListNullable: [Date] dateTimeList: [DateTime!] - dateTimeListNullable: [DateTime] durationList: [Duration!] - durationListNullable: [Duration] floatList: [Float!] - floatListNullable: [Float] idList: [ID!] - idListNullable: [ID] intList: [Int!] - intListNullable: [Int] localDateTimeList: [LocalDateTime!] - localDateTimeListNullable: [LocalDateTime] localTimeList: [LocalTime!] - localTimeListNullable: [LocalTime] stringList: [String!] - stringListNullable: [String] timeList: [Time!] - timeListNullable: [Time] } input RelatedNodePropertiesWhere { @@ -532,29 +388,17 @@ describe("Scalars", () => { NOT: RelatedNodePropertiesWhere OR: [RelatedNodePropertiesWhere!] bigIntList: BigIntListWhere - bigIntListNullable: BigIntListWhereNullable booleanList: BooleanWhere - booleanListNullable: BooleanWhere dateList: DateListWhere - dateListNullable: DateListWhereNullable dateTimeList: DateTimeListWhere - dateTimeListNullable: DateTimeListWhereNullable durationList: DurationListWhere - durationListNullable: DurationListWhereNullable floatList: FloatListWhere - floatListNullable: FloatListWhereNullable idList: IDListWhere - idListNullable: IDListWhereNullable intList: IntListWhere - intListNullable: IntListWhereNullable localDateTimeList: LocalDateTimeListWhere - localDateTimeListNullable: LocalDateTimeListWhereNullable localTimeList: LocalTimeListWhere - localTimeListNullable: LocalTimeListWhereNullable stringList: StringListWhere - stringListNullable: StringListWhereNullable timeList: TimeListWhere - timeListNullable: TimeListWhereNullable } input RelatedNodeWhere { @@ -562,48 +406,28 @@ describe("Scalars", () => { NOT: RelatedNodeWhere OR: [RelatedNodeWhere!] bigIntList: BigIntListWhere - bigIntListNullable: BigIntListWhereNullable booleanList: BooleanWhere - booleanListNullable: BooleanWhere dateList: DateListWhere - dateListNullable: DateListWhereNullable dateTimeList: DateTimeListWhere - dateTimeListNullable: DateTimeListWhereNullable durationList: DurationListWhere - durationListNullable: DurationListWhereNullable floatList: FloatListWhere - floatListNullable: FloatListWhereNullable idList: IDListWhere - idListNullable: IDListWhereNullable intList: IntListWhere - intListNullable: IntListWhereNullable localDateTimeList: LocalDateTimeListWhere - localDateTimeListNullable: LocalDateTimeListWhereNullable localTimeList: LocalTimeListWhere - localTimeListNullable: LocalTimeListWhereNullable stringList: StringListWhere - stringListNullable: StringListWhereNullable timeList: TimeListWhere - timeListNullable: TimeListWhereNullable } input StringListWhere { equals: [String!] } - input StringListWhereNullable { - equals: [String] - } - \\"\\"\\"A time, represented as an RFC3339 time string\\"\\"\\" scalar Time input TimeListWhere { equals: [Time!] - } - - input TimeListWhereNullable { - equals: [Time] }" `); }); 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 index 87826c17f3..45122a56ce 100644 --- 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 @@ -28,7 +28,7 @@ describe("Array filters", () => { typeDefs = /* GraphQL */ ` type Movie @node { title: String - alternativeTitles: [String] + alternativeTitles: [String!] } `; 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 index 4292f67ddd..4450a55236 100644 --- 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 @@ -27,43 +27,28 @@ describe("Temporal types", () => { beforeAll(() => { typeDefs = /* GraphQL */ ` type TypeNode @node { - dateTimeNullable: [DateTime] dateTime: [DateTime!] - localDateTimeNullable: [LocalDateTime] localDateTime: [LocalDateTime!] - durationNullable: [Duration] duration: [Duration!] - timeNullable: [Time] time: [Time!] - localTimeNullable: [LocalTime] localTime: [LocalTime!] relatedNode: [RelatedNode!]! @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") } type RelatedNodeProperties @relationshipProperties { - dateTimeNullable: [DateTime] dateTime: [DateTime!] - localDateTimeNullable: [LocalDateTime] localDateTime: [LocalDateTime!] - durationNullable: [Duration] duration: [Duration!] - timeNullable: [Time] time: [Time!] - localTimeNullable: [LocalTime] localTime: [LocalTime!] } type RelatedNode @node { - dateTimeNullable: [DateTime] dateTime: [DateTime!] - localDateTimeNullable: [LocalDateTime] localDateTime: [LocalDateTime!] - durationNullable: [Duration] duration: [Duration!] - timeNullable: [Time] time: [Time!] - localTimeNullable: [LocalTime] localTime: [LocalTime!] } `; @@ -83,11 +68,6 @@ describe("Temporal types", () => { duration: { equals: ["P1Y"] } time: { equals: ["22:00:15.555"] } localTime: { equals: ["12:50:35.556"] } - dateTimeNullable: { equals: ["2015-06-24T12:50:35.556+0100"] } - localDateTimeNullable: { equals: ["2003-09-14T12:00:00"] } - durationNullable: { equals: ["P1Y"] } - timeNullable: { equals: ["22:00:15.555"] } - localTimeNullable: { equals: ["12:50:35.556"] } } } ) { @@ -99,11 +79,6 @@ describe("Temporal types", () => { duration time localTime - dateTimeNullable - localDateTimeNullable - durationNullable - timeNullable - localTimeNullable } } } @@ -115,16 +90,16 @@ describe("Temporal types", () => { expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` "MATCH (this0:TypeNode) - WHERE (this0.dateTimeNullable = $param0 AND this0.dateTime = $param1 AND this0.localDateTimeNullable = $param2 AND this0.localDateTime = $param3 AND this0.durationNullable = $param4 AND this0.duration = $param5 AND this0.timeNullable = $param6 AND this0.time = $param7 AND this0.localTimeNullable = $param8 AND this0.localTime = $param9) + 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, dateTimeNullable: [var2 IN this0.dateTimeNullable | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this0.localDateTimeNullable, durationNullable: this0.durationNullable, timeNullable: this0.timeNullable, localTimeNullable: this0.localTimeNullable, __resolveType: \\"TypeNode\\" } }) AS var3 + 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: var3, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -142,29 +117,6 @@ describe("Temporal types", () => { } ], \\"param1\\": [ - { - \\"year\\": 2015, - \\"month\\": 6, - \\"day\\": 24, - \\"hour\\": 11, - \\"minute\\": 50, - \\"second\\": 35, - \\"nanosecond\\": 556000000, - \\"timeZoneOffsetSeconds\\": 0 - } - ], - \\"param2\\": [ - { - \\"year\\": 2003, - \\"month\\": 9, - \\"day\\": 14, - \\"hour\\": 12, - \\"minute\\": 0, - \\"second\\": 0, - \\"nanosecond\\": 0 - } - ], - \\"param3\\": [ { \\"year\\": 2003, \\"month\\": 9, @@ -175,21 +127,7 @@ describe("Temporal types", () => { \\"nanosecond\\": 0 } ], - \\"param4\\": [ - { - \\"months\\": 12, - \\"days\\": 0, - \\"seconds\\": { - \\"low\\": 0, - \\"high\\": 0 - }, - \\"nanoseconds\\": { - \\"low\\": 0, - \\"high\\": 0 - } - } - ], - \\"param5\\": [ + \\"param2\\": [ { \\"months\\": 12, \\"days\\": 0, @@ -203,16 +141,7 @@ describe("Temporal types", () => { } } ], - \\"param6\\": [ - { - \\"hour\\": 22, - \\"minute\\": 0, - \\"second\\": 15, - \\"nanosecond\\": 555000000, - \\"timeZoneOffsetSeconds\\": 0 - } - ], - \\"param7\\": [ + \\"param3\\": [ { \\"hour\\": 22, \\"minute\\": 0, @@ -221,15 +150,7 @@ describe("Temporal types", () => { \\"timeZoneOffsetSeconds\\": 0 } ], - \\"param8\\": [ - { - \\"hour\\": 12, - \\"minute\\": 50, - \\"second\\": 35, - \\"nanosecond\\": 556000000 - } - ], - \\"param9\\": [ + \\"param4\\": [ { \\"hour\\": 12, \\"minute\\": 50, @@ -257,11 +178,6 @@ describe("Temporal types", () => { duration: { equals: ["P1Y"] } time: { equals: ["22:00:15.555"] } localTime: { equals: ["12:50:35.556"] } - dateTimeNullable: { equals: ["2015-06-24T12:50:35.556+0100"] } - localDateTimeNullable: { equals: ["2003-09-14T12:00:00"] } - durationNullable: { equals: ["P1Y"] } - timeNullable: { equals: ["22:00:15.555"] } - localTimeNullable: { equals: ["12:50:35.556"] } } } } @@ -274,11 +190,6 @@ describe("Temporal types", () => { duration time localTime - dateTimeNullable - localDateTimeNullable - durationNullable - timeNullable - localTimeNullable } } } @@ -303,20 +214,20 @@ describe("Temporal types", () => { CALL { WITH this0 MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) - WHERE (this2.dateTimeNullable = $param0 AND this2.dateTime = $param1 AND this2.localDateTimeNullable = $param2 AND this2.localDateTime = $param3 AND this2.durationNullable = $param4 AND this2.duration = $param5 AND this2.timeNullable = $param6 AND this2.time = $param7 AND this2.localTimeNullable = $param8 AND this2.localTime = $param9) + 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, dateTimeNullable: [var4 IN this2.dateTimeNullable | apoc.date.convertFormat(toString(var4), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this2.localDateTimeNullable, durationNullable: this2.durationNullable, timeNullable: this2.timeNullable, localTimeNullable: this2.localTimeNullable, __resolveType: \\"RelatedNode\\" } }) AS var5 + 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: var5, totalCount: totalCount } } AS var6 + RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 } - RETURN collect({ node: { relatedNode: var6, __resolveType: \\"TypeNode\\" } }) AS var7 + RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 } - RETURN { connection: { edges: var7, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -334,29 +245,6 @@ describe("Temporal types", () => { } ], \\"param1\\": [ - { - \\"year\\": 2015, - \\"month\\": 6, - \\"day\\": 24, - \\"hour\\": 11, - \\"minute\\": 50, - \\"second\\": 35, - \\"nanosecond\\": 556000000, - \\"timeZoneOffsetSeconds\\": 0 - } - ], - \\"param2\\": [ - { - \\"year\\": 2003, - \\"month\\": 9, - \\"day\\": 14, - \\"hour\\": 12, - \\"minute\\": 0, - \\"second\\": 0, - \\"nanosecond\\": 0 - } - ], - \\"param3\\": [ { \\"year\\": 2003, \\"month\\": 9, @@ -367,21 +255,7 @@ describe("Temporal types", () => { \\"nanosecond\\": 0 } ], - \\"param4\\": [ - { - \\"months\\": 12, - \\"days\\": 0, - \\"seconds\\": { - \\"low\\": 0, - \\"high\\": 0 - }, - \\"nanoseconds\\": { - \\"low\\": 0, - \\"high\\": 0 - } - } - ], - \\"param5\\": [ + \\"param2\\": [ { \\"months\\": 12, \\"days\\": 0, @@ -395,16 +269,7 @@ describe("Temporal types", () => { } } ], - \\"param6\\": [ - { - \\"hour\\": 22, - \\"minute\\": 0, - \\"second\\": 15, - \\"nanosecond\\": 555000000, - \\"timeZoneOffsetSeconds\\": 0 - } - ], - \\"param7\\": [ + \\"param3\\": [ { \\"hour\\": 22, \\"minute\\": 0, @@ -413,15 +278,7 @@ describe("Temporal types", () => { \\"timeZoneOffsetSeconds\\": 0 } ], - \\"param8\\": [ - { - \\"hour\\": 12, - \\"minute\\": 50, - \\"second\\": 35, - \\"nanosecond\\": 556000000 - } - ], - \\"param9\\": [ + \\"param4\\": [ { \\"hour\\": 12, \\"minute\\": 50, @@ -449,11 +306,6 @@ describe("Temporal types", () => { duration: { equals: ["P1Y"] } time: { equals: ["22:00:15.555"] } localTime: { equals: ["12:50:35.556"] } - dateTimeNullable: { equals: ["2015-06-24T12:50:35.556+0100"] } - localDateTimeNullable: { equals: ["2003-09-14T12:00:00"] } - durationNullable: { equals: ["P1Y"] } - timeNullable: { equals: ["22:00:15.555"] } - localTimeNullable: { equals: ["12:50:35.556"] } } } } @@ -466,11 +318,6 @@ describe("Temporal types", () => { duration time localTime - dateTimeNullable - localDateTimeNullable - durationNullable - timeNullable - localTimeNullable } } } @@ -495,20 +342,20 @@ describe("Temporal types", () => { CALL { WITH this0 MATCH (this0)-[this1:RELATED_TO]->(this2:RelatedNode) - WHERE (this1.dateTimeNullable = $param0 AND this1.dateTime = $param1 AND this1.localDateTimeNullable = $param2 AND this1.localDateTime = $param3 AND this1.durationNullable = $param4 AND this1.duration = $param5 AND this1.timeNullable = $param6 AND this1.time = $param7 AND this1.localTimeNullable = $param8 AND this1.localTime = $param9) + 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, dateTimeNullable: [var4 IN this1.dateTimeNullable | apoc.date.convertFormat(toString(var4), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this1.localDateTimeNullable, durationNullable: this1.durationNullable, timeNullable: this1.timeNullable, localTimeNullable: this1.localTimeNullable }, node: { __id: id(this2), __resolveType: \\"RelatedNode\\" } }) AS var5 + 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: var5, totalCount: totalCount } } AS var6 + RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 } - RETURN collect({ node: { relatedNode: var6, __resolveType: \\"TypeNode\\" } }) AS var7 + RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 } - RETURN { connection: { edges: var7, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -526,29 +373,6 @@ describe("Temporal types", () => { } ], \\"param1\\": [ - { - \\"year\\": 2015, - \\"month\\": 6, - \\"day\\": 24, - \\"hour\\": 11, - \\"minute\\": 50, - \\"second\\": 35, - \\"nanosecond\\": 556000000, - \\"timeZoneOffsetSeconds\\": 0 - } - ], - \\"param2\\": [ - { - \\"year\\": 2003, - \\"month\\": 9, - \\"day\\": 14, - \\"hour\\": 12, - \\"minute\\": 0, - \\"second\\": 0, - \\"nanosecond\\": 0 - } - ], - \\"param3\\": [ { \\"year\\": 2003, \\"month\\": 9, @@ -559,21 +383,7 @@ describe("Temporal types", () => { \\"nanosecond\\": 0 } ], - \\"param4\\": [ - { - \\"months\\": 12, - \\"days\\": 0, - \\"seconds\\": { - \\"low\\": 0, - \\"high\\": 0 - }, - \\"nanoseconds\\": { - \\"low\\": 0, - \\"high\\": 0 - } - } - ], - \\"param5\\": [ + \\"param2\\": [ { \\"months\\": 12, \\"days\\": 0, @@ -587,16 +397,7 @@ describe("Temporal types", () => { } } ], - \\"param6\\": [ - { - \\"hour\\": 22, - \\"minute\\": 0, - \\"second\\": 15, - \\"nanosecond\\": 555000000, - \\"timeZoneOffsetSeconds\\": 0 - } - ], - \\"param7\\": [ + \\"param3\\": [ { \\"hour\\": 22, \\"minute\\": 0, @@ -605,15 +406,7 @@ describe("Temporal types", () => { \\"timeZoneOffsetSeconds\\": 0 } ], - \\"param8\\": [ - { - \\"hour\\": 12, - \\"minute\\": 50, - \\"second\\": 35, - \\"nanosecond\\": 556000000 - } - ], - \\"param9\\": [ + \\"param4\\": [ { \\"hour\\": 12, \\"minute\\": 50, 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 index 7b82c4a974..d33efa3e6c 100644 --- 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 @@ -27,43 +27,28 @@ describe("Temporal types", () => { beforeAll(() => { typeDefs = /* GraphQL */ ` type TypeNode @node { - dateTimeNullable: [DateTime] dateTime: [DateTime!] - localDateTimeNullable: [LocalDateTime] localDateTime: [LocalDateTime!] - durationNullable: [Duration] duration: [Duration!] - timeNullable: [Time] time: [Time!] - localTimeNullable: [LocalTime] localTime: [LocalTime!] relatedNode: [RelatedNode!]! @relationship(type: "RELATED_TO", direction: OUT, properties: "RelatedNodeProperties") } type RelatedNodeProperties @relationshipProperties { - dateTimeNullable: [DateTime] dateTime: [DateTime!] - localDateTimeNullable: [LocalDateTime] localDateTime: [LocalDateTime!] - durationNullable: [Duration] duration: [Duration!] - timeNullable: [Time] time: [Time!] - localTimeNullable: [LocalTime] localTime: [LocalTime!] } type RelatedNode @node { - dateTimeNullable: [DateTime] dateTime: [DateTime!] - localDateTimeNullable: [LocalDateTime] localDateTime: [LocalDateTime!] - durationNullable: [Duration] duration: [Duration!] - timeNullable: [Time] time: [Time!] - localTimeNullable: [LocalTime] localTime: [LocalTime!] } `; @@ -80,15 +65,10 @@ describe("Temporal types", () => { connection { edges { node { - dateTimeNullable dateTime - localDateTimeNullable localDateTime - durationNullable duration - timeNullable time - localTimeNullable localTime } } @@ -107,9 +87,9 @@ describe("Temporal types", () => { WITH edges UNWIND edges AS edge WITH edge.node AS this0 - RETURN collect({ node: { dateTimeNullable: [var1 IN this0.dateTimeNullable | apoc.date.convertFormat(toString(var1), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var2 IN this0.dateTime | apoc.date.convertFormat(toString(var2), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this0.localDateTimeNullable, localDateTime: this0.localDateTime, durationNullable: this0.durationNullable, duration: this0.duration, timeNullable: this0.timeNullable, time: this0.time, localTimeNullable: this0.localTimeNullable, localTime: this0.localTime, __resolveType: \\"TypeNode\\" } }) AS var3 + 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: var3, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var2, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -126,15 +106,10 @@ describe("Temporal types", () => { connection { edges { node { - dateTimeNullable dateTime - localDateTimeNullable localDateTime - durationNullable duration - timeNullable time - localTimeNullable localTime } } @@ -166,13 +141,13 @@ describe("Temporal types", () => { WITH edges UNWIND edges AS edge WITH edge.node AS this2, edge.relationship AS this1 - RETURN collect({ node: { dateTimeNullable: [var3 IN this2.dateTimeNullable | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var4 IN this2.dateTime | apoc.date.convertFormat(toString(var4), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this2.localDateTimeNullable, localDateTime: this2.localDateTime, durationNullable: this2.durationNullable, duration: this2.duration, timeNullable: this2.timeNullable, time: this2.time, localTimeNullable: this2.localTimeNullable, localTime: this2.localTime, __resolveType: \\"RelatedNode\\" } }) AS var5 + 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: var5, totalCount: totalCount } } AS var6 + RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 } - RETURN collect({ node: { relatedNode: var6, __resolveType: \\"TypeNode\\" } }) AS var7 + RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 } - RETURN { connection: { edges: var7, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -189,15 +164,10 @@ describe("Temporal types", () => { connection { edges { properties { - dateTimeNullable dateTime - localDateTimeNullable localDateTime - durationNullable duration - timeNullable time - localTimeNullable localTime } } @@ -229,13 +199,13 @@ describe("Temporal types", () => { WITH edges UNWIND edges AS edge WITH edge.node AS this2, edge.relationship AS this1 - RETURN collect({ properties: { dateTimeNullable: [var3 IN this1.dateTimeNullable | apoc.date.convertFormat(toString(var3), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], dateTime: [var4 IN this1.dateTime | apoc.date.convertFormat(toString(var4), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")], localDateTimeNullable: this1.localDateTimeNullable, localDateTime: this1.localDateTime, durationNullable: this1.durationNullable, duration: this1.duration, timeNullable: this1.timeNullable, time: this1.time, localTimeNullable: this1.localTimeNullable, localTime: this1.localTime }, node: { __id: id(this2), __resolveType: \\"RelatedNode\\" } }) AS var5 + 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: var5, totalCount: totalCount } } AS var6 + RETURN { connection: { edges: var4, totalCount: totalCount } } AS var5 } - RETURN collect({ node: { relatedNode: var6, __resolveType: \\"TypeNode\\" } }) AS var7 + RETURN collect({ node: { relatedNode: var5, __resolveType: \\"TypeNode\\" } }) AS var6 } - RETURN { connection: { edges: var7, totalCount: totalCount } } AS this" + RETURN { connection: { edges: var6, totalCount: totalCount } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); From 54511ab5ee670dc2f08f59aee7387ccf793d2925 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Mon, 12 Aug 2024 11:26:52 +0100 Subject: [PATCH 166/177] remove nullable list filters --- .../schema-types/StaticSchemaTypes.ts | 154 ++---------------- .../filter-schema-types/FilterSchemaTypes.ts | 44 ++--- 2 files changed, 28 insertions(+), 170 deletions(-) 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 index 9b939f682b..91392175e3 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/StaticSchemaTypes.ts @@ -119,17 +119,7 @@ class StaticFilterTypes { this.schemaBuilder = schemaBuilder; } - public getStringListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("StringListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.string.List, - }, - }; - }); - } - + public getStringListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("StringListWhere", () => { return { fields: { @@ -175,17 +165,7 @@ class StaticFilterTypes { }; }); } - public getDateListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("DateListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.date.List, - }, - }; - }); - } - + public getDateListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("DateListWhere", () => { return { fields: { @@ -207,17 +187,7 @@ class StaticFilterTypes { }); } - public getDateTimeListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("DateTimeListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.dateTime.List, - }, - }; - }); - } - + public getDateTimeListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("DateTimeListWhere", () => { return { fields: { @@ -239,17 +209,7 @@ class StaticFilterTypes { }); } - public getLocalDateTimeListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("LocalDateTimeListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.localDateTime.List, - }, - }; - }); - } - + public getLocalDateTimeListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("LocalDateTimeListWhere", () => { return { fields: { @@ -271,17 +231,7 @@ class StaticFilterTypes { }); } - public getDurationListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("DurationListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.duration.List, - }, - }; - }); - } - + public getDurationListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("DurationListWhere", () => { return { fields: { @@ -303,17 +253,7 @@ class StaticFilterTypes { }); } - public getTimeListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("TimeListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.time.List, - }, - }; - }); - } - + public getTimeListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("TimeListWhere", () => { return { fields: { @@ -335,17 +275,7 @@ class StaticFilterTypes { }); } - public getLocalTimeListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("LocalTimeListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.localTime.List, - }, - }; - }); - } - + public getLocalTimeListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("LocalTimeListWhere", () => { return { fields: { @@ -368,17 +298,7 @@ class StaticFilterTypes { }); } - public getIdListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("IDListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.id.List, - }, - }; - }); - } - + public getIdListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("IDListWhere", () => { return { fields: { @@ -400,17 +320,7 @@ class StaticFilterTypes { }); } - public getIntListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("IntListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.int.List, - }, - }; - }); - } - + public getIntListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("IntListWhere", () => { return { fields: { @@ -431,17 +341,7 @@ class StaticFilterTypes { }); } - public getBigIntListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("BigIntListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.bigInt.List, - }, - }; - }); - } - + public getBigIntListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("BigIntListWhere", () => { return { fields: { @@ -463,17 +363,7 @@ class StaticFilterTypes { }); } - public getFloatListWhere(nullable: boolean): InputTypeComposer { - if (nullable) { - return this.schemaBuilder.getOrCreateInputType("FloatListWhereNullable", () => { - return { - fields: { - equals: this.schemaBuilder.types.float.List, - }, - }; - }); - } - + public getFloatListWhere(): InputTypeComposer { return this.schemaBuilder.getOrCreateInputType("FloatListWhere", () => { return { fields: { @@ -495,16 +385,7 @@ class StaticFilterTypes { }); } - // public getCartesianListWhere(nullable: boolean): InputTypeComposer { - // if (nullable) { - // return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhereNullable", () => { - // return { - // fields: { - // equals: toGraphQLList(CartesianPointInput), - // }, - // }; - // }); - // } + // public getCartesianListWhere(): InputTypeComposer { // return this.schemaBuilder.getOrCreateInputType("CartesianListPointWhere", () => { // return { @@ -515,16 +396,7 @@ class StaticFilterTypes { // }); // } - // public getPointListWhere(nullable: boolean): InputTypeComposer { - // if (nullable) { - // return this.schemaBuilder.getOrCreateInputType("PointListPointWhereNullable", () => { - // return { - // fields: { - // equals: toGraphQLList(PointInput), - // }, - // }; - // }); - // } + // public getPointListWhere(): InputTypeComposer { // return this.schemaBuilder.getOrCreateInputType("PointListPointWhere", () => { // return { 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 index 74c29551bb..4cbd24d20e 100644 --- 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 @@ -130,43 +130,35 @@ export abstract class FilterSchemaTypes Date: Mon, 12 Aug 2024 14:53:01 +0100 Subject: [PATCH 167/177] add conflicting properties error on the CreateFactory --- .../queryIRFactory/CreateOperationFactory.ts | 7 + .../utils/raise-on-conflicting-input.ts | 45 +++++ .../alias/conflicting-properties.int.test.ts | 163 ++++++++++++++++++ ...t.ts => cypher-default-values.int.test.ts} | 2 +- .../directives/alias/nodes.int.test.ts | 86 +-------- .../tests/integration/info.int.test.ts | 56 +----- .../unwind-create/unwind-create.int.test.ts | 55 ------ 7 files changed, 218 insertions(+), 196 deletions(-) create mode 100644 packages/graphql/src/api-v6/queryIRFactory/utils/raise-on-conflicting-input.ts create mode 100644 packages/graphql/tests/api-v6/integration/directives/alias/conflicting-properties.int.test.ts rename packages/graphql/tests/integration/{default-values.int.test.ts => cypher-default-values.int.test.ts} (98%) diff --git a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts index f4016a2668..7bd8b24c11 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts @@ -32,6 +32,7 @@ import { V6CreateOperation } from "../queryIR/CreateOperation"; import { ReadOperationFactory } from "./ReadOperationFactory"; import { FactoryParseError } from "./factory-parse-error"; import type { GraphQLTreeCreate, GraphQLTreeCreateInput } from "./resolve-tree-parser/graphql-tree/graphql-tree"; +import { raiseOnConflictingInput } from "./utils/raise-on-conflicting-input"; export class CreateOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; @@ -72,6 +73,7 @@ export class CreateOperationFactory { entity, }); } + const inputFields = this.getInputFields({ target: targetAdapter, createInput: topLevelCreateInput, @@ -93,10 +95,15 @@ export class CreateOperationFactory { 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[] = []; + inputFields.push(...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); 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/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/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/info.int.test.ts b/packages/graphql/tests/integration/info.int.test.ts index e808c238a5..d69e6040da 100644 --- a/packages/graphql/tests/integration/info.int.test.ts +++ b/packages/graphql/tests/integration/info.int.test.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { afterEach } from "node:test"; import { generate } from "randomstring"; import type { UniqueType } from "../utils/graphql-types"; import { TestHelper } from "../utils/tests-helper"; @@ -24,70 +25,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/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"); From 3669777c1e2ea17dfc2d646c260cda288e8e9fd7 Mon Sep 17 00:00:00 2001 From: angrykoala Date: Tue, 13 Aug 2024 13:45:51 +0100 Subject: [PATCH 168/177] Remove list for update input --- .../api-v6/queryIRFactory/UpdateOperationFactory.ts | 10 ++++------ .../argument-parser/parse-update-args.ts | 11 +++++------ .../resolve-tree-parser/graphql-tree/graphql-tree.ts | 2 +- .../schema-types/TopLevelEntitySchemaTypes.ts | 2 +- .../api-v6/translators/translate-update-operation.ts | 8 ++++---- .../integration/mutations/update/update.int.test.ts | 6 +++--- .../tests/api-v6/schema/directives/relayId.test.ts | 2 +- .../graphql/tests/api-v6/schema/relationship.test.ts | 8 ++++---- packages/graphql/tests/api-v6/schema/simple.test.ts | 8 ++++---- .../graphql/tests/api-v6/schema/types/array.test.ts | 4 ++-- .../graphql/tests/api-v6/schema/types/scalars.test.ts | 4 ++-- .../graphql/tests/api-v6/schema/types/spatial.test.ts | 4 ++-- .../tests/api-v6/schema/types/temporals.test.ts | 4 ++-- .../tests/api-v6/tck/mutations/update/update.test.ts | 2 +- 14 files changed, 36 insertions(+), 39 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts index ef1e619d7e..d8fac854e5 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts @@ -103,13 +103,11 @@ export class UpdateOperationFactory { updateInput, }: { target: ConcreteEntityAdapter; - updateInput: GraphQLTreeUpdateInput[]; + updateInput: GraphQLTreeUpdateInput; }): UpdateProperty[] { - return updateInput.flatMap((input) => { - return Object.entries(input).flatMap(([attributeName, setOperations]) => { - const attribute = getAttribute(target, attributeName); - return this.getPropertyInputOperations(attribute, setOperations); - }); + return Object.entries(updateInput).flatMap(([attributeName, setOperations]) => { + const attribute = getAttribute(target, attributeName); + return this.getPropertyInputOperations(attribute, setOperations); }); } private getPropertyInputOperations( 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 index 9631ea93a6..be16f3315d 100644 --- 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 @@ -27,11 +27,10 @@ export function parseUpdateOperationArgsTopLevel(resolveTreeArgs: Record { - return input.node; - }); + + return resolveTreeUpdateInput.node; } 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 index 474320ae30..9432303835 100644 --- 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 @@ -41,7 +41,7 @@ export interface GraphQLTreeUpdate extends GraphQLTreeNode { name: string; args: { where: GraphQLWhereTopLevel; - input: GraphQLTreeUpdateInput[]; + input: GraphQLTreeUpdateInput; }; } 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 index 92cf9ac91d..eef7ea5ced 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -160,7 +160,7 @@ export class TopLevelEntitySchemaTypes { name: this.entity.typeNames.updateField, type: this.updateType, args: { - input: this.updateSchemaTypes.updateInput.NonNull.List.NonNull, + input: this.updateSchemaTypes.updateInput.NonNull, where: this.filterSchemaTypes.operationWhereTopLevel, }, resolver, diff --git a/packages/graphql/src/api-v6/translators/translate-update-operation.ts b/packages/graphql/src/api-v6/translators/translate-update-operation.ts index dab3d744e3..1680c7025d 100644 --- a/packages/graphql/src/api-v6/translators/translate-update-operation.ts +++ b/packages/graphql/src/api-v6/translators/translate-update-operation.ts @@ -36,9 +36,9 @@ export function translateUpdateOperation({ graphQLTreeUpdate: GraphQLTreeUpdate; entity: ConcreteEntity; }): Cypher.CypherResult { - const createFactory = new UpdateOperationFactory(context.schemaModel); - const createAST = createFactory.createAST({ graphQLTreeUpdate, entity }); - debug(createAST.print()); - const results = createAST.build(context); + 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/tests/api-v6/integration/mutations/update/update.int.test.ts b/packages/graphql/tests/api-v6/integration/mutations/update/update.int.test.ts index c6f3a0951f..6cdbcff8e5 100644 --- 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 @@ -56,9 +56,9 @@ describe("Top-Level Update", () => { } } } - input: [ - { node: { title: { set: "Another Movie"} } }, - ]) { + input: + { node: { title: { set: "Another Movie"} } } + ) { info { nodesCreated } diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index bb0675308f..24bac8a657 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -139,7 +139,7 @@ describe("RelayId", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } interface Node { diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index aaff71ac57..cb51a31608 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -300,8 +300,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateActors(input: [ActorUpdateInput!]!, where: ActorOperationWhere): ActorUpdateResponse - updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse + updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { @@ -652,8 +652,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateActors(input: [ActorUpdateInput!]!, where: ActorOperationWhere): ActorUpdateResponse - updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse + updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 3d4f0bb215..f3be390708 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -113,7 +113,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { @@ -314,8 +314,8 @@ describe("Simple Aura-API", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateActors(input: [ActorUpdateInput!]!, where: ActorOperationWhere): ActorUpdateResponse - updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse + updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { @@ -445,7 +445,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - updateMovies(input: [MovieUpdateInput!]!, where: MovieOperationWhere): MovieUpdateResponse + updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } type PageInfo { diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index 0952655dcc..5de5ad0fe1 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -259,8 +259,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - updateNodeTypes(input: [NodeTypeUpdateInput!]!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse - updateRelatedNodes(input: [RelatedNodeUpdateInput!]!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse + updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } type NodeType { diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index d1e782dad2..0ef6f8711b 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -161,8 +161,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - updateNodeTypes(input: [NodeTypeUpdateInput!]!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse - updateRelatedNodes(input: [RelatedNodeUpdateInput!]!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse + updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } type NodeType { diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 7613bb8f47..25a1afd80b 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -85,8 +85,8 @@ describe("Spatial Types", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - updateNodeTypes(input: [NodeTypeUpdateInput!]!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse - updateRelatedNodes(input: [RelatedNodeUpdateInput!]!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse + updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } type NodeType { diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index 1b1e350732..2a80e1688e 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -165,8 +165,8 @@ describe("Temporals", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - updateNodeTypes(input: [NodeTypeUpdateInput!]!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse - updateRelatedNodes(input: [RelatedNodeUpdateInput!]!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse + updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse + updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } type NodeType { 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 index 5ef2137c2f..1386a38cbc 100644 --- a/packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts +++ b/packages/graphql/tests/api-v6/tck/mutations/update/update.test.ts @@ -41,7 +41,7 @@ describe("Top-Level Update", () => { const mutation = /* GraphQL */ ` mutation UpdateMovies { updateMovies( - input: [{ node: { title: { set: "The Matrix" } } }] + input: { node: { title: { set: "The Matrix" } } } where: { node: { title: { equals: "Matrix" } } } ) { movies { From 28f339ad877787fd6f1c9ad5f88673acfcf53de3 Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 13 Aug 2024 16:13:37 +0100 Subject: [PATCH 169/177] initialiaze inputFields with autogeneratedFields --- .../src/api-v6/queryIRFactory/CreateOperationFactory.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts index 7bd8b24c11..fcfe922fd3 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/CreateOperationFactory.ts @@ -99,9 +99,8 @@ export class CreateOperationFactory { // 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[] = []; + const inputFields: InputField[] = this.addAutogeneratedFields(target, inputFieldsExistence); - inputFields.push(...this.addAutogeneratedFields(target, inputFieldsExistence)); for (const inputItem of createInput) { raiseOnConflictingInput(inputItem, target.entity); for (const key of Object.keys(inputItem)) { From dd49184e07142a15a1dcc6b353a82ec06a7f1f7b Mon Sep 17 00:00:00 2001 From: MacondoExpress Date: Tue, 13 Aug 2024 16:16:30 +0100 Subject: [PATCH 170/177] update info.int.test --- packages/graphql/tests/integration/info.int.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/graphql/tests/integration/info.int.test.ts b/packages/graphql/tests/integration/info.int.test.ts index d69e6040da..45b88a1176 100644 --- a/packages/graphql/tests/integration/info.int.test.ts +++ b/packages/graphql/tests/integration/info.int.test.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import { afterEach } from "node:test"; import { generate } from "randomstring"; import type { UniqueType } from "../utils/graphql-types"; import { TestHelper } from "../utils/tests-helper"; From f75f16621a5fc46eb6dbac785f5e6842802a518d Mon Sep 17 00:00:00 2001 From: angrykoala Date: Wed, 14 Aug 2024 13:30:27 +0100 Subject: [PATCH 171/177] Add test on nested filters with update --- .../queryIRFactory/UpdateOperationFactory.ts | 2 +- .../mutations/update/update.int.test.ts | 87 ++++++++++++++++++- 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts index d8fac854e5..85366a7dc7 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/UpdateOperationFactory.ts @@ -114,7 +114,7 @@ export class UpdateOperationFactory { attribute: AttributeAdapter, operations: GraphQLTreeUpdateField ): UpdateProperty[] { - return Object.entries(operations).map(([operation, value]) => { + return Object.entries(operations).map(([_operation, value]) => { // TODO: other operations return new UpdateProperty({ value, 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 index 6cdbcff8e5..a1ed5c6b45 100644 --- 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 @@ -24,23 +24,30 @@ describe("Top-Level Update", () => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; - beforeAll(async () => { + 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 }); }); - afterAll(async () => { + afterEach(async () => { await testHelper.close(); }); - test("should update a movies", async () => { + test("should update a movie", async () => { await testHelper.executeCypher(` CREATE(n:${Movie} {title: "The Matrix"}) CREATE(:${Movie} {title: "The Matrix 2"}) @@ -107,4 +114,78 @@ describe("Top-Level Update", () => { ]) ); }); + + 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", + }, + }, + ]) + ); + }); }); From e5398954546e912a3da63ae371fb4944e029ed77 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Mon, 12 Aug 2024 11:51:00 +0200 Subject: [PATCH 172/177] feat: top level delete --- .../src/api-v6/queryIR/DeleteOperation.ts | 189 ++++++++++++++++++ .../queryIRFactory/DeleteOperationFactory.ts | 78 ++++++++ .../graphql-tree/graphql-tree.ts | 7 + .../parse-resolve-info-tree.ts | 29 +++ .../resolvers/translate-delete-resolver.ts | 10 +- .../translators/translate-delete-operation.ts | 44 ++++ .../integration/delete/delete.int.test.ts | 148 ++++++++++++++ .../tests/api-v6/tck/delete/delete.test.ts | 66 ++++++ 8 files changed, 566 insertions(+), 5 deletions(-) create mode 100644 packages/graphql/src/api-v6/queryIR/DeleteOperation.ts create mode 100644 packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts create mode 100644 packages/graphql/src/api-v6/translators/translate-delete-operation.ts create mode 100644 packages/graphql/tests/api-v6/integration/delete/delete.int.test.ts create mode 100644 packages/graphql/tests/api-v6/tck/delete/delete.test.ts 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..1f95182c34 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIR/DeleteOperation.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 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 { DeleteOperation } from "../../translate/queryAST/ast/operations/DeleteOperation"; +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: MutationOperation[]; + + constructor({ + target, + selection, + nestedOperations = [], + filters = [], + }: { + target: ConcreteEntityAdapter | InterfaceEntityAdapter; + selection: EntitySelection; + filters?: Filter[]; + nestedOperations?: DeleteOperation[]; + }) { + 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/queryIRFactory/DeleteOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts new file mode 100644 index 0000000000..c3d06ce2a8 --- /dev/null +++ b/packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.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 { 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 { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection"; +import { filterTruthy } from "../../utils/utils"; +import { V6DeleteOperation } from "../queryIR/DeleteOperation"; +import { FilterFactory } from "./FilterFactory"; +import type { GraphQLTreeDelete } 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 deleteOP = new V6DeleteOperation({ + target: targetAdapter, + selection, + filters, + }); + + return deleteOP; + } +} 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 index 91af080064..5196267d61 100644 --- 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 @@ -30,6 +30,13 @@ export type GraphQLTreeUpdateInput = Record; export type UpdateOperation = "set"; export type GraphQLTreeUpdateField = Record; +export interface GraphQLTreeDelete extends GraphQLTreeNode { + name: string; + args: { + where?: GraphQLWhereTopLevel; + }; +} + export interface GraphQLTreeCreate extends GraphQLTreeNode { name: string; args: { 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 index 2c35ad324d..da73beb3f1 100644 --- 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 @@ -32,6 +32,7 @@ import type { GraphQLTreeConnection, GraphQLTreeConnectionTopLevel, GraphQLTreeCreate, + GraphQLTreeDelete, GraphQLTreeReadOperation, GraphQLTreeReadOperationTopLevel, GraphQLTreeUpdate, @@ -116,6 +117,34 @@ export function parseResolveInfoTreeUpdate({ }; } +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"); diff --git a/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts b/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts index 3a3707626e..0df06f1250 100644 --- a/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts +++ b/packages/graphql/src/api-v6/resolvers/translate-delete-resolver.ts @@ -22,8 +22,8 @@ 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"; +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( @@ -35,10 +35,10 @@ export function generateDeleteResolver({ entity }: { entity: ConcreteEntity }) { const resolveTree = getNeo4jResolveTree(info, { args }); context.resolveTree = resolveTree; // TODO: Implement delete resolver - const graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity }); - const { cypher, params } = translateCreateOperation({ + const graphQLTreeDelete = parseResolveInfoTreeDelete({ resolveTree: context.resolveTree, entity }); + const { cypher, params } = translateDeleteResolver({ context: context, - graphQLTreeCreate, + graphQLTreeDelete, entity, }); const executeResult = await execute({ 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/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..a094d4a678 --- /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; + 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 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/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\\" + }" + `); + }); +}); From f2fd26a321d887ceb70047199bbbe1a563570ab0 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Mon, 12 Aug 2024 12:02:05 +0200 Subject: [PATCH 173/177] fix: use beforeEach/afterEach --- .../tests/api-v6/integration/delete/delete.int.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index a094d4a678..9eb3ed478d 100644 --- a/packages/graphql/tests/api-v6/integration/delete/delete.int.test.ts +++ b/packages/graphql/tests/api-v6/integration/delete/delete.int.test.ts @@ -24,7 +24,7 @@ describe("Top-Level Delete", () => { const testHelper = new TestHelper({ v6Api: true }); let Movie: UniqueType; - beforeAll(async () => { + beforeEach(async () => { Movie = testHelper.createUniqueType("Movie"); const typeDefs = /* GraphQL */ ` @@ -36,7 +36,7 @@ describe("Top-Level Delete", () => { await testHelper.initNeo4jGraphQL({ typeDefs }); }); - afterAll(async () => { + afterEach(async () => { await testHelper.close(); }); From 88b20c16bfaa784a9e4a46c36607bbb7b458f816 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Thu, 15 Aug 2024 12:18:31 +0200 Subject: [PATCH 174/177] refactor: remove nested operations --- .../src/api-v6/queryIR/DeleteOperation.ts | 55 +------------------ 1 file changed, 1 insertion(+), 54 deletions(-) diff --git a/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts b/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts index 1f95182c34..a0f173abaa 100644 --- a/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts @@ -21,7 +21,6 @@ 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 { DeleteOperation } from "../../translate/queryAST/ast/operations/DeleteOperation"; 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"; @@ -33,28 +32,24 @@ export class V6DeleteOperation extends MutationOperation { public readonly target: ConcreteEntityAdapter | InterfaceEntityAdapter; private selection: EntitySelection; private filters: Filter[]; - private nestedOperations: MutationOperation[]; constructor({ target, selection, - nestedOperations = [], filters = [], }: { target: ConcreteEntityAdapter | InterfaceEntityAdapter; selection: EntitySelection; filters?: Filter[]; - nestedOperations?: DeleteOperation[]; }) { super(); this.target = target; this.selection = selection; this.filters = filters; - this.nestedOperations = nestedOperations; } public getChildren(): QueryASTNode[] { - return [this.selection, ...this.filters, ...this.nestedOperations]; + return [this.selection, ...this.filters]; } public transpile(context: QueryASTContext): OperationTranspileResult { @@ -62,9 +57,6 @@ export class V6DeleteOperation extends MutationOperation { 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); } @@ -77,50 +69,14 @@ export class V6DeleteOperation extends MutationOperation { 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 ( @@ -164,15 +120,6 @@ export class V6DeleteOperation extends MutationOperation { 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"); From f2696a5863d87a8aa8b41dde7d28a0f93ee8c934 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Fri, 16 Aug 2024 16:03:01 +0200 Subject: [PATCH 175/177] feat: add nested delete operations --- .../src/api-v6/queryIR/DeleteOperation.ts | 54 +++++++- .../queryIRFactory/DeleteOperationFactory.ts | 117 +++++++++++++++++- .../graphql-tree/graphql-tree.ts | 2 + .../api-v6/schema-generation/SchemaBuilder.ts | 2 +- .../schema-types/TopLevelEntitySchemaTypes.ts | 4 + .../RelatedEntityDeleteSchemaTypes.ts | 78 ++++++++++++ .../TopLevelDeleteSchemaTypes.ts | 85 +++++++++++++ .../RelatedEntityTypeNames.ts | 8 ++ .../TopLevelEntityTypeNames.ts | 8 ++ 9 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/RelatedEntityDeleteSchemaTypes.ts create mode 100644 packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/TopLevelDeleteSchemaTypes.ts diff --git a/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts b/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts index a0f173abaa..5530d0eda5 100644 --- a/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts +++ b/packages/graphql/src/api-v6/queryIR/DeleteOperation.ts @@ -32,24 +32,28 @@ 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]; + return [this.selection, ...this.filters, ...this.nestedOperations]; } public transpile(context: QueryASTContext): OperationTranspileResult { @@ -57,6 +61,9 @@ export class V6DeleteOperation extends MutationOperation { 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); } @@ -69,14 +76,50 @@ export class V6DeleteOperation extends MutationOperation { 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 ( @@ -120,6 +163,15 @@ export class V6DeleteOperation extends MutationOperation { 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"); diff --git a/packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts b/packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts index c3d06ce2a8..3ffa96e930 100644 --- a/packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts +++ b/packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts @@ -20,12 +20,16 @@ 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 { filterTruthy } from "../../utils/utils"; +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 } from "./resolve-tree-parser/graphql-tree/graphql-tree"; +import type { GraphQLTreeDelete, GraphQLTreeDeleteInput } from "./resolve-tree-parser/graphql-tree/graphql-tree"; export class DeleteOperationFactory { public schemaModel: Neo4jGraphQLSchemaModel; @@ -64,15 +68,122 @@ export class DeleteOperationFactory { }); const filters = filterTruthy([ - this.filterFactory.createFilters({ entity, where: graphQLTreeDelete.args.where }), + 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/resolve-tree-parser/graphql-tree/graphql-tree.ts b/packages/graphql/src/api-v6/queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree.ts index 5196267d61..bcfa8366b0 100644 --- 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 @@ -26,6 +26,7 @@ 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; @@ -34,6 +35,7 @@ export interface GraphQLTreeDelete extends GraphQLTreeNode { name: string; args: { where?: GraphQLWhereTopLevel; + input?: GraphQLTreeDeleteInput; }; } diff --git a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts index 51098e4f7e..257b118f7d 100644 --- a/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts +++ b/packages/graphql/src/api-v6/schema-generation/SchemaBuilder.ts @@ -59,7 +59,7 @@ export type InputFieldDefinition = { args?: Record; deprecationReason?: string | null; description?: string | null; - defaultValue: any; + defaultValue?: any; }; export class SchemaBuilder { 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 index e4d9bebcb5..76a471e569 100644 --- a/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/TopLevelEntitySchemaTypes.ts @@ -48,6 +48,7 @@ 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 { @@ -58,6 +59,7 @@ export class TopLevelEntitySchemaTypes { private schemaTypes: SchemaTypes; private createSchemaTypes: TopLevelCreateSchemaTypes; private updateSchemaTypes: TopLevelUpdateSchemaTypes; + private deleteSchemaTypes: TopLevelDeleteSchemaTypes; constructor({ entity, @@ -75,6 +77,7 @@ export class TopLevelEntitySchemaTypes { 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( @@ -179,6 +182,7 @@ export class TopLevelEntitySchemaTypes { name: this.entity.typeNames.deleteField, type: this.schemaTypes.staticTypes.deleteResponse, args: { + input: this.deleteSchemaTypes.deleteInput, where: this.filterSchemaTypes.operationWhereTopLevel, }, resolver, 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..6750ed92e4 --- /dev/null +++ b/packages/graphql/src/api-v6/schema-generation/schema-types/mutation-schema-types/RelatedEntityDeleteSchemaTypes.ts @@ -0,0 +1,78 @@ +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/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-model/graphql-type-names/RelatedEntityTypeNames.ts b/packages/graphql/src/api-v6/schema-model/graphql-type-names/RelatedEntityTypeNames.ts index 94fdb2896f..bb9d46bff8 100644 --- 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 @@ -83,4 +83,12 @@ export class RelatedEntityTypeNames extends EntityTypeNames { } 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 index ec7ed12423..6847970f9f 100644 --- 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 @@ -114,4 +114,12 @@ export class TopLevelEntityTypeNames extends EntityTypeNames { 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`; + } } From 145f1323c16224c52aaf3d0c807cda68f600da39 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Fri, 16 Aug 2024 16:03:22 +0200 Subject: [PATCH 176/177] test: update schema test snapshots --- .../schema/directives/default-array.test.ts | 10 ++- .../api-v6/schema/directives/default.test.ts | 10 ++- .../tests/api-v6/schema/directives/id.test.ts | 38 +++++++++- .../api-v6/schema/directives/relayId.test.ts | 10 ++- .../tests/api-v6/schema/relationship.test.ts | 76 ++++++++++++++++++- .../tests/api-v6/schema/simple.test.ts | 40 +++++++++- .../tests/api-v6/schema/types/array.test.ts | 29 ++++++- .../tests/api-v6/schema/types/scalars.test.ts | 29 ++++++- .../tests/api-v6/schema/types/spatial.test.ts | 29 ++++++- .../api-v6/schema/types/temporals.test.ts | 29 ++++++- 10 files changed, 279 insertions(+), 21 deletions(-) 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 index 3782b43d2c..f5d723bf6f 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default-array.test.ts @@ -139,6 +139,14 @@ describe("@default on array fields", () => { movies: [Movie!]! } + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + type MovieEdge { cursor: String node: Movie @@ -187,7 +195,7 @@ describe("@default on array fields", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } diff --git a/packages/graphql/tests/api-v6/schema/directives/default.test.ts b/packages/graphql/tests/api-v6/schema/directives/default.test.ts index 035926af02..9f1444d36e 100644 --- a/packages/graphql/tests/api-v6/schema/directives/default.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/default.test.ts @@ -174,6 +174,14 @@ describe("@default on fields", () => { movies: [Movie!]! } + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + type MovieEdge { cursor: String node: Movie @@ -231,7 +239,7 @@ describe("@default on fields", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } diff --git a/packages/graphql/tests/api-v6/schema/directives/id.test.ts b/packages/graphql/tests/api-v6/schema/directives/id.test.ts index 716239b69e..59bd9ad6d8 100644 --- a/packages/graphql/tests/api-v6/schema/directives/id.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/id.test.ts @@ -105,6 +105,14 @@ describe("@id", () => { info: CreateInfo } + input ActorDeleteInput { + node: ActorDeleteNode + } + + input ActorDeleteNode { + movies: ActorMoviesDeleteOperation + } + type ActorEdge { cursor: String node: Actor @@ -119,6 +127,15 @@ describe("@id", () => { edges: ActorMoviesEdgeSort } + input ActorMoviesDeleteInput { + input: MovieDeleteInput + where: ActorMoviesOperationWhere + } + + input ActorMoviesDeleteOperation { + delete: ActorMoviesDeleteInput + } + type ActorMoviesEdge { cursor: String node: Movie @@ -261,6 +278,15 @@ describe("@id", () => { edges: MovieActorsEdgeSort } + input MovieActorsDeleteInput { + input: ActorDeleteInput + where: MovieActorsOperationWhere + } + + input MovieActorsDeleteOperation { + delete: MovieActorsDeleteInput + } + type MovieActorsEdge { cursor: String node: Actor @@ -337,6 +363,14 @@ describe("@id", () => { movies: [Movie!]! } + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + actors: MovieActorsDeleteOperation + } + type MovieEdge { cursor: String node: Movie @@ -384,8 +418,8 @@ describe("@id", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteActors(where: ActorOperationWhere): DeleteResponse - deleteMovies(where: MovieOperationWhere): DeleteResponse + deleteActors(input: ActorDeleteInput, where: ActorOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } diff --git a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts index cffcff4029..11f823a1e8 100644 --- a/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts +++ b/packages/graphql/tests/api-v6/schema/directives/relayId.test.ts @@ -109,6 +109,14 @@ describe("RelayId", () => { movies: [Movie!]! } + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + type MovieEdge { cursor: String node: Movie @@ -155,7 +163,7 @@ describe("RelayId", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } diff --git a/packages/graphql/tests/api-v6/schema/relationship.test.ts b/packages/graphql/tests/api-v6/schema/relationship.test.ts index 4c9f189ad4..51c3aa713e 100644 --- a/packages/graphql/tests/api-v6/schema/relationship.test.ts +++ b/packages/graphql/tests/api-v6/schema/relationship.test.ts @@ -79,6 +79,14 @@ describe("Relationships", () => { info: CreateInfo } + input ActorDeleteInput { + node: ActorDeleteNode + } + + input ActorDeleteNode { + movies: ActorMoviesDeleteOperation + } + type ActorEdge { cursor: String node: Actor @@ -93,6 +101,15 @@ describe("Relationships", () => { edges: ActorMoviesEdgeSort } + input ActorMoviesDeleteInput { + input: MovieDeleteInput + where: ActorMoviesOperationWhere + } + + input ActorMoviesDeleteOperation { + delete: ActorMoviesDeleteInput + } + type ActorMoviesEdge { cursor: String node: Movie @@ -201,6 +218,15 @@ describe("Relationships", () => { edges: MovieActorsEdgeSort } + input MovieActorsDeleteInput { + input: ActorDeleteInput + where: MovieActorsOperationWhere + } + + input MovieActorsDeleteOperation { + delete: MovieActorsDeleteInput + } + type MovieActorsEdge { cursor: String node: Actor @@ -274,6 +300,14 @@ describe("Relationships", () => { movies: [Movie!]! } + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + actors: MovieActorsDeleteOperation + } + type MovieEdge { cursor: String node: Movie @@ -318,8 +352,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteActors(where: ActorOperationWhere): DeleteResponse - deleteMovies(where: MovieOperationWhere): DeleteResponse + deleteActors(input: ActorDeleteInput, where: ActorOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } @@ -433,6 +467,14 @@ describe("Relationships", () => { info: CreateInfo } + input ActorDeleteInput { + node: ActorDeleteNode + } + + input ActorDeleteNode { + movies: ActorMoviesDeleteOperation + } + type ActorEdge { cursor: String node: Actor @@ -447,6 +489,15 @@ describe("Relationships", () => { edges: ActorMoviesEdgeSort } + input ActorMoviesDeleteInput { + input: MovieDeleteInput + where: ActorMoviesOperationWhere + } + + input ActorMoviesDeleteOperation { + delete: ActorMoviesDeleteInput + } + type ActorMoviesEdge { cursor: String node: Movie @@ -570,6 +621,15 @@ describe("Relationships", () => { edges: MovieActorsEdgeSort } + input MovieActorsDeleteInput { + input: ActorDeleteInput + where: MovieActorsOperationWhere + } + + input MovieActorsDeleteOperation { + delete: MovieActorsDeleteInput + } + type MovieActorsEdge { cursor: String node: Actor @@ -646,6 +706,14 @@ describe("Relationships", () => { movies: [Movie!]! } + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + actors: MovieActorsDeleteOperation + } + type MovieEdge { cursor: String node: Movie @@ -690,8 +758,8 @@ describe("Relationships", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteActors(where: ActorOperationWhere): DeleteResponse - deleteMovies(where: MovieOperationWhere): DeleteResponse + deleteActors(input: ActorDeleteInput, where: ActorOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } diff --git a/packages/graphql/tests/api-v6/schema/simple.test.ts b/packages/graphql/tests/api-v6/schema/simple.test.ts index 56b31a0359..106b64983b 100644 --- a/packages/graphql/tests/api-v6/schema/simple.test.ts +++ b/packages/graphql/tests/api-v6/schema/simple.test.ts @@ -87,6 +87,14 @@ describe("Simple Aura-API", () => { movies: [Movie!]! } + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + type MovieEdge { cursor: String node: Movie @@ -129,7 +137,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } @@ -219,6 +227,14 @@ describe("Simple Aura-API", () => { info: CreateInfo } + input ActorDeleteInput { + node: ActorDeleteNode + } + + input ActorDeleteNode { + _emptyInput: Boolean + } + type ActorEdge { cursor: String node: Actor @@ -306,6 +322,14 @@ describe("Simple Aura-API", () => { movies: [Movie!]! } + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + type MovieEdge { cursor: String node: Movie @@ -349,8 +373,8 @@ describe("Simple Aura-API", () => { type Mutation { createActors(input: [ActorCreateInput!]!): ActorCreateResponse createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteActors(where: ActorOperationWhere): DeleteResponse - deleteMovies(where: MovieOperationWhere): DeleteResponse + deleteActors(input: ActorDeleteInput, where: ActorOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse updateActors(input: ActorUpdateInput!, where: ActorOperationWhere): ActorUpdateResponse updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } @@ -456,6 +480,14 @@ describe("Simple Aura-API", () => { movies: [Movie!]! } + input MovieDeleteInput { + node: MovieDeleteNode + } + + input MovieDeleteNode { + _emptyInput: Boolean + } + type MovieEdge { cursor: String node: Movie @@ -498,7 +530,7 @@ describe("Simple Aura-API", () => { type Mutation { createMovies(input: [MovieCreateInput!]!): MovieCreateResponse - deleteMovies(where: MovieOperationWhere): DeleteResponse + deleteMovies(input: MovieDeleteInput, where: MovieOperationWhere): DeleteResponse updateMovies(input: MovieUpdateInput!, where: MovieOperationWhere): MovieUpdateResponse } diff --git a/packages/graphql/tests/api-v6/schema/types/array.test.ts b/packages/graphql/tests/api-v6/schema/types/array.test.ts index c923b7d009..6ba7f68afc 100644 --- a/packages/graphql/tests/api-v6/schema/types/array.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/array.test.ts @@ -201,8 +201,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - deleteNodeTypes(where: NodeTypeOperationWhere): DeleteResponse - deleteRelatedNodes(where: RelatedNodeOperationWhere): DeleteResponse + deleteNodeTypes(input: NodeTypeDeleteInput, where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(input: RelatedNodeDeleteInput, where: RelatedNodeOperationWhere): DeleteResponse updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } @@ -259,6 +259,14 @@ describe("Scalars", () => { nodeTypes: [NodeType!]! } + input NodeTypeDeleteInput { + node: NodeTypeDeleteNode + } + + input NodeTypeDeleteNode { + relatedNode: NodeTypeRelatedNodeDeleteOperation + } + type NodeTypeEdge { cursor: String node: NodeType @@ -280,6 +288,15 @@ describe("Scalars", () => { pageInfo: PageInfo } + input NodeTypeRelatedNodeDeleteInput { + input: RelatedNodeDeleteInput + where: NodeTypeRelatedNodeOperationWhere + } + + input NodeTypeRelatedNodeDeleteOperation { + delete: NodeTypeRelatedNodeDeleteInput + } + type NodeTypeRelatedNodeEdge { cursor: String node: RelatedNode @@ -428,6 +445,14 @@ describe("Scalars", () => { relatedNodes: [RelatedNode!]! } + input RelatedNodeDeleteInput { + node: RelatedNodeDeleteNode + } + + input RelatedNodeDeleteNode { + _emptyInput: Boolean + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts index 6fbbebe2b6..34d50f0048 100644 --- a/packages/graphql/tests/api-v6/schema/types/scalars.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/scalars.test.ts @@ -175,8 +175,8 @@ describe("Scalars", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - deleteNodeTypes(where: NodeTypeOperationWhere): DeleteResponse - deleteRelatedNodes(where: RelatedNodeOperationWhere): DeleteResponse + deleteNodeTypes(input: NodeTypeDeleteInput, where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(input: RelatedNodeDeleteInput, where: RelatedNodeOperationWhere): DeleteResponse updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } @@ -237,6 +237,14 @@ describe("Scalars", () => { nodeTypes: [NodeType!]! } + input NodeTypeDeleteInput { + node: NodeTypeDeleteNode + } + + input NodeTypeDeleteNode { + relatedNode: NodeTypeRelatedNodeDeleteOperation + } + type NodeTypeEdge { cursor: String node: NodeType @@ -262,6 +270,15 @@ describe("Scalars", () => { edges: NodeTypeRelatedNodeEdgeSort } + input NodeTypeRelatedNodeDeleteInput { + input: RelatedNodeDeleteInput + where: NodeTypeRelatedNodeOperationWhere + } + + input NodeTypeRelatedNodeDeleteOperation { + delete: NodeTypeRelatedNodeDeleteInput + } + type NodeTypeRelatedNodeEdge { cursor: String node: RelatedNode @@ -434,6 +451,14 @@ describe("Scalars", () => { relatedNodes: [RelatedNode!]! } + input RelatedNodeDeleteInput { + node: RelatedNodeDeleteNode + } + + input RelatedNodeDeleteNode { + _emptyInput: Boolean + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts index 776ec5eef2..0c71dbf481 100644 --- a/packages/graphql/tests/api-v6/schema/types/spatial.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/spatial.test.ts @@ -99,8 +99,8 @@ describe("Spatial Types", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - deleteNodeTypes(where: NodeTypeOperationWhere): DeleteResponse - deleteRelatedNodes(where: RelatedNodeOperationWhere): DeleteResponse + deleteNodeTypes(input: NodeTypeDeleteInput, where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(input: RelatedNodeDeleteInput, where: RelatedNodeOperationWhere): DeleteResponse updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } @@ -141,6 +141,14 @@ describe("Spatial Types", () => { nodeTypes: [NodeType!]! } + input NodeTypeDeleteInput { + node: NodeTypeDeleteNode + } + + input NodeTypeDeleteNode { + relatedNode: NodeTypeRelatedNodeDeleteOperation + } + type NodeTypeEdge { cursor: String node: NodeType @@ -162,6 +170,15 @@ describe("Spatial Types", () => { pageInfo: PageInfo } + input NodeTypeRelatedNodeDeleteInput { + input: RelatedNodeDeleteInput + where: NodeTypeRelatedNodeOperationWhere + } + + input NodeTypeRelatedNodeDeleteOperation { + delete: NodeTypeRelatedNodeDeleteInput + } + type NodeTypeRelatedNodeEdge { cursor: String node: RelatedNode @@ -296,6 +313,14 @@ describe("Spatial Types", () => { relatedNodes: [RelatedNode!]! } + input RelatedNodeDeleteInput { + node: RelatedNodeDeleteNode + } + + input RelatedNodeDeleteNode { + _emptyInput: Boolean + } + type RelatedNodeEdge { cursor: String node: RelatedNode diff --git a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts index eeb5e73eb3..d1c9612346 100644 --- a/packages/graphql/tests/api-v6/schema/types/temporals.test.ts +++ b/packages/graphql/tests/api-v6/schema/types/temporals.test.ts @@ -179,8 +179,8 @@ describe("Temporals", () => { type Mutation { createNodeTypes(input: [NodeTypeCreateInput!]!): NodeTypeCreateResponse createRelatedNodes(input: [RelatedNodeCreateInput!]!): RelatedNodeCreateResponse - deleteNodeTypes(where: NodeTypeOperationWhere): DeleteResponse - deleteRelatedNodes(where: RelatedNodeOperationWhere): DeleteResponse + deleteNodeTypes(input: NodeTypeDeleteInput, where: NodeTypeOperationWhere): DeleteResponse + deleteRelatedNodes(input: RelatedNodeDeleteInput, where: RelatedNodeOperationWhere): DeleteResponse updateNodeTypes(input: NodeTypeUpdateInput!, where: NodeTypeOperationWhere): NodeTypeUpdateResponse updateRelatedNodes(input: RelatedNodeUpdateInput!, where: RelatedNodeOperationWhere): RelatedNodeUpdateResponse } @@ -229,6 +229,14 @@ describe("Temporals", () => { nodeTypes: [NodeType!]! } + input NodeTypeDeleteInput { + node: NodeTypeDeleteNode + } + + input NodeTypeDeleteNode { + relatedNode: NodeTypeRelatedNodeDeleteOperation + } + type NodeTypeEdge { cursor: String node: NodeType @@ -254,6 +262,15 @@ describe("Temporals", () => { edges: NodeTypeRelatedNodeEdgeSort } + input NodeTypeRelatedNodeDeleteInput { + input: RelatedNodeDeleteInput + where: NodeTypeRelatedNodeOperationWhere + } + + input NodeTypeRelatedNodeDeleteOperation { + delete: NodeTypeRelatedNodeDeleteInput + } + type NodeTypeRelatedNodeEdge { cursor: String node: RelatedNode @@ -396,6 +413,14 @@ describe("Temporals", () => { relatedNodes: [RelatedNode!]! } + input RelatedNodeDeleteInput { + node: RelatedNodeDeleteNode + } + + input RelatedNodeDeleteNode { + _emptyInput: Boolean + } + type RelatedNodeEdge { cursor: String node: RelatedNode From b323a458a395fd24ee004e7eabe9d761d196af31 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Fri, 16 Aug 2024 16:31:18 +0200 Subject: [PATCH 177/177] docs: add header --- .../RelatedEntityDeleteSchemaTypes.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 index 6750ed92e4..25ef1f9837 100644 --- 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 @@ -1,3 +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 { InputTypeComposer } from "graphql-compose"; import { ConcreteEntity } from "../../../../schema-model/entity/ConcreteEntity"; import type { Relationship } from "../../../../schema-model/relationship/Relationship";