UNPKG

@neo4j/graphql

Version:

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

503 lines 23.6 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.CreateFactory = void 0; const cypher_builder_1 = __importDefault(require("@neo4j/cypher-builder")); const utils_1 = require("../../../../utils/utils"); const OperationField_1 = require("../../ast/fields/OperationField"); const MutationOperationField_1 = require("../../ast/input-fields/MutationOperationField"); const ParamInputField_1 = require("../../ast/input-fields/ParamInputField"); const PropertyInputField_1 = require("../../ast/input-fields/PropertyInputField"); const CreateOperation_1 = require("../../ast/operations/CreateOperation"); const TopLevelCreateMutationOperation_1 = require("../../ast/operations/TopLevelCreateMutationOperation"); const UnwindCreateOperation_1 = require("../../ast/operations/UnwindCreateOperation"); 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"); class CreateFactory { constructor(queryASTFactory) { this.queryASTFactory = queryASTFactory; } createCreateOperation({ entity, resolveTree, callbackBucket, context, }) { const responseFields = Object.values(resolveTree.fieldsByTypeName[entity.operations.mutationResponseTypeNames.create] ?? {}); const rawInput = resolveTree.args.input; const input = rawInput ?? []; const createOperations = input.map((inputItem) => { const createOperation = new CreateOperation_1.CreateOperation({ target: entity, selectionPattern: new NodeSelectionPattern_1.NodeSelectionPattern({ target: entity, }), }); this.hydrateCreateOperation({ target: entity, input: inputItem, create: createOperation, callbackBucket, context, }); return createOperation; }); 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 TopLevelCreateMutationOperation_1.TopLevelCreateMutationOperation({ createOperations, projectionOperations, }); return topLevelMutation; } createUnwindCreateOperation(entity, resolveTree, context) { const responseFields = Object.values(resolveTree.fieldsByTypeName[entity.operations.mutationResponseTypeNames.create] ?? {}); const rawInput = resolveTree.args.input; const input = rawInput ?? []; const unwindCreate = this.parseUnwindCreate({ target: entity, input, context, argumentToUnwind: new cypher_builder_1.default.Param(input), }); const projectionFields = responseFields .filter((f) => f.name === entity.plural) .map((field) => { return this.queryASTFactory.operationsFactory.createReadOperation({ entityOrRel: entity, resolveTree: field, context, }); }); unwindCreate.addProjectionOperations(projectionFields); return unwindCreate; } parseUnwindCreate({ target, relationship, input, context, argumentToUnwind, }) { const isNested = Boolean(relationship); const unwindCreate = new UnwindCreateOperation_1.UnwindCreateOperation({ target: relationship ?? target, argumentToUnwind, }); this.addEntityAuthorization({ entity: target, context, operation: unwindCreate }); this.addAuthorizationsForAttributesInUnwind({ target, context, unwindCreate: unwindCreate, isNested, }); this.hydrateUnwindCreateOperation({ target, relationship, input, unwindCreate, context, }); return unwindCreate; } hydrateUnwindCreateOperation({ target, relationship, input, unwindCreate, context, }) { const isNested = Boolean(relationship); // TODO: there is no need to get always the autogenerated field as these are static fields and can be cached [target, relationship].forEach((t) => this.addAutogeneratedFieldsToUnwindCreate({ target: t, unwindCreate, })); (0, utils_1.asArray)(input).forEach((inputItem) => { const targetInput = this.getInputNode(inputItem, isNested); (0, raise_attribute_ambiguity_1.raiseAttributeAmbiguity)(Object.keys(targetInput), target); (0, raise_attribute_ambiguity_1.raiseAttributeAmbiguity)(Object.keys(this.getInputEdge(target)), relationship); for (const key of Object.keys(targetInput)) { const nestedRelationship = target.relationships.get(key); const attribute = target.attributes.get(key); if (!attribute && !nestedRelationship) { throw new Error(`Transpile Error: Input field ${key} not found in entity ${target.name}`); } if (attribute) { this.parseAttributeInputField({ target, attribute, unwindCreate, }); } else if (nestedRelationship) { const nestedEntity = nestedRelationship.target; (0, is_concrete_entity_1.assertIsConcreteEntity)(nestedEntity); const relField = unwindCreate.getField(key, "node"); const nestedCreateInput = targetInput[key]?.create; if (relField && relField instanceof MutationOperationField_1.MutationOperationField && relField.mutationOperation instanceof UnwindCreateOperation_1.UnwindCreateOperation) { // in case relationship field is already present in the unwind operation we want still to hydrate the unwind-create operation as it might have different fields. this.hydrateUnwindCreateOperation({ target: nestedEntity, relationship: nestedRelationship, input: nestedCreateInput, unwindCreate: relField.mutationOperation, context, }); } this.addRelationshipInputFieldToUnwindOperation({ relationship: nestedRelationship, unwindCreate, context, nestedCreateInput, isNested, }); } } if (relationship) { for (const key of Object.keys(this.getInputEdge(inputItem))) { const attribute = relationship.attributes.get(key); if (attribute) { this.parseAttributeInputField({ target: relationship, attribute, unwindCreate, }); } } } }); } hydrateCreateOperation({ target, relationship, input, create, callbackBucket, context, }) { const isNested = Boolean(relationship); // TODO: there is no need to get always the autogenerated field as these are static fields and can be cached [target, relationship].forEach((t) => { if (!t) { return; } const autoGeneratedFields = (0, get_autogenerated_fields_1.getAutogeneratedFields)(t); autoGeneratedFields.forEach((field) => { create.addField(field); }); }); this.addEntityAuthorization({ entity: target, context, operation: create }); (0, utils_1.asArray)(input).forEach((inputItem) => { const targetInput = this.getInputNode(inputItem, isNested); (0, raise_attribute_ambiguity_1.raiseAttributeAmbiguity)(Object.keys(targetInput), target); (0, raise_attribute_ambiguity_1.raiseAttributeAmbiguity)(Object.keys(this.getInputEdge(inputItem)), relationship); for (const key of Object.keys(targetInput)) { const nestedRelationship = target.relationships.get(key); const attribute = target.attributes.get(key); if (!attribute && !nestedRelationship) { throw new Error(`Transpile Error: Input field ${key} not found in entity ${target.name}`); } if (attribute) { const paramInputField = new ParamInputField_1.ParamInputField({ attachedTo: "node", attribute, inputValue: targetInput[key], }); create.addField(paramInputField); this.addAttributeAuthorization({ attribute, context, unwindCreate: create, 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, operationInput]) => { const nestedCreateInput = operationInput.create; if (nestedCreateInput) { this.createNestedCreateOperation({ targetEntity: nestedEntity, relationship: nestedRelationship, input: nestedCreateInput, callbackBucket, context, operation: create, key, }); } const nestedConnectInput = operationInput.connect; if (nestedConnectInput) { (0, utils_1.asArray)(nestedConnectInput).forEach((nestedConnectInputItem) => { const nestedConnectOperation = this.queryASTFactory.operationsFactory.createConnectOperation(nestedEntity, nestedRelationship, nestedConnectInputItem, context); const mutationOperationField = new MutationOperationField_1.MutationOperationField(nestedConnectOperation, key); create.addField(mutationOperationField); }); } }); } } if (relationship) { const targetInputEdge = this.getInputEdge(inputItem); for (const key of Object.keys(targetInputEdge)) { const attribute = relationship.attributes.get(key); if (attribute) { const attachedTo = "relationship"; const paramInputField = new ParamInputField_1.ParamInputField({ attachedTo, attribute, inputValue: targetInputEdge[key], }); create.addField(paramInputField); } } } }); this.addPopulatedByFieldToCreate({ entity: target, create, input, callbackBucket, isNested, relationship, }); } createNestedCreateOperation({ relationship, targetEntity, input, callbackBucket, context, operation, key, }) { (0, utils_1.asArray)(input).forEach((input) => { const edgeFields = input.edge ?? {}; const nodeInputFields = input.node ?? {}; const entityAndNodeInput = []; if ((0, is_concrete_entity_1.isConcreteEntity)(targetEntity)) { entityAndNodeInput.push([targetEntity, nodeInputFields]); } else { Object.entries(nodeInputFields).forEach(([concreteTypename, nodeInputFields]) => { const concreteEntity = targetEntity.concreteEntities.find((e) => e.name === concreteTypename); if (!concreteEntity) { throw new Error("Concrete entity not found in create, please contact support"); } entityAndNodeInput.push([concreteEntity, nodeInputFields]); }); } entityAndNodeInput.forEach(([concreteEntity, nodeInputFields]) => { const nestedCreateOperation = new CreateOperation_1.CreateOperation({ target: concreteEntity, relationship, selectionPattern: new RelationshipSelectionPattern_1.RelationshipSelectionPattern({ relationship, }), }); this.hydrateCreateOperation({ create: nestedCreateOperation, target: concreteEntity, relationship, input: { node: nodeInputFields, edge: edgeFields, }, callbackBucket, context, }); const mutationOperationField = new MutationOperationField_1.MutationOperationField(nestedCreateOperation, key); operation.addField(mutationOperationField); }); }); } addPopulatedByFieldToCreate({ entity, create, input, callbackBucket, isNested, relationship, }) { if (!isNested) { entity.getPopulatedByFields("CREATE").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("1234"); const field = new ParamInputField_1.ParamInputField({ attribute, attachedTo, inputValue: callbackParam, }); create.addField(field); const callbackFunctionName = attribute.annotations.populatedBy?.callback; if (!callbackFunctionName) { throw new Error(`PopulatedBy callback not found for attribute ${attribute.name}`); } callbackBucket.addCallback({ functionName: callbackFunctionName, param: callbackParam, parent: input, type: attribute.type, }); }); } else { relationship?.getPopulatedByFields("CREATE").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("5678"); const relField = new ParamInputField_1.ParamInputField({ attribute, attachedTo, inputValue: relCallbackParam, }); create.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.edge, type: attribute.type, }); }); } } getInputNode(inputItem, isNested) { if (isNested) { return inputItem.node ?? {}; } return inputItem; } getInputEdge(inputItem) { return inputItem.edge ?? {}; } addAutogeneratedFieldsToUnwindCreate({ target, unwindCreate, }) { if (!target) { return; } const attachedTo = (0, is_concrete_entity_1.isConcreteEntity)(target) ? "node" : "relationship"; const autoGeneratedFields = (0, get_autogenerated_fields_1.getAutogeneratedFields)(target); autoGeneratedFields.forEach((field) => { if (unwindCreate.getField(field.name, attachedTo)) { return; } unwindCreate.addField(field, attachedTo); }); } parseAttributeInputField({ target, attribute, unwindCreate, }) { const isConcreteEntityTarget = (0, is_concrete_entity_1.isConcreteEntity)(target); const attachedTo = isConcreteEntityTarget ? "node" : "relationship"; this.addAttributeInputFieldToUnwindOperation({ attribute, unwindCreate, attachedTo, }); } getEdgeOrNodePath({ unwindVariable, isNested, isRelField, }) { if (!isNested && isRelField) { throw new Error("Transpile error: invalid invoke of getEdgeOrNodePath for relationship field."); } if (isNested) { const path = isRelField ? "edge" : "node"; return unwindVariable.property(path); } return unwindVariable; } addAttributeInputFieldToUnwindOperation({ attribute, unwindCreate, attachedTo, }) { if (unwindCreate.getField(attribute.name, attachedTo)) { return; } const inputField = new PropertyInputField_1.PropertyInputField({ attribute, attachedTo, }); unwindCreate.addField(inputField, attachedTo); } addRelationshipInputFieldToUnwindOperation({ relationship, unwindCreate, context, nestedCreateInput, isNested, }) { const relField = unwindCreate.getField(relationship.name, "node"); if (!relField) { if (nestedCreateInput) { const partialPath = this.getEdgeOrNodePath({ unwindVariable: unwindCreate.getCypherVariable(), isNested, isRelField: false, }); const path = partialPath.property(relationship.name).property("create"); const nestedUnwind = this.parseUnwindCreate({ target: relationship.target, relationship: relationship, input: nestedCreateInput, argumentToUnwind: path, context, }); const mutationOperationField = new MutationOperationField_1.MutationOperationField(nestedUnwind, relationship.name); unwindCreate.addField(mutationOperationField, "node"); } else { throw new Error(`Expected create operation, but found: ${relationship.name}`); } } } addEntityAuthorization({ entity, context, operation, }) { const authFilters = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ entity, authAnnotation: entity.annotations.authorization, when: "AFTER", operations: ["CREATE"], context, }); if (authFilters) { operation.addAuthFilters(authFilters); } } addAttributeAuthorization({ attribute, context, unwindCreate, entity, conditionForEvaluation, }) { const attributeAuthorization = this.queryASTFactory.authorizationFactory.createAuthValidateRule({ entity, when: "AFTER", authAnnotation: attribute.annotations.authorization, conditionForEvaluation, operations: ["CREATE"], context, }); if (attributeAuthorization) { unwindCreate.addAuthFilters(attributeAuthorization); } } addAuthorizationsForAttributesInUnwind({ target, context, unwindCreate, isNested, }) { const edgeOrNodePath = this.getEdgeOrNodePath({ unwindVariable: unwindCreate.getCypherVariable(), isRelField: false, isNested, }); for (const attribute of target.attributes.values()) { const path = edgeOrNodePath.property(attribute.name); this.addAttributeAuthorization({ attribute, context, unwindCreate, entity: target, conditionForEvaluation: cypher_builder_1.default.isNotNull(path), }); } } } exports.CreateFactory = CreateFactory; //# sourceMappingURL=CreateFactory.js.map