Skip to content
37 changes: 37 additions & 0 deletions packages/graphql/src/schema/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { GraphQLResolveInfo } from "graphql";
import { execute } from "../utils";
import { translate } from "../translate";
import { NeoSchema, Node } from "../classes";

function deleteResolver({ node, getSchema }: { node: Node; getSchema: () => NeoSchema }) {
async function resolve(_root: any, _args: any, context: any, resolveInfo: GraphQLResolveInfo) {
const neoSchema = getSchema();
context.neoSchema = neoSchema;

const { driver } = context;
if (!driver) {
throw new Error("context.driver missing");
}

const [cypher, params] = translate({ context, resolveInfo });

const result = await execute({
cypher,
params,
driver,
defaultAccessMode: "WRITE",
neoSchema,
statistics: true,
});

return result;
}

return {
type: `DeleteInfo!`,
resolve,
args: { where: `${node.name}Where` },
};
}

export default deleteResolver;
18 changes: 14 additions & 4 deletions packages/graphql/src/schema/make-augmented-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import getCypherMeta from "./get-cypher-meta";
import getRelationshipMeta from "./get-relationship-meta";
import { RelationField, CypherField, PrimitiveField, BaseField } from "../types";
import { upperFirstLetter } from "../utils";
import find from "./find";
import create from "./create";
import findResolver from "./find";
import createResolver from "./create";
import deleteResolver from "./delete";

export interface MakeAugmentedSchemaOptions {
typeDefs: any;
Expand All @@ -26,6 +27,14 @@ function makeAugmentedSchema(options: MakeAugmentedSchemaOptions): NeoSchema {
options,
};

composer.createObjectTC({
name: "DeleteInfo",
fields: {
nodesDeleted: "Int!",
relationshipsDeleted: "Int!",
},
});

neoSchemaInput.nodes = (document.definitions.filter(
(x) => x.kind === "ObjectTypeDefinition" && !["Query", "Mutation", "Subscription"].includes(x.name.value)
) as ObjectTypeDefinitionNode[]).map((definition) => {
Expand Down Expand Up @@ -218,11 +227,12 @@ function makeAugmentedSchema(options: MakeAugmentedSchemaOptions): NeoSchema {
});

composer.Query.addFields({
[pluralize(node.name)]: find({ node, getSchema: () => neoSchema }),
[pluralize(node.name)]: findResolver({ node, getSchema: () => neoSchema }),
});

composer.Mutation.addFields({
[`create${pluralize(node.name)}`]: create({ node, getSchema: () => neoSchema }),
[`create${pluralize(node.name)}`]: createResolver({ node, getSchema: () => neoSchema }),
[`delete${pluralize(node.name)}`]: deleteResolver({ node, getSchema: () => neoSchema }),
});
});

