From 947650ab76965c805e745702a8057824b5232e4a Mon Sep 17 00:00:00 2001 From: a-alle Date: Wed, 19 Nov 2025 15:49:31 +0000 Subject: [PATCH 1/3] initial refactor operations --- .../ast/operations/ConnectOperation.ts | 37 +++------ .../ast/operations/DisconnectOperation.ts | 76 +++++-------------- .../ast/operations/UpdateOperation.ts | 48 ++++-------- 3 files changed, 48 insertions(+), 113 deletions(-) diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts index b32411d72a..153b210f1d 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ConnectOperation.ts @@ -153,16 +153,15 @@ export class ConnectOperation extends MutationOperation { const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, allFilters, [nestedContext.target]); + const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); let matchClause: Cypher.Clause; if (filterSubqueries.length > 0) { - const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); matchClause = Cypher.utils.concat( new Cypher.Match(matchPattern), ...filterSubqueries, new Cypher.With("*").where(predicate) ); } else { - const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); matchClause = new Cypher.Match(matchPattern).where(predicate); } @@ -187,22 +186,13 @@ export class ConnectOperation extends MutationOperation { return input.getSubqueries(connectContext); }); - const authClausesBefore = this.getAuthorizationClauses(nestedContext); - - const authClausesAfter = this.getAuthorizationClausesAfter(nestedContext); - const sourceAuthClausesAfter = this.getSourceAuthorizationClausesAfter(context); - - const authClauses: Cypher.Clause[] = []; - if (authClausesAfter.length > 0 || sourceAuthClausesAfter.length > 0) { - authClauses.push(Cypher.utils.concat(...authClausesAfter, ...sourceAuthClausesAfter)); - } - const clauses = Cypher.utils.concat( matchClause, - ...authClausesBefore, + ...this.getAuthorizationClauses(nestedContext), // THESE ARE "BEFORE" AUTH ...mutationSubqueries, connectClause, - ...authClauses + ...this.getAuthorizationClausesAfter(nestedContext), // THESE ARE "AFTER" AUTH + ...this.getSourceAuthorizationClausesAfter(context) // ONLY RUN "AFTER" AUTH ON THE SOURCE NODE ); const callClause = new Cypher.Call(clauses, [context.target]); @@ -214,19 +204,16 @@ export class ConnectOperation extends MutationOperation { } private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { - const { selections, subqueries, predicates, validations } = this.transpileAuthClauses(context); - const predicate = Cypher.and(...predicates); - const lastSelection = selections[selections.length - 1]; - - if (!predicates.length && !validations.length) { - return []; - } else { - if (lastSelection) { - lastSelection.where(predicate); - return [...subqueries, new Cypher.With("*"), ...selections, ...validations]; + const { subqueries, predicates, validations } = this.transpileAuthClauses(context); + if (!predicates.length) { + if (!validations.length) { + return []; } - return [...subqueries, new Cypher.With("*").where(predicate), ...selections, ...validations]; + return [...subqueries, ...validations]; } + + const predicate = Cypher.and(...predicates); + return [...subqueries, new Cypher.With("*").where(predicate), ...validations]; } private getAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { diff --git a/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts index 2dfa627393..3526c1f745 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/DisconnectOperation.ts @@ -148,16 +148,15 @@ export class DisconnectOperation extends MutationOperation { const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, allFilters, [nestedContext.target]); + const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); let matchClause: Cypher.Clause; if (filterSubqueries.length > 0) { - const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); matchClause = Cypher.utils.concat( new Cypher.OptionalMatch(matchPattern), ...filterSubqueries, new Cypher.With("*").where(predicate) ); } else { - const predicate = Cypher.and(...allFilters.map((f) => f.getPredicate(nestedContext))); matchClause = new Cypher.OptionalMatch(matchPattern).where(predicate); } @@ -173,30 +172,14 @@ export class DisconnectOperation extends MutationOperation { const deleteClause = new Cypher.With(nestedContext.relationship!).delete(nestedContext.relationship!); - const authClausesBefore = this.getAuthorizationClauses(nestedContext); - const sourceAuthClausesBefore = this.getSourceAuthorizationClausesBefore(context); - - const bothAuthClausesBefore: Cypher.Clause[] = []; - if (authClausesBefore.length === 0 && sourceAuthClausesBefore.length > 0) { - bothAuthClausesBefore.push(new Cypher.With("*"), ...sourceAuthClausesBefore); - } else { - bothAuthClausesBefore.push(Cypher.utils.concat(...authClausesBefore, ...sourceAuthClausesBefore)); - } - - const authClausesAfter = this.getAuthorizationClausesAfter(nestedContext); - const sourceAuthClausesAfter = this.getSourceAuthorizationClausesAfter(context); - - const authClauses: Cypher.Clause[] = []; - if (authClausesAfter.length > 0 || sourceAuthClausesAfter.length > 0) { - authClauses.push(Cypher.utils.concat(...authClausesAfter, ...sourceAuthClausesAfter)); - } - const clauses = Cypher.utils.concat( matchClause, - ...bothAuthClausesBefore, + ...this.getAuthorizationClauses(nestedContext), + ...this.getSourceAuthorizationClauses(context, "BEFORE"), ...mutationSubqueries, deleteClause, - ...authClauses + ...this.getAuthorizationClausesAfter(nestedContext), + ...this.getSourceAuthorizationClauses(context, "AFTER") ); const callClause = new Cypher.Call(clauses, [context.target]); @@ -205,24 +188,19 @@ export class DisconnectOperation extends MutationOperation { projectionExpr: context.returnVariable, clauses: [callClause], }; - - // return { projectionExpr: context.returnVariable, clauses: [clauses] }; } private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { - const { selections, subqueries, predicates, validations } = this.transpileAuthClauses(context); - const predicate = Cypher.and(...predicates); - const lastSelection = selections[selections.length - 1]; - - if (!predicates.length && !validations.length) { - return []; - } else { - if (lastSelection) { - lastSelection.where(predicate); - return [...subqueries, new Cypher.With("*"), ...selections, ...validations]; + const { subqueries, predicates, validations } = this.transpileAuthClauses(context); + if (!predicates.length) { + if (!validations.length) { + return []; } - return [...subqueries, new Cypher.With("*").where(predicate), ...selections, ...validations]; + return [...subqueries, ...validations]; } + + const predicate = Cypher.and(...predicates); + return [...subqueries, new Cypher.With("*").where(predicate), ...validations]; } private getAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { @@ -240,31 +218,17 @@ export class DisconnectOperation extends MutationOperation { return []; } - private getSourceAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { - const validationsAfter: Cypher.VoidProcedure[] = []; - for (const authFilter of this.sourceAuthFilters) { - const validationAfter = authFilter.getValidation(context, "AFTER"); - if (validationAfter) { - validationsAfter.push(validationAfter); - } - } - - if (validationsAfter.length > 0) { - return [new Cypher.With("*"), ...validationsAfter]; - } - return []; - } - private getSourceAuthorizationClausesBefore(context: QueryASTContext): Cypher.Clause[] { - const validationsAfter: Cypher.VoidProcedure[] = []; + private getSourceAuthorizationClauses(context: QueryASTContext, when: "BEFORE" | "AFTER"): Cypher.Clause[] { + const validations: Cypher.VoidProcedure[] = []; for (const authFilter of this.sourceAuthFilters) { - const validationAfter = authFilter.getValidation(context, "BEFORE"); - if (validationAfter) { - validationsAfter.push(validationAfter); + const validation = authFilter.getValidation(context, when); + if (validation) { + validations.push(validation); } } - if (validationsAfter.length > 0) { - return [new Cypher.With("*"), ...validationsAfter]; + if (validations.length > 0) { + return [new Cypher.With("*"), ...validations]; } return []; } diff --git a/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts index 5b1488047c..7734eba029 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/UpdateOperation.ts @@ -33,10 +33,7 @@ import type { RelationshipAdapter } from "../../../../schema-model/relationship/ import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls"; import type { Filter } from "../filters/Filter"; import { ParamInputField } from "../input-fields/ParamInputField"; -/** - * This is currently just a dummy tree node, - * The whole mutation part is still implemented in the old way, the current scope of this node is just to contains the nested fields. - **/ + export class UpdateOperation extends Operation { public readonly target: ConcreteEntityAdapter; public readonly relationship: RelationshipAdapter | undefined; @@ -128,14 +125,14 @@ export class UpdateOperation extends Operation { if (!authSubqueries.length && !subqueries.length) { return undefined; } - if (authSubqueries.length > 0) { + if (authSubqueries.length && subqueries.length) { return Cypher.utils.concat(...subqueries, new Cypher.With("*"), ...authSubqueries); } - return Cypher.utils.concat(...subqueries); + return Cypher.utils.concat(...subqueries, ...authSubqueries); }) .filter((s) => s !== undefined); - // This is a small optimisation, to avoid subqueries with no changes + // This is a small optimization, to avoid subqueries with no changes // Top level should still be generated for projection if (this.relationship) { if (setParams.length === 0 && mutationSubqueries.length === 0) { @@ -155,14 +152,9 @@ export class UpdateOperation extends Operation { const predicate = this.getPredicate(nestedContext); const matchClause = new Cypher.Match(pattern); - const filtersWith = new Cypher.With("*").where(predicate); - if (filtersWith) { - filtersWith.set(...setParams); - if (mutationSubqueries.length || afterFilterSubqueries.length) { - filtersWith.with("*"); - } - } else { - matchClause.set(...setParams); + const filtersWith = new Cypher.With("*").where(predicate).set(...setParams); + if (afterFilterSubqueries.length) { + filtersWith.with("*"); } const clauses = Cypher.utils.concat( @@ -188,31 +180,23 @@ export class UpdateOperation extends Operation { ); } - return [ - // ...this.getAuthorizationClauses(nestedContext), - // ...this.inputFields.flatMap((inputField) => { - // return inputField.getAuthorizationSubqueries(nestedContext); - // }), - ]; + return []; } private getAuthorizationClauses(context: QueryASTContext): Cypher.Clause[] { - const { selections, subqueries, predicates, validations } = this.transpileAuthClauses(context); - const predicate = Cypher.and(...predicates); - const lastSelection = selections[selections.length - 1]; - + const { subqueries, predicates, validations } = this.transpileAuthClauses(context); const authSubqueries = subqueries.map((sq) => { return new Cypher.Call(sq, "*"); }); - if (!predicates.length && !validations.length) { - return []; - } else { - if (lastSelection) { - lastSelection.where(predicate); - return [...authSubqueries, new Cypher.With("*"), ...selections, ...validations]; + if (!predicates.length) { + if (!validations.length) { + return []; } - return [...authSubqueries, new Cypher.With("*").where(predicate), ...selections, ...validations]; + return [...authSubqueries, ...validations]; } + + const predicate = Cypher.and(...predicates); + return [...authSubqueries, new Cypher.With("*").where(predicate), ...validations]; } private getAuthorizationClausesAfter(context: QueryASTContext): Cypher.Clause[] { From f67578e76c2f6cd5de23c7db5ab459bf2d3d3d5a Mon Sep 17 00:00:00 2001 From: a-alle Date: Thu, 20 Nov 2025 16:42:02 +0000 Subject: [PATCH 2/3] remove Node, Relationship, and everything else --- packages/graphql/src/classes/GraphElement.ts | 88 -- .../src/classes/LimitDirective.test.ts | 67 -- .../graphql/src/classes/LimitDirective.ts | 48 - packages/graphql/src/classes/Neo4jGraphQL.ts | 37 +- packages/graphql/src/classes/Node.test.ts | 880 ------------------ packages/graphql/src/classes/Node.ts | 298 ------ .../graphql/src/classes/NodeDirective.test.ts | 111 --- packages/graphql/src/classes/NodeDirective.ts | 51 - packages/graphql/src/classes/Relationship.ts | 85 -- packages/graphql/src/classes/index.ts | 3 - .../annotation/CustomResolverAnnotation.ts | 2 +- .../src/schema-model/generate-model.ts | 13 +- .../schema-model/parser/parse-attribute.ts | 9 +- .../src/schema/create-connection-fields.ts | 74 -- .../graphql/src/schema/create-global-nodes.ts | 10 +- .../schema/get-custom-resolver-meta.test.ts | 872 ----------------- .../src/schema/get-cypher-meta.test.ts | 157 ---- .../graphql/src/schema/get-cypher-meta.ts | 66 -- packages/graphql/src/schema/get-nodes.ts | 160 ---- .../graphql/src/schema/get-obj-field-meta.ts | 544 ----------- .../src/schema/get-populated-by-meta.ts | 39 - .../src/schema/get-relationship-meta.test.ts | 629 ------------- .../src/schema/get-relationship-meta.ts | 63 -- .../src/schema/make-augmented-schema.test.ts | 36 +- .../src/schema/make-augmented-schema.ts | 85 +- .../filter-interface-types.ts | 42 - .../src/schema/parse-node-directive.test.ts | 67 -- .../src/schema/parse-node-directive.ts | 39 - .../parse/parse-limit-directive.test.ts | 144 --- .../src/schema/parse/parse-limit-directive.ts | 82 -- .../parse/parse-plural-directive.test.ts | 36 - .../schema/parse/parse-plural-directive.ts | 32 - .../composition/wrap-query-and-mutation.ts | 26 +- .../schema/resolvers/mutation/create.test.ts | 6 +- .../src/schema/resolvers/mutation/create.ts | 14 +- .../schema/resolvers/mutation/update.test.ts | 15 +- .../src/schema/resolvers/mutation/update.ts | 17 +- .../src/schema/resolvers/query/cypher.test.ts | 11 +- .../src/schema/resolvers/query/cypher.ts | 5 +- ...ta.ts => selection-set-to-resolve-tree.ts} | 63 +- .../generate-subscription-types.ts | 9 +- .../authorization/check-authentication.ts | 27 - .../graphql/src/translate/translate-create.ts | 10 +- .../translate/translate-top-level-cypher.ts | 13 +- packages/graphql/src/types/index.ts | 175 +--- .../src/utils/find-conflicting-properties.ts | 39 - .../graphql/src/utils/map-to-db-property.ts | 30 - .../directives/customResolver.int.test.ts | 2 +- .../integration/multi-database.int.test.ts | 2 +- .../tests/utils/builders/context-builder.ts | 2 - .../tests/utils/builders/node-builder.ts | 62 -- 51 files changed, 87 insertions(+), 5310 deletions(-) delete mode 100644 packages/graphql/src/classes/GraphElement.ts delete mode 100644 packages/graphql/src/classes/LimitDirective.test.ts delete mode 100644 packages/graphql/src/classes/LimitDirective.ts delete mode 100644 packages/graphql/src/classes/Node.test.ts delete mode 100644 packages/graphql/src/classes/Node.ts delete mode 100644 packages/graphql/src/classes/NodeDirective.test.ts delete mode 100644 packages/graphql/src/classes/NodeDirective.ts delete mode 100644 packages/graphql/src/classes/Relationship.ts delete mode 100644 packages/graphql/src/schema/create-connection-fields.ts delete mode 100644 packages/graphql/src/schema/get-custom-resolver-meta.test.ts delete mode 100644 packages/graphql/src/schema/get-cypher-meta.test.ts delete mode 100644 packages/graphql/src/schema/get-cypher-meta.ts delete mode 100644 packages/graphql/src/schema/get-nodes.ts delete mode 100644 packages/graphql/src/schema/get-obj-field-meta.ts delete mode 100644 packages/graphql/src/schema/get-populated-by-meta.ts delete mode 100644 packages/graphql/src/schema/get-relationship-meta.test.ts delete mode 100644 packages/graphql/src/schema/get-relationship-meta.ts delete mode 100644 packages/graphql/src/schema/make-augmented-schema/filter-interface-types.ts delete mode 100644 packages/graphql/src/schema/parse-node-directive.test.ts delete mode 100644 packages/graphql/src/schema/parse-node-directive.ts delete mode 100644 packages/graphql/src/schema/parse/parse-limit-directive.test.ts delete mode 100644 packages/graphql/src/schema/parse/parse-limit-directive.ts delete mode 100644 packages/graphql/src/schema/parse/parse-plural-directive.test.ts delete mode 100644 packages/graphql/src/schema/parse/parse-plural-directive.ts rename packages/graphql/src/schema/{get-custom-resolver-meta.ts => selection-set-to-resolve-tree.ts} (80%) delete mode 100644 packages/graphql/src/utils/map-to-db-property.ts delete mode 100644 packages/graphql/tests/utils/builders/node-builder.ts diff --git a/packages/graphql/src/classes/GraphElement.ts b/packages/graphql/src/classes/GraphElement.ts deleted file mode 100644 index bca0d0ea75..0000000000 --- a/packages/graphql/src/classes/GraphElement.ts +++ /dev/null @@ -1,88 +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 { - BaseField, - CustomEnumField, - CustomResolverField, - CustomScalarField, - CypherField, - PointField, - PrimitiveField, - TemporalField, -} from "../types"; - -export interface GraphElementConstructor { - name: string; - description?: string; - cypherFields: CypherField[]; - primitiveFields: PrimitiveField[]; - scalarFields: CustomScalarField[]; - enumFields: CustomEnumField[]; - temporalFields: TemporalField[]; - pointFields: PointField[]; - customResolverFields: CustomResolverField[]; -} - -/** @deprecated */ -export abstract class GraphElement { - public name: string; - public description?: string; - public primitiveFields: PrimitiveField[]; - public scalarFields: CustomScalarField[]; - public enumFields: CustomEnumField[]; - public temporalFields: TemporalField[]; - public pointFields: PointField[]; - public customResolverFields: CustomResolverField[]; - - constructor(input: GraphElementConstructor) { - this.name = input.name; - this.description = input.description; - this.primitiveFields = input.primitiveFields; - this.scalarFields = input.scalarFields; - this.enumFields = input.enumFields; - this.temporalFields = input.temporalFields; - this.pointFields = input.pointFields; - this.customResolverFields = input.customResolverFields; - } - - public getField(name: string): BaseField | undefined { - for (const fieldList of this.getAllFields()) { - const field = this.searchFieldInList(name, fieldList); - if (field) return field; - } - } - - private getAllFields(): Array { - return [ - this.primitiveFields, - this.scalarFields, - this.enumFields, - this.temporalFields, - this.pointFields, - this.customResolverFields, - ]; - } - - private searchFieldInList(fieldName: string, fields: BaseField[]): BaseField | undefined { - return fields.find((field) => { - return field.fieldName === fieldName; - }); - } -} diff --git a/packages/graphql/src/classes/LimitDirective.test.ts b/packages/graphql/src/classes/LimitDirective.test.ts deleted file mode 100644 index 45ab35531f..0000000000 --- a/packages/graphql/src/classes/LimitDirective.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as neo4j from "neo4j-driver"; -import { LimitDirective } from "./LimitDirective"; - -describe("QueryOptionsDirective", () => { - describe("getLimit", () => { - test("should return default limit", () => { - const limit = new LimitDirective({ - default: neo4j.int(5), - max: neo4j.int(8), - }); - - expect(limit.getLimit()).toEqual(neo4j.int(5)); - }); - - test("should return max limit if default is not available", () => { - const limit = new LimitDirective({ - max: neo4j.int(8), - }); - - expect(limit.getLimit()).toEqual(neo4j.int(8)); - }); - - test("should override default limit", () => { - const limit = new LimitDirective({ - default: neo4j.int(5), - max: neo4j.int(8), - }); - - expect(limit.getLimit(neo4j.int(2))).toEqual(neo4j.int(2)); - expect(limit.getLimit(neo4j.int(6))).toEqual(neo4j.int(6)); - }); - - test("should return if a higher one is given max limit if a higher one is given", () => { - const limit = new LimitDirective({ - default: neo4j.int(5), - max: neo4j.int(8), - }); - - expect(limit.getLimit(neo4j.int(16))).toEqual(neo4j.int(8)); - }); - - test("should return undefined if no limit is given", () => { - const limit = new LimitDirective({}); - - expect(limit.getLimit()).toBeUndefined(); - }); - }); -}); diff --git a/packages/graphql/src/classes/LimitDirective.ts b/packages/graphql/src/classes/LimitDirective.ts deleted file mode 100644 index 6fd3e591eb..0000000000 --- a/packages/graphql/src/classes/LimitDirective.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as neo4j from "neo4j-driver"; -import type { Integer } from "neo4j-driver"; - -type LimitDirectiveConstructor = { - default?: Integer; - max?: Integer; -}; - -export class LimitDirective { - private default?: Integer; - private max?: Integer; - - constructor(limit: LimitDirectiveConstructor) { - this.default = limit.default; - this.max = limit.max; - } - - public getLimit(optionsLimit?: Integer | number): Integer | undefined { - if (optionsLimit) { - const integerLimit = neo4j.int(optionsLimit); - if (this.max && integerLimit.greaterThan(this.max)) { - return this.max; - } - return integerLimit; - } - - return this.default || this.max; - } -} diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index c1669dc8ac..006b57fc39 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -46,8 +46,6 @@ import type { ExecutorConstructorParam, Neo4jGraphQLSessionConfig } from "./Exec import { Executor } from "./Executor"; import type { Neo4jDatabaseInfo } from "./Neo4jDatabaseInfo"; import { getNeo4jDatabaseInfo } from "./Neo4jDatabaseInfo"; -import type Node from "./Node"; -import type Relationship from "./Relationship"; import { Neo4jGraphQLAuthorization } from "./authorization/Neo4jGraphQLAuthorization"; import { Neo4jGraphQLSubscriptionsCDCEngine } from "./subscription/Neo4jGraphQLSubscriptionsCDCEngine"; import { assertIndexesAndConstraints } from "./utils/asserts-indexes-and-constraints"; @@ -72,9 +70,6 @@ class Neo4jGraphQL { private driver?: Driver; private features: ContextFeatures; - private _nodes?: Node[]; - private _relationships?: Relationship[]; - private jwtFieldsMap?: Map; private schemaModel?: Neo4jGraphQLSchemaModel; @@ -201,22 +196,6 @@ class Neo4jGraphQL { }); } - private get nodes(): Node[] { - if (!this._nodes) { - throw new Error("You must await `.getSchema()` before accessing `nodes`"); - } - - return this._nodes; - } - - private get relationships(): Relationship[] { - if (!this._relationships) { - throw new Error("You must await `.getSchema()` before accessing `relationships`"); - } - - return this._relationships; - } - /** * Currently just merges all type definitions into a document. Eventual intention described below: * @@ -309,8 +288,6 @@ class Neo4jGraphQL { const wrapResolverArgs: WrapResolverArguments = { driver: this.driver, - nodes: this.nodes, - relationships: this.relationships, schemaModel: this.schemaModel, features: this.features, authorization: this.authorization, @@ -382,7 +359,7 @@ class Neo4jGraphQL { private generateSchemaModel(document: DocumentNode): Neo4jGraphQLSchemaModel { if (!this.schemaModel) { - return generateModel(document); + return generateModel(document, this.resolvers); } return this.schemaModel; } @@ -420,10 +397,9 @@ class Neo4jGraphQL { this.schemaModel = this.generateSchemaModel(document); - const { nodes, relationships, typeDefs, resolvers } = makeAugmentedSchema({ + const { typeDefs, resolvers } = makeAugmentedSchema({ document, features: this.features, - userCustomResolvers: this.resolvers, schemaModel: this.schemaModel, complexityEstimatorHelper: this.complexityEstimatorHelper, }); @@ -437,9 +413,6 @@ class Neo4jGraphQL { }); } - this._nodes = nodes; - this._relationships = relationships; - const schema = makeExecutableSchema({ typeDefs, resolvers, @@ -490,10 +463,9 @@ class Neo4jGraphQL { this.schemaModel = this.generateSchemaModel(document); - const { nodes, relationships, typeDefs, resolvers } = makeAugmentedSchema({ + const { typeDefs, resolvers } = makeAugmentedSchema({ document, features: this.features, - userCustomResolvers: this.resolvers, subgraph, schemaModel: this.schemaModel, complexityEstimatorHelper: this.complexityEstimatorHelper, @@ -510,9 +482,6 @@ class Neo4jGraphQL { }); } - this._nodes = nodes; - this._relationships = relationships; - // TODO: Move into makeAugmentedSchema, add resolvers alongside other resolvers const referenceResolvers = subgraph.getReferenceResolvers(this.schemaModel); diff --git a/packages/graphql/src/classes/Node.test.ts b/packages/graphql/src/classes/Node.test.ts deleted file mode 100644 index 95ed8565bd..0000000000 --- a/packages/graphql/src/classes/Node.test.ts +++ /dev/null @@ -1,880 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ContextBuilder } from "../../tests/utils/builders/context-builder"; -import { NodeBuilder } from "../../tests/utils/builders/node-builder"; -import type { MutationResponseTypeNames, NodeConstructor, RootTypeFieldNames, SubscriptionEvents } from "./Node"; -import Node from "./Node"; -import { NodeDirective } from "./NodeDirective"; - -describe("Node", () => { - const defaultContext = new ContextBuilder().instance(); - - test("should construct", () => { - // @ts-ignore - const input: NodeConstructor = { - name: "Movie", - cypherFields: [], - enumFields: [], - primitiveFields: [], - scalarFields: [], - temporalFields: [], - unionFields: [], - interfaceFields: [], - objectFields: [], - interfaces: [], - otherDirectives: [], - pointFields: [], - relationFields: [], - }; - - // @ts-ignore - expect(new Node(input)).toMatchObject({ name: "Movie" }); - }); - - test("should return labelString from node name", () => { - const node = new NodeBuilder({ - name: "Movie", - }).instance(); - - expect(node.getLabelString(defaultContext)).toBe(":Movie"); - }); - - test("should return labels from node name", () => { - const node = new NodeBuilder({ - name: "Movie", - }).instance(); - - expect(node.getLabels(defaultContext)).toEqual(["Movie"]); - }); - - describe("root type field names", () => { - test.each<[string, RootTypeFieldNames]>([ - [ - "Account", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "accountCreated", - updated: "accountUpdated", - deleted: "accountDeleted", - }, - }, - ], - [ - "AWSAccount", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - }, - }, - ], - [ - "AWS_ACCOUNT", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - }, - }, - ], - [ - "aws-account", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - }, - }, - ], - [ - "aws_account", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "awsAccountCreated", - updated: "awsAccountUpdated", - deleted: "awsAccountDeleted", - }, - }, - ], - [ - "account", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "accountCreated", - updated: "accountUpdated", - deleted: "accountDeleted", - }, - }, - ], - [ - "ACCOUNT", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "accountCreated", - updated: "accountUpdated", - deleted: "accountDeleted", - }, - }, - ], - [ - "A", - { - create: "createAs", - read: "as", - update: "updateAs", - delete: "deleteAs", - aggregate: "asAggregate", - subscribe: { - created: "aCreated", - updated: "aUpdated", - deleted: "aDeleted", - }, - }, - ], - [ - "_2number", - { - create: "create_2Numbers", - read: "_2Numbers", - update: "update_2Numbers", - delete: "delete_2Numbers", - aggregate: "_2NumbersAggregate", - subscribe: { - created: "_2NumberCreated", - updated: "_2NumberUpdated", - deleted: "_2NumberDeleted", - }, - }, - ], - [ - "__2number", - { - create: "create__2Numbers", - read: "__2Numbers", - update: "update__2Numbers", - delete: "delete__2Numbers", - aggregate: "__2NumbersAggregate", - subscribe: { - created: "__2NumberCreated", - updated: "__2NumberUpdated", - deleted: "__2NumberDeleted", - }, - }, - ], - [ - "_number", - { - create: "create_numbers", - read: "_numbers", - update: "update_numbers", - delete: "delete_numbers", - aggregate: "_numbersAggregate", - subscribe: { - created: "_numberCreated", - updated: "_numberUpdated", - deleted: "_numberDeleted", - }, - }, - ], - ])("should pluralize %s as expected", (typename: string, rootTypeFieldNames: RootTypeFieldNames) => { - const node = new NodeBuilder({ - name: typename, - }).instance(); - - expect(node.rootTypeFieldNames).toStrictEqual(rootTypeFieldNames); - }); - - test.each<[string, RootTypeFieldNames]>([ - [ - "Accounts", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "AWSAccounts", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "AWS_ACCOUNTS", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "aws-accounts", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "aws_accounts", - { - create: "createAwsAccounts", - read: "awsAccounts", - update: "updateAwsAccounts", - delete: "deleteAwsAccounts", - aggregate: "awsAccountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "accounts", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - [ - "ACCOUNTS", - { - create: "createAccounts", - read: "accounts", - update: "updateAccounts", - delete: "deleteAccounts", - aggregate: "accountsAggregate", - subscribe: { - created: "testCreated", - updated: "testUpdated", - deleted: "testDeleted", - }, - }, - ], - ])( - "should pluralize %s as expected with plural specified in @node directive", - (plural: string, rootTypeFieldNames: RootTypeFieldNames) => { - const node = new NodeBuilder({ - name: "Test", - nodeDirective: new NodeDirective({}), - plural, - }).instance(); - - expect(node.rootTypeFieldNames).toStrictEqual(rootTypeFieldNames); - } - ); - }); - - describe("mutation response type names", () => { - test.each<[string, MutationResponseTypeNames]>([ - [ - "Account", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "AWSAccount", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "AWS_ACCOUNT", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "aws-account", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "aws_account", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "account", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "ACCOUNT", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "A", - { - create: "CreateAsMutationResponse", - update: "UpdateAsMutationResponse", - }, - ], - [ - "_2number", - { - create: "Create_2NumbersMutationResponse", - update: "Update_2NumbersMutationResponse", - }, - ], - [ - "__2number", - { - create: "Create__2NumbersMutationResponse", - update: "Update__2NumbersMutationResponse", - }, - ], - [ - "_number", - { - create: "Create_numbersMutationResponse", - update: "Update_numbersMutationResponse", - }, - ], - ])( - "should generate mutation response type names for %s as expected", - (typename: string, mutationResponseTypeNames: MutationResponseTypeNames) => { - const node = new NodeBuilder({ - name: typename, - }).instance(); - - expect(node.mutationResponseTypeNames).toStrictEqual(mutationResponseTypeNames); - } - ); - - test.each<[string, MutationResponseTypeNames]>([ - [ - "Accounts", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "AWSAccounts", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "AWS_ACCOUNTS", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "aws-accounts", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "aws_accounts", - { - create: "CreateAwsAccountsMutationResponse", - update: "UpdateAwsAccountsMutationResponse", - }, - ], - [ - "accounts", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - [ - "ACCOUNTS", - { - create: "CreateAccountsMutationResponse", - update: "UpdateAccountsMutationResponse", - }, - ], - ])( - "should generate mutation response type names for %s as expected with plural specified in @node directive", - (plural: string, mutationResponseTypeNames: MutationResponseTypeNames) => { - const node = new NodeBuilder({ - name: "Test", - nodeDirective: new NodeDirective({}), - plural, - }).instance(); - - expect(node.mutationResponseTypeNames).toStrictEqual(mutationResponseTypeNames); - } - ); - }); - - describe("Subscription event type names", () => { - test.each<[string, SubscriptionEvents]>([ - [ - "Account", - { - create: "AccountCreatedEvent", - update: "AccountUpdatedEvent", - delete: "AccountDeletedEvent", - create_relationship: "AccountRelationshipCreatedEvent", - delete_relationship: "AccountRelationshipDeletedEvent", - }, - ], - [ - "AWSAccount", - { - create: "AwsAccountCreatedEvent", - update: "AwsAccountUpdatedEvent", - delete: "AwsAccountDeletedEvent", - create_relationship: "AwsAccountRelationshipCreatedEvent", - delete_relationship: "AwsAccountRelationshipDeletedEvent", - }, - ], - [ - "AWS_ACCOUNT", - { - create: "AwsAccountCreatedEvent", - update: "AwsAccountUpdatedEvent", - delete: "AwsAccountDeletedEvent", - create_relationship: "AwsAccountRelationshipCreatedEvent", - delete_relationship: "AwsAccountRelationshipDeletedEvent", - }, - ], - [ - "aws-account", - { - create: "AwsAccountCreatedEvent", - update: "AwsAccountUpdatedEvent", - delete: "AwsAccountDeletedEvent", - create_relationship: "AwsAccountRelationshipCreatedEvent", - delete_relationship: "AwsAccountRelationshipDeletedEvent", - }, - ], - [ - "aws_account", - { - create: "AwsAccountCreatedEvent", - update: "AwsAccountUpdatedEvent", - delete: "AwsAccountDeletedEvent", - create_relationship: "AwsAccountRelationshipCreatedEvent", - delete_relationship: "AwsAccountRelationshipDeletedEvent", - }, - ], - [ - "account", - { - create: "AccountCreatedEvent", - update: "AccountUpdatedEvent", - delete: "AccountDeletedEvent", - create_relationship: "AccountRelationshipCreatedEvent", - delete_relationship: "AccountRelationshipDeletedEvent", - }, - ], - [ - "ACCOUNT", - { - create: "AccountCreatedEvent", - update: "AccountUpdatedEvent", - delete: "AccountDeletedEvent", - create_relationship: "AccountRelationshipCreatedEvent", - delete_relationship: "AccountRelationshipDeletedEvent", - }, - ], - [ - "A", - { - create: "ACreatedEvent", - update: "AUpdatedEvent", - delete: "ADeletedEvent", - create_relationship: "ARelationshipCreatedEvent", - delete_relationship: "ARelationshipDeletedEvent", - }, - ], - [ - "_2number", - { - create: "_2NumberCreatedEvent", - update: "_2NumberUpdatedEvent", - delete: "_2NumberDeletedEvent", - create_relationship: "_2NumberRelationshipCreatedEvent", - delete_relationship: "_2NumberRelationshipDeletedEvent", - }, - ], - [ - "__2number", - { - create: "__2NumberCreatedEvent", - update: "__2NumberUpdatedEvent", - delete: "__2NumberDeletedEvent", - create_relationship: "__2NumberRelationshipCreatedEvent", - delete_relationship: "__2NumberRelationshipDeletedEvent", - }, - ], - [ - "_number", - { - create: "_numberCreatedEvent", - update: "_numberUpdatedEvent", - delete: "_numberDeletedEvent", - create_relationship: "_numberRelationshipCreatedEvent", - delete_relationship: "_numberRelationshipDeletedEvent", - }, - ], - ])( - "should generate Subscription event type names for %s as expected", - (typename: string, subscriptionEventTypeNames: SubscriptionEvents) => { - const node = new NodeBuilder({ - name: typename, - }).instance(); - - expect(node.subscriptionEventTypeNames).toStrictEqual(subscriptionEventTypeNames); - } - ); - }); - - describe("Subscription event payload field names", () => { - test.each<[string, SubscriptionEvents]>([ - [ - "Account", - { - create: "createdAccount", - update: "updatedAccount", - delete: "deletedAccount", - create_relationship: "account", - delete_relationship: "account", - }, - ], - [ - "AWSAccount", - { - create: "createdAwsAccount", - update: "updatedAwsAccount", - delete: "deletedAwsAccount", - create_relationship: "awsAccount", - delete_relationship: "awsAccount", - }, - ], - [ - "AWS_ACCOUNT", - { - create: "createdAwsAccount", - update: "updatedAwsAccount", - delete: "deletedAwsAccount", - create_relationship: "awsAccount", - delete_relationship: "awsAccount", - }, - ], - [ - "aws-account", - { - create: "createdAwsAccount", - update: "updatedAwsAccount", - delete: "deletedAwsAccount", - create_relationship: "awsAccount", - delete_relationship: "awsAccount", - }, - ], - [ - "aws_account", - { - create: "createdAwsAccount", - update: "updatedAwsAccount", - delete: "deletedAwsAccount", - create_relationship: "awsAccount", - delete_relationship: "awsAccount", - }, - ], - [ - "account", - { - create: "createdAccount", - update: "updatedAccount", - delete: "deletedAccount", - create_relationship: "account", - delete_relationship: "account", - }, - ], - [ - "ACCOUNT", - { - create: "createdAccount", - update: "updatedAccount", - delete: "deletedAccount", - create_relationship: "account", - delete_relationship: "account", - }, - ], - [ - "A", - { - create: "createdA", - update: "updatedA", - delete: "deletedA", - create_relationship: "a", - delete_relationship: "a", - }, - ], - [ - "_2number", - { - create: "created_2Number", - update: "updated_2Number", - delete: "deleted_2Number", - create_relationship: "_2Number", - delete_relationship: "_2Number", - }, - ], - [ - "__2number", - { - create: "created__2Number", - update: "updated__2Number", - delete: "deleted__2Number", - create_relationship: "__2Number", - delete_relationship: "__2Number", - }, - ], - [ - "_number", - { - create: "created_number", - update: "updated_number", - delete: "deleted_number", - create_relationship: "_number", - delete_relationship: "_number", - }, - ], - ])( - "should generate Subscription event type names for %s as expected", - (typename: string, subscriptionEventPayloadFieldNames: SubscriptionEvents) => { - const node = new NodeBuilder({ - name: typename, - }).instance(); - - expect(node.subscriptionEventPayloadFieldNames).toStrictEqual(subscriptionEventPayloadFieldNames); - } - ); - }); - - describe("global node resolution", () => { - test("should return true if it is a global node", () => { - const node = new NodeBuilder({ - name: "Film", - primitiveFields: [], - isGlobalNode: true, - globalIdField: "dbId", - }).instance(); - - const isGlobalNode = node.isGlobalNode; - expect(isGlobalNode).toBe(true); - }); - - test("should convert the db id to a global relay id with the correct typename", () => { - const node = new NodeBuilder({ - name: "Film", - primitiveFields: [], - isGlobalNode: true, - globalIdField: "title", - }).instance(); - - const value = "the Matrix"; - - const relayId = node.toGlobalId(value); - - expect(relayId).toBe("RmlsbTp0aXRsZTp0aGUgTWF0cml4"); - - expect(node.fromGlobalId(relayId)).toEqual({ - typeName: "Film", - field: "title", - id: value, - }); - }); - test("should properly convert a relay id to an object when the id has a colon in the name", () => { - const node = new NodeBuilder({ - name: "Film", - primitiveFields: [], - isGlobalNode: true, - globalIdField: "title", - }).instance(); - - const value = "2001: A Space Odyssey"; - - const relayId = node.toGlobalId(value); - - expect(node.fromGlobalId(relayId)).toMatchObject({ field: "title", typeName: "Film", id: value }); - }); - }); - - describe("NodeDirective", () => { - test("should return labels updated with jwt values from Context", () => { - const node = new NodeBuilder({ - name: "Film", - }) - .withNodeDirective({ - labels: ["$jwt.movielabel"], - }) - .instance(); - - const context = new ContextBuilder() - .with({ - jwt: { - movielabel: "Movie", - }, - }) - .instance(); - - const labels = node.getLabels(context); - const labelString = node.getLabelString(context); - - expect(labels).toEqual(["Movie"]); - expect(labelString).toBe(":Movie"); - }); - - test("should return labels updated with context values from Context", () => { - const node = new NodeBuilder({ - name: "Film", - }) - .withNodeDirective({ - labels: ["$context.myKey"], - }) - .instance(); - - const context = new ContextBuilder().instance(); - - const labels = node.getLabels({ ...context, myKey: "Movie" } as Record); - const labelString = node.getLabelString({ ...context, myKey: "Movie" } as Record); - - expect(labels).toEqual(["Movie"]); - expect(labelString).toBe(":Movie"); - }); - - test("should return additional labels updated with jwt values from Context", () => { - const node = new NodeBuilder({ - name: "Film", - }) - .withNodeDirective({ - labels: ["Film", "$jwt.movielabel"], - }) - .instance(); - - const context = new ContextBuilder() - .with({ - jwt: { - movielabel: "Movie", - }, - }) - .instance(); - - const labels = node.getLabels(context); - const labelString = node.getLabelString(context); - - expect(labels).toEqual(["Film", "Movie"]); - expect(labelString).toBe(":Film:Movie"); - }); - }); -}); diff --git a/packages/graphql/src/classes/Node.ts b/packages/graphql/src/classes/Node.ts deleted file mode 100644 index b83b5451f0..0000000000 --- a/packages/graphql/src/classes/Node.ts +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import camelcase from "camelcase"; -import type { DirectiveNode, NamedTypeNode } from "graphql"; -import pluralize from "pluralize"; -import type { - ConnectionField, - CustomEnumField, - CustomScalarField, - CypherField, - FullText, - InterfaceField, - ObjectField, - PointField, - PrimitiveField, - RelationField, - TemporalField, - UnionField, -} from "../types"; -import type { Neo4jGraphQLContext } from "../types/neo4j-graphql-context"; -import type { DecodedGlobalId } from "../utils/global-ids"; -import { fromGlobalId, toGlobalId } from "../utils/global-ids"; -import { leadingUnderscores } from "../utils/leading-underscore"; -import { upperFirst } from "../utils/upper-first"; -import type { GraphElementConstructor } from "./GraphElement"; -import { GraphElement } from "./GraphElement"; -import type { LimitDirective } from "./LimitDirective"; -import type { NodeDirective } from "./NodeDirective"; - -export interface NodeConstructor extends GraphElementConstructor { - name: string; - relationFields: RelationField[]; - connectionFields: ConnectionField[]; - cypherFields: CypherField[]; - primitiveFields: PrimitiveField[]; - scalarFields: CustomScalarField[]; - enumFields: CustomEnumField[]; - otherDirectives: DirectiveNode[]; - propagatedDirectives: DirectiveNode[]; - unionFields: UnionField[]; - interfaceFields: InterfaceField[]; - interfaces: NamedTypeNode[]; - objectFields: ObjectField[]; - temporalFields: TemporalField[]; - pointFields: PointField[]; - plural?: string; - fulltextDirective?: FullText; - nodeDirective?: NodeDirective; - description?: string; - limitDirective?: LimitDirective; - isGlobalNode?: boolean; - globalIdField?: string; - globalIdFieldIsInt?: boolean; -} - -export type MutableField = - | PrimitiveField - | CustomScalarField - | CustomEnumField - | UnionField - | TemporalField - | CypherField; - -export type RootTypeFieldNames = { - create: string; - read: string; - update: string; - delete: string; - aggregate: string; - subscribe: { - created: string; - updated: string; - deleted: string; - }; -}; - -export type AggregateTypeNames = { - selection: string; - input: string; -}; - -export type MutationResponseTypeNames = { - create: string; - update: string; -}; - -export type SubscriptionEvents = { - create: string; - update: string; - delete: string; - create_relationship: string; - delete_relationship: string; -}; - -/** @deprecated */ -class Node extends GraphElement { - public relationFields: RelationField[]; - public connectionFields: ConnectionField[]; - public cypherFields: CypherField[]; - public otherDirectives: DirectiveNode[]; - public propagatedDirectives: DirectiveNode[]; - public unionFields: UnionField[]; - public interfaceFields: InterfaceField[]; - public interfaces: NamedTypeNode[]; - public objectFields: ObjectField[]; - public nodeDirective?: NodeDirective; - public limit?: LimitDirective; - public singular: string; - public plural: string; - public isGlobalNode: boolean | undefined; - private _idField: string | undefined; - private _idFieldIsInt?: boolean; - - constructor(input: NodeConstructor) { - super(input); - this.relationFields = input.relationFields; - this.connectionFields = input.connectionFields; - this.cypherFields = input.cypherFields; - this.otherDirectives = input.otherDirectives; - this.propagatedDirectives = input.propagatedDirectives; - this.unionFields = input.unionFields; - this.interfaceFields = input.interfaceFields; - this.interfaces = input.interfaces; - this.objectFields = input.objectFields; - this.nodeDirective = input.nodeDirective; - this.limit = input.limitDirective; - this.isGlobalNode = input.isGlobalNode; - this._idField = input.globalIdField; - this._idFieldIsInt = input.globalIdFieldIsInt; - this.singular = this.generateSingular(); - this.plural = this.generatePlural(input.plural); - } - - // Fields you can set in a create or update mutation - public get mutableFields(): MutableField[] { - return [ - ...this.temporalFields, - ...this.enumFields, - ...this.objectFields, - ...this.scalarFields, // these are just custom scalars - ...this.primitiveFields, // these are instead built-in scalars - ...this.interfaceFields, - ...this.objectFields, - ...this.unionFields, - ...this.pointFields, - ]; - } - - /** Fields you can apply auth allow and bind to */ - // Maybe we can remove this as they may not be used anymore in the new auth system - public get authableFields(): MutableField[] { - return [ - ...this.primitiveFields, - ...this.scalarFields, - ...this.enumFields, - ...this.unionFields, - ...this.objectFields, - ...this.temporalFields, - ...this.pointFields, - ...this.cypherFields, - ]; - } - - private get pascalCaseSingular(): string { - return upperFirst(this.singular); - } - - private get pascalCasePlural(): string { - return upperFirst(this.plural); - } - - public get rootTypeFieldNames(): RootTypeFieldNames { - const pascalCasePlural = this.pascalCasePlural; - - return { - create: `create${pascalCasePlural}`, - read: this.plural, - update: `update${pascalCasePlural}`, - delete: `delete${pascalCasePlural}`, - aggregate: `${this.plural}Aggregate`, - subscribe: { - created: `${this.singular}Created`, - updated: `${this.singular}Updated`, - deleted: `${this.singular}Deleted`, - }, - }; - } - - public get aggregateTypeNames(): AggregateTypeNames { - return { - selection: `${this.name}AggregateSelection`, - input: `${this.name}AggregateSelectionInput`, - }; - } - - public get mutationResponseTypeNames(): MutationResponseTypeNames { - const pascalCasePlural = this.pascalCasePlural; - - return { - create: `Create${pascalCasePlural}MutationResponse`, - update: `Update${pascalCasePlural}MutationResponse`, - }; - } - - public get subscriptionEventTypeNames(): SubscriptionEvents { - const pascalCaseSingular = this.pascalCaseSingular; - - return { - create: `${pascalCaseSingular}CreatedEvent`, - update: `${pascalCaseSingular}UpdatedEvent`, - delete: `${pascalCaseSingular}DeletedEvent`, - create_relationship: `${pascalCaseSingular}RelationshipCreatedEvent`, - delete_relationship: `${pascalCaseSingular}RelationshipDeletedEvent`, - }; - } - - public get subscriptionEventPayloadFieldNames(): SubscriptionEvents { - const pascalCaseSingular = this.pascalCaseSingular; - - return { - create: `created${pascalCaseSingular}`, - update: `updated${pascalCaseSingular}`, - delete: `deleted${pascalCaseSingular}`, - create_relationship: `${this.singular}`, - delete_relationship: `${this.singular}`, - }; - } - - public getLabelString(context: Neo4jGraphQLContext): string { - return this.nodeDirective?.getLabelsString(this.name, context) || `:${this.name}`; - } - /** - * Returns the list containing labels mapped with the values contained in the Context. - * Be careful when using this method, labels returned are unescaped. - **/ - public getLabels(context: Neo4jGraphQLContext): string[] { - return this.nodeDirective?.getLabels(this.name, context) || [this.name]; - } - - public getMainLabel(): string { - return this.nodeDirective?.labels?.[0] || this.name; - } - - public getAllLabels(): string[] { - return this.nodeDirective?.labels.length ? this.nodeDirective.labels : [this.name]; - } - - public getGlobalIdField(): string { - if (!this.isGlobalNode || !this._idField) { - throw new Error( - "The 'global' property needs to be set to true on an @id directive before accessing the unique node id field" - ); - } - return this._idField; - } - - public toGlobalId(id: string): string { - const typeName = this.name; - const field = this.getGlobalIdField(); - return toGlobalId({ typeName, field, id }); - } - - public fromGlobalId(relayId: string): DecodedGlobalId { - return fromGlobalId(relayId, this._idFieldIsInt); - } - - private generateSingular(): string { - const singular = camelcase(this.name); - - return `${leadingUnderscores(this.name)}${singular}`; - } - - private generatePlural(inputPlural: string | undefined): string { - const name = inputPlural || this.plural || this.name; - const plural = inputPlural || this.plural ? camelcase(name) : pluralize(camelcase(name)); - - return `${leadingUnderscores(name)}${plural}`; - } -} - -export default Node; diff --git a/packages/graphql/src/classes/NodeDirective.test.ts b/packages/graphql/src/classes/NodeDirective.test.ts deleted file mode 100644 index ed11645834..0000000000 --- a/packages/graphql/src/classes/NodeDirective.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { NodeDirective } from "./NodeDirective"; -import { ContextBuilder } from "../../tests/utils/builders/context-builder"; - -describe("NodeDirective", () => { - const defaultContext = new ContextBuilder().instance(); - - test("should generate label string with only the input typename", () => { - const instance = new NodeDirective({}); - const labelString = instance.getLabelsString("MyLabel", defaultContext); - - expect(labelString).toBe(":MyLabel"); - }); - - test("should generate label string with directive label", () => { - const instance = new NodeDirective({ labels: ["MyOtherLabel"] }); - const labelString = instance.getLabelsString("MyLabel", defaultContext); - - expect(labelString).toBe(":MyOtherLabel"); - }); - - test("should generate label string adding additional labels to input typename", () => { - const instance = new NodeDirective({ labels: ["MyLabel", "Label1", "Label2"] }); - const labelString = instance.getLabelsString("MyLabel", defaultContext); - - expect(labelString).toBe(":MyLabel:Label1:Label2"); - }); - - test("should generate label string adding additional labels to directive label", () => { - const instance = new NodeDirective({ labels: ["MyOtherLabel", "Label1", "Label2"] }); - const labelString = instance.getLabelsString("MyLabel", defaultContext); - - expect(labelString).toBe(":MyOtherLabel:Label1:Label2"); - }); - - test("should throw an error if there are no labels", () => { - const instance = new NodeDirective({}); - expect(() => { - instance.getLabelsString("", defaultContext); - }).toThrow(); - }); - - test("should escape context labels", () => { - const context = new ContextBuilder().instance(); - const instance = new NodeDirective({ - labels: ["label", "$context.escapeTest1", "$context.escapeTest2"], - }); - const labelString = instance.getLabelsString("label", { - ...context, - escapeTest1: "123-321", - escapeTest2: "He`l`lo", - } as Record); - expect(labelString).toBe(":label:`123-321`:`He``l``lo`"); - }); - - test("should escape jwt labels", () => { - const context = new ContextBuilder({ jwt: { escapeTest1: "123-321", escapeTest2: "He`l`lo" } }).instance(); - const instance = new NodeDirective({ - labels: ["label", "$jwt.escapeTest1", "$jwt.escapeTest2"], - }); - const labelString = instance.getLabelsString("label", context); - expect(labelString).toBe(":label:`123-321`:`He``l``lo`"); - }); - - test("should throw if jwt variable is missing in context", () => { - const context = new ContextBuilder({}).instance(); - const instance = new NodeDirective({ - labels: ["label", "$jwt.var1"], - }); - expect(() => { - instance.getLabelsString("label", context); - }).toThrow("Label value not found in context."); - }); - - test("should throw if context variable is missing in context", () => { - const context = new ContextBuilder({}).instance(); - const instance = new NodeDirective({ - labels: ["label", "$context.var1"], - }); - expect(() => { - instance.getLabelsString("label", context); - }).toThrow("Label value not found in context."); - }); - - test("should map labels from cypherParams", () => { - const context = new ContextBuilder({ cypherParams: { tenant: "BULK" } }).instance(); - const instance = new NodeDirective({ - labels: ["label", "$tenant"], - }); - const labelString = instance.getLabelsString("label", context); - expect(labelString).toBe(":label:BULK"); - }); -}); diff --git a/packages/graphql/src/classes/NodeDirective.ts b/packages/graphql/src/classes/NodeDirective.ts deleted file mode 100644 index 265371b88f..0000000000 --- a/packages/graphql/src/classes/NodeDirective.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Neo4jGraphQLError } from "./Error"; -import Cypher from "@neo4j/cypher-builder"; -import type { Neo4jGraphQLContext } from "../types/neo4j-graphql-context"; -import { mapLabelsWithContext } from "../schema-model/utils/map-labels-with-context"; - -export interface NodeDirectiveConstructor { - labels?: string[]; -} - -export class NodeDirective { - public readonly labels: string[]; - - constructor(input: NodeDirectiveConstructor) { - this.labels = input.labels || []; - } - - public getLabelsString(typeName: string, context: Neo4jGraphQLContext): string { - if (!typeName) { - throw new Neo4jGraphQLError("Could not generate label string in @node directive due to empty typeName"); - } - const labels = this.getLabels(typeName, context).map((label) => Cypher.utils.escapeLabel(label)); - return `:${labels.join(":")}`; - } - /** - * Returns the list containing labels mapped with the values contained in the Context. - * Be careful when using this method, labels returned are unescaped. - **/ - public getLabels(typeName: string, context: Neo4jGraphQLContext): string[] { - const labels = !this.labels.length ? [typeName] : this.labels; - return mapLabelsWithContext(labels, context); - } -} diff --git a/packages/graphql/src/classes/Relationship.ts b/packages/graphql/src/classes/Relationship.ts deleted file mode 100644 index 0d082f0e21..0000000000 --- a/packages/graphql/src/classes/Relationship.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { - CustomEnumField, - CustomResolverField, - CustomScalarField, - CypherField, - PointField, - PrimitiveField, - TemporalField, -} from "../types"; -import { GraphElement } from "./GraphElement"; -import type { MutableField } from "./Node"; - -interface RelationshipConstructor { - name: string; - type?: string; - source: string; // temporary addition to infer the source using the schema model - target: string; // temporary addition to infer the target using the schema model - relationshipFieldName: string; // temporary addition to infer the fieldName using the schema model - description?: string; - properties?: string; - cypherFields?: CypherField[]; - primitiveFields?: PrimitiveField[]; - scalarFields?: CustomScalarField[]; - enumFields?: CustomEnumField[]; - temporalFields?: TemporalField[]; - pointFields?: PointField[]; - customResolverFields?: CustomResolverField[]; -} -/** @deprecated */ -class Relationship extends GraphElement { - public properties?: string; - public source: string; - public target: string; - public relationshipFieldName: string; - - constructor(input: RelationshipConstructor) { - super({ - name: input.name, - description: input.description, - cypherFields: input.cypherFields || [], - primitiveFields: input.primitiveFields || [], - scalarFields: input.scalarFields || [], - enumFields: input.enumFields || [], - temporalFields: input.temporalFields || [], - pointFields: input.pointFields || [], - customResolverFields: input.customResolverFields || [], - }); - - this.properties = input.properties; - this.source = input.source; - this.target = input.target; - this.relationshipFieldName = input.relationshipFieldName; - } - // Fields you can set in a create or update mutation - public get mutableFields(): MutableField[] { - return [ - ...this.temporalFields, - ...this.enumFields, - ...this.scalarFields, // these are just custom scalars - ...this.primitiveFields, // these are instead built-in scalars - ...this.pointFields, - ]; - } -} - -export default Relationship; diff --git a/packages/graphql/src/classes/index.ts b/packages/graphql/src/classes/index.ts index e1f4ee462e..48cf1391f2 100644 --- a/packages/graphql/src/classes/index.ts +++ b/packages/graphql/src/classes/index.ts @@ -20,9 +20,6 @@ import { fieldExtensionsEstimator, simpleEstimator } from "graphql-query-complexity"; export * from "./Error"; -export { GraphElement } from "./GraphElement"; export { Neo4jDatabaseInfo } from "./Neo4jDatabaseInfo"; export { default as Neo4jGraphQL, Neo4jGraphQLConstructor } from "./Neo4jGraphQL"; -export { default as Node, NodeConstructor } from "./Node"; -export { default as Relationship } from "./Relationship"; export const DefaultComplexityEstimators = [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })]; diff --git a/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts b/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts index 2f32b87267..f4b3b76529 100644 --- a/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/CustomResolverAnnotation.ts @@ -20,7 +20,7 @@ import type { DocumentNode, FieldDefinitionNode } from "graphql"; import { parse } from "graphql"; import type { ResolveTree } from "graphql-parse-resolve-info"; -import { selectionSetToResolveTree } from "../../schema/get-custom-resolver-meta"; +import { selectionSetToResolveTree } from "../../schema/selection-set-to-resolve-tree"; import { getDefinitionCollection } from "../parser/definition-collection"; import type { Annotation } from "./Annotation"; diff --git a/packages/graphql/src/schema-model/generate-model.ts b/packages/graphql/src/schema-model/generate-model.ts index 8a04e9e5aa..bfc3d4989c 100644 --- a/packages/graphql/src/schema-model/generate-model.ts +++ b/packages/graphql/src/schema-model/generate-model.ts @@ -52,8 +52,12 @@ import { findDirective } from "./parser/utils"; import type { NestedOperation, QueryDirection, RelationshipDirection } from "./relationship/Relationship"; import { Relationship } from "./relationship/Relationship"; import { RelationshipDeclaration } from "./relationship/RelationshipDeclaration"; +import type { IResolvers } from "@graphql-tools/utils"; -export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { +export function generateModel( + document: DocumentNode, + userDefinedCustomResolvers?: IResolvers | IResolvers[] +): Neo4jGraphQLSchemaModel { const definitionCollection: DefinitionCollection = getDefinitionCollection(document); const operations: Operations = definitionCollection.operations.reduce((acc, definition): Operations => { @@ -65,7 +69,7 @@ export function generateModel(document: DocumentNode): Neo4jGraphQLSchemaModel { hydrateInterfacesToTypeNamesMap(definitionCollection); const concreteEntities = Array.from(definitionCollection.nodes.values()).map((node) => - generateConcreteEntity(node, definitionCollection) + generateConcreteEntity(node, definitionCollection, userDefinedCustomResolvers) ); const concreteEntitiesMap = concreteEntities.reduce((acc, entity) => { @@ -517,14 +521,15 @@ function generateRelationshipDeclaration( function generateConcreteEntity( definition: ObjectTypeDefinitionNode, - definitionCollection: DefinitionCollection + definitionCollection: DefinitionCollection, + userDefinedCustomResolvers?: IResolvers | IResolvers[] ): ConcreteEntity { const fields = (definition.fields || []).map((fieldDefinition) => { const isRelationshipAttribute = findDirective(fieldDefinition.directives, relationshipDirective.name); if (isRelationshipAttribute) { return; } - return parseAttribute(fieldDefinition, definitionCollection, definition.fields); + return parseAttribute(fieldDefinition, definitionCollection, definition.fields, userDefinedCustomResolvers); }); // schema configuration directives are propagated onto concrete entities diff --git a/packages/graphql/src/schema-model/parser/parse-attribute.ts b/packages/graphql/src/schema-model/parser/parse-attribute.ts index 9eb4029168..afe4881db6 100644 --- a/packages/graphql/src/schema-model/parser/parse-attribute.ts +++ b/packages/graphql/src/schema-model/parser/parse-attribute.ts @@ -44,6 +44,7 @@ import type { DefinitionCollection } from "./definition-collection"; import { parseAnnotations } from "./parse-annotation"; import { parseArguments } from "./parse-arguments"; import { findDirective } from "./utils"; +import type { IResolvers } from "@graphql-tools/utils"; export function parseAttributeArguments( fieldArgs: readonly InputValueDefinitionNode[], @@ -62,7 +63,8 @@ export function parseAttributeArguments( export function parseAttribute( field: FieldDefinitionNode, definitionCollection: DefinitionCollection, - definitionFields?: ReadonlyArray + definitionFields?: ReadonlyArray, + userDefinedCustomResolvers?: IResolvers | IResolvers[] ): Attribute { const name = field.name.value; const type = parseTypeNode(definitionCollection, field.type); @@ -70,6 +72,11 @@ export function parseAttribute( const annotations = parseAnnotations(field.directives || []); annotations.customResolver?.parseRequire(definitionCollection.document, definitionFields); + if (annotations.customResolver) { + if (!userDefinedCustomResolvers?.[name]) { + console.warn(`Custom resolver for ${name} has not been provided`); + } + } const databaseName = getDatabaseName(field); return new Attribute({ diff --git a/packages/graphql/src/schema/create-connection-fields.ts b/packages/graphql/src/schema/create-connection-fields.ts deleted file mode 100644 index a9ceaacf64..0000000000 --- a/packages/graphql/src/schema/create-connection-fields.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Relationship } from "../classes"; -import { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import type { InterfaceEntityAdapter } from "../schema-model/entity/model-adapters/InterfaceEntityAdapter"; -import { RelationshipAdapter } from "../schema-model/relationship/model-adapters/RelationshipAdapter"; -import type { RelationshipDeclarationAdapter } from "../schema-model/relationship/model-adapters/RelationshipDeclarationAdapter"; -import type { ObjectFields } from "./get-obj-field-meta"; - -/** - * TODO [translation-layer-compatibility] - * this file only contains old Relationship class construction - * safe to delete when no longer needed - */ -export function createConnectionFields({ - entityAdapter, - relationshipFields, -}: { - entityAdapter: ConcreteEntityAdapter | InterfaceEntityAdapter; - relationshipFields: Map; -}): Relationship[] { - const relationships: Relationship[] = []; - - const entityRelationships = - entityAdapter instanceof ConcreteEntityAdapter - ? entityAdapter.relationships - : entityAdapter.relationshipDeclarations; - - entityRelationships.forEach((relationship: RelationshipAdapter | RelationshipDeclarationAdapter) => { - const relFields: ObjectFields | undefined = - relationship instanceof RelationshipAdapter && relationship.propertiesTypeName - ? relationshipFields.get(relationship.propertiesTypeName) - : undefined; - - const r = new Relationship({ - name: relationship.operations.relationshipFieldTypename, - type: relationship instanceof RelationshipAdapter ? relationship.type : undefined, - source: relationship.source.name, - target: relationship.target.name, - properties: relationship instanceof RelationshipAdapter ? relationship.propertiesTypeName : undefined, - relationshipFieldName: relationship.name, - ...(relFields - ? { - temporalFields: relFields.temporalFields, - scalarFields: relFields.scalarFields, - primitiveFields: relFields.primitiveFields, - enumFields: relFields.enumFields, - pointFields: relFields.pointFields, - customResolverFields: relFields.customResolverFields, - } - : {}), - }); - relationships.push(r); - }); - - return relationships; -} diff --git a/packages/graphql/src/schema/create-global-nodes.ts b/packages/graphql/src/schema/create-global-nodes.ts index b2bbe1381d..c3a8d15355 100644 --- a/packages/graphql/src/schema/create-global-nodes.ts +++ b/packages/graphql/src/schema/create-global-nodes.ts @@ -20,22 +20,16 @@ import type { GraphQLResolveInfo } from "graphql"; import type { ObjectTypeComposerFieldConfigAsObjectDefinition, SchemaComposer } from "graphql-compose"; import { nodeDefinitions } from "graphql-relay"; -import type { Node } from "../types"; import { globalNodeResolver } from "./resolvers/query/global-node"; import type { Neo4jGraphQLComposedContext } from "./resolvers/composition/wrap-query-and-mutation"; import type { ConcreteEntity } from "../schema-model/entity/ConcreteEntity"; import { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; // returns true if globalNodeFields added or false if not -export function addGlobalNodeFields( - nodes: Node[], - composer: SchemaComposer, - concreteEntities: ConcreteEntity[] -): boolean { - const globalNodes = nodes.filter((n) => n.isGlobalNode); +export function addGlobalNodeFields(composer: SchemaComposer, concreteEntities: ConcreteEntity[]): boolean { const globalEntities = concreteEntities.map((e) => new ConcreteEntityAdapter(e)).filter((e) => e.isGlobalNode()); - if (globalNodes.length === 0) return false; + if (globalEntities.length === 0) return false; const fetchById = (id: string, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) => { const resolver = globalNodeResolver({ entities: globalEntities }); diff --git a/packages/graphql/src/schema/get-custom-resolver-meta.test.ts b/packages/graphql/src/schema/get-custom-resolver-meta.test.ts deleted file mode 100644 index 405baedf47..0000000000 --- a/packages/graphql/src/schema/get-custom-resolver-meta.test.ts +++ /dev/null @@ -1,872 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { - FieldDefinitionNode, - InterfaceTypeDefinitionNode, - ObjectTypeDefinitionNode, - UnionTypeDefinitionNode, -} from "graphql"; -import { Kind } from "graphql"; -import { generateResolveTree } from "../translate/utils/resolveTree"; -import { getCustomResolverMeta, INVALID_SELECTION_SET_ERROR } from "./get-custom-resolver-meta"; - -describe("getCustomResolverMeta", () => { - const authorType = "Author"; - const bookType = "Book"; - const journalType = "Journal"; - const publicationInterface = "Publication"; - - const customResolverField = "publicationsWithAuthor"; - - const objects: ObjectTypeDefinitionNode[] = [ - { - kind: Kind.OBJECT_TYPE_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: authorType, - }, - interfaces: [], - directives: [], - fields: [ - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "name", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "String", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "publications", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.LIST_TYPE, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: publicationInterface, - }, - }, - }, - }, - }, - directives: [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "relationship", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "type", - }, - value: { - kind: Kind.STRING, - value: "WROTE", - block: false, - }, - }, - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "direction", - }, - value: { - kind: Kind.ENUM, - value: "OUT", - }, - }, - ], - }, - ], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: customResolverField, - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.LIST_TYPE, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "String", - }, - }, - }, - }, - }, - directives: [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "customResolver", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "requires", - }, - value: { - kind: Kind.STRING, - value: "name publications { publicationYear ...on Book { title } ... on Journal { subject } }", - block: false, - }, - }, - ], - }, - ], - }, - ], - }, - { - kind: Kind.OBJECT_TYPE_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: bookType, - }, - interfaces: [ - { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: publicationInterface, - }, - }, - ], - directives: [], - fields: [ - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "title", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "String", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "publicationYear", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "Int", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "author", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.LIST_TYPE, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: authorType, - }, - }, - }, - }, - }, - directives: [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "relationship", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "type", - }, - value: { - kind: Kind.STRING, - value: "WROTE", - block: false, - }, - }, - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "direction", - }, - value: { - kind: Kind.ENUM, - value: "IN", - }, - }, - ], - }, - ], - }, - ], - }, - { - kind: Kind.OBJECT_TYPE_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: journalType, - }, - interfaces: [ - { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: publicationInterface, - }, - }, - ], - directives: [], - fields: [ - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "subject", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "String", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "publicationYear", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "Int", - }, - }, - }, - directives: [], - }, - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "author", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.LIST_TYPE, - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: authorType, - }, - }, - }, - }, - }, - directives: [ - { - kind: Kind.DIRECTIVE, - name: { - kind: Kind.NAME, - value: "relationship", - }, - arguments: [ - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "type", - }, - value: { - kind: Kind.STRING, - value: "WROTE", - block: false, - }, - }, - { - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: "direction", - }, - value: { - kind: Kind.ENUM, - value: "IN", - }, - }, - ], - }, - ], - }, - ], - }, - ]; - - const interfaces: InterfaceTypeDefinitionNode[] = [ - { - kind: Kind.INTERFACE_TYPE_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: publicationInterface, - }, - interfaces: [], - directives: [], - fields: [ - { - kind: Kind.FIELD_DEFINITION, - description: undefined, - name: { - kind: Kind.NAME, - value: "publicationYear", - }, - arguments: [], - type: { - kind: Kind.NON_NULL_TYPE, - type: { - kind: Kind.NAMED_TYPE, - name: { - kind: Kind.NAME, - value: "Int", - }, - }, - }, - directives: [], - }, - ], - }, - ]; - - const unions: UnionTypeDefinitionNode[] = []; - - const object = objects.find((obj) => obj.name.value === authorType) as ObjectTypeDefinitionNode; - - const resolvers = { - [customResolverField]: () => 25, - }; - - let warn: jest.SpyInstance; - - beforeEach(() => { - warn = jest.spyOn(console, "warn").mockImplementation(() => {}); - }); - - afterEach(() => { - warn.mockReset(); - }); - - test("should return undefined if no directive found", () => { - // @ts-ignore - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "RANDOM 1" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const result = getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).not.toHaveBeenCalled(); - - expect(result).toBeUndefined(); - }); - test("should return no required fields if no requires argument", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const result = getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).not.toHaveBeenCalled(); - - expect(result).toEqual({ - requiredFields: {}, - }); - }); - test("should return the correct meta when a list of required fields is provided", () => { - const requiredFields = "name"; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const result = getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).not.toHaveBeenCalled(); - - expect(result).toEqual({ - requiredFields: generateResolveTree({ name: requiredFields }), - }); - }); - - test("should return the correct meta when a selection set of required fields provided", () => { - const requiredFields = `name publications { publicationYear ...on ${bookType} { title } ... on ${journalType} { subject } }`; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const result = getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).not.toHaveBeenCalled(); - - expect(result).toEqual({ - requiredFields: { - name: { - name: "name", - alias: "name", - args: {}, - fieldsByTypeName: {}, - }, - publications: { - name: "publications", - alias: "publications", - args: {}, - fieldsByTypeName: { - Publication: { - publicationYear: { - name: "publicationYear", - alias: "publicationYear", - args: {}, - fieldsByTypeName: {}, - }, - }, - Book: { - title: { - name: "title", - alias: "title", - args: {}, - fieldsByTypeName: {}, - }, - }, - Journal: { - subject: { - name: "subject", - alias: "subject", - args: {}, - fieldsByTypeName: {}, - }, - }, - }, - }, - }, - }); - }); - - test("should throw an error if a non-existant field is passed to the required selection set", () => { - const requiredFields = `name publications doesNotExist { publicationYear ...on ${bookType} { title } ... on ${journalType} { subject } }`; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - expect(() => - getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }) - ).toThrow(INVALID_SELECTION_SET_ERROR); - }); - - test("Check throws error if customResolver is not provided", () => { - const requiredFields = "name"; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const resolvers = {}; - - getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).toHaveBeenCalledWith(`Custom resolver for ${customResolverField} has not been provided`); - }); - test("Check throws error if customResolver defined on interface", () => { - const requiredFields = "name"; - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "customResolver", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "requires" }, - // @ts-ignore - value: { - kind: Kind.STRING, - value: requiredFields, - }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - name: { - kind: Kind.NAME, - value: customResolverField, - }, - }; - - const resolvers = { - [publicationInterface]: { - [customResolverField]: () => "Hello World!", - }, - }; - - getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers: resolvers, - }); - - expect(warn).toHaveBeenCalledWith(`Custom resolver for ${customResolverField} has not been provided`); - }); -}); diff --git a/packages/graphql/src/schema/get-cypher-meta.test.ts b/packages/graphql/src/schema/get-cypher-meta.test.ts deleted file mode 100644 index 9f93ff06d9..0000000000 --- a/packages/graphql/src/schema/get-cypher-meta.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { FieldDefinitionNode } from "graphql"; -import { getCypherMeta } from "./get-cypher-meta"; - -describe("getCypherMeta", () => { - test("should return undefined if no directive found", () => { - // @ts-ignore - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "RANDOM 1" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - const result = getCypherMeta(field); - - expect(result).toBeUndefined(); - }); - - test("should throw statement required", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "cypher", arguments: [] }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - expect(() => getCypherMeta(field)).toThrow("@cypher statement required"); - }); - - test("should throw statement not a string", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "cypher", - // @ts-ignore - }, - arguments: [ - { - // @ts-ignore - name: { value: "statement" }, - // @ts-ignore - value: { kind: "NOT A STRING!" }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - expect(() => getCypherMeta(field)).toThrow("@cypher statement is not a string"); - }); - - test("should return the correct meta", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "cypher", - }, - arguments: [ - { - // @ts-ignore - name: { value: "statement" }, - // @ts-ignore - value: { kind: "StringValue", value: "MATCH (m:Movie) RETURN m" }, - }, - { - // @ts-ignore - name: { value: "columnName" }, - // @ts-ignore - value: { kind: "StringValue", value: "m" }, - }, - ], - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - const result = getCypherMeta(field); - - expect(result).toMatchObject({ - statement: "MATCH (m:Movie) RETURN m", - }); - }); -}); diff --git a/packages/graphql/src/schema/get-cypher-meta.ts b/packages/graphql/src/schema/get-cypher-meta.ts deleted file mode 100644 index ebc327a164..0000000000 --- a/packages/graphql/src/schema/get-cypher-meta.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { Kind, type DirectiveNode, type FieldDefinitionNode } from "graphql"; - -type CypherMeta = { - statement: string; - columnName: string; -}; - -export function getCypherMeta( - field: FieldDefinitionNode, - interfaceField?: FieldDefinitionNode -): CypherMeta | undefined { - const directive = - field.directives?.find((x) => x.name.value === "cypher") || - interfaceField?.directives?.find((x) => x.name.value === "cypher"); - if (!directive) { - return undefined; - } - - return { - statement: parseStatementFlag(directive), - columnName: parseColumnNameFlag(directive), - }; -} - -function parseStatementFlag(directive: DirectiveNode): string { - const stmtArg = directive.arguments?.find((x) => x.name.value === "statement"); - if (!stmtArg) { - throw new Error("@cypher statement required"); - } - if (stmtArg.value.kind !== Kind.STRING) { - throw new Error("@cypher statement is not a string"); - } - - return stmtArg.value.value; -} - -function parseColumnNameFlag(directive: DirectiveNode): string { - const stmtArg = directive.arguments?.find((x) => x.name.value === "columnName"); - if (!stmtArg) { - throw new Error("@cypher columnName required"); - } - if (stmtArg.value.kind !== Kind.STRING) { - throw new Error("@cypher columnName is not a string"); - } - - return stmtArg.value.value; -} diff --git a/packages/graphql/src/schema/get-nodes.ts b/packages/graphql/src/schema/get-nodes.ts deleted file mode 100644 index 93fe13276e..0000000000 --- a/packages/graphql/src/schema/get-nodes.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { IResolvers } from "@graphql-tools/utils"; -import type { NamedTypeNode } from "graphql"; -import { Node } from "../classes"; -import type { LimitDirective } from "../classes/LimitDirective"; -import type { NodeDirective } from "../classes/NodeDirective"; -import type { DefinitionCollection } from "../schema-model/parser/definition-collection"; -import type { Neo4jGraphQLCallbacks } from "../types"; -import { asArray } from "../utils/utils"; -import { getObjFieldMeta } from "./get-obj-field-meta"; -import parseNodeDirective from "./parse-node-directive"; -import { parseLimitDirective } from "./parse/parse-limit-directive"; -import parsePluralDirective from "./parse/parse-plural-directive"; - -type Nodes = { - nodes: Node[]; - pointInTypeDefs: boolean; - cartesianPointInTypeDefs: boolean; - floatWhereInTypeDefs: boolean; - relationshipPropertyInterfaceNames: Set; - interfaceRelationshipNames: Set; -}; - -function getNodes( - definitionCollection: DefinitionCollection, - options: { - callbacks?: Neo4jGraphQLCallbacks; - userCustomResolvers?: IResolvers | Array; - } -): Nodes { - let pointInTypeDefs = false; - let cartesianPointInTypeDefs = false; - const floatWhereInTypeDefs = false; - - const relationshipPropertyInterfaceNames = new Set(); - const interfaceRelationshipNames = new Set(); - - const nodes: Node[] = []; - for (const definition of definitionCollection.nodes.values()) { - const otherDirectives = (definition.directives || []).filter( - (x) => - ![ - "authorization", - "authentication", - "exclude", - "node", - "fulltext", - "limit", - "plural", - "shareable", - "subscriptionsAuthorization", - "deprecated", - "query", - "mutation", - "subscription", - "jwt", - ].includes(x.name.value) - ); - const propagatedDirectives = (definition.directives || []).filter((x) => - ["deprecated", "shareable"].includes(x.name.value) - ); - - const nodeDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "node"); - const pluralDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "plural"); - const limitDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "limit"); - const nodeInterfaces = [...(definition.interfaces || [])] as NamedTypeNode[]; - - let nodeDirective: NodeDirective; - if (nodeDirectiveDefinition) { - nodeDirective = parseNodeDirective(nodeDirectiveDefinition); - } - - const userCustomResolvers = asArray(options.userCustomResolvers); - const customResolvers = userCustomResolvers.find((r) => !!r[definition.name.value])?.[ - definition.name.value - ] as IResolvers; - - const nodeFields = getObjFieldMeta({ - obj: definition, - definitionCollection, - interfaces: [...definitionCollection.interfaceTypes.values()], - callbacks: options.callbacks, - customResolvers, - }); - - let limitDirective: LimitDirective | undefined; - if (limitDirectiveDefinition) { - limitDirective = parseLimitDirective({ - directive: limitDirectiveDefinition, - definition, - }); - } - - nodeFields.relationFields.forEach((relationship) => { - if (relationship.properties) { - relationshipPropertyInterfaceNames.add(relationship.properties); - } - if (relationship.interface) { - interfaceRelationshipNames.add(relationship.typeMeta.name); - } - }); - - if (!pointInTypeDefs) { - pointInTypeDefs = nodeFields.pointFields.some((field) => field.typeMeta.name === "Point"); - } - if (!cartesianPointInTypeDefs) { - cartesianPointInTypeDefs = nodeFields.pointFields.some((field) => field.typeMeta.name === "CartesianPoint"); - } - - const globalIdFields = nodeFields.primitiveFields.filter((field) => field.isGlobalIdField); - - const globalIdField = globalIdFields[0]; - - const node = new Node({ - name: definition.name.value, - interfaces: nodeInterfaces, - otherDirectives, - propagatedDirectives, - ...nodeFields, - // @ts-ignore we can be sure it's defined - nodeDirective, - limitDirective, - description: definition.description?.value, - isGlobalNode: Boolean(globalIdField), - globalIdField: globalIdField?.fieldName, - globalIdFieldIsInt: globalIdField?.typeMeta?.name === "Int", - plural: parsePluralDirective(pluralDirectiveDefinition), - }); - - nodes.push(node); - } - return { - nodes, - pointInTypeDefs, - cartesianPointInTypeDefs, - floatWhereInTypeDefs, - relationshipPropertyInterfaceNames, - interfaceRelationshipNames, - }; -} - -export default getNodes; diff --git a/packages/graphql/src/schema/get-obj-field-meta.ts b/packages/graphql/src/schema/get-obj-field-meta.ts deleted file mode 100644 index 0f1362bf1e..0000000000 --- a/packages/graphql/src/schema/get-obj-field-meta.ts +++ /dev/null @@ -1,544 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { IResolvers } from "@graphql-tools/utils"; -import type { - BooleanValueNode, - DirectiveNode, - EnumValueNode, - InterfaceTypeDefinitionNode, - ListValueNode, - NamedTypeNode, - ObjectTypeDefinitionNode, - StringValueNode, -} from "graphql"; -import { Kind } from "graphql"; -import { SCALAR_TYPES, SPATIAL_TYPES, TEMPORAL_SCALAR_TYPES } from "../constants"; -import type { DefinitionCollection } from "../schema-model/parser/definition-collection"; -import { parseArgumentsFromUnknownDirective } from "../schema-model/parser/parse-arguments"; -import { parseValueNode } from "../schema-model/parser/parse-value-node"; -import type { - BaseField, - ConnectionField, - CustomEnumField, - CustomResolverField, - CustomScalarField, - CypherField, - FilterableOptions, - InterfaceField, - Neo4jGraphQLCallbacks, - ObjectField, - PointField, - PrimitiveField, - RelationField, - SelectableOptions, - SettableOptions, - TemporalField, - TimeStampOperations, - UnionField, -} from "../types"; -import { upperFirst } from "../utils/upper-first"; -import getAliasMeta from "./get-alias-meta"; -import { getCustomResolverMeta } from "./get-custom-resolver-meta"; -import { getCypherMeta } from "./get-cypher-meta"; -import getFieldTypeMeta from "./get-field-type-meta"; -import { getPopulatedByMeta } from "./get-populated-by-meta"; -import getRelationshipMeta from "./get-relationship-meta"; - -export interface ObjectFields { - relationFields: RelationField[]; - connectionFields: ConnectionField[]; - primitiveFields: PrimitiveField[]; - cypherFields: CypherField[]; - scalarFields: CustomScalarField[]; - enumFields: CustomEnumField[]; - unionFields: UnionField[]; - interfaceFields: InterfaceField[]; - objectFields: ObjectField[]; - temporalFields: TemporalField[]; - pointFields: PointField[]; - customResolverFields: CustomResolverField[]; -} - -export function getObjFieldMeta({ - obj, - definitionCollection, - interfaces, - callbacks, - customResolvers, -}: { - obj: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode; - definitionCollection: DefinitionCollection; - interfaces: InterfaceTypeDefinitionNode[]; - callbacks?: Neo4jGraphQLCallbacks; - customResolvers?: IResolvers | Array; -}): ObjectFields { - const objInterfaceNames = [...(obj.interfaces || [])] as NamedTypeNode[]; - const objects = [...definitionCollection.nodes.values()]; - const unions = [...definitionCollection.unionTypes.values()]; - const objInterfaces = interfaces.filter((i) => objInterfaceNames.map((n) => n.name.value).includes(i.name.value)); - - return obj?.fields?.reduce( - (res: ObjectFields, field) => { - const interfaceField = objInterfaces - .find((i) => i.fields?.map((f) => f.name.value).includes(field.name.value)) - ?.fields?.find((f) => f.name.value === field.name.value); - - // Create array of directives for this field. Field directives override interface field directives. - const directives = [ - ...(field?.directives || []), - ...(interfaceField?.directives || []).filter( - (d) => !field.directives?.find((fd) => fd.name.value === d.name.value) - ), - ]; - - if (directives.some((x) => x.name.value === "private")) { - return res; - } - - const relationshipMeta = getRelationshipMeta(field, interfaceField); - const cypherMeta = getCypherMeta(field, interfaceField); - const customResolverMeta = getCustomResolverMeta({ - field, - object: obj, - objects, - interfaces, - unions, - customResolvers, - interfaceField, - }); - const typeMeta = getFieldTypeMeta(field.type); - const idDirective = directives.find((x) => x.name.value === "id"); - const relayIdDirective = directives.find((x) => x.name.value === "relayId"); - const defaultDirective = directives.find((x) => x.name.value === "default"); - const coalesceDirective = directives.find((x) => x.name.value === "coalesce"); - const timestampDirective = directives.find((x) => x.name.value === "timestamp"); - const aliasDirective = directives.find((x) => x.name.value === "alias"); - const populatedByDirective = directives.find((x) => x.name.value === "populatedBy"); - const selectableDirective = directives.find((x) => x.name.value === "selectable"); - const settableDirective = directives.find((x) => x.name.value === "settable"); - const filterableDirective = directives.find((x) => x.name.value === "filterable"); - - const fieldInterface = definitionCollection.interfaceTypes.get(typeMeta.name); - const fieldUnion = definitionCollection.unionTypes.get(typeMeta.name); - const fieldScalar = definitionCollection.scalarTypes.get(typeMeta.name); - const fieldEnum = definitionCollection.enumTypes.get(typeMeta.name); - const fieldObject = definitionCollection.objectTypes.get(typeMeta.name); - const fieldTemporal = TEMPORAL_SCALAR_TYPES.includes(typeMeta.name); - const fieldPoint = SPATIAL_TYPES.includes(typeMeta.name); - - const selectableOptions = parseSelectableDirective(selectableDirective); - const settableOptions = parseSettableDirective(settableDirective); - const filterableOptions = parseFilterableDirective(filterableDirective); - - const baseField: BaseField = { - fieldName: field.name.value, - dbPropertyName: field.name.value, - typeMeta, - selectableOptions, - settableOptions, - filterableOptions, - otherDirectives: (directives || []).filter( - (x) => - ![ - "relationship", - "cypher", - "id", - "authorization", - "authentication", - "customResolver", - "default", - "coalesce", - "timestamp", - "alias", - "callback", - "populatedBy", - "jwtClaim", - "selectable", - "settable", - "subscriptionsAuthorization", - "filterable", - "relayId", - ].includes(x.name.value) - ), - arguments: [...(field.arguments || [])], - description: field.description?.value, - }; - - if (aliasDirective) { - const aliasMeta = getAliasMeta(aliasDirective); - if (aliasMeta) { - baseField.dbPropertyName = aliasMeta.property; - baseField.dbPropertyNameUnescaped = aliasMeta.propertyUnescaped; - } - } - - if (relationshipMeta) { - const relationField: RelationField = { - ...baseField, - ...relationshipMeta, - inherited: false, - }; - - if (fieldUnion) { - const nodes: string[] = []; - - fieldUnion.types?.forEach((type) => { - nodes.push(type.name.value); - }); - - const unionField: UnionField = { - ...baseField, - nodes, - }; - - relationField.union = unionField; - } - - if (fieldInterface) { - const implementations = objects - .filter((n) => n.interfaces?.some((i) => i.name.value === fieldInterface.name.value)) - .map((n) => n.name.value); - - relationField.interface = { - ...baseField, - implementations, - }; - } - - // TODO: This will be brittle if more than one interface - - let connectionPrefix = obj.name.value; - - if (obj.interfaces && obj.interfaces.length) { - const firstInterface = obj.interfaces[0]; - if (!firstInterface) { - throw new Error("Cannot get interface in getObjFieldMeta"); - } - - const inter = interfaces.find( - (i) => i.name.value === firstInterface.name.value - ) as InterfaceTypeDefinitionNode; - - if (inter.fields?.some((f) => f.name.value === baseField.fieldName)) { - connectionPrefix = firstInterface.name.value; - relationField.inherited = true; - } - } - - relationField.connectionPrefix = connectionPrefix; - - const connectionTypeName = `${connectionPrefix}${upperFirst(`${baseField.fieldName}Connection`)}`; - const relationshipTypeName = `${connectionPrefix}${upperFirst(`${baseField.fieldName}Relationship`)}`; - - const connectionField: ConnectionField = { - fieldName: `${baseField.fieldName}Connection`, - relationshipTypeName, - selectableOptions, - settableOptions, - filterableOptions, - typeMeta: { - name: connectionTypeName, - required: true, - pretty: `${connectionTypeName}!`, - input: { - where: { - type: `${connectionTypeName}Where`, - pretty: `${connectionTypeName}Where`, - }, - create: { - type: "", - pretty: "", - }, - update: { - type: "", - pretty: "", - }, - }, - }, - otherDirectives: baseField.otherDirectives, - arguments: [...(field.arguments || [])], - description: field.description?.value, - relationship: relationField, - }; - - res.relationFields.push(relationField); - res.connectionFields.push(connectionField); - } else if (cypherMeta) { - const cypherField: CypherField = { - ...baseField, - ...cypherMeta, - isEnum: !!fieldEnum, - isScalar: !!fieldScalar || SCALAR_TYPES.includes(typeMeta.name), - }; - res.cypherFields.push(cypherField); - } else if (customResolverMeta) { - res.customResolverFields.push({ ...baseField, ...customResolverMeta }); - } else if (fieldScalar) { - const scalarField: CustomScalarField = { - ...baseField, - }; - res.scalarFields.push(scalarField); - } else if (fieldEnum) { - const enumField: CustomEnumField = { - kind: "Enum", - ...baseField, - }; - - if (defaultDirective) { - const defaultValue = defaultDirective.arguments?.find((a) => a.name.value === "value")?.value; - - if (enumField.typeMeta.array) { - enumField.defaultValue = (defaultValue as ListValueNode).values.map((v) => { - return (v as EnumValueNode).value; - }); - } else { - enumField.defaultValue = (defaultValue as EnumValueNode).value; - } - } - - if (coalesceDirective) { - const coalesceValue = coalesceDirective.arguments?.find((a) => a.name.value === "value")?.value; - - if (enumField.typeMeta.array) { - enumField.coalesceValue = (coalesceValue as ListValueNode).values.map((v) => { - return (v as EnumValueNode).value; - }); - } else { - // TODO: avoid duplication with primitives - enumField.coalesceValue = `"${(coalesceValue as EnumValueNode).value}"`; - } - } - - res.enumFields.push(enumField); - } else if (fieldUnion) { - const unionField: UnionField = { - ...baseField, - }; - res.unionFields.push(unionField); - } else if (fieldInterface) { - res.interfaceFields.push({ - ...baseField, - }); - } else if (fieldObject) { - const objectField: ObjectField = { - ...baseField, - }; - res.objectFields.push(objectField); - } else { - if (fieldTemporal) { - const temporalField: TemporalField = { - ...baseField, - }; - - if (populatedByDirective) { - const callback = getPopulatedByMeta(populatedByDirective, callbacks); - temporalField.callback = callback; - } - - if (timestampDirective) { - const operations = timestampDirective?.arguments?.find((x) => x.name.value === "operations") - ?.value as ListValueNode; - - const timestamps = operations - ? (operations?.values.map((x) => parseValueNode(x)) as TimeStampOperations[]) - : (["CREATE", "UPDATE"] as TimeStampOperations[]); - - temporalField.timestamps = timestamps; - } - - if (defaultDirective) { - const value = defaultDirective.arguments?.find((a) => a.name.value === "value")?.value; - temporalField.defaultValue = (value as StringValueNode).value; - } - - res.temporalFields.push(temporalField); - } else if (fieldPoint) { - const pointField: PointField = { - ...baseField, - }; - res.pointFields.push(pointField); - } else { - const primitiveField: PrimitiveField = { - ...baseField, - }; - - if (populatedByDirective) { - const callback = getPopulatedByMeta(populatedByDirective, callbacks); - primitiveField.callback = callback; - } - - if (idDirective) { - // TODO: remove the following as the argument "autogenerate" does not exists anymore - const autogenerate = idDirective.arguments?.find((a) => a.name.value === "autogenerate"); - if (!autogenerate || (autogenerate.value as BooleanValueNode).value) { - primitiveField.autogenerate = true; - } - } - - if (relayIdDirective) { - primitiveField.isGlobalIdField = true; - } - - if (defaultDirective) { - const value = defaultDirective.arguments?.find((a) => a.name.value === "value")?.value; - - const typeError = `Default value for ${obj.name.value}.${primitiveField.fieldName} does not have matching type ${primitiveField.typeMeta.name}`; - - switch (baseField.typeMeta.name) { - case "ID": - case "String": - if (value?.kind !== Kind.STRING) { - throw new Error(typeError); - } - primitiveField.defaultValue = value.value; - break; - case "Boolean": - if (value?.kind !== Kind.BOOLEAN) { - throw new Error(typeError); - } - primitiveField.defaultValue = value.value; - break; - case "Int": - if (value?.kind !== Kind.INT) { - throw new Error(typeError); - } - primitiveField.defaultValue = parseInt(value.value, 10); - break; - case "BigInt": - if (value?.kind !== Kind.INT && value?.kind !== Kind.STRING) { - throw new Error(typeError); - } - primitiveField.defaultValue = parseInt(value.value, 10); - break; - case "Float": - if (value?.kind !== Kind.FLOAT && value?.kind !== Kind.INT) { - throw new Error(typeError); - } - primitiveField.defaultValue = parseFloat(value.value); - break; - default: - throw new Error( - "@default directive can only be used on fields of type Int, Float, String, Boolean, ID, BigInt, DateTime, Date, Time, LocalDateTime or LocalTime." - ); - } - } - - if (coalesceDirective) { - const value = coalesceDirective.arguments?.find((a) => a.name.value === "value")?.value; - - const typeError = `coalesce() value for ${obj.name.value}.${primitiveField.fieldName} does not have matching type ${primitiveField.typeMeta.name}`; - - switch (baseField.typeMeta.name) { - case "ID": - case "String": - if (value?.kind !== Kind.STRING) { - throw new Error(typeError); - } - primitiveField.coalesceValue = `"${value.value}"`; - break; - case "Boolean": - if (value?.kind !== Kind.BOOLEAN) { - throw new Error(typeError); - } - primitiveField.coalesceValue = value.value; - break; - case "Int": - if (value?.kind !== Kind.INT) { - throw new Error(typeError); - } - primitiveField.coalesceValue = parseInt(value.value, 10); - break; - case "Float": - if (value?.kind !== Kind.FLOAT) { - throw new Error(typeError); - } - primitiveField.coalesceValue = parseFloat(value.value); - break; - default: - throw new Error( - "@coalesce directive can only be used on types: Int | Float | String | Boolean | ID | DateTime" - ); - } - } - - res.primitiveFields.push(primitiveField); - } - } - - return res; - }, - { - relationFields: [], - connectionFields: [], - primitiveFields: [], - cypherFields: [], - scalarFields: [], - enumFields: [], - unionFields: [], - interfaceFields: [], - objectFields: [], - temporalFields: [], - pointFields: [], - customResolverFields: [], - } - ) as ObjectFields; -} - -function parseSelectableDirective(directive: DirectiveNode | undefined): SelectableOptions { - const defaultArguments = { - onRead: true, - onAggregate: true, - }; - - const args: Partial = directive ? parseArgumentsFromUnknownDirective(directive) : {}; - - return { - onRead: args.onRead ?? defaultArguments.onRead, - onAggregate: args.onAggregate ?? defaultArguments.onAggregate, - }; -} - -function parseSettableDirective(directive: DirectiveNode | undefined): SettableOptions { - const defaultArguments = { - onCreate: true, - onUpdate: true, - }; - - const args: Partial = directive ? parseArgumentsFromUnknownDirective(directive) : {}; - - return { - onCreate: args.onCreate ?? defaultArguments.onCreate, - onUpdate: args.onUpdate ?? defaultArguments.onUpdate, - }; -} - -function parseFilterableDirective(directive: DirectiveNode | undefined): FilterableOptions { - const defaultArguments = { - byValue: true, - byAggregate: directive === undefined ? true : false, - }; - - const args: Partial = directive ? parseArgumentsFromUnknownDirective(directive) : {}; - - return { - byValue: args.byValue ?? defaultArguments.byValue, - byAggregate: args.byAggregate ?? defaultArguments.byAggregate, - }; -} diff --git a/packages/graphql/src/schema/get-populated-by-meta.ts b/packages/graphql/src/schema/get-populated-by-meta.ts deleted file mode 100644 index db95373c39..0000000000 --- a/packages/graphql/src/schema/get-populated-by-meta.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode, ArgumentNode, ListValueNode, StringValueNode } from "graphql"; -import type { Callback, CallbackOperations, Neo4jGraphQLCallbacks } from "../types"; - -export function getPopulatedByMeta(directive: DirectiveNode, callbacks?: Neo4jGraphQLCallbacks): Callback { - const operationsArg = directive.arguments?.find((x) => x.name.value === "operations") as ArgumentNode; - const callbackArg = directive.arguments?.find((x) => x.name.value === "callback") as ArgumentNode; - - const operationsList = operationsArg.value as ListValueNode; - const operations = operationsList.values.map((value) => (value as StringValueNode).value) as CallbackOperations[]; - const callbackName = (callbackArg?.value as StringValueNode)?.value; - - if (typeof (callbacks || {})[callbackName] !== "function") { - throw new Error(`PopulatedBy callback '${callbackName}' must be of type function`); - } - - return { - operations, - callbackName, - }; -} diff --git a/packages/graphql/src/schema/get-relationship-meta.test.ts b/packages/graphql/src/schema/get-relationship-meta.test.ts deleted file mode 100644 index d6234a1f73..0000000000 --- a/packages/graphql/src/schema/get-relationship-meta.test.ts +++ /dev/null @@ -1,629 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { FieldDefinitionNode } from "graphql"; -import { Kind } from "graphql"; -import getRelationshipMeta from "./get-relationship-meta"; - -describe("getRelationshipMeta", () => { - test("should return undefined if no directive found", () => { - // @ts-ignore - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "RANDOM 1" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toBeUndefined(); - }); - - test("should throw when relationship has no arguments", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { value: "relationship", arguments: [] }, - }, - { - // @ts-ignore - name: { value: "RANDOM 2" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 3" }, - }, - { - // @ts-ignore - name: { value: "RANDOM 4" }, - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"type\\" of required type \\"String!\\" was not provided."` - ); - }); - - test("should throw when direction is missing", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "type" }, - // @ts-ignore - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"direction\\" of required type \\"RelationshipDirection!\\" was not provided."` - ); - }); - - test("should throw when direction not a string", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "type" }, - - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.BOOLEAN, value: true }, - }, - ], - }, - ], - }; - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"direction\\" has invalid value true."` - ); - }); - - test("should throw when direction is invalid", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "INVALID!" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"direction\\" has invalid value INVALID!."` - ); - }); - - test("should throw when type is missing", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"type\\" of required type \\"String!\\" was not provided."` - ); - }); - - test("should throw when type not a string", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - // @ts-ignore - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.FLOAT, value: "1.3" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"type\\" has invalid value 1.3."` - ); - }); - - test("should return the correct meta with direction and escaped type", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - // @ts-ignore - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - // @ts-ignore - value: { kind: Kind.STRING, value: "ACTED_IN$" }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "`ACTED_IN$`", - direction: "IN", - typeUnescaped: "ACTED_IN$", - }); - }); - - test("should throw when properties is not a string", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "properties" }, - value: { kind: Kind.BOOLEAN, value: true }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"properties\\" has invalid value true."` - ); - }); - - test("should return the correct meta with direction, type and properties", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "properties" }, - value: { kind: Kind.STRING, value: "ActedIn" }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "ACTED_IN", - direction: "IN", - properties: "ActedIn", - }); - }); - - test("should throw when queryDirection is not an enum", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "queryDirection" }, - value: { kind: Kind.STRING, value: "IN" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"queryDirection\\" has invalid value \\"IN\\"."` - ); - }); - - test("should return the correct meta with direction, type and queryDirection", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "queryDirection" }, - value: { kind: Kind.ENUM, value: "UNDIRECTED" }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "ACTED_IN", - direction: "IN", - queryDirection: "UNDIRECTED", - }); - }); - - test("should throw when nestedOperations is not a list", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { kind: Kind.STRING, value: "IN" }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"nestedOperations\\" has invalid value \\"IN\\"."` - ); - }); - - test("should throw when nestedOperations is invalid", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { - kind: Kind.LIST, - values: [ - { - kind: Kind.STRING, - value: "FAIL", - }, - ], - }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"nestedOperations\\" has invalid value [\\"FAIL\\"]."` - ); - }); - - test("should throw when nestedOperations value at index position 1 is invalid", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { - kind: Kind.LIST, - values: [ - { - kind: Kind.ENUM, - value: "CONNECT", - }, - { - kind: Kind.ENUM, - value: "FAIL", - }, - ], - }, - }, - ], - }, - ], - }; - - expect(() => getRelationshipMeta(field)).toThrowErrorMatchingInlineSnapshot( - `"Argument \\"nestedOperations\\" has invalid value [CONNECT, FAIL]."` - ); - }); - - test("should return the correct meta with direction, type and nestedOperations", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { - kind: Kind.LIST, - values: [ - { - kind: Kind.ENUM, - value: "CONNECT", - }, - { - kind: Kind.ENUM, - value: "CREATE", - }, - ], - }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "ACTED_IN", - direction: "IN", - nestedOperations: ["CONNECT", "CREATE"], - }); - }); - - test("should return the correct meta for all possible arguments", () => { - const field: FieldDefinitionNode = { - directives: [ - { - // @ts-ignore - name: { - value: "relationship", - }, - arguments: [ - { - // @ts-ignore - name: { value: "direction" }, - value: { kind: Kind.ENUM, value: "IN" }, - }, - { - // @ts-ignore - name: { value: "type" }, - value: { kind: Kind.STRING, value: "ACTED_IN" }, - }, - { - // @ts-ignore - name: { value: "properties" }, - value: { kind: Kind.STRING, value: "ActedIn" }, - }, - { - // @ts-ignore - name: { value: "queryDirection" }, - value: { kind: Kind.ENUM, value: "UNDIRECTED" }, - }, - { - // @ts-ignore - name: { value: "nestedOperations" }, - value: { - kind: Kind.LIST, - values: [ - { - kind: Kind.ENUM, - value: "CONNECT", - }, - { - kind: Kind.ENUM, - value: "CREATE", - }, - ], - }, - }, - ], - }, - ], - }; - - const result = getRelationshipMeta(field); - - expect(result).toMatchObject({ - type: "ACTED_IN", - direction: "IN", - properties: "ActedIn", - queryDirection: "UNDIRECTED", - nestedOperations: ["CONNECT", "CREATE"], - }); - }); -}); diff --git a/packages/graphql/src/schema/get-relationship-meta.ts b/packages/graphql/src/schema/get-relationship-meta.ts deleted file mode 100644 index e7baaa43f0..0000000000 --- a/packages/graphql/src/schema/get-relationship-meta.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode, FieldDefinitionNode } from "graphql"; -import type { RelationshipNestedOperationsOption, RelationshipQueryDirectionOption } from "../constants"; -import { relationshipDirective } from "../graphql/directives/relationship"; -import { parseArguments } from "../schema-model/parser/parse-arguments"; -import Cypher from "@neo4j/cypher-builder"; -import type { RelationshipDirection } from "../schema-model/relationship/Relationship"; - -type RelationshipMeta = { - direction: RelationshipDirection; - type: string; - typeUnescaped: string; - properties?: string; - queryDirection: RelationshipQueryDirectionOption; - nestedOperations: RelationshipNestedOperationsOption[]; - aggregate: boolean; -}; - -function getRelationshipMeta( - field: FieldDefinitionNode, - interfaceField?: FieldDefinitionNode -): RelationshipMeta | undefined { - const directive = - field.directives?.find((x) => x.name.value === "relationship") || - interfaceField?.directives?.find((x) => x.name.value === "relationship"); - if (!directive) { - return undefined; - } - - const relationshipMetaObject = getRelationshipDirectiveArguments(directive); - const typeUnescaped = relationshipMetaObject.type as string; - const type = Cypher.utils.escapeLabel(typeUnescaped); - - return { - ...relationshipMetaObject, - type, - typeUnescaped, - } as RelationshipMeta; -} - -function getRelationshipDirectiveArguments(directiveNode: DirectiveNode) { - return parseArguments(relationshipDirective, directiveNode); -} - -export default getRelationshipMeta; diff --git a/packages/graphql/src/schema/make-augmented-schema.test.ts b/packages/graphql/src/schema/make-augmented-schema.test.ts index 61f7793628..b5a8be88b3 100644 --- a/packages/graphql/src/schema/make-augmented-schema.test.ts +++ b/packages/graphql/src/schema/make-augmented-schema.test.ts @@ -29,7 +29,6 @@ import { } from "graphql"; import { pluralize } from "graphql-compose"; import { gql } from "graphql-tag"; -import { Node } from "../classes"; import { generateModel } from "../schema-model/generate-model"; import makeAugmentedSchema from "./make-augmented-schema"; import { ComplexityEstimatorHelper } from "../classes/ComplexityEstimatorHelper"; @@ -53,20 +52,17 @@ describe("makeAugmentedSchema", () => { `; const schemaModel = generateModel(mergeTypeDefs(typeDefs)); - const neoSchema = makeAugmentedSchema({ document: typeDefs, schemaModel, complexityEstimatorHelper: new ComplexityEstimatorHelper(false) }); + const neoSchema = makeAugmentedSchema({ + document: typeDefs, + schemaModel, + complexityEstimatorHelper: new ComplexityEstimatorHelper(false), + }); const document = neoSchema.typeDefs; const queryObject = document.definitions.find( (x) => x.kind === Kind.OBJECT_TYPE_DEFINITION && x.name.value === "Query" ) as ObjectTypeDefinitionNode; ["Actor", "Movie"].forEach((type) => { - const node = neoSchema.nodes.find((x) => x.name === type); - expect(node).toBeInstanceOf(Node); - const nodeObject = document.definitions.find( - (x) => x.kind === Kind.OBJECT_TYPE_DEFINITION && x.name.value === type - ); - expect(nodeObject).toBeTruthy(); - // Find const nodeFindQuery = queryObject.fields?.find((x) => x.name.value === pluralize(camelCase(type))); const nodeFindQueryType = ( @@ -97,7 +93,11 @@ describe("makeAugmentedSchema", () => { `; const schemaModel = generateModel(mergeTypeDefs(typeDefs)); - const neoSchema = makeAugmentedSchema({ document: typeDefs, schemaModel, complexityEstimatorHelper: new ComplexityEstimatorHelper(false) }); + const neoSchema = makeAugmentedSchema({ + document: typeDefs, + schemaModel, + complexityEstimatorHelper: new ComplexityEstimatorHelper(false), + }); const document = neoSchema.typeDefs; @@ -271,7 +271,11 @@ describe("makeAugmentedSchema", () => { `; const schemaModel = generateModel(mergeTypeDefs(typeDefs)); - const neoSchema = makeAugmentedSchema({ document: typeDefs, schemaModel, complexityEstimatorHelper: new ComplexityEstimatorHelper(false) }); + const neoSchema = makeAugmentedSchema({ + document: typeDefs, + schemaModel, + complexityEstimatorHelper: new ComplexityEstimatorHelper(false), + }); const document = neoSchema.typeDefs; @@ -291,9 +295,13 @@ describe("makeAugmentedSchema", () => { `; const schemaModel = generateModel(mergeTypeDefs(typeDefs)); - expect(() => makeAugmentedSchema({ document: typeDefs, schemaModel, complexityEstimatorHelper: new ComplexityEstimatorHelper(false) })).not.toThrow( - 'Error: Type with name "ActionMapping" does not exists' - ); + expect(() => + makeAugmentedSchema({ + document: typeDefs, + schemaModel, + complexityEstimatorHelper: new ComplexityEstimatorHelper(false), + }) + ).not.toThrow('Error: Type with name "ActionMapping" does not exists'); }); }); }); diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index e38560ae68..9ef4506be1 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -32,15 +32,11 @@ import type { import { GraphQLBoolean, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLString, Kind, parse, print } from "graphql"; import type { ObjectTypeComposer } from "graphql-compose"; import { SchemaComposer } from "graphql-compose"; -import type { Node } from "../classes"; -import type Relationship from "../classes/Relationship"; import * as Scalars from "../graphql/scalars"; import { AggregationTypesMapper } from "./aggregations/aggregation-types-mapper"; import { augmentFulltextSchema } from "./augment/fulltext"; import { ensureNonEmptyInput } from "./ensure-non-empty-input"; import getCustomResolvers from "./get-custom-resolvers"; -import type { ObjectFields } from "./get-obj-field-meta"; -import { getObjFieldMeta } from "./get-obj-field-meta"; import { createResolver } from "./resolvers/mutation/create"; import { deleteResolver } from "./resolvers/mutation/delete"; import { updateResolver } from "./resolvers/mutation/update"; @@ -68,10 +64,9 @@ import { InterfaceEntityAdapter } from "../schema-model/entity/model-adapters/In import { UnionEntityAdapter } from "../schema-model/entity/model-adapters/UnionEntityAdapter"; import { getDefinitionCollection } from "../schema-model/parser/definition-collection"; import { RelationshipDeclarationAdapter } from "../schema-model/relationship/model-adapters/RelationshipDeclarationAdapter"; -import type { CypherField, Neo4jFeaturesSettings } from "../types"; +import type { Neo4jFeaturesSettings } from "../types"; import { asArray, filterTruthy } from "../utils/utils"; import { augmentVectorSchema } from "./augment/vector"; -import { createConnectionFields } from "./create-connection-fields"; import { addGlobalNodeFields } from "./create-global-nodes"; import { createRelationshipFields } from "./create-relationship-fields/create-relationship-fields"; import { AugmentedSchemaGenerator } from "./generation/AugmentedSchemaGenerator"; @@ -82,9 +77,7 @@ import { withObjectType } from "./generation/object-type"; import { withMutationResponseTypes } from "./generation/response-types"; import { withUpdateInputType } from "./generation/update-input"; import { withUniqueWhereInputType, withWhereInputType } from "./generation/where-input"; -import getNodes from "./get-nodes"; import { getResolveAndSubscriptionMethods } from "./get-resolve-and-subscription-methods"; -import { filterInterfaceTypes } from "./make-augmented-schema/filter-interface-types"; import { getUserDefinedDirectives } from "./make-augmented-schema/user-defined-directives"; import { cypherResolver } from "./resolvers/query/cypher"; import { generateSubscriptionTypes } from "./subscriptions/generate-subscription-types"; @@ -96,33 +89,27 @@ function definitionNodeHasName(x: DefinitionNode): x is DefinitionNode & { name: function makeAugmentedSchema({ document, features, - userCustomResolvers, subgraph, schemaModel, complexityEstimatorHelper, }: { document: DocumentNode; features?: Neo4jFeaturesSettings; - userCustomResolvers?: IResolvers | Array; subgraph?: Subgraph; schemaModel: Neo4jGraphQLSchemaModel; complexityEstimatorHelper: ComplexityEstimatorHelper; }): { - nodes: Node[]; - relationships: Relationship[]; typeDefs: DocumentNode; resolvers: IResolvers; } { const composer = new SchemaComposer(); - const callbacks = features?.populatedBy?.callbacks; - let relationships: Relationship[] = []; //TODO: definition collection is being used to harmonize schema generation with schema model, //however make augmented schema inner methods are still accepting arrays as they were defined by the previous getDefinitionNodes const definitionCollection = getDefinitionCollection(document); const { - interfaceTypes, + // interfaceTypes, scalarTypes, userDefinedObjectTypes, enumTypes, @@ -267,14 +254,7 @@ function makeAugmentedSchema({ } const aggregationTypesMapper = new AggregationTypesMapper(composer, subgraph); - - const getNodesResult = getNodes(definitionCollection, { callbacks, userCustomResolvers }); - - const { nodes, interfaceRelationshipNames } = getNodesResult; - - const hasGlobalNodes = addGlobalNodeFields(nodes, composer, schemaModel.concreteEntities); - - const { filteredInterfaceTypes } = filterInterfaceTypes(interfaceTypes.values(), interfaceRelationshipNames); + const hasGlobalNodes = addGlobalNodeFields(composer, schemaModel.concreteEntities); const { userDefinedFieldDirectivesForNode, @@ -284,24 +264,6 @@ function makeAugmentedSchema({ userDefinedDirectivesForUnion, } = getUserDefinedDirectives(definitionCollection); - /** - * TODO [translation-layer-compatibility] - * keeping this `relationshipFields` scaffold for backwards compatibility on translation layer - * actual functional logic is in schemaModel.concreteEntities.forEach - */ - const relationshipFields = new Map(); - for (const relationship of definitionCollection.relationshipProperties.values()) { - const relFields = getObjFieldMeta({ - interfaces: filteredInterfaceTypes, - definitionCollection, - obj: relationship, - callbacks, - }); - - relationshipFields.set(relationship.name.value, relFields); - } - - // this is the new "functional" way for the above forEach // helper to only create relationshipProperties Interface types once, even if multiple relationships reference it const seenRelationshipPropertiesTypes = new Set(); schemaModel.entities.forEach((entity) => { @@ -348,23 +310,9 @@ function makeAugmentedSchema({ complexityEstimatorHelper, schemaModel, }); - const connectionFields = createConnectionFields({ - entityAdapter: interfaceEntityAdapter, - relationshipFields, - }); - relationships = [...relationships, ...connectionFields]; return; } if (entity instanceof ConcreteEntity) { - /** - * TODO [translation-layer-compatibility] - * need the node for fulltext translation - */ - const node = nodes.find((n) => n.name === entity.name); - if (!node) { - throw new Error(`Node not found with the name ${entity.name}`); - } - const concreteEntityAdapter = new ConcreteEntityAdapter(entity); const userDefinedFieldDirectives = userDefinedFieldDirectivesForNode.get(concreteEntityAdapter.name); if (!userDefinedFieldDirectives) { @@ -384,24 +332,17 @@ function makeAugmentedSchema({ userDefinedFieldDirectives, propagatedDirectives, aggregationTypesMapper, - node, seenRelationshipPropertiesTypes, userDefinedDirectivesForNode, userDefinedFieldDirectivesForNode, complexityEstimatorHelper, schemaModel, }); - - const connectionFields = createConnectionFields({ - entityAdapter: concreteEntityAdapter, - relationshipFields, - }); - relationships = [...relationships, ...connectionFields]; return; } }); - if (nodes.length) { + if (schemaModel.concreteEntities.length) { generateSubscriptionTypes({ schemaComposer: composer, schemaModel, @@ -421,19 +362,7 @@ function makeAugmentedSchema({ const userDefinedFieldDirectives = userDefinedFieldDirectivesForNode.get(type) as Map; for (const attributeAdapter of operationAdapter.attributes.values()) { - /** - * TODO [translation-layer-compatibility] - * needed for compatibility with translation layer - */ - const objectFields = getObjFieldMeta({ - obj: customResolvers[`customCypher${type}`], - interfaces: filteredInterfaceTypes, - definitionCollection, - callbacks, - }); - const field = objectFields.cypherFields.find((f) => f.fieldName === attributeAdapter.name) as CypherField; const customResolver = cypherResolver({ - field, attributeAdapter, type: type as "Query" | "Mutation", }); @@ -537,8 +466,6 @@ function makeAugmentedSchema({ }; return { - nodes, - relationships, typeDefs: parsedDoc, resolvers: generatedResolvers, }; @@ -614,7 +541,6 @@ function generateObjectType({ userDefinedObjectDirectives, propagatedDirectives, aggregationTypesMapper, - node, seenRelationshipPropertiesTypes, userDefinedDirectivesForNode, userDefinedFieldDirectivesForNode, @@ -629,7 +555,6 @@ function generateObjectType({ userDefinedObjectDirectives: DirectiveNode[]; propagatedDirectives: DirectiveNode[]; aggregationTypesMapper: AggregationTypesMapper; - node: Node; seenRelationshipPropertiesTypes: Set; userDefinedDirectivesForNode: Map; userDefinedFieldDirectivesForNode: Map>; @@ -716,7 +641,6 @@ function generateObjectType({ if (concreteEntityAdapter.isCreatable) { composer.Mutation.addFields({ [concreteEntityAdapter.operations.rootTypeFieldNames.create]: createResolver({ - node, concreteEntityAdapter, }), }); @@ -742,7 +666,6 @@ function generateObjectType({ if (concreteEntityAdapter.isUpdatable) { composer.Mutation.addFields({ [concreteEntityAdapter.operations.rootTypeFieldNames.update]: updateResolver({ - node, concreteEntityAdapter, }), }); diff --git a/packages/graphql/src/schema/make-augmented-schema/filter-interface-types.ts b/packages/graphql/src/schema/make-augmented-schema/filter-interface-types.ts deleted file mode 100644 index c75b0fddd0..0000000000 --- a/packages/graphql/src/schema/make-augmented-schema/filter-interface-types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { InterfaceTypeDefinitionNode } from "graphql"; - -export function filterInterfaceTypes( - interfaceTypes: Iterable, - interfaceRelationshipNames: Set -): { - interfaceRelationships: InterfaceTypeDefinitionNode[]; - filteredInterfaceTypes: InterfaceTypeDefinitionNode[]; -} { - const interfaceRelationships: InterfaceTypeDefinitionNode[] = []; - const filteredInterfaceTypes: InterfaceTypeDefinitionNode[] = []; - for (const interfaceType of interfaceTypes) { - if (interfaceRelationshipNames.has(interfaceType.name.value)) { - interfaceRelationships.push(interfaceType); - } else { - filteredInterfaceTypes.push(interfaceType); - } - } - return { - interfaceRelationships, - filteredInterfaceTypes, - }; -} diff --git a/packages/graphql/src/schema/parse-node-directive.test.ts b/packages/graphql/src/schema/parse-node-directive.test.ts deleted file mode 100644 index 406cf951ef..0000000000 --- a/packages/graphql/src/schema/parse-node-directive.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode, ObjectTypeDefinitionNode } from "graphql"; -import { parse } from "graphql"; -import parseNodeDirective from "./parse-node-directive"; -import { NodeDirective } from "../classes/NodeDirective"; - -describe("parseNodeDirective", () => { - test("should throw an error if incorrect directive is passed in", () => { - const typeDefs = ` - type TestType @wrongdirective { - label: String - } - `; - - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - expect(() => parseNodeDirective(directive)).toThrow( - "Undefined or incorrect directive passed into parseNodeDirective function" - ); - }); - - test("should return a node directive with labels, type name ignored", () => { - const typeDefs = ` - type TestType @node(labels:["Label", "AnotherLabel"]) { - name: String - } - `; - - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - const expected = new NodeDirective({ labels: ["Label", "AnotherLabel"] }); - - expect(parseNodeDirective(directive)).toMatchObject(expected); - }); - - test("should return a node directive with labels, type name included", () => { - const typeDefs = ` - type TestType @node(labels:["TestType", "Label", "AnotherLabel"]) { - name: String - } - `; - - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - const expected = new NodeDirective({ labels: ["TestType", "Label", "AnotherLabel"] }); - - expect(parseNodeDirective(directive)).toMatchObject(expected); - }); -}); diff --git a/packages/graphql/src/schema/parse-node-directive.ts b/packages/graphql/src/schema/parse-node-directive.ts deleted file mode 100644 index deafe053c4..0000000000 --- a/packages/graphql/src/schema/parse-node-directive.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode } from "graphql"; -import { valueFromASTUntyped } from "graphql"; -import { NodeDirective } from "../classes/NodeDirective"; - -function parseNodeDirective(nodeDirective: DirectiveNode | undefined) { - if (!nodeDirective || nodeDirective.name.value !== "node") { - throw new Error("Undefined or incorrect directive passed into parseNodeDirective function"); - } - - return new NodeDirective({ - labels: getArgumentValue(nodeDirective, "labels"), - }); -} - -function getArgumentValue(directive: DirectiveNode, name: string): T | undefined { - const argument = directive.arguments?.find((a) => a.name.value === name); - return argument ? (valueFromASTUntyped(argument.value) as T) : undefined; -} - -export default parseNodeDirective; diff --git a/packages/graphql/src/schema/parse/parse-limit-directive.test.ts b/packages/graphql/src/schema/parse/parse-limit-directive.test.ts deleted file mode 100644 index 4722ee6d78..0000000000 --- a/packages/graphql/src/schema/parse/parse-limit-directive.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode, ObjectTypeDefinitionNode } from "graphql"; -import gql from "graphql-tag"; -import * as neo4j from "neo4j-driver"; -import { LimitDirective } from "../../classes/LimitDirective"; -import { parseLimitDirective } from "./parse-limit-directive"; - -describe("parseLimitDirective", () => { - test("max and default argument", () => { - const maxLimit = 100; - const defaultLimit = 10; - - const typeDefs = gql` - type Movie @limit(max: ${maxLimit}, default: ${defaultLimit}) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - - const result = parseLimitDirective({ - directive, - definition, - }); - - expect(result).toEqual(new LimitDirective({ max: neo4j.int(maxLimit), default: neo4j.int(defaultLimit) })); - }); - - test("should return correct object if default limit is undefined", () => { - const typeDefs = gql` - type Movie @limit @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - - const result = parseLimitDirective({ - directive, - definition, - }); - expect(result).toEqual(new LimitDirective({})); - }); - - test("fail if default argument is bigger than max", () => { - const maxLimit = 10; - const defaultLimit = 100; - - const typeDefs = gql` - type Movie @limit(max: ${maxLimit}, default: ${defaultLimit}) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - - expect(() => - parseLimitDirective({ - directive, - definition, - }) - ).toThrow( - `Movie @limit(max: ${maxLimit}, default: ${defaultLimit}) invalid default value, 'default' must be smaller than 'max'` - ); - }); - - describe("default argument", () => { - test("should throw error when default limit is less than or equal to 0", () => { - [-10, -100, 0].forEach((i) => { - const typeDefs = gql` - type Movie @limit(default: ${i}) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - expect(() => - parseLimitDirective({ - directive, - definition, - }) - ).toThrow(`Movie @limit(default: ${i}) invalid value: '${i}', it should be a number greater than 0`); - }); - }); - }); - - describe("max argument", () => { - test("should fail if value is 0", () => { - const typeDefs = gql` - type Movie @limit(max: 0) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - expect(() => - parseLimitDirective({ - directive, - definition, - }) - ).toThrow(`Movie @limit(max: 0) invalid value: '0', it should be a number greater than 0`); - }); - - test("should fail if value is less 0", () => { - const typeDefs = gql` - type Movie @limit(max: -10) @node { - id: ID - } - `; - - const definition = typeDefs.definitions[0] as ObjectTypeDefinitionNode; - const directive = (definition.directives || [])[0] as DirectiveNode; - expect(() => - parseLimitDirective({ - directive, - definition, - }) - ).toThrow(`Movie @limit(max: -10) invalid value: '-10', it should be a number greater than 0`); - }); - }); -}); diff --git a/packages/graphql/src/schema/parse/parse-limit-directive.ts b/packages/graphql/src/schema/parse/parse-limit-directive.ts deleted file mode 100644 index 66d62c48b7..0000000000 --- a/packages/graphql/src/schema/parse/parse-limit-directive.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ArgumentNode, DirectiveNode, ObjectTypeDefinitionNode } from "graphql"; -import * as neo4j from "neo4j-driver"; -import { LimitDirective } from "../../classes/LimitDirective"; -import { Neo4jGraphQLError } from "../../classes/Error"; -import { parseValueNode } from "../../schema-model/parser/parse-value-node"; - -export function parseLimitDirective({ - directive, - definition, -}: { - directive: DirectiveNode; - definition: ObjectTypeDefinitionNode; -}): LimitDirective { - const defaultLimitArgument = directive.arguments?.find((argument) => argument.name.value === "default"); - const maxLimitArgument = directive.arguments?.find((argument) => argument.name.value === "max"); - - const defaultLimit = parseArgumentToInt(defaultLimitArgument); - const maxLimit = parseArgumentToInt(maxLimitArgument); - - const limit = { default: defaultLimit, max: maxLimit }; - - const limitError = validateLimitArguments(limit, definition.name.value); - if (limitError) { - throw limitError; - } - - return new LimitDirective(limit); -} - -function parseArgumentToInt(argument: ArgumentNode | undefined): neo4j.Integer | undefined { - if (argument) { - const parsed = parseValueNode(argument.value) as number; - return neo4j.int(parsed); - } - return undefined; -} - -function validateLimitArguments( - arg: { default: neo4j.Integer | undefined; max: neo4j.Integer | undefined }, - typeName: string -): Neo4jGraphQLError | undefined { - const maxLimit = arg.max?.toNumber(); - const defaultLimit = arg.default?.toNumber(); - - if (defaultLimit !== undefined && defaultLimit <= 0) { - return new Neo4jGraphQLError( - `${typeName} @limit(default: ${defaultLimit}) invalid value: '${defaultLimit}', it should be a number greater than 0` - ); - } - if (maxLimit !== undefined && maxLimit <= 0) { - return new Neo4jGraphQLError( - `${typeName} @limit(max: ${maxLimit}) invalid value: '${maxLimit}', it should be a number greater than 0` - ); - } - if (maxLimit && defaultLimit) { - if (maxLimit < defaultLimit) { - return new Neo4jGraphQLError( - `${typeName} @limit(max: ${maxLimit}, default: ${defaultLimit}) invalid default value, 'default' must be smaller than 'max'` - ); - } - } - return undefined; -} diff --git a/packages/graphql/src/schema/parse/parse-plural-directive.test.ts b/packages/graphql/src/schema/parse/parse-plural-directive.test.ts deleted file mode 100644 index fa079cdc45..0000000000 --- a/packages/graphql/src/schema/parse/parse-plural-directive.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode, ObjectTypeDefinitionNode } from "graphql"; -import { parse } from "graphql"; -import parsePluralDirective from "./parse-plural-directive"; - -describe("parsePluralDirective", () => { - test("Should return the custom plural string", () => { - const plural = "testTypes123"; - const typeDefs = ` - type TestType @plural(value: "${plural}") { - name: String - } - `; - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const directive = definition?.directives?.length ? (definition.directives[0] as DirectiveNode) : undefined; - expect(parsePluralDirective(directive)).toBe(plural); - }); -}); diff --git a/packages/graphql/src/schema/parse/parse-plural-directive.ts b/packages/graphql/src/schema/parse/parse-plural-directive.ts deleted file mode 100644 index 10e7e6f822..0000000000 --- a/packages/graphql/src/schema/parse/parse-plural-directive.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode, StringValueNode } from "graphql"; - -/** - * Parse the plural directive and return the plural value. - * @param pluralDirective The plural directicve to parse. - * @returns The plural value. - */ -export default function parsePluralDirective(pluralDirective: DirectiveNode | undefined): string | undefined { - return ( - (pluralDirective?.arguments?.find((argument) => argument.name.value === "value")?.value as StringValueNode) - ?.value || undefined - ); -} diff --git a/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts b/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts index 2026a74a61..0225b5da1f 100644 --- a/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts +++ b/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts @@ -20,7 +20,6 @@ import Debug from "debug"; import type { GraphQLFieldResolver, GraphQLResolveInfo } from "graphql"; import type { Driver } from "neo4j-driver"; -import type { Node, Relationship } from "../../../classes"; import { Executor } from "../../../classes/Executor"; import type { Neo4jDatabaseInfo } from "../../../classes/Neo4jDatabaseInfo"; import { getNeo4jDatabaseInfo } from "../../../classes/Neo4jDatabaseInfo"; @@ -37,8 +36,6 @@ const debug = Debug(DEBUG_GRAPHQL); export type WrapResolverArguments = { driver?: Driver; - nodes: Node[]; - relationships: Relationship[]; jwtPayloadFieldsMap?: Map; schemaModel: Neo4jGraphQLSchemaModel; dbInfo?: Neo4jDatabaseInfo; @@ -50,14 +47,6 @@ export type WrapResolverArguments = { * The type describing the context generated by {@link wrapQueryAndMutation}. */ export interface Neo4jGraphQLComposedContext extends Neo4jGraphQLContext { - /** - * @deprecated The use of this field is now deprecated in favour of {@link schemaModel}. - */ - nodes: Node[]; - /** - * @deprecated The use of this field is now deprecated in favour of {@link schemaModel}. - */ - relationships: Relationship[]; schemaModel: Neo4jGraphQLSchemaModel; features: ContextFeatures; executor: Executor; @@ -70,16 +59,7 @@ export interface Neo4jGraphQLComposedContext extends Neo4jGraphQLContext { let neo4jDatabaseInfo: Neo4jDatabaseInfo; export const wrapQueryAndMutation = - ({ - driver, - nodes, - relationships, - jwtPayloadFieldsMap, - schemaModel, - dbInfo, - authorization, - features, - }: WrapResolverArguments) => + ({ driver, jwtPayloadFieldsMap, schemaModel, dbInfo, authorization, features }: WrapResolverArguments) => (next: GraphQLFieldResolver) => async (root, args, context: Neo4jGraphQLContext, info: GraphQLResolveInfo) => { debugGraphQLResolveInfo(debug, info); @@ -106,7 +86,7 @@ export const wrapQueryAndMutation = cypherParams: context.cypherParams, transaction: { ...context.transaction, - metadata: {...context.transaction?.metadata, ...context.transactionMetadata}, + metadata: { ...context.transaction?.metadata, ...context.transactionMetadata }, }, }); @@ -118,8 +98,6 @@ export const wrapQueryAndMutation = } const internalContext = { - nodes, - relationships, schemaModel, features, executor, diff --git a/packages/graphql/src/schema/resolvers/mutation/create.test.ts b/packages/graphql/src/schema/resolvers/mutation/create.test.ts index 68cebb94f2..4b765f1a64 100644 --- a/packages/graphql/src/schema/resolvers/mutation/create.test.ts +++ b/packages/graphql/src/schema/resolvers/mutation/create.test.ts @@ -17,16 +17,12 @@ * limitations under the License. */ -import { NodeBuilder } from "../../../../tests/utils/builders/node-builder"; import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { createResolver } from "./create"; describe("Create resolver", () => { test("should return the correct; type, args and resolve", () => { - const node = new NodeBuilder({ - name: "Movie", - }).instance(); const concreteEntity = new ConcreteEntity({ name: "Movie", labels: ["Movie"], @@ -38,7 +34,7 @@ describe("Create resolver", () => { }); const concreteEntityAdapter = new ConcreteEntityAdapter(concreteEntity); - const result = createResolver({ node, concreteEntityAdapter }); + const result = createResolver({ concreteEntityAdapter }); expect(result.type).toBe("CreateMoviesMutationResponse!"); expect(result.resolve).toBeInstanceOf(Function); expect(result.args).toMatchObject({ diff --git a/packages/graphql/src/schema/resolvers/mutation/create.ts b/packages/graphql/src/schema/resolvers/mutation/create.ts index 90ef4423eb..8507314b7a 100644 --- a/packages/graphql/src/schema/resolvers/mutation/create.ts +++ b/packages/graphql/src/schema/resolvers/mutation/create.ts @@ -18,7 +18,6 @@ */ import { Kind, type FieldNode, type GraphQLResolveInfo } from "graphql"; -import type { Node } from "../../../classes"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { translateCreate } from "../../../translate/translate-create"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; @@ -26,19 +25,16 @@ import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; -export function createResolver({ - node, - concreteEntityAdapter, -}: { - node: Node; - concreteEntityAdapter: ConcreteEntityAdapter; -}) { +export function createResolver({ concreteEntityAdapter }: { concreteEntityAdapter: ConcreteEntityAdapter }) { async function resolve(_root: any, args: any, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) { const resolveTree = getNeo4jResolveTree(info, { args }); (context as Neo4jGraphQLTranslationContext).resolveTree = resolveTree; - const { cypher, params } = await translateCreate({ context: context as Neo4jGraphQLTranslationContext, node }); + const { cypher, params } = await translateCreate({ + context: context as Neo4jGraphQLTranslationContext, + entityAdapter: concreteEntityAdapter, + }); const executeResult = await execute({ cypher, diff --git a/packages/graphql/src/schema/resolvers/mutation/update.test.ts b/packages/graphql/src/schema/resolvers/mutation/update.test.ts index d7899edc37..e3a8cccfc5 100644 --- a/packages/graphql/src/schema/resolvers/mutation/update.test.ts +++ b/packages/graphql/src/schema/resolvers/mutation/update.test.ts @@ -17,18 +17,12 @@ * limitations under the License. */ -import { NodeBuilder } from "../../../../tests/utils/builders/node-builder"; import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { updateResolver } from "./update"; describe("Update resolver", () => { test("should return the correct; type, args and resolve", () => { - const node = new NodeBuilder({ - name: "Movie", - // @ts-ignore - relationFields: [{}, {}], - }).instance(); const concreteEntity = new ConcreteEntity({ name: "Movie", labels: ["Movie"], @@ -40,7 +34,7 @@ describe("Update resolver", () => { }); const concreteEntityAdapter = new ConcreteEntityAdapter(concreteEntity); - const result = updateResolver({ node, concreteEntityAdapter }); + const result = updateResolver({ concreteEntityAdapter }); expect(result.type).toBe("UpdateMoviesMutationResponse!"); expect(result.resolve).toBeInstanceOf(Function); expect(result.args).toMatchObject({ @@ -49,11 +43,6 @@ describe("Update resolver", () => { }); }); test("should return fewer fields based on number of InputTCs created", () => { - const node = new NodeBuilder({ - name: "Movie", - // @ts-ignore - relationFields: [{}, {}], - }).instance(); const concreteEntity = new ConcreteEntity({ name: "Movie", labels: ["Movie"], @@ -65,7 +54,7 @@ describe("Update resolver", () => { }); const concreteEntityAdapter = new ConcreteEntityAdapter(concreteEntity); - const result = updateResolver({ node, concreteEntityAdapter }); + const result = updateResolver({ concreteEntityAdapter }); expect(result.type).toBe("UpdateMoviesMutationResponse!"); expect(result.resolve).toBeInstanceOf(Function); diff --git a/packages/graphql/src/schema/resolvers/mutation/update.ts b/packages/graphql/src/schema/resolvers/mutation/update.ts index 17ab942932..fdf58adcfd 100644 --- a/packages/graphql/src/schema/resolvers/mutation/update.ts +++ b/packages/graphql/src/schema/resolvers/mutation/update.ts @@ -24,7 +24,6 @@ import type { ObjectTypeComposerFieldConfigAsObjectDefinition, } from "graphql-compose"; import type { ResolveTree } from "graphql-parse-resolve-info"; -import type { Node } from "../../../classes"; import type { EntityAdapter } from "../../../schema-model/entity/EntityAdapter"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { QueryASTFactory } from "../../../translate/queryAST/factory/QueryASTFactory"; @@ -36,10 +35,8 @@ import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; export function updateResolver({ - node, concreteEntityAdapter, }: { - node: Node; concreteEntityAdapter: ConcreteEntityAdapter; }): ObjectTypeComposerFieldConfigAsObjectDefinition { async function resolve(_root: any, args: any, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) { @@ -47,7 +44,10 @@ export function updateResolver({ (context as Neo4jGraphQLTranslationContext).resolveTree = resolveTree; - const { cypher, params } = await translateUpdate({ context: context as Neo4jGraphQLTranslationContext, node }); + const { cypher, params } = await translateUpdate({ + context: context as Neo4jGraphQLTranslationContext, + entityAdapter: concreteEntityAdapter, + }); const executeResult = await execute({ cypher, params, @@ -124,17 +124,12 @@ async function translateUsingQueryAST({ async function translateUpdate({ context, - node, + entityAdapter, }: { context: Neo4jGraphQLTranslationContext; - node: Node; + entityAdapter: ConcreteEntityAdapter; }): Promise<{ cypher: string; params: Record }> { const { resolveTree } = context; - const entityAdapter = context.schemaModel.getConcreteEntityAdapter(node.name); - if (!entityAdapter) { - throw new Error(`Transpilation error: ${node.name} is not a concrete entity`); - } - const varName = "this"; const result = await translateUsingQueryAST({ context, entityAdapter, resolveTree, varName }); diff --git a/packages/graphql/src/schema/resolvers/query/cypher.test.ts b/packages/graphql/src/schema/resolvers/query/cypher.test.ts index 6b2d236a58..ec3c96cb6f 100644 --- a/packages/graphql/src/schema/resolvers/query/cypher.test.ts +++ b/packages/graphql/src/schema/resolvers/query/cypher.test.ts @@ -20,19 +20,10 @@ import { Attribute } from "../../../schema-model/attribute/Attribute"; import { GraphQLBuiltInScalarType, ScalarType } from "../../../schema-model/attribute/AttributeType"; import { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; -import type { CypherField } from "../../../types"; import { cypherResolver } from "./cypher"; describe("Cypher resolver", () => { test("should return the correct; type, args and resolve", () => { - // @ts-ignore - const field: CypherField = { - // @ts-ignore - typeMeta: { name: "Test", pretty: "[Test]" }, - arguments: [], - isEnum: false, - isScalar: true, - }; const attribute = new Attribute({ name: "test", annotations: {}, @@ -40,7 +31,7 @@ describe("Cypher resolver", () => { args: [], }); const attributeAdapter = new AttributeAdapter(attribute); - const result = cypherResolver({ field, attributeAdapter, type: "Query" }); + const result = cypherResolver({ attributeAdapter, type: "Query" }); expect(result.type).toBe("String!"); expect(result.resolve).toBeInstanceOf(Function); expect(result.args).toMatchObject({}); diff --git a/packages/graphql/src/schema/resolvers/query/cypher.ts b/packages/graphql/src/schema/resolvers/query/cypher.ts index b0fb2f0506..39a2bf5575 100644 --- a/packages/graphql/src/schema/resolvers/query/cypher.ts +++ b/packages/graphql/src/schema/resolvers/query/cypher.ts @@ -20,7 +20,6 @@ import type { GraphQLResolveInfo } from "graphql"; import type { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; import { translateTopLevelCypher } from "../../../translate/translate-top-level-cypher"; -import type { CypherField } from "../../../types"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; @@ -29,11 +28,9 @@ import { graphqlArgsToCompose } from "../../to-compose"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; export function cypherResolver({ - field, attributeAdapter, type, }: { - field: CypherField; // TODO: make this go away attributeAdapter: AttributeAdapter; type: "Query" | "Mutation"; }) { @@ -44,7 +41,7 @@ export function cypherResolver({ const { cypher, params } = translateTopLevelCypher({ context: context as Neo4jGraphQLTranslationContext, - field, + attributeAdapter, type, }); diff --git a/packages/graphql/src/schema/get-custom-resolver-meta.ts b/packages/graphql/src/schema/selection-set-to-resolve-tree.ts similarity index 80% rename from packages/graphql/src/schema/get-custom-resolver-meta.ts rename to packages/graphql/src/schema/selection-set-to-resolve-tree.ts index 10d6ed93f3..2eab6ea536 100644 --- a/packages/graphql/src/schema/get-custom-resolver-meta.ts +++ b/packages/graphql/src/schema/selection-set-to-resolve-tree.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import type { IResolvers } from "@graphql-tools/utils"; import type { FieldDefinitionNode, InterfaceTypeDefinitionNode, @@ -28,76 +27,16 @@ import type { UnionTypeDefinitionNode, FieldNode, } from "graphql"; -import { Kind, parse } from "graphql"; +import { Kind } from "graphql"; import type { FieldsByTypeName, ResolveTree } from "graphql-parse-resolve-info"; import { generateResolveTree } from "../translate/utils/resolveTree"; -type CustomResolverMeta = { - requiredFields: Record; -}; - const INVALID_DIRECTIVES_TO_REQUIRE = ["customResolver"]; export const INVALID_REQUIRED_FIELD_ERROR = `It is not possible to require fields that use the following directives: ${INVALID_DIRECTIVES_TO_REQUIRE.map( (name) => `\`@${name}\`` ).join(", ")}`; export const INVALID_SELECTION_SET_ERROR = "Invalid selection set passed to @customResolver required"; -export function getCustomResolverMeta({ - field, - object, - objects, - interfaces, - unions, - customResolvers, - interfaceField, -}: { - field: FieldDefinitionNode; - object: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode; - objects: ObjectTypeDefinitionNode[]; - interfaces: InterfaceTypeDefinitionNode[]; - unions: UnionTypeDefinitionNode[]; - customResolvers?: IResolvers | IResolvers[]; - interfaceField?: FieldDefinitionNode; -}): CustomResolverMeta | undefined { - const directive = - field.directives?.find((x) => x.name.value === "customResolver") || - interfaceField?.directives?.find((x) => x.name.value === "customResolver"); - - if (!directive) { - return undefined; - } - - if (object.kind !== Kind.INTERFACE_TYPE_DEFINITION && !customResolvers?.[field.name.value]) { - console.warn(`Custom resolver for ${field.name.value} has not been provided`); - } - - const directiveRequiresArgument = directive?.arguments?.find((arg) => arg.name.value === "requires"); - - if (!directiveRequiresArgument) { - return { - requiredFields: {}, - }; - } - - if (directiveRequiresArgument?.value.kind !== Kind.STRING) { - throw new Error("@customResolver requires expects a string"); - } - - const selectionSetDocument = parse(`{ ${directiveRequiresArgument.value.value} }`); - const requiredFieldsResolveTree = selectionSetToResolveTree( - object.fields || [], - objects, - interfaces, - unions, - selectionSetDocument - ); - if (requiredFieldsResolveTree) { - return { - requiredFields: requiredFieldsResolveTree, - }; - } -} - export function selectionSetToResolveTree( objectFields: ReadonlyArray, objects: ObjectTypeDefinitionNode[], diff --git a/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts b/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts index 325028ee60..961e0b199b 100644 --- a/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts +++ b/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts @@ -20,7 +20,6 @@ import type { DirectiveNode } from "graphql"; import { GraphQLFloat, GraphQLNonNull } from "graphql"; import type { ObjectTypeComposer, SchemaComposer } from "graphql-compose"; -import type { SubscriptionEvents } from "../../classes/Node"; import { EventType } from "../../graphql/enums/EventType"; import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel"; import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; @@ -28,7 +27,13 @@ import type { Neo4jFeaturesSettings, NodeSubscriptionsEvent, SubscriptionsEvent import { withWhereInputType } from "../generation/where-input"; import { generateSubscribeMethod, subscriptionResolve } from "../resolvers/subscriptions/subscribe"; import { attributeAdapterToComposeFields } from "../to-compose"; - +type SubscriptionEvents = { + create: string; + update: string; + delete: string; + create_relationship: string; + delete_relationship: string; +}; export function generateSubscriptionTypes({ schemaComposer, schemaModel, diff --git a/packages/graphql/src/translate/authorization/check-authentication.ts b/packages/graphql/src/translate/authorization/check-authentication.ts index ec0cafa6b1..6909957679 100644 --- a/packages/graphql/src/translate/authorization/check-authentication.ts +++ b/packages/graphql/src/translate/authorization/check-authentication.ts @@ -17,7 +17,6 @@ * limitations under the License. */ -import type { Node } from "../../classes"; import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity"; import type { AuthenticationAnnotation, @@ -27,32 +26,6 @@ import { applyAuthentication } from "./utils/apply-authentication"; import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; import type { Operation } from "../../schema-model/Operation"; -export function checkAuthentication({ - context, - node, - targetOperations, - field, -}: { - context: Neo4jGraphQLTranslationContext; - node: Node; - targetOperations: AuthenticationOperation[]; - field?: string; -}) { - const concreteEntities = context.schemaModel.getEntitiesByNameAndLabels(node.name, node.getAllLabels()); - - if (concreteEntities.length !== 1) { - throw new Error("Couldn't match entity"); - } - const entity = concreteEntities[0] as ConcreteEntity; - - return checkEntityAuthentication({ - context, - entity, - targetOperations, - field, - }); -} - export function checkEntityAuthentication({ context, entity, diff --git a/packages/graphql/src/translate/translate-create.ts b/packages/graphql/src/translate/translate-create.ts index 64e617ea45..a5996ca17d 100644 --- a/packages/graphql/src/translate/translate-create.ts +++ b/packages/graphql/src/translate/translate-create.ts @@ -20,7 +20,6 @@ import type Cypher from "@neo4j/cypher-builder"; import Debug from "debug"; import type { ResolveTree } from "graphql-parse-resolve-info"; -import type { Node } from "../classes"; import { DEBUG_TRANSLATE } from "../constants"; import type { EntityAdapter } from "../schema-model/entity/EntityAdapter"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; @@ -30,6 +29,7 @@ import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; import { CallbackBucket } from "./queryAST/utils/callback-bucket"; import unwindCreate from "./unwind-create"; import { buildClause } from "./utils/build-clause"; +import { type ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; const debug = Debug(DEBUG_TRANSLATE); @@ -67,16 +67,12 @@ async function translateUsingQueryAST({ export async function translateCreate({ context, - node, + entityAdapter, }: { context: Neo4jGraphQLTranslationContext; - node: Node; + entityAdapter: ConcreteEntityAdapter; }): Promise<{ cypher: string; params: Record }> { const { resolveTree } = context; - const entityAdapter = context.schemaModel.getConcreteEntityAdapter(node.name); - if (!entityAdapter) { - throw new Error(`Transpilation error: ${node.name} is not a concrete entity`); - } const mutationInputs = resolveTree.args.input as any[]; const { isSupported, reason } = isUnwindCreateSupported(entityAdapter, asArray(mutationInputs), context); if (isSupported) { diff --git a/packages/graphql/src/translate/translate-top-level-cypher.ts b/packages/graphql/src/translate/translate-top-level-cypher.ts index 9a051c3d7c..0dcd525319 100644 --- a/packages/graphql/src/translate/translate-top-level-cypher.ts +++ b/packages/graphql/src/translate/translate-top-level-cypher.ts @@ -22,22 +22,22 @@ import Debug from "debug"; import { DEBUG_TRANSLATE } from "../constants"; import type { AuthenticationOperation } from "../schema-model/annotation/AuthenticationAnnotation"; import { getEntityAdapter } from "../schema-model/utils/get-entity-adapter"; -import type { CypherField } from "../types"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; import { applyAuthentication } from "./authorization/utils/apply-authentication"; import { QueryASTContext, QueryASTEnv } from "./queryAST/ast/QueryASTContext"; import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; import { buildClause } from "./utils/build-clause"; +import { type AttributeAdapter } from "../schema-model/attribute/model-adapters/AttributeAdapter"; const debug = Debug(DEBUG_TRANSLATE); export function translateTopLevelCypher({ context, - field, + attributeAdapter, type, }: { context: Neo4jGraphQLTranslationContext; - field: CypherField; + attributeAdapter: AttributeAdapter; type: "Query" | "Mutation"; }): Cypher.CypherResult { @@ -45,12 +45,11 @@ export function translateTopLevelCypher({ if (!operation) { throw new Error(`Failed to find operation ${type} in Schema Model.`); } - const operationField = operation.findAttribute(field.fieldName); + const operationField = operation.findAttribute(attributeAdapter.name); if (!operationField) { - throw new Error(`Failed to find field ${field.fieldName} on operation ${type}.`); + throw new Error(`Failed to find field ${attributeAdapter.name} on operation ${type}.`); } - const entity = context.schemaModel.entities.get(field.typeMeta.name); - + const entity = context.schemaModel.entities.get(attributeAdapter.getTypeName()); const annotation = operationField.annotations.authentication; if (annotation) { const targetOperations: AuthenticationOperation[] = diff --git a/packages/graphql/src/types/index.ts b/packages/graphql/src/types/index.ts index f46ce84b73..596f6a7e5b 100644 --- a/packages/graphql/src/types/index.ts +++ b/packages/graphql/src/types/index.ts @@ -19,23 +19,18 @@ import type Cypher from "@neo4j/cypher-builder"; import type { EventEmitter } from "events"; -import type { DirectiveNode, InputValueDefinitionNode, TypeNode } from "graphql"; +import type { TypeNode } from "graphql"; import type { Directive } from "graphql-compose"; -import type { ResolveTree } from "graphql-parse-resolve-info"; import type { JWTVerifyOptions, RemoteJWKSetOptions } from "jose"; import type { Integer } from "neo4j-driver"; import type { Neo4jGraphQLSubscriptionsCDCEngine } from "../classes/subscription/Neo4jGraphQLSubscriptionsCDCEngine"; -import type { RelationshipNestedOperationsOption, RelationshipQueryDirectionOption } from "../constants"; import type { Neo4jGraphQLSchemaModel } from "../schema-model/Neo4jGraphQLSchemaModel"; import type { DefaultAnnotationValue } from "../schema-model/annotation/DefaultAnnotation"; import type { FulltextField } from "../schema-model/annotation/FulltextAnnotation"; import type { VectorField } from "../schema-model/annotation/VectorAnnotation"; -import type { RelationshipDirection } from "../schema-model/relationship/Relationship"; import type { JwtPayload } from "./jwt-payload"; import type { Neo4jGraphQLContext } from "./neo4j-graphql-context"; -export { Node } from "../classes"; - export type AuthorizationContext = { jwt?: JwtPayload; jwtParam: Cypher.Param; @@ -59,10 +54,6 @@ export type VectorContext = { vectorSettings: Neo4jVectorSettings; }; -export type FullText = { - indexes: FulltextContext[]; -}; - /** * Metadata about a field.type on either * FieldDefinitionNode or InputValueDefinitionNode. @@ -89,122 +80,6 @@ export interface TypeMeta { originalType?: TypeNode; } -export type Unique = { - constraintName: string; -}; - -export interface Callback { - operations: CallbackOperations[]; - callbackName: string; -} - -export type SelectableOptions = { - onRead: boolean; - onAggregate: boolean; -}; - -export type SettableOptions = { - onCreate: boolean; - onUpdate: boolean; -}; - -export type FilterableOptions = { - byValue: boolean; - byAggregate: boolean; -}; - -/** - * Representation a ObjectTypeDefinitionNode field. - */ -export interface BaseField { - fieldName: string; - typeMeta: TypeMeta; - otherDirectives: DirectiveNode[]; - arguments: InputValueDefinitionNode[]; - private?: boolean; - description?: string; - dbPropertyName?: string; - dbPropertyNameUnescaped?: string; - unique?: Unique; - selectableOptions: SelectableOptions; - settableOptions: SettableOptions; - filterableOptions: FilterableOptions; -} - -/** - * Representation of the `@relationship` directive and its meta. - */ -export interface RelationField extends BaseField { - direction: RelationshipDirection; - typeUnescaped: string; - type: string; - connectionPrefix?: string; - inherited: boolean; - properties?: string; - union?: UnionField; - interface?: InterfaceField; - queryDirection: RelationshipQueryDirectionOption; - nestedOperations: RelationshipNestedOperationsOption[]; - aggregate: boolean; -} - -export interface ConnectionField extends BaseField { - relationship: RelationField; - relationshipTypeName: string; -} - -/** - * Representation of the `@cypher` directive and its meta. - */ -export interface CypherField extends BaseField { - statement: string; - columnName: string; - isEnum: boolean; - isScalar: boolean; -} - -/** - * Representation of any field thats not - * a cypher directive or relationship directive - * String, Int, Float, ID, Boolean... (custom scalars). - */ -export interface PrimitiveField extends BaseField { - autogenerate?: boolean; - defaultValue?: any; - coalesceValue?: any; - callback?: Callback; - isGlobalIdField?: boolean; -} - -export type CustomScalarField = BaseField; - -export interface CustomEnumField extends BaseField { - // TODO Must be "Enum" - really needs refactoring into classes - kind: string; - defaultValue?: string | string[]; - coalesceValue?: string | string[]; -} - -export interface UnionField extends BaseField { - nodes?: string[]; -} - -export interface CustomResolverField extends BaseField { - requiredFields: Record; -} - -export interface InterfaceField extends BaseField { - implementations?: string[]; -} - -export type ObjectField = BaseField; - -export interface TemporalField extends PrimitiveField { - timestamps?: TimeStampOperations[]; -} - -export type PointField = BaseField; - export type SortDirection = "ASC" | "DESC"; export interface GraphQLSortArg { @@ -256,10 +131,6 @@ export interface ConnectionWhereArg { NOT?: ConnectionWhereArg; } -export type TimeStampOperations = "CREATE" | "UPDATE"; - -export type CallbackOperations = "CREATE" | "UPDATE"; - /* Object keys and enum values map to values at https://neo4j.com/docs/cypher-manual/current/query-tuning/query-options/#cypher-query-options */ @@ -281,43 +152,6 @@ export interface CypherQueryOptions { /** Input field for graphql-compose */ export type InputField = { type: string; defaultValue?: DefaultAnnotationValue; directives?: Directive[] } | string; -/** Raw event metadata returned from queries */ -export type NodeSubscriptionMeta = { - event: "create" | "update" | "delete"; - typename: string; - properties: { - old: Record; - new: Record; - }; - id: Integer | string | number; - timestamp: Integer | string | number; -}; -export type RelationshipSubscriptionMeta = - | RelationshipSubscriptionMetaTypenameParameters - | RelationshipSubscriptionMetaLabelsParameters; -type RelationshipSubscriptionMetaCommonParameters = { - event: "create_relationship" | "delete_relationship"; - relationshipName: string; - id_from: Integer | string | number; - id_to: Integer | string | number; - properties: { - from: Record; - to: Record; - relationship: Record; - }; - id: Integer | string | number; - timestamp: Integer | string | number; -}; -export type RelationshipSubscriptionMetaTypenameParameters = RelationshipSubscriptionMetaCommonParameters & { - fromTypename: string; - toTypename: string; -}; -export type RelationshipSubscriptionMetaLabelsParameters = RelationshipSubscriptionMetaCommonParameters & { - fromLabels: string[]; - toLabels: string[]; -}; -export type EventMeta = NodeSubscriptionMeta | RelationshipSubscriptionMeta; - export type NodeSubscriptionsEvent = | { event: "create"; @@ -480,10 +314,3 @@ export type Neo4jFeaturesSettings = { export type ContextFeatures = Neo4jFeaturesSettings & { subscriptionsEngine?: Neo4jGraphQLSubscriptionsEngine; }; - -export type PredicateReturn = { - predicate: Cypher.Predicate | undefined; - preComputedSubqueries?: Cypher.CompositeClause | undefined; -}; - -export type CypherFieldReferenceMap = Record; diff --git a/packages/graphql/src/utils/find-conflicting-properties.ts b/packages/graphql/src/utils/find-conflicting-properties.ts index 6af7bac200..9173c3d311 100644 --- a/packages/graphql/src/utils/find-conflicting-properties.ts +++ b/packages/graphql/src/utils/find-conflicting-properties.ts @@ -17,48 +17,9 @@ * limitations under the License. */ -import type { GraphElement } from "../classes"; import type { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { RelationshipAdapter } from "../schema-model/relationship/model-adapters/RelationshipAdapter"; import { parseMutationField } from "../translate/queryAST/factory/parsers/parse-mutation-field"; -import mapToDbProperty from "./map-to-db-property"; - -/** returns conflicting mutation input properties - * @deprecated - */ -export function findConflictingProperties({ - graphElement, - input, -}: { - graphElement: GraphElement; - input: Record | undefined; -}): string[] { - if (!input) { - return []; - } - const dbPropertiesToInputFieldNames: Record = Object.keys(input).reduce((acc, rawField) => { - const { fieldName } = parseMutationField(rawField); - - const dbName = mapToDbProperty(graphElement, fieldName); - // some input fields (eg relation fields) have no corresponding db name in the map - if (!dbName) { - return acc; - } - if (acc[dbName]) { - acc[dbName].push(rawField); - } else { - acc[dbName] = [rawField]; - } - return acc; - }, {}); - - return Object.values(dbPropertiesToInputFieldNames) - .filter((v) => v.length > 1) - .reduce((acc, el) => { - acc.push(...el); - return acc; - }, []); -} export function findConflictingAttributes( fields: string[], diff --git a/packages/graphql/src/utils/map-to-db-property.ts b/packages/graphql/src/utils/map-to-db-property.ts deleted file mode 100644 index c01e8dcb5b..0000000000 --- a/packages/graphql/src/utils/map-to-db-property.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { GraphElement } from "../classes"; - -// For the @alias directive to be used -function mapToDbProperty(item: GraphElement, graphQLField: string): string { - const itemProp = item.primitiveFields - .concat(item.temporalFields, item.pointFields) - .find(({ fieldName }) => fieldName === graphQLField); - return itemProp?.dbPropertyNameUnescaped || graphQLField; -} - -export default mapToDbProperty; diff --git a/packages/graphql/tests/integration/directives/customResolver.int.test.ts b/packages/graphql/tests/integration/directives/customResolver.int.test.ts index aaceaec113..f4a7067d3c 100644 --- a/packages/graphql/tests/integration/directives/customResolver.int.test.ts +++ b/packages/graphql/tests/integration/directives/customResolver.int.test.ts @@ -18,7 +18,7 @@ */ import gql from "graphql-tag"; -import { INVALID_REQUIRED_FIELD_ERROR } from "../../../src/schema/get-custom-resolver-meta"; +import { INVALID_REQUIRED_FIELD_ERROR } from "../../../src/schema/selection-set-to-resolve-tree"; import { createBearerToken } from "../../utils/create-bearer-token"; import type { UniqueType } from "../../utils/graphql-types"; import { TestHelper } from "../../utils/tests-helper"; diff --git a/packages/graphql/tests/integration/multi-database.int.test.ts b/packages/graphql/tests/integration/multi-database.int.test.ts index 6c4d674a28..d08bc9b0ba 100644 --- a/packages/graphql/tests/integration/multi-database.int.test.ts +++ b/packages/graphql/tests/integration/multi-database.int.test.ts @@ -23,7 +23,7 @@ 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", () => { +describe.skip("multi-database", () => { let driver: Driver; const testHelper = new TestHelper(); const id = generate({ diff --git a/packages/graphql/tests/utils/builders/context-builder.ts b/packages/graphql/tests/utils/builders/context-builder.ts index 335a918931..cbdfaad286 100644 --- a/packages/graphql/tests/utils/builders/context-builder.ts +++ b/packages/graphql/tests/utils/builders/context-builder.ts @@ -32,8 +32,6 @@ export class ContextBuilder extends Builder = {}) { super({ resolveTree: {} as ResolveTree, - nodes: [], - relationships: [], schemaModel: new Neo4jGraphQLSchemaModel({ concreteEntities: [] as ConcreteEntity[], compositeEntities: [] as CompositeEntity[], diff --git a/packages/graphql/tests/utils/builders/node-builder.ts b/packages/graphql/tests/utils/builders/node-builder.ts deleted file mode 100644 index 6581292d93..0000000000 --- a/packages/graphql/tests/utils/builders/node-builder.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { NodeConstructor } from "../../../src/classes"; -import { Node } from "../../../src/classes"; -import type { NodeDirectiveConstructor } from "../../../src/classes/NodeDirective"; -import { NodeDirective } from "../../../src/classes/NodeDirective"; -import { Builder } from "./builder"; - -export class NodeBuilder extends Builder { - constructor(newOptions: Partial = {}) { - super({ - name: "", - relationFields: [], - connectionFields: [], - cypherFields: [], - primitiveFields: [], - scalarFields: [], - enumFields: [], - otherDirectives: [], - propagatedDirectives: [], - unionFields: [], - interfaceFields: [], - interfaces: [], - objectFields: [], - temporalFields: [], - pointFields: [], - customResolverFields: [], - ...newOptions, - }); - } - - public with(newOptions: Partial): NodeBuilder { - this.options = { ...this.options, ...newOptions }; - return this; - } - - public withNodeDirective(directiveOptions: NodeDirectiveConstructor): NodeBuilder { - const nodeDirective = new NodeDirective(directiveOptions); - return this.with({ nodeDirective }); - } - - public instance(): Node { - return new Node(this.options); - } -} From fd5db6c54d2b76d2933e32b5b42991086a5fe680 Mon Sep 17 00:00:00 2001 From: a-alle Date: Thu, 20 Nov 2025 17:13:35 +0000 Subject: [PATCH 3/3] remove skip --- packages/graphql/tests/integration/multi-database.int.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql/tests/integration/multi-database.int.test.ts b/packages/graphql/tests/integration/multi-database.int.test.ts index d08bc9b0ba..6c4d674a28 100644 --- a/packages/graphql/tests/integration/multi-database.int.test.ts +++ b/packages/graphql/tests/integration/multi-database.int.test.ts @@ -23,7 +23,7 @@ import type { UniqueType } from "../utils/graphql-types"; import { isMultiDbUnsupportedError } from "../utils/is-multi-db-unsupported-error"; import { TestHelper } from "../utils/tests-helper"; -describe.skip("multi-database", () => { +describe("multi-database", () => { let driver: Driver; const testHelper = new TestHelper(); const id = generate({