paljs / prisma-tools

Prisma tools to help you generate CRUD system for GraphQL servers
https://paljs.com
MIT License
683 stars 54 forks source link

Prisma Select FragmentReplacements support #185

Closed ZachHaber closed 3 years ago

ZachHaber commented 3 years ago

I was using this to set up a prisma application, and one of the things I realized is that I couldn't use fragmentReplacements in the API.

PrismaSelect would be a good place to be able to resolve those to be able to make sure that I have enough information on the field resolvers to properly resolve them. As the fragmentReplacement values end up on the info.schema object

What are your thoughts on this?

AhmedElywa commented 3 years ago

sorry but I don't have any info about fragmentReplacements can you give me an example about what you try to do

ZachHaber commented 3 years ago

fragmentReplacements are a piece of information that are added onto the schema for individual fields.

https://github.com/maticzav/graphql-middleware/blob/master/src/fragments.ts

For example in the tutorial I was going through, the resolver for User's email requires the id property to be defined:

export const User: IResolverObject<
  UserType,
  Context
> = {
  email: {
    fragment: `fragment userId on User { id }`,
    resolve(parent: UserType, args, { user }, info) {
      if (user?.id && parent.id !== user?.id) {
        return null;
      }
      return parent.email;
    },
  },
}

You can get the query fragments if included on the schema from schema.fragmentReplacements, but it might not exist - The only library I've seen that has that easily included is graphql-middleware. So, I'm not entirely sure if this is worth it over just making notes in your schema that if you want x field, also pull y field.

AhmedElywa commented 3 years ago

Yes, I understand now. We already have a solution for that without using any middleware package You can pass a default field to add even if the client request not included

https://paljs.com/plugins/select#constructor look to options here

And look to this issue #180 for advanced use

ZachHaber commented 3 years ago

The main downside is that you have to set up the default fields in every root query/mutation at that point especially since due to the nature of graphql, you can usually get from one type to a completely different one.

I.e. Post->Comments->User. Which would require the default fields to be set up in Posts() toot query as well for it to be truly accurate

ZachHaber commented 3 years ago

I've figured out how to use the new defaultFields function form and class extension to get much closer to what I needed without having to completely rewrite the PrismaSelect class.

The only thing I was missing here was the ability to see what the parent model was, but there's a close enough approximation here that it's not needed.

I feel like it's a fairly clean way to handle it, and all of my default replacements are in one centralized spot.

import { PrismaSelect as OriginalPrismaSelect } from "@paljs/plugins";

type ConstructorArgs = ConstructorParameters<typeof OriginalPrismaSelect>;
export const DefaultSelect = Symbol("defaultSelect");
export const ParentModel = Symbol("parentModel");

type Properties = Record<string, boolean>;
type ExtraData = {
  /**
   *  Normal lookup - If property on Model is being selected, grab other properties as those are needed for it
   */
  [property: string]: Properties;
  /**
   * These properties are always needed on a model
   */
  [DefaultSelect]?: Properties;
  /**
   * These properties are needed
   */
  [ParentModel]?: { [model: string]: Properties };
};

type ExtraDataByModel = {
  [model: string]: ExtraData;
};

const Fragments: ExtraDataByModel = {
  // On User model, if email is selected, we need id as well!
  User: { email: { id: true } },
  // On Post model, if the parent is a User, then we need published and userId in the Post!
  Post: { [ParentModel]: { User: { published: true, userId: true } } },
};

export class PrismaSelect extends OriginalPrismaSelect {
  constructor(info: ConstructorArgs[0], options?: ConstructorArgs[1]) {
    const originalDefaultFields = options?.defaultFields;
    // The base is the original defaults as those are important to keep
    const newDefaultFields = { ...originalDefaultFields };
    // Keep track of the last model seen as a decent approximation of the parent
    let lastModel: string = "";
    // Map over all fragments to include them
    for (const [model, values] of Object.entries(Fragments)) {
      newDefaultFields[model] = (select) => {
        const originalDefaults = originalDefaultFields?.[model];
        let defaultFields: { [key: string]: boolean } = {
          // Fold in the original defaults passed in so as not to break original functionality
          ...(typeof originalDefaults === "function"
            ? originalDefaults(select)
            : originalDefaults),
          // Fold in the defaults for the model
          ...values[DefaultSelect],
          // Fold in what is necessary based on the likely parent model
          ...values[ParentModel]?.[lastModel],
        };
        lastModel = model;
        for (const key of Object.keys(values)) {
          if (select[key]) {
            defaultFields = { ...defaultFields, ...values[key] };
          }
        }
        return defaultFields;
      };
    }
    super(info, { ...options, defaultFields: newDefaultFields });
  }
}