UNPKG

@neo4j/graphql

Version:

A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations

418 lines 20.1 kB
"use strict"; /* * Copyright (c) "Neo4j" * Neo4j Sweden AB [http://neo4j.com] * * This file is part of Neo4j. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ConnectionFactory = void 0; const graphql_compose_1 = require("graphql-compose"); const graphql_relay_1 = require("graphql-relay"); const neo4j_driver_1 = require("neo4j-driver"); const InterfaceEntity_1 = require("../../../../schema-model/entity/InterfaceEntity"); const ConcreteEntityAdapter_1 = require("../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"); const deep_merge_1 = require("../../../../utils/deep-merge"); const check_authentication_1 = require("../../../authorization/check-authentication"); const ConnectionAggregationField_1 = require("../../ast/fields/ConnectionAggregationField"); const ConnectionReadOperation_1 = require("../../ast/operations/ConnectionReadOperation"); const CompositeConnectionPartial_1 = require("../../ast/operations/composite/CompositeConnectionPartial"); const CompositeConnectionReadOperation_1 = require("../../ast/operations/composite/CompositeConnectionReadOperation"); const NodeSelection_1 = require("../../ast/selection/NodeSelection"); const RelationshipSelection_1 = require("../../ast/selection/RelationshipSelection"); const get_concrete_entities_1 = require("../../utils/get-concrete-entities"); const get_entity_interfaces_1 = require("../../utils/get-entity-interfaces"); const is_interface_entity_1 = require("../../utils/is-interface-entity"); const is_relationship_entity_1 = require("../../utils/is-relationship-entity"); const is_union_entity_1 = require("../../utils/is-union-entity"); const find_fields_by_name_in_fields_by_type_name_field_1 = require("../parsers/find-fields-by-name-in-fields-by-type-name-field"); const get_fields_by_type_name_1 = require("../parsers/get-fields-by-type-name"); const AggregateFactory_1 = require("./AggregateFactory"); const FulltextFactory_1 = require("./FulltextFactory"); class ConnectionFactory { constructor(queryASTFactory) { this.queryASTFactory = queryASTFactory; this.fulltextFactory = new FulltextFactory_1.FulltextFactory(queryASTFactory); this.aggregateFactory = new AggregateFactory_1.AggregateFactory(queryASTFactory); } createCompositeConnectionOperationAST({ relationship, target, resolveTree, context, }) { const resolveTreeWhere = this.queryASTFactory.operationsFactory.getWhereArgs(resolveTree); let nodeWhere; if ((0, is_interface_entity_1.isInterfaceEntity)(target)) { nodeWhere = (0, graphql_compose_1.isObject)(resolveTreeWhere) ? resolveTreeWhere.node : {}; } else { nodeWhere = resolveTreeWhere; } const concreteEntities = (0, get_concrete_entities_1.getConcreteEntities)(target, nodeWhere); const concreteConnectionOperations = concreteEntities.map((concreteEntity) => { let selection; let resolveTreeEdgeFields; if (relationship) { selection = new RelationshipSelection_1.RelationshipSelection({ relationship, targetOverride: concreteEntity, }); resolveTreeEdgeFields = this.parseConnectionFields({ entityOrRel: relationship, target: concreteEntity, resolveTree, }).edges; } else { selection = new NodeSelection_1.NodeSelection({ target: concreteEntity, }); resolveTreeEdgeFields = this.parseConnectionFields({ entityOrRel: concreteEntity, target: concreteEntity, resolveTree, }).edges; } const connectionPartial = new CompositeConnectionPartial_1.CompositeConnectionPartial({ relationship, target: concreteEntity, selection, }); return this.hydrateConnectionOperationAST({ relationship, target: concreteEntity, resolveTree, context, operation: connectionPartial, whereArgs: resolveTreeWhere, resolveTreeEdgeFields, partialOf: target, }); }); const compositeConnectionOp = new CompositeConnectionReadOperation_1.CompositeConnectionReadOperation(concreteConnectionOperations); // These sort fields will be duplicated on nested "CompositeConnectionPartial" this.hydrateConnectionOperationsASTWithSort({ entityOrRel: relationship ?? target, resolveTree, operation: compositeConnectionOp, context, }); if ((0, is_interface_entity_1.isInterfaceEntity)(target)) { let fields; if (relationship) { fields = resolveTree.fieldsByTypeName[relationship.operations.connectionFieldTypename]; } else { fields = resolveTree.fieldsByTypeName[target.operations.connectionFieldTypename]; } if (fields) { const resolveTreeAggregate = (0, find_fields_by_name_in_fields_by_type_name_field_1.findFieldsByNameInFieldsByTypeNameField)(fields, "aggregate")[0]; this.hydrateConnectionOperationWithAggregation({ target, resolveTreeAggregate, relationship, context, operation: compositeConnectionOp, whereArgs: resolveTreeWhere, // Cascades the filters from connection down to the aggregation generation, to appply them to aggregation match }); } } return compositeConnectionOp; } createConnectionOperationAST({ relationship, target, resolveTree, context, }) { if (!(target instanceof ConcreteEntityAdapter_1.ConcreteEntityAdapter)) { return this.createCompositeConnectionOperationAST({ relationship, target, resolveTree, context, }); } const resolveTreeWhere = this.queryASTFactory.operationsFactory.getWhereArgs(resolveTree); (0, check_authentication_1.checkEntityAuthentication)({ entity: target.entity, targetOperations: ["READ"], context, }); let selection; let resolveTreeEdgeFields; let totalCountEdgeField; let pageInfoEdgeField; if (relationship) { selection = new RelationshipSelection_1.RelationshipSelection({ relationship, }); const { edges, totalCount, pageInfo } = this.parseConnectionFields({ entityOrRel: relationship, target, resolveTree, }); resolveTreeEdgeFields = edges; totalCountEdgeField = totalCount; pageInfoEdgeField = pageInfo; } else { if (context.resolveTree.args.fulltext || context.resolveTree.args.phrase) { selection = this.fulltextFactory.getFulltextSelection(target, context); } else { selection = new NodeSelection_1.NodeSelection({ target, }); } const { edges, totalCount, pageInfo } = this.parseConnectionFields({ entityOrRel: target, target, resolveTree, }); resolveTreeEdgeFields = edges; totalCountEdgeField = totalCount; pageInfoEdgeField = pageInfo; } const operation = new ConnectionReadOperation_1.ConnectionReadOperation({ relationship, target, selection }); if (Object.keys(resolveTreeEdgeFields).length === 0 && !totalCountEdgeField && !pageInfoEdgeField) { operation.skipConnection = true; } this.hydrateConnectionOperationAST({ relationship, target: target, resolveTree, context, operation, whereArgs: resolveTreeWhere, resolveTreeEdgeFields, }); const resolveTreeAggregate = this.parseAggregateFields({ entityOrRel: relationship ?? target, target, resolveTree, }); this.hydrateConnectionOperationWithAggregation({ target, resolveTreeAggregate: resolveTreeAggregate[0], relationship, context, operation, whereArgs: resolveTreeWhere, // Cascades the filters from connection down to the aggregation generation, to appply them to aggregation match }); return operation; } hydrateConnectionOperationWithAggregation({ target, resolveTreeAggregate, relationship, context, operation, whereArgs, }) { if (relationship) { const resolveTreeAggregateFields = resolveTreeAggregate?.fieldsByTypeName[relationship.operations.getAggregateFieldTypename()]; if (resolveTreeAggregate && resolveTreeAggregateFields) { const aggregationOperation = this.aggregateFactory.createAggregationOperation({ entityOrRel: relationship ?? target, resolveTree: resolveTreeAggregate, context, extraWhereArgs: whereArgs, }); const aggregationField = new ConnectionAggregationField_1.ConnectionAggregationField({ alias: resolveTreeAggregate.name, // Alias is hanlded by graphql on top level nodeAlias: "node", operation: aggregationOperation, }); operation.setAggregationField(aggregationField); } } else { const resolveTreeAggregateFields = resolveTreeAggregate?.fieldsByTypeName[target.operations.aggregateTypeNames.connection]; if (resolveTreeAggregate && resolveTreeAggregateFields) { const aggregationOperation = this.aggregateFactory.createAggregationOperation({ entityOrRel: relationship ?? target, resolveTree: resolveTreeAggregate, context, extraWhereArgs: whereArgs, }); const aggregationField = new ConnectionAggregationField_1.ConnectionAggregationField({ alias: resolveTreeAggregate.name, // Alias is hanlded by graphql on top level nodeAlias: "node", operation: aggregationOperation, }); operation.setAggregationField(aggregationField); } } } hydrateConnectionOperationsASTWithSort({ entityOrRel, resolveTree, operation, context, }) { let options; const target = (0, is_relationship_entity_1.isRelationshipEntity)(entityOrRel) ? entityOrRel.target : entityOrRel; if (!(0, is_union_entity_1.isUnionEntity)(target)) { options = this.getConnectionOptions(target, resolveTree.args); } else { options = resolveTree.args; } const first = options?.first; const sort = options?.sort; const afterArg = options?.after; const offset = (0, graphql_compose_1.isString)(afterArg) ? (0, graphql_relay_1.cursorToOffset)(afterArg) + 1 : undefined; if (first || offset) { const pagination = this.queryASTFactory.sortAndPaginationFactory.createPagination({ limit: first, offset, }); if (pagination) { operation.addPagination(pagination); } } if (sort) { sort.forEach((options) => { const sort = this.queryASTFactory.sortAndPaginationFactory.createConnectionSortFields(options, entityOrRel, context); operation.addSort(sort); }); } return operation; } // The current top-level Connection API is inconsistent with the rest of the API making the parsing more complex than it should be. // This function temporary adjust some inconsistencies waiting for the new API. // TODO: Remove it when the new API is ready. normalizeResolveTreeForTopLevelConnection(resolveTree) { const topLevelConnectionResolveTree = Object.assign({}, resolveTree); // Move the sort arguments inside a "node" object. if (topLevelConnectionResolveTree.args.sort) { topLevelConnectionResolveTree.args.sort = resolveTree.args.sort.map((sortField) => { return { node: sortField }; }); } // move the where arguments inside a "node" object. if (topLevelConnectionResolveTree.args.where) { topLevelConnectionResolveTree.args.where = { node: resolveTree.args.where }; } return topLevelConnectionResolveTree; } splitConnectionFields(rawFields) { let nodeField; let edgeField; const fields = {}; Object.entries(rawFields).forEach(([key, field]) => { if (field.name === "node") { nodeField = field; } else if (field.name === "edge") { edgeField = field; } else { fields[key] = field; } }); const result = { node: nodeField, edge: edgeField, fields, }; return result; } getConnectionOptions(entity, args) { const limitDirective = entity.annotations.limit; let limit = args?.first ?? limitDirective?.default ?? limitDirective?.max; if (limit instanceof neo4j_driver_1.Integer) { limit = limit.toNumber(); } const maxLimit = limitDirective?.max; if (limit !== undefined && maxLimit !== undefined) { limit = Math.min(limit, maxLimit); } if (limit === undefined && args.after === undefined && args.sort === undefined) return undefined; return { first: limit, after: args.after, sort: args.sort, }; } hydrateConnectionOperationAST({ relationship, target, resolveTree, context, operation, whereArgs, resolveTreeEdgeFields, partialOf, }) { const entityOrRel = relationship ?? target; const nodeFieldsRaw = (0, find_fields_by_name_in_fields_by_type_name_field_1.findFieldsByNameInFieldsByTypeNameField)(resolveTreeEdgeFields, "node"); const propertiesFieldsRaw = (0, find_fields_by_name_in_fields_by_type_name_field_1.findFieldsByNameInFieldsByTypeNameField)(resolveTreeEdgeFields, "properties"); this.hydrateConnectionOperationsASTWithSort({ entityOrRel, resolveTree, operation, context, }); const isTopLevel = !relationship; const resolveTreeNodeFieldsTypesNames = [ target.name, ...target.compositeEntities.filter((e) => e instanceof InterfaceEntity_1.InterfaceEntity).map((e) => e.name), ]; if (!isTopLevel) { resolveTreeNodeFieldsTypesNames.push(relationship.target.name); } const resolveTreeNodeFields = (0, get_fields_by_type_name_1.getFieldsByTypeName)(nodeFieldsRaw, resolveTreeNodeFieldsTypesNames); const nodeFields = this.queryASTFactory.fieldFactory.createFields(target, resolveTreeNodeFields, context); let edgeFields = []; if (!isTopLevel && relationship.propertiesTypeName) { const resolveTreePropertiesFields = (0, get_fields_by_type_name_1.getFieldsByTypeName)(propertiesFieldsRaw, [ relationship.propertiesTypeName, ]); edgeFields = this.queryASTFactory.fieldFactory.createFields(relationship, resolveTreePropertiesFields, context); } const authFilters = this.queryASTFactory.authorizationFactory.getAuthFilters({ entity: target, operations: ["READ"], attributes: this.queryASTFactory.operationsFactory.getSelectedAttributes(target, resolveTreeNodeFields), context, }); const filters = this.queryASTFactory.filterFactory.createConnectionPredicates({ rel: relationship, entity: target, where: whereArgs, partialOf, }); operation.setNodeFields(nodeFields); operation.setEdgeFields(edgeFields); operation.addFilters(...filters); operation.addAuthFilters(...authFilters); return operation; } parseAggregateFields({ target, resolveTree, entityOrRel, }) { const resolveTreeConnectionFields = this.parseConnectionResolveTree({ entityOrRel, target, resolveTree, }); return (0, find_fields_by_name_in_fields_by_type_name_field_1.findFieldsByNameInFieldsByTypeNameField)(resolveTreeConnectionFields, "aggregate"); } parseConnectionFields({ target, resolveTree, entityOrRel, }) { const resolveTreeConnectionFields = this.parseConnectionResolveTree({ entityOrRel, target, resolveTree, }); const entityInterfaces = (0, get_entity_interfaces_1.getEntityInterfaces)(target); const edgeFieldsRaw = (0, find_fields_by_name_in_fields_by_type_name_field_1.findFieldsByNameInFieldsByTypeNameField)(resolveTreeConnectionFields, "edges"); const totalCountFieldRaw = (0, find_fields_by_name_in_fields_by_type_name_field_1.findFieldsByNameInFieldsByTypeNameField)(resolveTreeConnectionFields, "totalCount"); const pageInfoFieldRaw = (0, find_fields_by_name_in_fields_by_type_name_field_1.findFieldsByNameInFieldsByTypeNameField)(resolveTreeConnectionFields, "pageInfo"); const interfacesEdgeFields = entityInterfaces.map((interfaceAdapter) => { return (0, get_fields_by_type_name_1.getFieldsByTypeName)(edgeFieldsRaw, `${interfaceAdapter.name}Edge`); }); const concreteEdgeFields = (0, get_fields_by_type_name_1.getFieldsByTypeName)(edgeFieldsRaw, entityOrRel.operations.relationshipFieldTypename); return { edges: (0, deep_merge_1.deepMerge)([...interfacesEdgeFields, concreteEdgeFields]), totalCount: totalCountFieldRaw[0], pageInfo: pageInfoFieldRaw[0], }; } parseConnectionResolveTree({ target, resolveTree, entityOrRel, }) { // Get interfaces of the entity const entityInterfaces = (0, get_entity_interfaces_1.getEntityInterfaces)(target); const interfacesFields = entityInterfaces.map((interfaceAdapter) => { return resolveTree.fieldsByTypeName[interfaceAdapter.operations.connectionFieldTypename] ?? {}; }); const concreteProjectionFields = { ...(resolveTree.fieldsByTypeName[entityOrRel.operations.connectionFieldTypename] ?? resolveTree.fieldsByTypeName[entityOrRel.operations.vectorTypeNames.connection]), }; return (0, deep_merge_1.deepMerge)([...interfacesFields, concreteProjectionFields]); } } exports.ConnectionFactory = ConnectionFactory; //# sourceMappingURL=ConnectionFactory.js.map