Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extensions support #29

Open
n1ru4l opened this issue Oct 26, 2020 · 6 comments
Open

Extensions support #29

n1ru4l opened this issue Oct 26, 2020 · 6 comments

Comments

@n1ru4l
Copy link
Collaborator

n1ru4l commented Oct 26, 2020

It would be nice if extensions could be specified for the fields/types. We could make it type-safe by allowing a generic extension type on the createTypesFactory function.

@sikanhe
Copy link
Owner

sikanhe commented Oct 28, 2020

Could be a great idea - any rough example what would the API look like?

@n1ru4l
Copy link
Collaborator Author

n1ru4l commented Oct 28, 2020

A quick sketch:

type VisibilityExtension = {
  visibilityAccess?: "public" | "hidden";
}

type LiveQueryExtensions = {
  liveQuery?: {
    buildResourceIdentifier: (source: unknown) => string;
  },
}

type TExtensions = VisibilityExtension & LiveQueryExtensions

const t = createTypesFactory<TContext, TExtensions>();

const Character = t.objectType({
  name: "Character",
  extensions: {
    visibilityAccess: "public"
  },
  fields: () => [
    t.field("name", {
      type: t.NonNull(t.String),
      resolve: (character) => "HI",
    }),
  ],
});

Note extensions are just there for adding metadata to the schema. Actually doing sth with the metadata must be done in user-land. We could however provide some way of at least providing typings for the available extensions.

See graphql/graphql-js#2097

I guess we will hit limits on what we can do. E.g. buildResourceIdentifier would receive the source object of the object type (in the user-land implementation), however, I am not aware of how that could be typed properly in a general way.

@n1ru4l
Copy link
Collaborator Author

n1ru4l commented Jan 30, 2021

I created #36 for a basic implementation. It does however not use any typing magic, but I guess stuff like that could still be introduced later on.

@Ericnr
Copy link
Contributor

Ericnr commented Feb 8, 2021

I was asked for some examples of use cases, here are some.

interface TExtensions {
  authorize?: (ctx: TContext) => boolean;
  authenticate?: boolean;
}

const t = createTypesFactory<TContext, TExtensions>({
  extensions: {
    authorize: { // first arg is whatever value passed to authorize, in this case a fn 
      extendObjectTypeResolver: (checkAuth) => async (parent, args, ctx, info) => {
        const isAuthorized = await checkAuth(ctx);
        if (!isAuthorized) throw new AuthorizationError()
      }
    },
    authenticate: {
      extendObjectTypeResolver: (needsAuthentication) => async (parent, args, ctx, info) => {
         const isAuthenticated = await ctx.checkDbForSession(ctx.user.id);
          if (!isAuthenticated) throw new AuthenticationError()
      }
    },
  }
});

const Mutation = t.mutationType({
  fields: [
    t.field('doStuffOnlyAdminShouldDo', {
      type: SomeResponseType,
      args: {
        id: t.arg(t.NonNullInput(t.ID)),
      },
      extensions: {
        authorize: (ctx) => ctx.checkRoles(['ADMIN']),
        authenticate: true,
      },
      resolve: (_, args, ctx) => {
        ...
      },
    }),
  ],
});

Now for a more complicated example, and probably much harder to define with TS

import { fromGlobalId } from 'graphql-relay';
const t = createTypesFactory<TContext, TExtensions>({
  extensions: {
    parse: {
      extendInputType: (parseFn) => async (args, argName) => {
        // mutates the args object before the resolver runs
        // parse function will also validate inputs and throw if invalid
        args[argName] = await parseFn(args[argName]);
      },
    },
  },
});

const MyInputType = t.inputObjectType<{ id: string }>({
  name: 'MyInputType',
  extensions: {
    // arg should have the same type as input
    parse: async (input) => {
      // input.id is a Relay Global Object Identifier, it needs to be parsed into the id that is actually used in the db
      return {
        realId: fromGlobalId(input.id),
      };
    },
  },
  fields: () => [t.defaultField('id', t.ID)],
});

const Mutation = t.mutationType({
  fields: [
    t.field('doStuffOnlyAdminShouldDo', {
      type: SomeResponseType,
      args: {
        input: t.arg(t.NonNullInput(MyInputType)),
      }, 
      // type of `args.input` is now the return type of its parse extension
      // { realId: string } 
      resolve: (_, args, ctx) => {},
    }),
  ],
});

I expect it wouldnt be feasible, but a simpler version where you only validate the input and throw an exception if its bad (without mutating args) might be.

There is a lot of inspiration to be taken from nexus's plugins https://nexusjs.org/docs/api/plugins

@sikanhe
Copy link
Owner

sikanhe commented Feb 8, 2021

I looked through Nexus' plugin, most of them should not need extensions. Just plain, reuse-able higher order functions.

Take your authentication for example, we just create a function that takes a regular resolver, and return a new resolver, and can be used on any field that requires auth

const adminOnly = (resolver) => (parent, args, ctx) => {
   if (Permissions.isAdmin(ctx.currentUser)) return resolver(ctx, parent, args);
   return null // or return error
}

fields: [
   t.field('allUsers', {
      type: ListOfUsers,
      resolver: adminOnly(() => { return Users.getAll() })
   })
]

The same pattern can be applied to pretty everything on that plugin list.

For the relay ID example. You create a new GraphQL RelayId Scalar type, which is used for parsing and serializing a leaf node. Roughly

const globalId = t.scalarType('globalId', {
   serialize: toGlobalId,
   parse: fromGlobalId
})

fields: [
  t.defaultField("id", globalId)
]

@Ericnr
Copy link
Contributor

Ericnr commented Feb 8, 2021

yea, thats a totally fair decision. These are quality of life features only that could make the complexity explode

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants