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