Skip to content
This repository was archived by the owner on Sep 3, 2021. It is now read-only.

Commit e3d297f

Browse files
committedAug 1, 2018
Add update, delete, and remove relationship mutations
These are now added to the Mutation type when calling augmentSchema
1 parent a0dee6a commit e3d297f

9 files changed

+358
-11
lines changed
 

‎example/apollo-server/movies-schema.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ type Book {
6666
}
6767
6868
type Query {
69-
Movie(id: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float, first: Int, offset: Int, orderBy: _MovieOrdering): [Movie] MoviesByYear(year: Int, first: Int = 10, offset: Int = 0): [Movie]
69+
Movie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float, first: Int, offset: Int, orderBy: _MovieOrdering): [Movie] MoviesByYear(year: Int, first: Int = 10, offset: Int = 0): [Movie]
7070
AllMovies: [Movie]
7171
MovieById(movieId: ID!): Movie
7272
GenresBySubstring(substring: String): [Genre] @cypher(statement: "MATCH (g:Genre) WHERE toLower(g.name) CONTAINS toLower($substring) RETURN g")

‎src/augmentSchema.js

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { makeExecutableSchema, mergeSchemas } from 'graphql-tools';
22
import { neo4jgraphql } from './index';
33
import { printSchema } from 'graphql';
4-
import { lowFirstLetter } from './utils';
4+
import { lowFirstLetter, isUpdateMutation, isDeleteMutation } from './utils';
55
import { GraphQLID, astFromValue, buildSchema, GraphQLList } from 'graphql';
66

77
export function addMutationsToSchema(schema) {
@@ -25,6 +25,8 @@ export function addMutationsToSchema(schema) {
2525
(acc, t) => {
2626
// FIXME: inspect actual mutations, not construct mutation names here
2727
acc.Mutation[`Create${t}`] = neo4jgraphql;
28+
acc.Mutation[`Update${t}`] = neo4jgraphql;
29+
acc.Mutation[`Delete${t}`] = neo4jgraphql;
2830
types.forEach(t => {
2931
addRelationshipMutations(schema.getTypeMap()[t], true).forEach(m => {
3032
acc.Mutation[m] = neo4jgraphql;
@@ -255,6 +257,8 @@ function augmentMutations(types, schema, sdl) {
255257
acc +
256258
`
257259
${createMutation(schema.getTypeMap()[t])}
260+
${updateMutation(schema.getTypeMap()[t])}
261+
${deleteMutation(schema.getTypeMap()[t])}
258262
${addRelationshipMutations(schema.getTypeMap()[t])}
259263
`
260264
);
@@ -268,6 +272,15 @@ function createMutation(type) {
268272
return `Create${type.name}(${paramSignature(type)}): ${type.name}`;
269273
}
270274

275+
function updateMutation(type) {
276+
return `Update${type.name}(${paramSignature(type)}): ${type.name}`;
277+
}
278+
279+
function deleteMutation(type) {
280+
const pk = primaryKey(type);
281+
return `Delete${type.name}(${pk.name}:${pk.type}): ${type.name}`;
282+
}
283+
271284
function addRelationshipMutations(type, namesOnly = false) {
272285
let mutations = ``;
273286
let mutationNames = [];
@@ -332,7 +345,20 @@ function addRelationshipMutations(type, namesOnly = false) {
332345
}", to: "${toType.name}")
333346
`;
334347

348+
mutations += `
349+
Remove${fromType.name}${toType.name}(${lowFirstLetter(
350+
fromType.name + fromPk.name
351+
)}: ${innerType(fromPk.type).name}!, ${lowFirstLetter(
352+
toType.name + toPk.name
353+
)}: ${innerType(toPk.type).name}!): ${
354+
fromType.name
355+
} @MutationMeta(relationship: "${relTypeArg.value.value}", from: "${
356+
fromType.name
357+
}", to: "${toType.name}")
358+
`;
359+
335360
mutationNames.push(`Add${fromType.name}${toType.name}`);
361+
mutationNames.push(`Remove${fromType.name}${toType.name}`);
336362
});
337363

338364
if (namesOnly) {

‎src/index.js

+129-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import {
77
innerFilterParams,
88
isAddMutation,
99
isCreateMutation,
10+
isUpdateMutation,
11+
isRemoveMutation,
12+
isDeleteMutation,
1013
isMutation,
1114
lowFirstLetter,
1215
typeIdentifiers
@@ -187,7 +190,8 @@ export function cypherMutation(
187190
});
188191

189192
let params =
190-
isCreateMutation(resolveInfo) && !mutationTypeCypherDirective
193+
(isCreateMutation(resolveInfo) || isUpdateMutation(resolveInfo)) &&
194+
!mutationTypeCypherDirective
191195
? { params: otherParams, ...{ first, offset } }
192196
: { ...otherParams, ...{ first, offset } };
193197

@@ -217,10 +221,6 @@ export function cypherMutation(
217221
WITH apoc.map.values(value, [keys(value)[0]])[0] AS ${variableName}
218222
RETURN ${variableName} {${subQuery}} AS ${variableName}${orderByValue} ${outerSkipLimit}`;
219223
} else if (isCreateMutation(resolveInfo)) {
220-
// CREATE node
221-
// TODO: handle for create relationship
222-
// TODO: update / delete
223-
// TODO: augment schema
224224
query = `CREATE (${variableName}:${typeName}) `;
225225
query += `SET ${variableName} = $params `;
226226
//query += `RETURN ${variable}`;
@@ -309,9 +309,132 @@ export function cypherMutation(
309309
}})
310310
CREATE (${fromVar})-[:${relationshipName}]->(${toVar})
311311
RETURN ${fromVar} {${subQuery}} AS ${fromVar};`;
312+
} else if (isUpdateMutation(resolveInfo)) {
313+
const idParam = resolveInfo.schema.getMutationType().getFields()[
314+
resolveInfo.fieldName
315+
].astNode.arguments[0].name.value;
316+
317+
query = `MATCH (${variableName}:${typeName} {${idParam}: $params.${
318+
resolveInfo.schema.getMutationType().getFields()[resolveInfo.fieldName]
319+
.astNode.arguments[0].name.value
320+
}}) `;
321+
query += `SET ${variableName} += $params `;
322+
323+
const [subQuery, subParams] = buildCypherSelection({
324+
initial: ``,
325+
selections,
326+
variableName,
327+
schemaType,
328+
resolveInfo,
329+
paramIndex: 1
330+
});
331+
params = { ...params, ...subParams };
332+
333+
query += `RETURN ${variableName} {${subQuery}} AS ${variableName}`;
334+
} else if (isDeleteMutation(resolveInfo)) {
335+
const idParam = resolveInfo.schema.getMutationType().getFields()[
336+
resolveInfo.fieldName
337+
].astNode.arguments[0].name.value;
338+
339+
const [subQuery, subParams] = buildCypherSelection({
340+
initial: ``,
341+
selections,
342+
variableName,
343+
schemaType,
344+
resolveInfo,
345+
paramIndex: 1
346+
});
347+
params = { ...params, ...subParams };
348+
349+
// Cannot execute a map projection on a deleted node in Neo4j
350+
// so the projection is executed and aliased before the delete
351+
query = `MATCH (${variableName}:${typeName} {${idParam}: $${
352+
resolveInfo.schema.getMutationType().getFields()[resolveInfo.fieldName]
353+
.astNode.arguments[0].name.value
354+
}})
355+
WITH ${variableName} AS ${variableName +
356+
'_toDelete'}, ${variableName} {${subQuery}} AS ${variableName}
357+
DETACH DELETE ${variableName + '_toDelete'}
358+
RETURN ${variableName}`;
359+
} else if (isRemoveMutation(resolveInfo)) {
360+
let mutationMeta, relationshipNameArg, fromTypeArg, toTypeArg;
361+
362+
try {
363+
mutationMeta = resolveInfo.schema
364+
.getMutationType()
365+
.getFields()
366+
[resolveInfo.fieldName].astNode.directives.find(x => {
367+
return x.name.value === 'MutationMeta';
368+
});
369+
} catch (e) {
370+
throw new Error(
371+
'Missing required MutationMeta directive on add relationship directive'
372+
);
373+
}
374+
375+
try {
376+
relationshipNameArg = mutationMeta.arguments.find(x => {
377+
return x.name.value === 'relationship';
378+
});
379+
380+
fromTypeArg = mutationMeta.arguments.find(x => {
381+
return x.name.value === 'from';
382+
});
383+
384+
toTypeArg = mutationMeta.arguments.find(x => {
385+
return x.name.value === 'to';
386+
});
387+
} catch (e) {
388+
throw new Error(
389+
'Missing required argument in MutationMeta directive (relationship, from, or to)'
390+
);
391+
}
392+
//TODO: need to handle one-to-one and one-to-many
393+
394+
const fromType = fromTypeArg.value.value,
395+
toType = toTypeArg.value.value,
396+
fromVar = lowFirstLetter(fromType),
397+
toVar = lowFirstLetter(toType),
398+
relationshipName = relationshipNameArg.value.value,
399+
fromParam = resolveInfo.schema
400+
.getMutationType()
401+
.getFields()
402+
[resolveInfo.fieldName].astNode.arguments[0].name.value.substr(
403+
fromVar.length
404+
),
405+
toParam = resolveInfo.schema
406+
.getMutationType()
407+
.getFields()
408+
[resolveInfo.fieldName].astNode.arguments[1].name.value.substr(
409+
toVar.length
410+
);
411+
412+
const [subQuery, subParams] = buildCypherSelection({
413+
initial: '',
414+
selections,
415+
variableName,
416+
schemaType,
417+
resolveInfo,
418+
paramIndex: 1
419+
});
420+
params = { ...params, ...subParams };
421+
422+
query = `MATCH (${fromVar}:${fromType} {${fromParam}: $${
423+
resolveInfo.schema.getMutationType().getFields()[resolveInfo.fieldName]
424+
.astNode.arguments[0].name.value
425+
}})
426+
MATCH (${toVar}:${toType} {${toParam}: $${
427+
resolveInfo.schema.getMutationType().getFields()[resolveInfo.fieldName]
428+
.astNode.arguments[1].name.value
429+
}})
430+
OPTIONAL MATCH (${fromVar})-[${fromVar + toVar}:${relationshipName}]->(${toVar})
431+
DELETE ${fromVar + toVar}
432+
RETURN ${fromVar} {${subQuery}} AS ${fromVar};`;
312433
} else {
313434
// throw error - don't know how to handle this type of mutation
314-
throw new Error('Mutation does not follow naming convention.');
435+
throw new Error(
436+
'Do not know how to handle this type of mutation. Mutation does not follow naming convention.'
437+
);
315438
}
316439
return [query, params];
317440
}

‎src/utils.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ export const isCreateMutation = _isNamedMutation('create');
8585

8686
export const isAddMutation = _isNamedMutation('add');
8787

88+
export const isUpdateMutation = _isNamedMutation('update');
89+
90+
export const isDeleteMutation = _isNamedMutation('delete');
91+
92+
export const isRemoveMutation = _isNamedMutation('remove');
93+
8894
export function isAddRelationshipMutation(resolveInfo) {
8995
return (
9096
isAddMutation(resolveInfo) &&
@@ -271,7 +277,7 @@ export function extractSelections(selections, fragments) {
271277
if (cur.kind === 'FragmentSpread') {
272278
const recursivelyExtractedSelections = extractSelections(
273279
fragments[cur.name.value].selectionSet.selections,
274-
fragments,
280+
fragments
275281
);
276282
return [...acc, ...recursivelyExtractedSelections];
277283
} else {

‎test/augmentSchemaTest.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,29 @@ type Movie {
6262
6363
type Mutation {
6464
CreateMovie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float, degree: Int, avgStars: Float, scaleRating: Float, scaleRatingFloat: Float): Movie
65+
UpdateMovie(movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float, degree: Int, avgStars: Float, scaleRating: Float, scaleRatingFloat: Float): Movie
66+
DeleteMovie(movieId: ID!): Movie
6567
AddMovieGenre(moviemovieId: ID!, genrename: String!): Movie
68+
RemoveMovieGenre(moviemovieId: ID!, genrename: String!): Movie
6669
AddMovieState(moviemovieId: ID!, statename: String!): Movie
70+
RemoveMovieState(moviemovieId: ID!, statename: String!): Movie
6771
CreateGenre(name: String): Genre
72+
UpdateGenre(name: String): Genre
73+
DeleteGenre(name: String): Genre
6874
CreateActor(id: ID, name: String): Actor
75+
UpdateActor(id: ID, name: String): Actor
76+
DeleteActor(id: ID!): Actor
6977
AddActorMovie(actorid: ID!, moviemovieId: ID!): Actor
78+
RemoveActorMovie(actorid: ID!, moviemovieId: ID!): Actor
7079
CreateState(name: String): State
80+
UpdateState(name: String): State
81+
DeleteState(name: String): State
7182
CreateBook(genre: BookGenre): Book
83+
UpdateBook(genre: BookGenre): Book
84+
DeleteBook(genre: BookGenre): Book
7285
CreateUser(id: ID, name: String): User
86+
UpdateUser(id: ID, name: String): User
87+
DeleteUser(id: ID!): User
7388
}
7489
7590
interface Person {
@@ -78,7 +93,7 @@ interface Person {
7893
}
7994
8095
type Query {
81-
Movie(_id: Int, id: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float, first: Int, offset: Int, orderBy: _MovieOrdering): [Movie]
96+
Movie(_id: Int, movieId: ID, title: String, year: Int, plot: String, poster: String, imdbRating: Float, first: Int, offset: Int, orderBy: _MovieOrdering): [Movie]
8297
MoviesByYear(year: Int): [Movie]
8398
MovieById(movieId: ID!): Movie
8499
MovieBy_Id(_id: Int!): Movie

‎test/cypherTest.js

+63
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,47 @@ test.cb('Create node mutation', t => {
605605
});
606606
});
607607

608+
test.cb('Update node mutation', t => {
609+
const graphQLQuery = `mutation updateMutation {
610+
UpdateMovie(movieId: "12dd334d5", year: 2010) {
611+
_id
612+
title
613+
year
614+
}
615+
}`,
616+
expectedCypherQuery = `MATCH (movie:Movie {movieId: $params.movieId}) SET movie += $params RETURN movie {_id: ID(movie), .title , .year } AS movie`;
617+
618+
t.plan(2);
619+
cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, {
620+
params: {
621+
movieId: '12dd334d5',
622+
year: 2010
623+
},
624+
first: -1,
625+
offset: 0
626+
});
627+
});
628+
629+
test.cb('Delete node mutation', t => {
630+
const graphQLQuery = `mutation deleteMutation{
631+
DeleteMovie(movieId: "12dd334d5") {
632+
_id
633+
movieId
634+
}
635+
}`,
636+
expectedCypherQuery = `MATCH (movie:Movie {movieId: $movieId})
637+
WITH movie AS movie_toDelete, movie {_id: ID(movie), .movieId } AS movie
638+
DETACH DELETE movie_toDelete
639+
RETURN movie`;
640+
641+
t.plan(2);
642+
cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, {
643+
movieId: '12dd334d5',
644+
first: -1,
645+
offset: 0
646+
});
647+
});
648+
608649
test.cb('Add relationship mutation', t => {
609650
const graphQLQuery = `mutation someMutation {
610651
AddMovieGenre(moviemovieId:"123", genrename: "Action") {
@@ -659,6 +700,28 @@ test.cb('Add relationship mutation with GraphQL variables', t => {
659700
);
660701
});
661702

703+
test.cb('Remove relationship mutation', t => {
704+
const graphQLQuery = `mutation removeRelationship {
705+
RemoveMovieGenre(moviemovieId: "123", genrename: "Action") {
706+
_id
707+
title
708+
}
709+
}`,
710+
expectedCypherQuery = `MATCH (movie:Movie {movieId: $moviemovieId})
711+
MATCH (genre:Genre {name: $genrename})
712+
OPTIONAL MATCH (movie)-[moviegenre:IN_GENRE]->(genre)
713+
DELETE moviegenre
714+
RETURN movie {_id: ID(movie), .title } AS movie;`;
715+
716+
t.plan(2);
717+
cypherTestRunner(t, graphQLQuery, {}, expectedCypherQuery, {
718+
moviemovieId: '123',
719+
genrename: 'Action',
720+
first: -1,
721+
offset: 0
722+
});
723+
});
724+
662725
test('Handle GraphQL variables in nested selection - first/offset', t => {
663726
const graphQLQuery = `query ($year: Int!, $first: Int!) {
664727

0 commit comments

Comments
 (0)