Skip to content

Commit

Permalink
Merge pull request #5476 from mjfwebb/nested-delete-v6
Browse files Browse the repository at this point in the history
Schema changes for nested delete v6
  • Loading branch information
angrykoala authored Aug 22, 2024
2 parents 14e1ede + b323a45 commit 710227c
Show file tree
Hide file tree
Showing 19 changed files with 651 additions and 26 deletions.
54 changes: 53 additions & 1 deletion packages/graphql/src/api-v6/queryIR/DeleteOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,38 @@ export class V6DeleteOperation extends MutationOperation {
public readonly target: ConcreteEntityAdapter | InterfaceEntityAdapter;
private selection: EntitySelection;
private filters: Filter[];
private nestedOperations: V6DeleteOperation[];

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

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

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

Expand All @@ -69,14 +76,50 @@ export class V6DeleteOperation extends MutationOperation {
const predicate = this.getPredicate(context);
const extraSelections = this.getExtraSelections(context);

const nestedOperations: (Cypher.Call | Cypher.With)[] = this.getNestedDeleteSubQueries(context);

let statements = [selection, ...extraSelections, ...filterSubqueries];

statements = this.appendFilters(statements, predicate);
if (nestedOperations.length) {
statements.push(new Cypher.With("*"), ...nestedOperations);
}
statements = this.appendDeleteClause(statements, context);
const ret = Cypher.concat(...statements);

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

private transpileNested(
selection: SelectionClause,
context: QueryASTContext<Cypher.Node>
): OperationTranspileResult {
this.validateSelection(selection);
if (!context.relationship) {
throw new Error("Transpile error: No relationship found!");
}
const filterSubqueries = wrapSubqueriesInCypherCalls(context, this.filters, [context.target]);
const predicate = this.getPredicate(context);
const extraSelections = this.getExtraSelections(context);
const collect = Cypher.collect(context.target).distinct();
const deleteVar = new Cypher.Variable();
const withBeforeDeleteBlock = new Cypher.With(context.relationship, [collect, deleteVar]);

const unwindDeleteVar = new Cypher.Variable();
const deleteClause = new Cypher.Unwind([deleteVar, unwindDeleteVar]).detachDelete(unwindDeleteVar);

const deleteBlock = new Cypher.Call(deleteClause).importWith(deleteVar);
const nestedOperations: (Cypher.Call | Cypher.With)[] = this.getNestedDeleteSubQueries(context);
const statements = this.appendFilters([selection, ...extraSelections, ...filterSubqueries], predicate);

if (nestedOperations.length) {
statements.push(new Cypher.With("*"), ...nestedOperations);
}
statements.push(withBeforeDeleteBlock, deleteBlock);
const ret = Cypher.concat(...statements);
return { clauses: [ret], projectionExpr: Cypher.Null };
}

private appendDeleteClause(clauses: Cypher.Clause[], context: QueryASTContext<Cypher.Node>): Cypher.Clause[] {
const lastClause = this.getLastClause(clauses);
if (
Expand Down Expand Up @@ -120,6 +163,15 @@ export class V6DeleteOperation extends MutationOperation {
return clauses;
}

private getNestedDeleteSubQueries(context: QueryASTContext): Cypher.Call[] {
const nestedOperations: Cypher.Call[] = [];
for (const nestedDeleteOperation of this.nestedOperations) {
const { clauses } = nestedDeleteOperation.transpile(context);
nestedOperations.push(...clauses.map((c) => new Cypher.Call(c).importWith("*")));
}
return nestedOperations;
}

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");
Expand Down
117 changes: 114 additions & 3 deletions packages/graphql/src/api-v6/queryIRFactory/DeleteOperationFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
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 type { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter";
import { QueryAST } from "../../translate/queryAST/ast/QueryAST";
import { NodeSelection } from "../../translate/queryAST/ast/selection/NodeSelection";
import { filterTruthy } from "../../utils/utils";
import { RelationshipSelection } from "../../translate/queryAST/ast/selection/RelationshipSelection";
import { isInterfaceEntity } from "../../translate/queryAST/utils/is-interface-entity";
import { isUnionEntity } from "../../translate/queryAST/utils/is-union-entity";
import { asArray, filterTruthy, isRecord } from "../../utils/utils";
import { V6DeleteOperation } from "../queryIR/DeleteOperation";
import { FilterFactory } from "./FilterFactory";
import type { GraphQLTreeDelete } from "./resolve-tree-parser/graphql-tree/graphql-tree";
import type { GraphQLTreeDelete, GraphQLTreeDeleteInput } from "./resolve-tree-parser/graphql-tree/graphql-tree";

export class DeleteOperationFactory {
public schemaModel: Neo4jGraphQLSchemaModel;
Expand Down Expand Up @@ -64,15 +68,122 @@ export class DeleteOperationFactory {
});

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

const nestedDeleteOperations: V6DeleteOperation[] = [];
if (graphQLTreeDelete.args.input) {
nestedDeleteOperations.push(
...this.createNestedDeleteOperations(graphQLTreeDelete.args.input, targetAdapter)
);
}

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

return deleteOP;
}

private createNestedDeleteOperations(
deleteArg: GraphQLTreeDeleteInput,
source: ConcreteEntityAdapter
): V6DeleteOperation[] {
return filterTruthy(
Object.entries(deleteArg).flatMap(([key, valueArr]) => {
return asArray(valueArr).flatMap((value) => {
const relationship = source.findRelationship(key);
if (!relationship) {
throw new Error(`Failed to find relationship ${key}`);
}
const target = relationship.target;

if (isInterfaceEntity(target)) {
// TODO: Implement createNestedDeleteOperationsForInterface
// return this.createNestedDeleteOperationsForInterface({
// deleteArg: value,
// relationship,
// target,
// });
return;
}
if (isUnionEntity(target)) {
// TODO: Implement createNestedDeleteOperationsForUnion
// return this.createNestedDeleteOperationsForUnion({
// deleteArg: value,
// relationship,
// target,
// });
return;
}

return this.createNestedDeleteOperation({
relationship,
target,
args: value as Record<string, any>,
});
});
})
);
}

private parseDeleteArgs(
args: Record<string, any>,
isTopLevel: boolean
): {
whereArg: { node: Record<string, any>; edge: Record<string, any> };
deleteArg: Record<string, any>;
} {
let whereArg;
const rawWhere = isRecord(args.where) ? args.where : {};
if (isTopLevel) {
whereArg = { node: rawWhere.node ?? {}, edge: rawWhere.edge ?? {} };
} else {
whereArg = { node: rawWhere, edge: {} };
}
const deleteArg = isRecord(args.delete) ? args.delete : {};
return { whereArg, deleteArg };
}

private createNestedDeleteOperation({
relationship,
target,
args,
}: {
relationship: RelationshipAdapter;
target: ConcreteEntityAdapter;
args: Record<string, any>;
}): V6DeleteOperation[] {
const { whereArg, deleteArg } = this.parseDeleteArgs(args, true);

const selection = new RelationshipSelection({
relationship,
directed: true,
optional: true,
targetOverride: target,
});

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

const nestedDeleteOperations = this.createNestedDeleteOperations(deleteArg, target);
return [
new V6DeleteOperation({
target,
selection,
filters,
nestedOperations: nestedDeleteOperations,
}),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import type { GraphQLWhere, GraphQLWhereTopLevel } from "./where";
// TODO GraphQLTreeCreateInput should be a union of PrimitiveTypes and relationship fields
export type GraphQLTreeCreateInput = Record<string, unknown>;
export type GraphQLTreeUpdateInput = Record<string, GraphQLTreeUpdateField>;
export type GraphQLTreeDeleteInput = Record<string, unknown>;

export type UpdateOperation = "set";
export type GraphQLTreeUpdateField = Record<UpdateOperation, any>;
Expand All @@ -34,6 +35,7 @@ export interface GraphQLTreeDelete extends GraphQLTreeNode {
name: string;
args: {
where?: GraphQLWhereTopLevel;
input?: GraphQLTreeDeleteInput;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export type InputFieldDefinition = {
args?: Record<string, any>;
deprecationReason?: string | null;
description?: string | null;
defaultValue: any;
defaultValue?: any;
};

export class SchemaBuilder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { RelatedEntitySchemaTypes } from "./RelatedEntitySchemaTypes";
import type { SchemaTypes } from "./SchemaTypes";
import { TopLevelFilterSchemaTypes } from "./filter-schema-types/TopLevelFilterSchemaTypes";
import { TopLevelCreateSchemaTypes } from "./mutation-schema-types/TopLevelCreateSchemaTypes";
import { TopLevelDeleteSchemaTypes } from "./mutation-schema-types/TopLevelDeleteSchemaTypes";
import { TopLevelUpdateSchemaTypes } from "./mutation-schema-types/TopLevelUpdateSchemaTypes";

export class TopLevelEntitySchemaTypes {
Expand All @@ -58,6 +59,7 @@ export class TopLevelEntitySchemaTypes {
private schemaTypes: SchemaTypes;
private createSchemaTypes: TopLevelCreateSchemaTypes;
private updateSchemaTypes: TopLevelUpdateSchemaTypes;
private deleteSchemaTypes: TopLevelDeleteSchemaTypes;

constructor({
entity,
Expand All @@ -75,6 +77,7 @@ export class TopLevelEntitySchemaTypes {
this.schemaTypes = schemaTypes;
this.createSchemaTypes = new TopLevelCreateSchemaTypes({ schemaBuilder, entity });
this.updateSchemaTypes = new TopLevelUpdateSchemaTypes({ schemaBuilder, entity, schemaTypes });
this.deleteSchemaTypes = new TopLevelDeleteSchemaTypes({ schemaBuilder, entity, schemaTypes });
}

public addTopLevelQueryField(
Expand Down Expand Up @@ -179,6 +182,7 @@ export class TopLevelEntitySchemaTypes {
name: this.entity.typeNames.deleteField,
type: this.schemaTypes.staticTypes.deleteResponse,
args: {
input: this.deleteSchemaTypes.deleteInput,
where: this.filterSchemaTypes.operationWhereTopLevel,
},
resolver,
Expand Down
Loading

0 comments on commit 710227c

Please sign in to comment.