Resolver implementation

prisma

#1

Hi all,

I have a question concerning the implementation of resolvers in Prisma. I have a project based on the typescript advanced boilerplate.

Moreover, I have defined a query resolver as such:

story(parent, { id, after }, ctx: Context, info) {
    return ctx.db.query.story(
      {
        where: {
          id
        }
      },
      info
    );
  },

Via the info object, a client can query posts on that story. I want to implement a custom resolver on the posts object using the after parameter to only query posts after the “after” id.
I implemented a resolver as such:

export const Story = {
  posts: async (parent, args, ctx: Context, info) => {
    console.log("Parent: " + JSON.stringify(parent));
    console.log("args: " + JSON.stringify(args));
    const { after, id } = args;
    if (after) {
      return ctx.db.query.posts(
        {
          where: {
            story: {
              id
            }
          },
          after: after
        },
        info
      );
    }
    return ctx.db.query.posts(
      {
        where: {
          story: {
            id: id
          }
        }
      },
      info
    );
  }
};

The resolver gets called fine, however, when I print the parent object I see that all the posts have already been fetched by prisma. Is there a way to add a resolver before or instead of the resolver automatically called by prisma? I dont want to fetch all the posts associated with a story automatically.

I would be glad for any examples of a clean resolver architecture, or any tips, I am quite new to graphql. I am also a little unclear as to in which order the resolvers implemented in yoga and the prisma generated resolver functions are being called/ related.

THANKS!


Where clause on query of a mutation
How to modify SelectionSet in binding?
Pass a filter to a relation in a binding?
#2

Hi @moritz :wave:,

could you also provide the Query resolver that you are using?

I think we’ll find the answer there :slightly_smiling_face:


#3

Hi,
what do you mean by the Query resolver? I think I posted all that are relevant.

For the posts Query resolver, I simply forward it to Primsa via resolver forwarding (posts: forwardTo("db"))? I tried without forwarding explicitly and it does not change the behavior.
Besides that, I have not implemented any resolvers that are related to the Story type and leave it all up to Prisma to figure out the query resolution.

Am I misunderstanding something severely here? I am rather new to the whole graphql environment, so feel free to point me to basic misconceptions :slight_smile:

Or do i need to write a Post resolver to override the prisma default? if so, how would I write a post resolver for the whole post and not just a sub parameter like in the custom story parameter above?

Thanks for your time!


#4

Resolvers are executed recursively, according to the hierarchy of fields that appear in a query.

Fields are resolved one by one, and their resolvers can delegate to other resolvers according to the returned type, until the leaves of the query are reached (scalar fields).

The entrypoint to this recursion is the resolver for the operation itself - query, mutation, and subscription. You can also define a resolver for these operations.

You haven’t shared the “top-level” resolver for the query operation, so it’s difficult to say what happens during query resolution before the resolver you shared are executed.


#5

Ok, so far I think my understanding of resolvers has thus been right. (I went through this explanation). I am just confused when prisma resolvers are called and when my implementations are called.

Do you mean this by top level resolver? (this is the resolver index imported and used by the prisma initialization like in the typescript boilerplate)

export default {
  Query,
  Mutation: {
   ....
  },
  ...
  Story
};

where Query contains the resolver posted above:

export const Query = {
story(parent, { id, after }, ctx: Context, info) {
    return ctx.db.query.story(
      {
        where: {
          id
        }
      },
      info
    );
  },
  ...
}

The full Story resolver code is posted above. Is this what you mean?


#6

Or is there a chance that I am not seeing my implemented changes reflected due to caching? How can I reset the cache?


#7

Just to clarify further (in accordance with the above):