Expand Down
35 changes: 35 additions & 0 deletions packages/graphql/src/translate/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,37 @@ function translateCreate({
return [cypher, { ...params, ...replacedProjectionParams }];
}

function translateDelete({
neoSchema,
resolveTree,
}: {
neoSchema: NeoSchema;
resolveTree: ResolveTree;
}): [string, any] {
const node = neoSchema.nodes.find(
(x) => x.name === pluralize.singular(resolveTree.name.split("delete")[1])
) as Node;
const whereInput = resolveTree.args.where as GraphQLWhereArg;
const varName = "this";

const matchStr = `MATCH (${varName}:${node.name})`;
let whereStr = "";
let cypherParams: { [k: string]: any } = {};

if (whereInput) {
const where = createWhereAndParams({
whereInput,
varName,
});
whereStr = where[0];
cypherParams = { ...cypherParams, ...where[1] };
}

const cypher = [matchStr, whereStr, `DETACH DELETE ${varName}`];

return [cypher.filter(Boolean).join("\n"), cypherParams];
}

function translate({ context, resolveInfo }: { context: any; resolveInfo: GraphQLResolveInfo }): [string, any] {
const neoSchema: NeoSchema = context.neoSchema;
if (!neoSchema || !(neoSchema instanceof NeoSchema)) {
Expand All @@ -156,6 +187,10 @@ function translate({ context, resolveInfo }: { context: any; resolveInfo: GraphQ
if (operationName.includes("create")) {
return translateCreate({ resolveTree, neoSchema });
}

if (operationName.includes("delete")) {
return translateDelete({ resolveTree, neoSchema });
}
}

return translateRead({ resolveTree, neoSchema });
Expand Down
5 changes: 5 additions & 0 deletions packages/graphql/src/utils/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ async function execute(input: {
params: any;
defaultAccessMode: "READ" | "WRITE";
neoSchema: NeoSchema;
statistics?: boolean;
}): Promise<any> {
const session = input.driver.session({ defaultAccessMode: input.defaultAccessMode });

Expand All @@ -33,6 +34,10 @@ async function execute(input: {
tx.run(input.cypher, serializedParams)
);

if (input.statistics) {
return result.summary.updateStatistics._stats;
}

return deserialize(result.records.map((r) => r.toObject()));
} finally {
await session.close();
Expand Down
131 changes: 131 additions & 0 deletions packages/graphql/tests/integration/delete.int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Driver } from "neo4j-driver";
import { graphql } from "graphql";
import { generate } from "randomstring";
import neo4j from "./neo4j";
import makeAugmentedSchema from "../../src/schema/make-augmented-schema";

describe("delete", () => {
let driver: Driver;

beforeAll(async () => {
driver = await neo4j();
});

afterAll(async () => {
await driver.close();
});

test("should delete a single movie", async () => {
const session = driver.session();

const typeDefs = `
type Movie {
id: ID!
}
`;

const neoSchema = makeAugmentedSchema({ typeDefs });

const id = generate({
charset: "alphabetic",
});

const mutation = `
mutation($id: ID!) {
deleteMovies(where: { id: $id }) {
nodesDeleted
relationshipsDeleted
}
}
`;

try {
await session.run(
`
CREATE (:Movie {id: $id})
`,
{ id }
);

const gqlResult = await graphql({
schema: neoSchema.schema,
source: mutation,
variableValues: { id },
contextValue: { driver },
});

expect(gqlResult.errors).toBeFalsy();

expect(gqlResult?.data?.deleteMovies).toEqual({ nodesDeleted: 1, relationshipsDeleted: 0 });

const reFind = await session.run(
`
MATCH (m:Movie {id: $id})
RETURN m
`,
{ id }
);

expect(reFind.records.length).toEqual(0);
} finally {
await session.close();
}
});

test("should not delete a movie if predicate does not yield true", async () => {
const session = driver.session();

const typeDefs = `
type Movie {
id: ID!
}
`;

const neoSchema = makeAugmentedSchema({ typeDefs });

const id = generate({
charset: "alphabetic",
});

const mutation = `
mutation($id: ID!) {
deleteMovies(where: { id: $id }) {
nodesDeleted
relationshipsDeleted
}
}
`;

try {
await session.run(
`
CREATE (:Movie {id: $id})
`,
{ id }
);

const gqlResult = await graphql({
schema: neoSchema.schema,
source: mutation,
variableValues: { id: "NOT FOUND" },
contextValue: { driver },
});

expect(gqlResult.errors).toBeFalsy();

expect(gqlResult?.data?.deleteMovies).toEqual({ nodesDeleted: 0, relationshipsDeleted: 0 });

const reFind = await session.run(
`
MATCH (m:Movie {id: $id})
RETURN m
`,
{ id }
);

expect(reFind.records.length).toEqual(1);
} finally {
await session.close();
}
});
});
43 changes: 43 additions & 0 deletions packages/graphql/tests/tck/tck-test-files/cypher-delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## Cypher Delete

Tests delete operations.

Schema:

```schema
type Movie {
id: ID
}
```

---

### Simple Delete

**GraphQL input**

```graphql
mutation {
deleteMovies(where: {id: "123"}) {
nodesDeleted
}
}
```

**Expected Cypher output**

```cypher
MATCH (this:Movie)
WHERE this.id = $this_id
DETACH DELETE this
```

**Expected Cypher params**

```cypher-params
{
"this_id": "123"
}
```

---
Loading