Skip to content

Commit

Permalink
Merge pull request #5459 from mjfwebb/top-level-delete-6.x
Browse files Browse the repository at this point in the history
Top-level Delete v6
  • Loading branch information
mjfwebb authored Aug 15, 2024
2 parents 38c50b0 + 88b20c1 commit 75d7715
Show file tree
Hide file tree
Showing 8 changed files with 513 additions and 5 deletions.
136 changes: 136 additions & 0 deletions packages/graphql/src/api-v6/queryIR/DeleteOperation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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.
*/

import Cypher from "@neo4j/cypher-builder";
import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter";
import type { InterfaceEntityAdapter } from "../../schema-model/entity/model-adapters/InterfaceEntityAdapter";
import type { Filter } from "../../translate/queryAST/ast/filters/Filter";
import type { OperationTranspileResult } from "../../translate/queryAST/ast/operations/operations";
import { MutationOperation } from "../../translate/queryAST/ast/operations/operations";
import type { QueryASTContext } from "../../translate/queryAST/ast/QueryASTContext";
import type { QueryASTNode } from "../../translate/queryAST/ast/QueryASTNode";
import type { EntitySelection, SelectionClause } from "../../translate/queryAST/ast/selection/EntitySelection";
import { wrapSubqueriesInCypherCalls } from "../../translate/queryAST/utils/wrap-subquery-in-calls";