export const Story = {
    posts: async (parent, args, ctx: Context, info) => {
    console.log("Parent: " + JSON.stringify(parent));
    const { after, id } = args;
...........
}

Prints this to the console when querying some random story with the post:

Parent: {"posts":[{"id":"cjfrxs0js81uc0a42l5my715e"},{"id":"cjfrxs71s82660a42zxptrlym"}]}

For the query:

query ($_where: StoryWhereUniqueInput!) {
story(where: $_where) {
    posts {
      id
    }
  }
}

Showing that the prisma resolver has already fetched all the posts before my resolver is called. How can I override this behavior?


#8

Hey @moritz :wave:,

Thank you for all the information you’ve provided. Here’s the situation, as @nilan already mentioned above, GraphQL resolvers resolve recursively. What this means is that each query execution follows the query schema chain and tries to process information based on the provided parent data and resolver function assigned to specific field.

To give you a bit of a context, your schema has type Query which by default serves as query handler. As you query the story the GraphQL Engine first looks for the implementation of Query type in your resolvers and tries to identify story field resolver. What story resolver does is it passes all the fields in the info property, which match Prisma API (this is, in your case, posts.id), to Prisma directly. Thus, Prisma will by default already return all the posts with their ids - just as you have told her to.

Once story resolver finishes execution, GraphQL Engine once again takes a look at your schema. This time it tries to identify the succeeding type of story. Because we’ve defined that story field returns Story type, engine will try to find Story type resolver. (Do keep in mind that even though you only implement type-fields which require additional logic or no resolvers at all, GraphQL Yoga by default defines resolvers for all the scalar fields type defines. Examples of this are String, Boolean and ID.

Since Post under Story requires no additional logic, you can leave it as it is and omit the Story type resolver thoroughly. Type resolvers are meant to serve additional field logic which is not yet implemented by database (Prisma) itself. Example of this would be, hasViewerLikedThePost - which would have to take a look at additional data obtained from other resources (not story field directly).

Hope this gives you a better feeling of the execution of your query. :slightly_smiling_face:

A few “reminders” or better “suggestions”; you don’t have to define a separate case for after. Afaik, the after property filtering will be omitted completely if the forwarded property has null value or undefined.

When defining type resolvers, it is best to think of them very narrowed down or maybe locally is a better word. I encourage you to use args property only in Query fields directly as this are the fields which arguments are meant for. In type resolvers use parent property instead and pass the info as a return value. I have included a link below which will give you a better overview of what I have in mind.

Hope this helps you as well! :slight_smile:


#9

Hey @matic,

Thank you very much for this very detailed response! It cleared up a lot for me.

So far, I always assumed that type resolvers were a mechanism for overriding part of the generated prisma resolvers. But yes thinking about it now it makes a lot more sense, especially since the whole prisma database should resolve its queries independent of the yoga middleman. Thanks for clearing that up for me, really an awesome response! :raised_hands:

I remember that the reason I tried to find a setup like the one described above was that I was not able to find a solution for passing the after parameter to the nested post parameter. I will look into that again!

Thanks a million!


#10

So as a quick follow up, if I want to pass the after parameter on post to prisma directly, how would this look in my resolver?

Am I right in assuming that I will have to edit the info object for this? I would use this read as guidance.

I have a situation where I dont want to leave it up to the client to pass the after parameter, as I want to ensure that only the latest posts can be queried by the client.


#11

@moritz could you share your schema definition? :slightly_smiling_face:


#12

@matic of course. This is my database schema:

type Post {
  id: ID! @unique
  createdAt: DateTime!
  updatedAt: DateTime!
  author: User!
  text: String
  ...
  story: Story!
}

type Story {
  id: ID! @unique
  createdAt: DateTime!
  updatedAt: DateTime!
  posts: [Post!]!
}

and this is the query from the schema yoga server schema file:

story(id: ID!, after: String): Story!

I want to only return the last x posts, no matter what. I want to enforce this so client is unable to query more.


#13

@moritz what you shared is great and might be hepful later on, but what I was curious of is not your datamodel, but schema that you use for the server.

Are you also defining the Story type there?


#14

@matic that is all there is to my schema concerning my Story type. I use the prisma generated types and dont override them in my server schema. My setup is really quite simple, you can assume the typescript advanced boilerplate with an added Story type in the data backend.

I am neither importing nor overriding the Story datatype from the generated folder. Is that a problem?

THX


#15

Hey, @moritz that is not a problem at all. Nevertheless, I would encourage you to rethink the arguments you are passing to the story query. I always try to follow a rule of a thumb; Would another person reading schema know what particular parameter does without looking at the resolvers?

I am a firm believer that this is highly undervalued when designing models for personal projects but is going to help you a lot afterward. What is the purpose of the after field? Is it directly related to Story? Is it passed down to another field during the execution?

In your case, the resolver forwards after argument to posts field. Even though this is a good enough solution, it is not what we are aiming for. In my opinion after argument should be placed as an argument where it affects the output, this is in posts field. Furthermore, arguments from the parent fields cannot be obtained through the args argument of the resolver which would make the current approach inadequate.

Taking this into consideration, we can rebuild our model;

type Query {
  story(id: ID!): Story
}

type Story {
  id: ID!
  posts(after: String): [Post!]!
}

type Post {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  ...
  story: Story!
}

This way you can see what effect your arguments are going to have on the output of the query very quickly.

Luckily, this also matches the Prisma API, and you do not have to write any additional functionality to make this work out of the box.

On the other hand, there might be an argument which wouldn’t be implemented by Prisma out of the box. In such case, you would have to implement a particular field resolver for your particular type. Because we have included arguments parameter in our posts field, it should not be too complicated to access the provided arguments and calculate the result.

I hope this gives you an even better idea of the execution process. :slightly_smiling_face:

A quick tip, I usually reimplement every type I have defined in my data model in my schema. Because of this, I can be more certain of the output certain resolvers are going to produce and confident about the accessibility of my data as I can very easily control the input/output users can produce.


#16

Hi @matic, thanks again for your incredibly helpful response. If you should ever be in Berlin, I owe you a beer :slight_smile:

When I created the after parameter, I overlooked the fact that I can pass nested parameters in my queries from the client. This is, of course, essential for designing clean endpoints :sunny: Thanks for pointing me to that, I dont quite remember what I was thinking there… It looked wrong all along but sometimes you just overlook the obvious…

The conventions you suggested seem like a very good pointer. Time to refactor :slight_smile:

Concerning my original question: So, if I want to enforce a parameter on a query, like for example always only return the last 20 posts, no matter what the client requests, where would I enforce this?

When I implement the Story{posts(...)} resolver locally, it gets called only at the end of the resolver chain, as we discussed above, and potentially much more posts have already been fetched by prisma, which is bad for performance.

Would the solution then be to manipulate the info object at the top level story resolver to enforce the last parameter on the nested posts in the query? Or is there another way to prevent prisma from resolving the query as a whole before calling locally defined resolvers?

THANKS!


#17

Hey @moritz, thanks for such a great response. I will certainly hit you up if I come to Berlin! :beers:

Furthermore, I might not be able to give you such unambiguous answer as I was able to with the other two. Instead, I am going to present you the pros and cons of two or maybe three options that you have, and you can decide for yourself.

Starting with the one you already mentioned - modifying the info property, I would say that it is possible to modify it, but it might not be the best option there is. I, personally, like to stick with the offered boundaries and instead explore the possibilities of turning the question upside down. The major flaw I see in changing the info object directly, is the fact that you are modifying the fractions of the core code which seems frightening. On the other hand, you get to the point very quickly which might play a significant role.

The second option I see is writing a posts resolver regardless of the already obtained data. In comparison with the first option, this one sticks to the limits, and we can test the code very easily - it has certain promises, it can promise something. Honestly, I think there’s a different, better word than “promise” - check the video below, I know it’s somewhere in there. Nevertheless, this approach raises a very well known con; it is inefficiency.

Inefficiency problem brings me to the third option. Maybe this is the best part of the answer to rethink the problem we are having - we want to limit the number of posts we query as there might be millions of them and executing such query would make our server extraordinarily sloppy and slow, plus we want to fix the problem as elegantly as possible. Since the cause of the problem is a nested type, the posts field that we are querying, we might reconsider querying it at all. Furthermore, we also know that Prisma by default without passing the info property, queries all scalar fields of the object we want to obtain. Because of this excellent feature, we can leave the implementation of the story field hanging in the air, not knowing anything about the information we want to obtain.

Nevertheless, we still need to implement the execution of posts field. Because the parent resolver returns null, we have to query posts by hand in our Story type resolver. We can do this by obtaining the id of the parent story via fragment and, instead of querying a Story, query the actual posts. This can be achieved very quickly using the code chunk below.

export const Story = {
  posts: {
    fragment: `fragment StoryId on Story { id }`,
    resolve: async ({ id }, args, ctx: Context, info) => {
      return ctx.db.query.posts({
        where: {
          story: { id }
        },
        first: 2
      });
    }
  }
};

I won’t go into much detail about the actual code, but feel free to ask if anything seems unclear.

I hope this answers the question you had. I would personally stick with the last option as it seems the most elegant one, but as I said, reconsider the trade-offs. :slightly_smiling_face:


#18

Hi @matic,

thanks for the considerations and the video, you’ve given me a lot to think about.

Concerning your third approach, I understand the appeal of it, however, I think it imposes to many limitations on more complex data models. For example, my Post type has an additional, non scalar Image type dependency (I simplified my types for the sake of argument) that should be accessible via the info object. From my understanding, the third approach would make it impossible to access non scalar references on the posts object, or at least to access a custom subset of fields via the info object. (Correct me if I’m wrong).

Concerning the second approach, I find it simply imposes to great a limitation on scalability.

Concerning the first approach and the info object, I quite agree that meddling with the info object seems like bad practice. (So far, being new to the graphql environment, the info object has been my greatest enemy :slight_smile: ). However, so far I consider it the only way to achieve what I desire.

I think it would be great to have a mechanism or library that allows manipulation of the info object in a non-harmful or safe way. Sticking to our example, to put it into words, a function that would do for me: split the info object, and give me a info object that looks like posts was never queried by the client in the first place, i.e. take out all the posts related stuff, and, in addition, a new and standalone info object that only contains all the posts related query parameters, as if the client had only queried posts without any story. Then I could tell prisma to resolve the story with all its (possibly other) subtypes, query the posts with a where parameter on the story id and the desired last parameter using the second info object, stitch it all together, and be done.

Does there exists a mechanism of the sort, or is this approach somehow inconsistent with the architecture? I am wondering if a situation like this is not a common use case.

Thanks for your opinion!