Open filoscoder opened 4 years ago
I'm very curious to see what you built. We also built a few plugins including:
t.paginatedField("users", {
type: "User",
searchable: true,
sortableBy: ["name", "createdAt"], // <- type-safe, pulling property names from the specified type.
...
});
export const doSomething = mutationField(
"doSomething",
{
...
args: {...},
validateArgs: validateAnd(
notEmptyArray((args) => args.petitionIds, "petitionIds"),
notEmptyArray((args) => args.userIds, "userIds"),
maxLength((args) => args.message, "message", 1000)
),
...
});
I'm very curious to see what you built. We also built a few plugins including:
- An offset/limit based pagination plugin with other capabilities
t.paginatedField("users", { type: "User", searchable: true, sortableBy: ["name", "createdAt"], // <- type-safe, pulling property names from the specified type. ... });
- An argument validation plugin
export const doSomething = mutationField( "doSomething", { ... args: {...}, validateArgs: validateAnd( notEmptyArray((args) => args.petitionIds, "petitionIds"), notEmptyArray((args) => args.userIds, "userIds"), maxLength((args) => args.message, "message", 1000) ), ... });
Actually I'm just planning to build the plugin. I didn't build it yet. What we need is an argument validation, I found very interesting your approach. Seems very customizable and pretty straight forward.
What is the return value
from the validateAnd()
method?
In my plugin all validators
have the following type.
export type FieldValidateArgsResolver<
TypeName extends string,
FieldName extends string
> = (
root: core.RootValue<TypeName>,
args: core.ArgsValue<TypeName, FieldName>,
context: core.GetGen<"context">,
info: GraphQLResolveInfo
) => core.MaybePromise<void>;
export function validateAnd<TypeName extends string, FieldName extends string>(
...validators: FieldValidateArgsResolver<TypeName, FieldName>[]
) {
return (async (root, args, ctx, info) => {
await Promise.all(
validators.map((validator) => validator(root, args, ctx, info))
);
}) as FieldValidateArgsResolver<TypeName, FieldName>;
}
Have a look at what other people have built: https://github.com/sytten/nexus-shield by @Sytten seems to be very well thought. I probably wouldn't have built mine if I had found his earlier.
Glad I could help, I also looked at the example plugins when I started. Note that I currently have a typing issue with mine on the objectType level (typing on attribute level is fine) which I was not able to resolve yet. I am happy to write a start guide once the whole thing is a bit more stable. The biggest pain is really when TypeName
or FieldName
decides that is sticks to string rather than being specialized. @jasonkuhrt might want to discuss if we could improve that actually (off topic though).
Thank you both @santialbo @Sytten, this helps me a lot!
besides, I'm following up this guide provided by nexusjs
I think those plugins are different from @nexus/schema plugins (which are the ones I was talking about) In order to write mine I followed the existing plugins on https://github.com/graphql-nexus/schema/tree/develop/src/plugins
Yeah nexus plugins are another beast, I have a branch to support the nexus framework for my plugin but I currently only support schema since I don't use the framework.
@santialbo thanks for your aclaration.. I was struggling with the 'beast' hehe I'll share here when I got something ready.
Arguments Validator plugin
This plugin works inside the @nexus/schema standalone component of the Nexus Framework
This plugin helps us validate configured nexus arguments values during the execution of our queries and mutations. It does this by defining the
validation
field when nexusqueryField
andmutationField
configuration it's defined. In order to proceed withvalidation
, it needs to provide the path of the arguments as an Array. If an invalid value is found, a very kindError message
will be thrown indicating thepath
of the argument, which datatype
it is, and finally which was thevalue
of the argument that causes the crashing.Installation
import { argsValidatorPlugin } from './utils/argsValidatorPlugin'; const schema = makeSchema({ // ... types, plugins: [ // ... other plugins argsValidatorPlugin(), ], // ... etc, })
Usage 1: simple path
// signIn mutation resolver export const signIn = mutationField('signInEmail', { type: 'Auth', args: { email: stringArg({ nullable: false }), password: stringArg({ nullable: false }), }, validation: { shouldValidate: (args) => { // Some resolver logic... return true; // must be true to proceed with validation }, argsPath: ['email', 'password'], }, resolve: async (_parent, args, ctx) => { // Some resolver logic... }, });
Usage 2: deep path
// @Nexus/schema type definition export const UserInputType = inputObjectType({ name: 'UserCreateInput', definition(t) { t.string('email', { nullable: false }); t.string('password', { nullable: false }); t.string('name'); t.string('nickname'); t.date('birthday'); t.gender('gender'); t.string('phone'); t.string('statusMessage'); }, }); // signUp mutation resolver export const signUp = mutationField('signUp', { type: 'User', args: { user: 'UserCreateInput', }, validation: { shouldValidate: (args) => { // Some resolver logic... return true; // must be true to proceed with validation }, argsPath: ['user.email', 'user.password', 'user.name', 'user.nickname'], }, resolve: async (_parent, { user }, ctx) => { // Some resolver logic... });
Validation data types
More patterns (like
password
pattern) & data types will be covered.For now, these are the data types that validation works:
string
: An empty string is an invalid value.number
: NaN is an invalid value.object
: Null is an invalid value.undefined
is invalid.Error message
When a mutation with an empty string value is executed.
Result
References
- Code reference: @sytten: nexus-shield @graphql-nexus/schema
I built an argument validator
plugin. Above is how was implemented. (Thanks for your advice @santialbo, @Sytten)
What are your thoughts about it?
All kinds of feedbacks are welcomed 👍🏾
@filoscoder looks great, maybe a link to the repo would be better? My first impression is: why not put the verifier on the input type directly instead of trying to match the path?
export const UserInputType = inputObjectType({
name: 'UserCreateInput',
definition(t) {
t.string('email', { nullable: false, validate: { /* stuff here */ } });
t.string('password', { nullable: false });
t.string('name');
t.string('nickname');
t.date('birthday');
t.gender('gender');
t.string('phone');
t.string('statusMessage');
},
});
@filoscoder looks great, maybe a link to the repo would be better?
The plugin is arranged inside of a private repo, but I'm planning to build it as an independent package. When it became public I'll share the repo link here!
My first impression is: why not put the verifier on the input type directly instead of trying to match the path?
export const UserInputType = inputObjectType({ name: 'UserCreateInput', definition(t) { t.string('email', { nullable: false, validate: { /* stuff here */ } }); t.string('password', { nullable: false }); t.string('name'); t.string('nickname'); t.date('birthday'); t.gender('gender'); t.string('phone'); t.string('statusMessage'); }, });
Good point @Sytten, but I implemented the plugin to check if the values are valid when the resolver is executed.
I didn't want to make users confused about how to use the validation plugin. That's why I put the validation
config inside the mutationField
.
If I follow to put the verifier on the input type, as you suggest, this is how I'd proceed :
validate configs
logic.In theory I believe that each field of an input is also visited/resolved, but I am not sure if nexus supports that.
In theory I believe that each field of an input is also visited/resolved, but I am not sure if nexus supports that.
You're right, each field is _visited, but as much as I know, only when the fields are created not when a query or mutation is resolved. Anyways I'm writing some test code, and I think for the next week will be ready on npm 👍🏾
Thanks all for providing some code samples and example plugin projects, super helpful!
That said, I'm writing a plugin inside a larger @nexus/schema
project right now and one thing that hasn't clicked for me yet is the actual typing of fieldDefTypes. If you do everything correctly, is the new plugin resolver field supposed to be strongly typed in the schema? No matter what I do my argSchema
plugin field just has an any
type :(, whereas I'd like it to enforce that a valid Joi schema is passed in.
I've got the ArgSchemaResolver
based off code found here https://github.com/graphql-nexus/schema/blob/develop/src/plugins/fieldAuthorizePlugin.ts:
export type ArgSchemaResolver<
TypeName extends string,
FieldName extends string
> = (
root: RootValue<TypeName>,
args: ArgsValue<TypeName, FieldName>,
ctx: GetGen<"context">,
info: GraphQLResolveInfo
) => MaybePromise<boolean | Error>;
The ArgSchemaResolverImport
:
const ArgSchemaResolverImport = printedGenTypingImport({
module: "nexus-plugin-arg-validation",
bindings: ["ArgSchemaResolver"],
});
And finally the fieldDefTypes
:
const fieldDefTypes = printedGenTyping({
optional: true,
name: "argSchema",
type: "ArgSchemaResolver<TypeName, FieldName>",
imports: [ArgSchemaResolverImport],
});
tbh I don't even know where to specify the Joi.ObjectSchema
type within all that. If anyone has an example of how to define a type for the fieldDefTypes property that would be greatly appreciated!
@ben-walker You are on the right track, your issue is the ArgSchemaResolver
, it should have been something like:
type ArgSchemaResolver = Joi.ObjectSchema
If you want to change the schema based on the input. then something like:
type ArgSchemaResolver<
TypeName extends string,
FieldName extends string
> = (
root: RootValue<TypeName>,
args: ArgsValue<TypeName, FieldName>,
ctx: GetGen<"context">,
info: GraphQLResolveInfo
) => MaybePromise<Joi.ObjectSchema>;
Then in the function onCreateFieldResolver
, you can retrieve that schema/function and do your validation then.
So say in example 1 (direct schema):
const schema = config.fieldConfig.extensions?.nexus?.config.argSchema;
Thanks @Sytten! Still having some "type troubles" though, or maybe I'm just expecting the wrong outcome here.
When I declare the plugin's fieldDefTypes
like this:
export type ArgSchemaResolver = Joi.Schema;
const fieldDefTypes = printedGenTyping({
optional: true,
name: "argSchema",
description: "A joi schema to validate resolver args against",
type: "ArgSchemaResolver", // TODO: This should be strongly typed as joi.Schema
});
Using this new field in the schema still shows the type as any
:
t.field("register", {
type: "AuthPayload",
args: {
email: stringArg({ required: true }),
username: stringArg({ required: true }),
password: stringArg({ required: true }),
},
argSchema: Joi.object({ // <-- argSchema has "any" type here
email: Joi.string().email().required(),
username: Joi.string().min(3).max(20).trim().required(),
password: Joi.string().min(8).required(),
}),
...
And then grabbing the field in onCreateFieldResolver()
returns an any
value as well:
console.log(config.fieldConfig.extensions?.nexus?.config.argSchema); // <-- argSchema is "any" here as well
Maybe it's not possible to define a type like I'm expecting? I've seen other plugin authors use explicit type checks in the onCreateFieldResolver
function, so perhaps I just have to try and live my best any
life, not sure.
It is possible to type the argsSchema on your field for sure, but it is not possible for the config.fieldConfig.extensions?.nexus?.config.argSchema
.
Can you paste what you have in your generated TS typegen?
Ahh the TS typegen was the clue I needed, thanks! I solved it with the below, not sure if this is the best approach but it seems to work:
const ArgSchemaResolverImport = printedGenTypingImport({
module: "joi",
bindings: ["Schema"],
});
const fieldDefTypes = printedGenTyping({
optional: true,
name: "argSchema",
description: "A joi schema to validate resolver args against",
type: "Schema", // TODO: This should be strongly typed as joi.Schema
imports: [ArgSchemaResolverImport],
});
Which results in a typegen file like this:
import * as ContextModule from "../../../api/nexus/context"
import * as prisma from "../../prisma/client/index"
import { Schema } from "joi"
...
declare global {
interface NexusGenPluginTypeConfig<TypeName extends string> {
}
interface NexusGenPluginFieldConfig<TypeName extends string, FieldName extends string> {
/**
* A joi schema to validate resolver args against
*/
argSchema?: Schema
}
interface NexusGenPluginSchemaConfig {
}
}
So in the above, argSchema?
in the typegen file has the Joi.Schema
type, and likewise in the nexus schema itself.
Yeah that is it. I think usually nexus prefers namespace import with * as ...
to avoid name conflicts, but this works too.
Hi,
In my team, on the server-side, we're loving this nexus schema thing.
We're using nexus all over our project resolvers, and we are founding some gaps to fill, and I thought it would be fun to contribute with a specific
plugin
inside the schema. (ref)Oh, by the way, I'm planning to build something to check
customizable arguments validation
.