diff --git a/packages/graphql/src/translate/queryAST/factory/Operations/ConnectionFactory.ts b/packages/graphql/src/translate/queryAST/factory/Operations/ConnectionFactory.ts index f87c9d7076..e809dc27f0 100644 --- a/packages/graphql/src/translate/queryAST/factory/Operations/ConnectionFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/Operations/ConnectionFactory.ts @@ -25,12 +25,11 @@ import { Integer } from "neo4j-driver"; import type { EntityAdapter } from "../../../../schema-model/entity/EntityAdapter"; import { InterfaceEntity } from "../../../../schema-model/entity/InterfaceEntity"; import { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import { InterfaceEntityAdapter } from "../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; +import type { InterfaceEntityAdapter } from "../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; import type { UnionEntityAdapter } from "../../../../schema-model/entity/model-adapters/UnionEntityAdapter"; import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import type { ConnectionQueryArgs } from "../../../../types"; import type { Neo4jGraphQLTranslationContext } from "../../../../types/neo4j-graphql-translation-context"; -import { filterTruthy } from "../../../../utils/utils"; import { checkEntityAuthentication } from "../../../authorization/check-authentication"; import type { Field } from "../../ast/fields/Field"; import { ConnectionReadOperation } from "../../ast/operations/ConnectionReadOperation"; @@ -40,6 +39,7 @@ import type { EntitySelection } from "../../ast/selection/EntitySelection"; import { NodeSelection } from "../../ast/selection/NodeSelection"; import { RelationshipSelection } from "../../ast/selection/RelationshipSelection"; import { getConcreteEntities } from "../../utils/get-concrete-entities"; +import { getEntityInterfaces } from "../../utils/get-entity-interfaces"; import { isInterfaceEntity } from "../../utils/is-interface-entity"; import { isRelationshipEntity } from "../../utils/is-relationship-entity"; import { isUnionEntity } from "../../utils/is-union-entity"; @@ -239,6 +239,57 @@ export class ConnectionFactory { return topLevelConnectionResolveTree; } + public splitConnectionFields(rawFields: Record): { + node: ResolveTree | undefined; + edge: ResolveTree | undefined; + fields: Record; + } { + let nodeField: ResolveTree | undefined; + let edgeField: ResolveTree | undefined; + + const fields: Record = {}; + + Object.entries(rawFields).forEach(([key, field]) => { + if (field.name === "node") { + nodeField = field; + } else if (field.name === "edge") { + edgeField = field; + } else { + fields[key] = field; + } + }); + + return { + node: nodeField, + edge: edgeField, + fields, + }; + } + + public getConnectionOptions( + entity: ConcreteEntityAdapter | InterfaceEntityAdapter, + options: Record + ): Pick | undefined { + const limitDirective = entity.annotations.limit; + + let limit: Integer | number | undefined = options?.first ?? limitDirective?.default ?? limitDirective?.max; + if (limit instanceof Integer) { + limit = limit.toNumber(); + } + const maxLimit = limitDirective?.max; + if (limit !== undefined && maxLimit !== undefined) { + limit = Math.min(limit, maxLimit); + } + + if (limit === undefined && options.after === undefined && options.sort === undefined) return undefined; + + return { + first: limit, + after: options.after, + sort: options.sort, + }; + } + private hydrateConnectionOperationAST({ relationship, target, @@ -254,49 +305,14 @@ export class ConnectionFactory { operation: T; whereArgs: Record; }): T { - // hydrate hydrateConnectionOperationAST is used for both top-level and nested connections. - // If the relationship is defined use the RelationshipAdapter to infer the typeNames, if not use the target. - - // Get the abstract types of the interface - const entityInterfaces = filterTruthy( - target.compositeEntities.map((compositeEntity) => { - if (compositeEntity instanceof InterfaceEntity) { - return new InterfaceEntityAdapter(compositeEntity); - } - }) - ); - - const interfacesFields = entityInterfaces.map((interfaceAdapter) => { - return resolveTree.fieldsByTypeName[interfaceAdapter.operations.connectionFieldTypename] ?? {}; - }); - const entityOrRel = relationship ?? target; - const concreteProjectionFields = { - ...resolveTree.fieldsByTypeName[entityOrRel.operations.connectionFieldTypename], - }; - - const resolveTreeConnectionFields = mergeDeep[]>([ - ...interfacesFields, - concreteProjectionFields, - ]); - - const edgeFieldsRaw = findFieldsByNameInFieldsByTypeNameField(resolveTreeConnectionFields, "edges"); - - const interfacesEdgeFields = entityInterfaces.map((interfaceAdapter) => { - return getFieldsByTypeName(edgeFieldsRaw, `${interfaceAdapter.name}Edge`); + const resolveTreeEdgeFields = this.parseConnectionFields({ + entityOrRel, + target, + resolveTree, }); - const concreteEdgeFields = getFieldsByTypeName( - edgeFieldsRaw, - entityOrRel.operations.relationshipFieldTypename // Use interface operation - ); - - const resolveTreeEdgeFields = mergeDeep[]>([ - ...interfacesEdgeFields, - concreteEdgeFields, - ]); - const nodeFieldsRaw = findFieldsByNameInFieldsByTypeNameField(resolveTreeEdgeFields, "node"); const propertiesFieldsRaw = findFieldsByNameInFieldsByTypeNameField(resolveTreeEdgeFields, "properties"); this.hydrateConnectionOperationsASTWithSort({ @@ -350,54 +366,39 @@ export class ConnectionFactory { return operation; } - public splitConnectionFields(rawFields: Record): { - node: ResolveTree | undefined; - edge: ResolveTree | undefined; - fields: Record; - } { - let nodeField: ResolveTree | undefined; - let edgeField: ResolveTree | undefined; - - const fields: Record = {}; + private parseConnectionFields({ + target, + resolveTree, + entityOrRel, + }: { + entityOrRel: RelationshipAdapter | ConcreteEntityAdapter; + target: ConcreteEntityAdapter; + resolveTree: ResolveTree; + }): Record { + // Get interfaces of the entity + const entityInterfaces = getEntityInterfaces(target); - Object.entries(rawFields).forEach(([key, field]) => { - if (field.name === "node") { - nodeField = field; - } else if (field.name === "edge") { - edgeField = field; - } else { - fields[key] = field; - } + const interfacesFields = entityInterfaces.map((interfaceAdapter) => { + return resolveTree.fieldsByTypeName[interfaceAdapter.operations.connectionFieldTypename] ?? {}; }); - return { - node: nodeField, - edge: edgeField, - fields, + const concreteProjectionFields = { + ...resolveTree.fieldsByTypeName[entityOrRel.operations.connectionFieldTypename], }; - } - public getConnectionOptions( - entity: ConcreteEntityAdapter | InterfaceEntityAdapter, - options: Record - ): Pick | undefined { - const limitDirective = entity.annotations.limit; + const resolveTreeConnectionFields: Record = mergeDeep[]>([ + ...interfacesFields, + concreteProjectionFields, + ]); - let limit: Integer | number | undefined = options?.first ?? limitDirective?.default ?? limitDirective?.max; - if (limit instanceof Integer) { - limit = limit.toNumber(); - } - const maxLimit = limitDirective?.max; - if (limit !== undefined && maxLimit !== undefined) { - limit = Math.min(limit, maxLimit); - } + const edgeFieldsRaw = findFieldsByNameInFieldsByTypeNameField(resolveTreeConnectionFields, "edges"); - if (limit === undefined && options.after === undefined && options.sort === undefined) return undefined; + const interfacesEdgeFields = entityInterfaces.map((interfaceAdapter) => { + return getFieldsByTypeName(edgeFieldsRaw, `${interfaceAdapter.name}Edge`); + }); - return { - first: limit, - after: options.after, - sort: options.sort, - }; + const concreteEdgeFields = getFieldsByTypeName(edgeFieldsRaw, entityOrRel.operations.relationshipFieldTypename); + + return mergeDeep([...interfacesEdgeFields, concreteEdgeFields]); } } diff --git a/packages/graphql/src/translate/queryAST/utils/get-entity-interfaces.ts b/packages/graphql/src/translate/queryAST/utils/get-entity-interfaces.ts new file mode 100644 index 0000000000..3d63af12c2 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/utils/get-entity-interfaces.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InterfaceEntity } from "../../../schema-model/entity/InterfaceEntity"; +import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; +import { filterTruthy } from "../../../utils/utils"; + +/** + * Return all the interfaces the provided concrete entity inherits + * Note that this functions accepts and returns Adapters, not the raw entities + */ +export function getEntityInterfaces(entity: ConcreteEntityAdapter): InterfaceEntityAdapter[] { + return filterTruthy( + entity.compositeEntities.map((compositeEntity) => { + if (compositeEntity instanceof InterfaceEntity) { + return new InterfaceEntityAdapter(compositeEntity); + } + }) + ); +}