I'm using GraphQL and want to delete an entity from a database and another entity which is assigned to the first one by relation.
Let's say I have three types in my schema:
User
Assignment
Task
The logic is as follows: User is somehow related to a Task. This relation is expressed by "intermediate" object Assignment. (In Assignment I can set that User is a supervisor of a task or worker or really anything, it's not important.)
Now, I want to delete User and related Assignment (Task should not be deleted).
Can I do that while executing only one mutation query with only one parameter: User ID?
I was thinking of something like:
mutation deleteUser($userId: ID!){
deleteUser(id: $userId){
id
assignment {
id # use that id somehow below
}
} {
deleteAssignment(id: id_from_above) {
}
}
}
Is something like that possible?
I think you will benefit from reading the Mutation section of the GraphQL spec.
If the operation is a mutation, the result of the operation is the result of executing the mutation’s top level selection set on the mutation root object type. This selection set should be executed serially.
The key point here is that mutations are:
considered to be a type of query, except they have side-effects
they are not done in whichever order is convenient or even concurrently - like queries - but done sequentially
To be specific about your question: if deleting a user needs to conceptually also delete that user's assignments, and deleting an assignment deletes all tasks, then the exposed mutation (i.e. "query") could be just:
mutation deleteUser($userId: ID!) {
deleteUser(id: $userId)
}
and the associated deletions just happen, and don't need to return anything. If you did want to return things, you could add those things as available for viewing:
mutation deleteUser($userId: ID!) {
deleteUser(id: $userId) {
assignment {
task
}
}
}
Or, if you want the deletion of assignments and tasks to be controlled by the client, so that "viewing" the sub-deletions (which triggers such actual deletions - these would be queries that mutate the data, so would be questionable from a design point of view):
mutation deleteUser($userId: ID!) {
deleteUser(id: $userId) {
deleteAssignment {
deleteTask
}
}
}
Assume of course that you define this Mutation field's return-type appropriately to have these fields available, and that the underlying software acts accordingly with the above required behaviour.
If you simply wish to cascade the deletion, there's nothing for the client to do - no special query needed. Simply execute the appropriate logic in your mutation resolver.
E.g. if you have a service method for deleting users that the mutation resolver will end up calling (the example is in Java, but whatever language you have (you didn't mention), the logic is the same):
boolean deleteUser(String id) {
// either do the assignment deletion yourself here (not good)
// or set your database to cascade the deletions (preferable)
dataBase.execute("DELETE FROM User WHERE id = :id");
return true; //have to return *something*
}
The client does not need to care about this, they just tell your system to delete the user:
mutation deleteUser($userId: ID!){
deleteUser(id: $userId)
}
If you want the client to be able to get something better than a boolean success flag, return that something (this of course means changing the schema accordingly):
String deleteUser(String id) {
dataBase.execute("DELETE FROM User WHERE id = :id");
//return the e.g. the ID
return id;
}
or
String deleteUser(String id) {
User user = dataBase.execute("SELECT FROM User WHERE id = :id");
dataBase.execute("DELETE FROM User WHERE id = :id");
//return the whole deleted user
return user;
}
The latter enables the client to query the result (these are sub-queries, not sub-mutations, there is no such thing as sub-mutations):
mutation deleteUser($userId: ID!){
deleteUser(id: $userId) {
id
assignments {
id
}
}
}
The thing to note is that, unlike the queries, the mutations can not be nested, but yes, you can send multiple top-level mutations (as in your example). Unfortunately, there is no way to use the results from the first as the input for the second. There are requests to introduce something this into the GraphQL spec, but it may or may not ever happen.
Meaning your example:
mutation deleteUser($userId: ID!) {
deleteUser(id: $userId) {
id
assignment {
id # use that id somehow below
}
} {
deleteAssignment(id: id_from_above) {
id
}
}
is unfortunately not possible.
You'll have to do this either as two separate requests, or come up with a more elaborate approach. What you can do if you need to allow the client a deeper level of control is to accept a more complex input, not just an ID, e.g:
input DeleteUserInput {
id: ID!
deleteOwnAssignments: Boolean
deleteManagedAssignments: Boolean
}
mutation deleteUser($input: DeleteUserInput!) {
deleteUser(input: $input)
}
boolean deleteUser(String id, boolean deleteOwnAssignments, boolean deleteManagedAssignments) {
if (deleteOwnAssignments) {
dataBase.execute("DELETE FROM Assignment WHERE assigned_to = :id");
}
if (deleteManagedAssignments) {
dataBase.execute("DELETE FROM Assignment WHERE manager_id = :id");
}
dataBase.execute("DELETE FROM User WHERE id = :id");
return true; //or return whatever is appropriate
}
Related
For example, if there are two types User and Item
type User {
items: [Item!]!
}
type Item {
id: ID!
name: String!
price: Int!
}
If one user has PARTNER role.
I want to prevent it from being called only in the form of the query below.
query Query1 {
user {
items {
name
}
}
}
If user call another query, I want to indicate that user doesn't have permission.
query Query2 {
user {
items {
id
name
}
}
}
In short. if (Query1 != Query2) throw new Error;
Your question is a bit hard to follow but a couple things:
A GraphQL server is stateless - you cannot (and really should not) have a query behave differently based on a previous query. (If there's a mutation in between sure but not two queries back to back)
access management is normally implemented in your resolvers. You can have the resolver for the item id check to see if the user making the query has the right to see that or not and return an error if they don't have access.
Note that it can be bad practice to hide the id of objects from queries as these are used as keys for caching on the client.
Imagine the following query:
query {
user {
id
}
SomeOtherStuff(id: <--- I want to pass the id obtained from user) {
id
}
}
How do you pass a parameter obtained from one query to another ?
In GraphQL, fields at each "level" of the request are executed and resolved in parallel. In your example, user and SomeOtherStuff are both fields of the same type (the root Query type) -- so they will be resolved at the same time. That means each query essentially is not aware of the other or what the other resolved to.
You would have to handle this kind of scenario client side. In other words, request the user first, parse the response for the id and then make the second request.
Edit: In Apollo, you would utilize compose for this purpose:
const userQuery = gql`query User { user { id } }`;
const stuffQuery = gql`query SomeOtherStuff($id: ID) { someOtherStuff(id: $id){ stuff } }`;
export default compose(
graphql(userQuery, { name: 'userData' })
graphql(stuffQuery, { name: 'stuffData', options: ({userData:{id}={}}) => ({variables: {id}}) }),
)(YourComponent)
I agree with #DanielRearden. You should make type-resolvers so you can go infinitely deep into the graph. I made a simple server example here that shows deep relationships. Because all the noun-fields are references, it goes infinitely deep for any query.
With that server, you can run a query like this, for example:
{
hero {
name
friends {
name
friends {
name
friends {
name
friends: {
name
}
}
}
}
}
}
So, in your example, structure it like this:
query {
user {
id
otherStuff {
id
}
}
}
I was looking for same scenario and landed on this question. You can get it work other way around. It all depends how you have written your graphql resolver and you need to make sure that your database relations are intact. I have got it working like this.
If I have 2 types: User and Note with the following schema:
query {
getUser(userId: ID!): User
}
type User {
userId: ID
email: String
notes: [Note]
}
type Note {
noteId: ID
text: String
}
I am writing a resolver for User#notes. Now say notes need to be retrieved by email address, so I actually need the root object passed to the resolver to contain the email field, is there anyway I can force GraphQL to query the email field in the User object even if the user has not requested it?
In terms of code, from what I see, this is how I can write a resolver. How can I ensure obj.email is requested whenever the user requests the note field?
User: {
notes(obj, args, context, info) {
// How can I ensure obj.email is requested?
return NoteRetriever.getNotesByEmail(obj.email);
}
}
Edit
I am wondering about the case where the parent resolver doesn't resolve the email field unless explicitly requested. What if we need to make an API call to get the email for the user? So by default we don't request it. However, when the notes is requested, it makes sense to request the email too.
Is there a way for the resolver to specify dependency on parent fields - to ensure that gets requested?
The "parent" value passed to your resolver as the first parameter is exactly what was returned in the parent field's resolver (unless a Promise was returned, in which case it will be whatever the Promise resolved to). So if we have a resolver like this:
Query: {
getUser: () => {
return {
userId: 10,
email: 'user#example.com',
foobar: 42,
}
}
}
and a query like:
query {
getUser {
id
notes
}
}
What's passed to our notes resolver is the entire object we returned inside the resolver for getUser.
User: {
notes(obj, args, context, info) {
console.log(obj.userId) // 10
console.log(obj.email) // "user#example.com"
console.log(obj.foobar) // 42
}
}
The parent value will be the same, regardless of the fields requested, unless the parent field resolver's logic actually returns a different value depending on the requested fields. This means you can also pass down any number of other, arbitrary entries (like foobar above) from the parent to each child field.
EDIT:
Fields are resolved independently of one another, so there is no mechanism for declaring dependencies between fields. If the getUser resolver is looking at the requested fields and making certain API calls based on requested fields (and omitting others if those fields were not requested), then you'll need to modify that logic to account for the notes field needing the user email.
I think the expectation is that if you control the query of the parent, and expect the value in the child, you should ensure the required value is always resolved by the parent.
There is, however, a way to do what you are asking when merging schemas. This is described here https://www.apollographql.com/docs/graphql-tools/schema-stitching.
Basically would need to have a base schema that is something like
type Query {
getUser(userId: ID!): User
}
type User {
userId: ID
email: String
}
With the same resolvers as you have now, and a second schema that is something like
type Note {
noteId: ID
text: String
}
extend type User {
notes: [Note]
}
Along with something like
import { mergeSchemas } from 'graphql-tools';
const finalSchema = mergeSchemas({
schemas: [userSchema, noteSchema],
resolvers: {
User {
notes: {
fragment: '... on User { email }',
resolve: notesResolver
}
}
}
});
console.dir(await graphql(finalSchema, `query { ... }`));
Notice the fragment property defining the email field. In the link above it describes how this will force resolution of the given field on the parent when this child is resolved.
I learn to use GraphQL these days. In my opinion, To build a query, I need to build three parts:
Schema
type User{
id String
name String
cars [Car!]!
}
type Car{
id String
}
type Query{
user(id: String): User
}
DB Query function
{
user: async function ({id}) {
const user = await DB.user.findOne({id});
const userCars = await DB.car.find({userId: id});
user.cars = userCars;
return cars;
}
}
Client query
{
user (id: "1") {
name
cars {
id
}
}
}
That query returns a user's name and his cars. The DB query function always query for cars.
But sometimes I just need user's info:
{
user (id: "1") {
name
}
}
I don't want to query for cars, so I hope to make my DB query function can auto choose to query for cars or not.
How can I do this?
GraphQL.js will support either object properties or methods for resolver functions; this is discussed in its page on Object Types.
One way to deal with this is just to insert an anonymous function directly into the returned object:
{
user: async function ({id}) {
const user = await DB.user.findOne({id});
user.cars = () => DB.car.find({userId: id});
return cars;
}
}
Another is to create a wrapper object with a class that provides the id property and (asynchronous, lazy) cars method; some examples of this are in the GraphQL.js documentation. This approach tends to work in most GraphQL implementations in most languages.
I think you looking into auto-creating/mapping from GraphQL query into db query.
Every queries are db/project specific, so you should create this mapping. You can easily do that with graphql-fields package.
There is copy pasted WHY section from the package:
An underlying REST api may only return fields based on query params.
{
user {
profile {
firstName
},
id
}
}
should request /api/user?fields=profile,id
while
{
user {
email
}
}
should request /api/user?fields=email
Implement your resolve method like so:
resolve(root, args, context, info) {
const topLevelFields = Object.keys(graphqlFields(info));
return fetch(`/api/user?fields=${topLevelFields.join(',')}`);
}
It's best to avoid squeezing it all into one resolver function. Instead, create a separate ObjectType for Cars which has its own fields and its own resolver function. This way, the car query is only called if that field is requested.
In case you are using a RDS, join monster and data louder can help optimize performance of your queries.
Join Monster which relies on generating one big join query and also solve the problem of only requesting exactly the fields you need from the DB
Cached and Batched SQL Data Source which uses facebook's dataloader under the hood - it wont solve the problem of which fields to query (although the example uses knex, which will make that a lot easier), but instead it can cache and batch your queries
Imagine the following query:
query {
user {
id
}
SomeOtherStuff(id: <--- I want to pass the id obtained from user) {
id
}
}
How do you pass a parameter obtained from one query to another ?
In GraphQL, fields at each "level" of the request are executed and resolved in parallel. In your example, user and SomeOtherStuff are both fields of the same type (the root Query type) -- so they will be resolved at the same time. That means each query essentially is not aware of the other or what the other resolved to.
You would have to handle this kind of scenario client side. In other words, request the user first, parse the response for the id and then make the second request.
Edit: In Apollo, you would utilize compose for this purpose:
const userQuery = gql`query User { user { id } }`;
const stuffQuery = gql`query SomeOtherStuff($id: ID) { someOtherStuff(id: $id){ stuff } }`;
export default compose(
graphql(userQuery, { name: 'userData' })
graphql(stuffQuery, { name: 'stuffData', options: ({userData:{id}={}}) => ({variables: {id}}) }),
)(YourComponent)
I agree with #DanielRearden. You should make type-resolvers so you can go infinitely deep into the graph. I made a simple server example here that shows deep relationships. Because all the noun-fields are references, it goes infinitely deep for any query.
With that server, you can run a query like this, for example:
{
hero {
name
friends {
name
friends {
name
friends {
name
friends: {
name
}
}
}
}
}
}
So, in your example, structure it like this:
query {
user {
id
otherStuff {
id
}
}
}
I was looking for same scenario and landed on this question. You can get it work other way around. It all depends how you have written your graphql resolver and you need to make sure that your database relations are intact. I have got it working like this.