@neo4j/graphql
Version:
A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations
559 lines • 28.8 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.
*/
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