export class V6DeleteOperation extends MutationOperation {
public readonly target: ConcreteEntityAdapter | InterfaceEntityAdapter;
private selection: EntitySelection;
private filters: Filter[];

constructor({
target,
selection,
filters = [],
}: {
target: ConcreteEntityAdapter | InterfaceEntityAdapter;
selection: EntitySelection;
filters?: Filter[];
}) {
super();
this.target = target;
this.selection = selection;
this.filters = filters;
}

public getChildren(): QueryASTNode[] {
return [this.selection, ...this.filters];
}

public transpile(context: QueryASTContext): OperationTranspileResult {
if (!context.hasTarget()) {
throw new Error("No parent node found!");
}
const { selection, nestedContext } = this.selection.apply(context);
return this.transpileTopLevel(selection, nestedContext);
}

private transpileTopLevel(
selection: SelectionClause,
context: QueryASTContext<Cypher.Node>
): OperationTranspileResult {
this.validateSelection(selection);
const filterSubqueries = wrapSubqueriesInCypherCalls(context, this.filters, [context.target]);
const predicate = this.getPredicate(context);
const extraSelections = this.getExtraSelections(context);

let statements = [selection, ...extraSelections, ...filterSubqueries];
statements = this.appendFilters(statements, predicate);
statements = this.appendDeleteClause(statements, context);
const ret = Cypher.concat(...statements);

return { clauses: [ret], projectionExpr: context.target };
}

private appendDeleteClause(clauses: Cypher.Clause[], context: QueryASTContext<Cypher.Node>): Cypher.Clause[] {
const lastClause = this.getLastClause(clauses);
if (
lastClause instanceof Cypher.Match ||
lastClause instanceof Cypher.OptionalMatch ||
lastClause instanceof Cypher.With
) {
lastClause.detachDelete(context.target);
return clauses;
}
const extraWith = new Cypher.With("*");
extraWith.detachDelete(context.target);
clauses.push(extraWith);
return clauses;
}

private getLastClause(clauses: Cypher.Clause[]): Cypher.Clause {
const lastClause = clauses[clauses.length - 1];
if (!lastClause) {
throw new Error("Transpile error");
}
return lastClause;
}

private appendFilters(clauses: Cypher.Clause[], predicate: Cypher.Predicate | undefined): Cypher.Clause[] {
if (!predicate) {
return clauses;
}
const lastClause = this.getLastClause(clauses);
if (
lastClause instanceof Cypher.Match ||
lastClause instanceof Cypher.OptionalMatch ||
lastClause instanceof Cypher.With
) {
lastClause.where(predicate);
return clauses;
}
const withClause = new Cypher.With("*");
withClause.where(predicate);
clauses.push(withClause);
return clauses;
}

private validateSelection(selection: SelectionClause): asserts selection is Cypher.Match | Cypher.With {
if (!(selection instanceof Cypher.Match || selection instanceof Cypher.With)) {
throw new Error("Cypher Yield statement is not a valid selection for Delete Operation");
}
}

private getPredicate(queryASTContext: QueryASTContext): Cypher.Predicate | undefined {
return Cypher.and(...this.filters.map((f) => f.getPredicate(queryASTContext)));
}

private getExtraSelections(context: QueryASTContext): (Cypher.Match | Cypher.With)[] {
return this.getChildren().flatMap((f) => f.getSelection(context));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.
*/

import type { Neo4jGraphQLSchemaModel } from "../../schema-model/Neo4jGraphQLSchemaModel";
import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity";
import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter";
import { QueryAST } from "../../translate/queryAST/ast/QueryAST";
import { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection";
import { filterTruthy } from "../../utils/utils";
import { V6DeleteOperation } from "../queryIR/DeleteOperation";
import { FilterFactory } from "./FilterFactory";
import type { GraphQLTreeDelete } from "./resolve-tree-parser/graphql-tree/graphql-tree";

export class DeleteOperationFactory {
public schemaModel: Neo4jGraphQLSchemaModel;
private filterFactory: FilterFactory;

constructor(schemaModel: Neo4jGraphQLSchemaModel) {
this.schemaModel = schemaModel;
this.filterFactory = new FilterFactory(schemaModel);
}

public deleteAST({
graphQLTreeDelete,
entity,
}: {
graphQLTreeDelete: GraphQLTreeDelete;
entity: ConcreteEntity;
}): QueryAST {
const operation = this.generateDeleteOperation({
graphQLTreeDelete,
entity,
});
return new QueryAST(operation);
}

private generateDeleteOperation({
graphQLTreeDelete,
entity,
}: {
graphQLTreeDelete: GraphQLTreeDelete;
entity: ConcreteEntity;
}): V6DeleteOperation {
const targetAdapter = new ConcreteEntityAdapter(entity);

const selection = new NodeSelection({
target: targetAdapter,
});

const filters = filterTruthy([
this.filterFactory.createFilters({ entity, where: graphQLTreeDelete.args.where }),
]);

const deleteOP = new V6DeleteOperation({
target: targetAdapter,
selection,
filters,
});

return deleteOP;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export type GraphQLTreeUpdateInput = Record<string, GraphQLTreeUpdateField>;
export type UpdateOperation = "set";
export type GraphQLTreeUpdateField = Record<UpdateOperation, any>;

export interface GraphQLTreeDelete extends GraphQLTreeNode {
name: string;
args: {
where?: GraphQLWhereTopLevel;
};
}

export interface GraphQLTreeCreate extends GraphQLTreeNode {
name: string;
args: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
GraphQLTreeConnection,
GraphQLTreeConnectionTopLevel,
GraphQLTreeCreate,
GraphQLTreeDelete,
GraphQLTreeReadOperation,
GraphQLTreeReadOperationTopLevel,
GraphQLTreeUpdate,
Expand Down Expand Up @@ -116,6 +117,34 @@ export function parseResolveInfoTreeUpdate({
};
}

export function parseResolveInfoTreeDelete({
resolveTree,
entity,
}: {
resolveTree: ResolveTree;
entity: ConcreteEntity;
}): GraphQLTreeDelete {
const entityTypes = entity.typeNames;
const deleteResponse = findFieldByName(resolveTree, "DeleteResponse", entityTypes.queryField);
const deleteArgs = parseOperationArgsTopLevel(resolveTree.args);
if (!deleteResponse) {
return {
alias: resolveTree.alias,
name: resolveTree.name,
args: deleteArgs,
fields: {},
};
}
const fieldsResolveTree = deleteResponse.fieldsByTypeName[entityTypes.node] ?? {};
const fields = getNodeFields(fieldsResolveTree, entity);
return {
alias: resolveTree.alias,
name: resolveTree.name,
args: deleteArgs,
fields,
};
}

export function parseConnection(resolveTree: ResolveTree, entity: Relationship): GraphQLTreeConnection {
const entityTypes = entity.typeNames;
const edgesResolveTree = findFieldByName(resolveTree, entityTypes.connection, "edges");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity";
import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context";
import { execute } from "../../utils";
import getNeo4jResolveTree from "../../utils/get-neo4j-resolve-tree";
import { parseResolveInfoTreeCreate } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree";
import { translateCreateOperation } from "../translators/translate-create-operation";
import { parseResolveInfoTreeDelete } from "../queryIRFactory/resolve-tree-parser/parse-resolve-info-tree";
import { translateDeleteResolver } from "../translators/translate-delete-operation";

export function generateDeleteResolver({ entity }: { entity: ConcreteEntity }) {
return async function resolve(
Expand All @@ -35,10 +35,10 @@ export function generateDeleteResolver({ entity }: { entity: ConcreteEntity }) {
const resolveTree = getNeo4jResolveTree(info, { args });
context.resolveTree = resolveTree;
// TODO: Implement delete resolver
const graphQLTreeCreate = parseResolveInfoTreeCreate({ resolveTree: context.resolveTree, entity });
const { cypher, params } = translateCreateOperation({
const graphQLTreeDelete = parseResolveInfoTreeDelete({ resolveTree: context.resolveTree, entity });
const { cypher, params } = translateDeleteResolver({
context: context,
graphQLTreeCreate,
graphQLTreeDelete,
entity,
});
const executeResult = await execute({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.
*/

import type Cypher from "@neo4j/cypher-builder";
import Debug from "debug";
import { DEBUG_TRANSLATE } from "../../constants";
import type { ConcreteEntity } from "../../schema-model/entity/ConcreteEntity";
import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context";
import { DeleteOperationFactory } from "../queryIRFactory/DeleteOperationFactory";
import type { GraphQLTreeDelete } from "../queryIRFactory/resolve-tree-parser/graphql-tree/graphql-tree";

const debug = Debug(DEBUG_TRANSLATE);

export function translateDeleteResolver({
context,
entity,
graphQLTreeDelete,
}: {
context: Neo4jGraphQLTranslationContext;
graphQLTreeDelete: GraphQLTreeDelete;
entity: ConcreteEntity;
}): Cypher.CypherResult {
const deleteFactory = new DeleteOperationFactory(context.schemaModel);
const deleteAST = deleteFactory.deleteAST({ graphQLTreeDelete, entity });
debug(deleteAST.print());
const results = deleteAST.build(context);
return results.build();
}
Loading

0 comments on commit 75d7715

Please sign in to comment.