UNPKG

@neo4j/graphql

Version:

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

559 lines 28.8 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. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UpdateFactory = void 0; const cypher_builder_1 = __importDefault(require("@neo4j/cypher-builder")); const graphql_1 = require("graphql"); const utils_1 = require("../../../../utils/utils"); const OperationField_1 = require("../../ast/fields/OperationField"); const MutationOperationField_1 = require("../../ast/input-fields/MutationOperationField"); const MathInputField_1 = require("../../ast/input-fields/operators/MathInputField"); const PopInputField_1 = require("../../ast/input-fields/operators/PopInputField"); const PushInputField_1 = require("../../ast/input-fields/operators/PushInputField"); const ParamInputField_1 = require("../../ast/input-fields/ParamInputField"); const TopLevelUpdateMutationOperation_1 = require("../../ast/operations/TopLevelUpdateMutationOperation"); const UpdateOperation_1 = require("../../ast/operations/UpdateOperation"); const NodeSelectionPattern_1 = require("../../ast/selection/SelectionPattern/NodeSelectionPattern"); const RelationshipSelectionPattern_1 = require("../../ast/selection/SelectionPattern/RelationshipSelectionPattern"); const is_concrete_entity_1 = require("../../utils/is-concrete-entity"); const is_union_entity_1 = require("../../utils/is-union-entity"); const raise_attribute_ambiguity_1 = require("../../utils/raise-attribute-ambiguity"); const get_autogenerated_fields_1 = require("../parsers/get-autogenerated-fields"); const parse_mutation_field_1 = require("../parsers/parse-mutation-field"); class UpdateFactory { constructor(queryASTFactory) { this.queryASTFactory = queryASTFactory; } createUpdateOperation(entity, resolveTree, context, callbackBucket, varName) { const rawInput = resolveTree.args.update; const input = (0, utils_1.asArray)(rawInput) ?? []; if (!input.length) { // dummy input to translate top level match for the projection to work input.push({}); } const updateOperations = input.map((inputItem) => { const updateOperation = new UpdateOperation_1.UpdateOperation({ target: entity, selectionPattern: new NodeSelectionPattern_1.NodeSelectionPattern({ target: entity, alias: varName, }), }); this.hydrateUpdateOperation({ target: entity, input: inputItem, update: updateOperation, callbackBucket, context, whereArgs: { node: resolveTree.args.where ?? {}, }, }); return updateOperation; }); const responseFields = Object.values(resolveTree.fieldsByTypeName[entity.operations.mutationResponseTypeNames.update] ?? {}); const projectionOperations = responseFields .filter((f) => f.name === entity.plural) .map((field) => { const readOP = this.queryASTFactory.operationsFactory.createReadOperation({ entityOrRel: entity, resolveTree: field, context, }); const fieldOperation = new OperationField_1.OperationField({ operation: readOP, alias: field.alias, }); return fieldOperation; }); const topLevelMutation = new TopLevelUpdateMutationOperation_1.TopLevelUpdateMutationOperation({ updateOperations, projectionOperations, }); return topLevelMutation; } hydrateUpdateOperation({ target, relationship, input, update, callbackBucket, context, whereArgs, }) { const isNested = Boolean(relationship); const autoGeneratedFields = (0, get_autogenerated_fields_1.getAutogeneratedFieldsForUpdate)(target); autoGeneratedFields.forEach((field) => { update.addField(field); }); if (relationship) { const autoGeneratedFields = (0, get_autogenerated_fields_1.getAutogeneratedFieldsForUpdate)(relationship); autoGeneratedFields.forEach((field) => { field.attachedTo = "relationship"; update.addField(field); }); } if (this.shouldApplyUpdateAuthorization(input, target, isNested)) { this.addEntityAuthorization({ entity: target, context, operation: update }); } (0, utils_1.asArray)(input).forEach((inputItem) => { const targetInput = this.getInputNode(inputItem, isNested); (0, raise_attribute_ambiguity_1.raiseAttributeAmbiguityForUpdate)(Object.keys(targetInput), target); (0, raise_attribute_ambiguity_1.raiseAttributeAmbiguityForUpdate)(Object.keys(this.getInputEdge(inputItem)), relationship); if (whereArgs) { const filters = this.queryASTFactory.filterFactory.createConnectionPredicates({ rel: relationship, entity: target, where: whereArgs, }); update.addFilters(...filters); } for (const key of Object.keys(targetInput)) { const { fieldName, operator } = (0, parse_mutation_field_1.parseMutationField)(key); const nestedRelationship = target.relationships.get(fieldName); const attribute = target.attributes.get(fieldName); if (!attribute && !nestedRelationship) { throw new Error(`Transpile Error: Input field ${key} not found in entity ${target.name}`); } if (attribute) { if (operator) { const value = targetInput[key]; if (attribute.typeHelper.isRequired() && value === null && operator === "SET") { throw new Error(`Cannot set non-nullable field ${target.name}.${attribute.name} to null`); } const paramInputField = this.getInputFieldDeprecated("node", operator, attribute, value); update.addField(paramInputField); this.addAttributeAuthorization({ attribute, context, update, entity: target, }); } else { const operations = Object.keys(targetInput[fieldName]); if (operations.length > 1) { const conflictingOperations = operations.map((op) => `[[${op}]]`); throw new graphql_1.GraphQLError(`Conflicting modification of field ${fieldName}: ${conflictingOperations.join(", ")} on type ${target.name}`); } for (const op of Object.keys(targetInput[fieldName])) { const value = targetInput[fieldName][op]; if (attribute.typeHelper.isRequired() && value === null && op === "set") { throw new Error(`Cannot set non-nullable field ${target.name}.${attribute.name} to null`); } const paramInputField = this.getInputField("node", op, attribute, value); update.addField(paramInputField); this.addAttributeAuthorization({ attribute, context, update, entity: target, }); } } } else if (nestedRelationship) { const nestedEntity = nestedRelationship.target; const operationInput = targetInput[key] ?? {}; const entityAndNodeInput = []; if ((0, is_union_entity_1.isUnionEntity)(nestedEntity)) { Object.entries(operationInput).forEach(([entityTypename, input]) => { const concreteNestedEntity = nestedEntity.concreteEntities.find((e) => e.name === entityTypename); if (!concreteNestedEntity) { throw new Error("Concrete entity not found in create, please contact support"); } entityAndNodeInput.push([concreteNestedEntity, input]); }); } else { entityAndNodeInput.push([nestedEntity, operationInput]); } entityAndNodeInput.forEach(([nestedEntity, operations]) => { operations.forEach((operationInput) => { const nestedUpdateInput = operationInput.update; if (nestedUpdateInput) { (0, utils_1.asArray)(nestedUpdateInput).forEach((nestedUpdateInputItem) => { this.createNestedUpdateOperation({ nestedEntity, nestedRelationship, nestedUpdateInputItem, context, callbackBucket, operation: update, key, }); }); } const nestedCreateInput = operationInput.create; if (nestedCreateInput) { (0, utils_1.asArray)(nestedCreateInput).forEach((nestedCreateInputItem) => { let edgeField = nestedCreateInputItem.edge ?? {}; // This is to parse the create input for a declareRelationship // We are checking the relationship target, because for nestedRelationship is // already disambiguated into concrete entity if (relationship?.target && !(0, is_concrete_entity_1.isConcreteEntity)(relationship?.target)) { if (nestedRelationship.propertiesTypeName) { edgeField = edgeField[nestedRelationship.propertiesTypeName] ?? {}; } } const concreteNestedCreateInput = { node: nestedCreateInputItem.node ?? {}, edge: edgeField, }; this.queryASTFactory.operationsFactory.createNestedCreateOperation({ targetEntity: nestedEntity, relationship: nestedRelationship, input: concreteNestedCreateInput, context, callbackBucket, key, operation: update, }); }); } const nestedConnectInput = operationInput.connect; if (nestedConnectInput) { (0, utils_1.asArray)(nestedConnectInput).forEach((nestedConnectInputItem) => { const nestedConnectOperation = this.queryASTFactory.operationsFactory.createConnectOperation(nestedEntity, nestedRelationship, nestedConnectInputItem, context, callbackBucket); const mutationOperationField = new MutationOperationField_1.MutationOperationField(nestedConnectOperation, key); update.addField(mutationOperationField); }); } const nestedDeleteInput = operationInput.delete; if (nestedDeleteInput) { (0, utils_1.asArray)(nestedDeleteInput).forEach((nestedDeleteInputItem) => { const nestedDeleteOperations = this.queryASTFactory.operationsFactory.createNestedDeleteOperationsForUpdate(nestedDeleteInputItem, nestedRelationship, context, nestedEntity); for (const nestedDeleteOperation of nestedDeleteOperations) { const mutationOperationField = new MutationOperationField_1.MutationOperationField(nestedDeleteOperation, key); update.addField(mutationOperationField); } }); } const nestedDisconnectInput = operationInput.disconnect; if (nestedDisconnectInput) { (0, utils_1.asArray)(nestedDisconnectInput).forEach((nestedDisconnectInputItem) => { const nestedDisconnectOperation = this.queryASTFactory.operationsFactory.createDisconnectOperation(nestedEntity, nestedRelationship, nestedDisconnectInputItem, context, callbackBucket); const mutationOperationField = new MutationOperationField_1.MutationOperationField(nestedDisconnectOperation, key); update.addField(mutationOperationField); }); } }); }); } } if (relationship) { const targetInputEdge = this.getInputEdge(inputItem); for (const key of Object.keys(targetInputEdge)) { const { fieldName, operator } = (0, parse_mutation_field_1.parseMutationField)(key); const attribute = relationship.attributes.get(fieldName); if (attribute) { if (operator) { const paramInputField = this.getInputFieldDeprecated("relationship", operator, attribute, targetInputEdge[key]); update.addField(paramInputField); this.addAttributeAuthorization({ attribute, context, update, entity: target, }); } else { const operations = Object.keys(targetInputEdge[fieldName]); if (operations.length > 1) { const conflictingOperations = operations.map((op) => `[[${op}]]`); throw new graphql_1.GraphQLError(`Conflicting modification of field ${fieldName}: ${conflictingOperations.join(", ")} on relationship ${target.name}.${relationship.name}`); } for (const op of operations) { const paramInputField = this.getInputField("relationship", op, attribute, targetInputEdge[fieldName][op]); update.addField(paramInputField); this.addAttributeAuthorization({ attribute, context, update, entity: target, }); } } } else if (key === relationship.propertiesTypeName) { const edgeInput = targetInputEdge[key]; // ActedIn: {..} for (const k of Object.keys(edgeInput)) { const { fieldName, operator } = (0, parse_mutation_field_1.parseMutationField)(k); const attribute = relationship.attributes.get(fieldName); if (attribute) { if (operator) { const paramInputField = this.getInputFieldDeprecated("relationship", operator, attribute, edgeInput[k]); update.addField(paramInputField); this.addAttributeAuthorization({ attribute, context, update, entity: target, }); } else { for (const op of Object.keys(edgeInput[k][fieldName])) { const paramInputField = this.getInputField("relationship", op, attribute, edgeInput[fieldName][op]); update.addField(paramInputField); this.addAttributeAuthorization({ attribute, context, update, entity: target, }); } } } } } } if (Object.keys(targetInputEdge).length > 0) { this.addPopulatedByFieldToUpdate({ entity: target, update, input: targetInputEdge, callbackBucket, relationship, }); } } this.addPopulatedByFieldToUpdate({ entity: target, update, input: targetInput, callbackBucket, }); }); } addPopulatedByFieldToUpdate({ entity, update, input, callbackBucket, relationship, }) { entity.getPopulatedByFields("UPDATE").forEach((attribute) => { const attachedTo = "node"; // the param value it's irrelevant as it will be overwritten by the callback function const callbackParam = new cypher_builder_1.default.Param(""); const field = new ParamInputField_1.ParamInputField({ attribute, attachedTo, inputValue: callbackParam, }); update.addField(field); const callbackFunctionName = attribute.annotations.populatedBy?.callback; if (!callbackFunctionName) { throw new Error(`PopulatedBy callback not found for attribute ${attribute.name}`); } const callbackParent = relationship ? input.node : input; callbackBucket.addCallback({ functionName: callbackFunctionName, param: callbackParam, parent: callbackParent, type: attribute.type, operation: "UPDATE", }); }); if (relationship) { relationship.getPopulatedByFields("UPDATE").forEach((attribute) => { const attachedTo = "relationship"; // the param value it's irrelevant as it will be overwritten by the callback function const relCallbackParam = new cypher_builder_1.default.Param(""); const relField = new ParamInputField_1.ParamInputField({ attribute, attachedTo, inputValue: relCallbackParam, }); update.addField(relField); const callbackFunctionName = attribute.annotations.populatedBy?.callback; if (!callbackFunctionName) { throw new Error(`PopulatedBy callback not found for attribute ${attribute.name}`); } callbackBucket.addCallback({ functionName: callbackFunctionName, param: relCallbackParam, parent: input, type: attribute.type, operation: "UPDATE", }); }); } } addEntityAuthorization({ entity, context, operation, }) { const authFilters = this.queryASTFactory.authorizationFactory.getAuthFilters({ entity, operations: ["UPDATE"], context, afterValidation: true, }); operation.addAuthFilters(...authFilters); } addAttributeAuthorization({ attribute, context, update, entity, conditionForEvaluation, }) { const authBeforeFilters = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ entity, authAnnotation: attribute.annotations.authorization, when: "BEFORE", conditionForEvaluation, operations: ["UPDATE"], context, }); if (authBeforeFilters) { update.addAuthFilters(authBeforeFilters); } const attributeAuthorization = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ entity, when: "AFTER", authAnnotation: attribute.annotations.authorization, conditionForEvaluation, operations: ["UPDATE"], context, }); if (attributeAuthorization) { update.addAuthFilters(attributeAuthorization); } } // returns true only if actual attributes are modified // UPDATE rules should not be applied for (dis)connections shouldApplyUpdateAuthorization(input, entity, isNested) { const actualInput = this.getInputNode(input, isNested); const affectedKeys = Object.keys(actualInput).map((key) => { // old version compatibility (eg id_SET) const { fieldName } = (0, parse_mutation_field_1.parseMutationField)(key); return fieldName; }); const areAttributesAffected = affectedKeys.filter((k) => entity.attributes.has(k)).length > 0; const isRelationshipUpdated = isNested && affectedKeys .filter((k) => entity.relationships.has(k)) .some((k) => { return (0, utils_1.asArray)(actualInput[k]).filter((inputItem) => inputItem.update); }); return areAttributesAffected || isRelationshipUpdated; } getInputNode(inputItem, isNested) { if (isNested) { return inputItem.node ?? {}; } return inputItem; } getInputEdge(inputItem) { return inputItem.edge ?? {}; } getInputFieldDeprecated(attachedTo, operator, attribute, value) { switch (operator) { case "SET": return new ParamInputField_1.ParamInputField({ attachedTo, attribute, inputValue: value, }); case "INCREMENT": case "DECREMENT": case "ADD": case "SUBTRACT": case "DIVIDE": case "MULTIPLY": return new MathInputField_1.MathInputField({ attachedTo, attribute, inputValue: value, operation: operator.toLowerCase(), }); case "PUSH": return new PushInputField_1.PushInputField({ attachedTo, attribute, inputValue: value, }); case "POP": return new PopInputField_1.PopInputField({ attachedTo, attribute, inputValue: value, }); default: throw new Error(`Unsupported update operator ${operator} on field ${attribute.name} `); } } getInputField(attachedTo, operator, attribute, value) { switch (operator) { case "set": return new ParamInputField_1.ParamInputField({ attachedTo: "node", attribute, inputValue: value, }); case "increment": case "decrement": case "add": case "subtract": case "divide": case "multiply": return new MathInputField_1.MathInputField({ attachedTo, attribute, inputValue: value, operation: operator, }); case "push": return new PushInputField_1.PushInputField({ attachedTo, attribute, inputValue: value, }); case "pop": return new PopInputField_1.PopInputField({ attachedTo, attribute, inputValue: value, }); default: throw new Error(`Unsupported update operator ${operator} on field ${attribute.name} `); } } createNestedUpdateOperation({ nestedEntity, nestedRelationship, nestedUpdateInputItem, context, callbackBucket, operation, key, }) { (0, utils_1.asArray)(nestedUpdateInputItem).forEach((input) => { const edgeFields = input.edge ?? {}; const nodeInputFields = input.node ?? {}; const entityAndNodeInput = []; if ((0, is_concrete_entity_1.isConcreteEntity)(nestedEntity)) { entityAndNodeInput.push([nestedEntity, nodeInputFields]); } else { nestedEntity.concreteEntities.forEach((concreteEntity) => { entityAndNodeInput.push([concreteEntity, nodeInputFields]); }); } entityAndNodeInput.forEach(([concreteEntity, nodeInputFields]) => { const nestedUpdateOperation = new UpdateOperation_1.UpdateOperation({ target: concreteEntity, relationship: nestedRelationship, selectionPattern: new RelationshipSelectionPattern_1.RelationshipSelectionPattern({ relationship: nestedRelationship, targetOverride: concreteEntity, }), }); this.hydrateUpdateOperation({ target: concreteEntity, relationship: nestedRelationship, input: { node: nodeInputFields, edge: edgeFields }, update: nestedUpdateOperation, callbackBucket, context, whereArgs: input.where ?? {}, }); const mutationOperationField = new MutationOperationField_1.MutationOperationField(nestedUpdateOperation, key); operation.addField(mutationOperationField); }); }); } } exports.UpdateFactory = UpdateFactory; //# sourceMappingURL=UpdateFactory.js.map