@neo4j/graphql
Version:
A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations
418 lines • 20.1 kB
JavaScript
"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