-
Notifications
You must be signed in to change notification settings - Fork 13
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
Comments
Could be a great idea - any rough example what would the API look like? |
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. I guess we will hit limits on what we can do. E.g. |
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. |
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 |
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)
] |
yea, thats a totally fair decision. These are quality of life features only that could make the complexity explode |
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.The text was updated successfully, but these errors were encountered: