Open valerii15298 opened 1 year ago
An example using zod
should be added but I don't know if it is possible. @MichalLytek surely have an idea on this 🥳🤗
I have no experience with zod
so I can't help you.
I only have feelings that zod
won't play nice with decorators because it's also "schema declaration" library, so it focuses on declaring the shape of objects, which we already have with TS classes.
I guess there is a question how much and are we gonna allow for zod to validate. For example this plugin for nest https://github.com/incetarik/nestjs-graphql-zod allows to validate nested objects too.
Agnostic approach for type-graphql could be nice, for example if we can assign some metadata to specific field and then access it in validation function, this will allow integrate any validation library with type-graphql. If we just have access to field metadata then writing zod validation function is quite trivial:
@InputType()
class Bar {
@Extensions({ zodSchema: z.string() }) // some way to assign metadata to the function
@Field()
field: string;
}
const schema = await buildSchema({
// ...other options
validate: (argValue, argType, fieldMetadata) => {
// we just need to access extensions metadata in this validate function
fieldMetadata?.zodSchema.parse(argValue) // this will throw on validation error
// the above same as `z.string().parse(argValue)`
},
});
I do not know anything about how hard is to implement this for type-graphql, it is just as an example idea
@Extensions
are GraphQL specific. All you need is a generic decorator approach that will work with any framework, just like class-validator
. So storing the metadata should be done on, let's name it, zod-decorators
package:
@InputType()
class Bar {
@Zod(z => z.string()) // some way to assign metadata to the function
@Field()
field: string;
}
validate: (argValue, argType) => {
zodDecorators.validate(argType, argValue); // reads validation schema from own storage for `argType` class and parse `argValue` value
},
I have just tried to create a custom validator for any args passed to a graphql query or mutation:
import { createMethodDecorator, ArgumentValidationError } from 'type-graphql';
import { ValidationError } from 'class-validator';
import { z } from 'zod';
type SchemaMap = { [argName: string]: z.Schema<any> };
function convertZodErrorToClassValidatorError(zodError: z.ZodError, argName: string): ValidationError[] {
return zodError.errors.map((error) => {
const validationError = new ValidationError();
validationError.property = argName;
validationError.constraints = { [error.code]: error.message };
return validationError;
});
}
export function ZodValidate(schemaMap: SchemaMap) {
return createMethodDecorator(async ({ args }, next) => {
for (const argName in schemaMap) {
const schema = schemaMap[argName];
const argValue = args[argName];
const result = schema.safeParse(argValue);
if (!result.success) {
const validationErrors = convertZodErrorToClassValidatorError(result.error, argName);
throw new ArgumentValidationError(validationErrors);
}
}
return next();
});
}
Usage example:
@InputType()
export class TestInput {
@Field()
targetAudience: string;
@Field()
annualLaunches: string;
@Field(() => Float)
employees: number;
}
....
const schema = z.object({ targetAudience: z.string().max(3), annualLaunches: z.string(), employees: z.number() });
@Mutation(() => Boolean)
@ZodValidate({ input: schema })
async zodValidated(@Arg('input') input: TestInput): Promise<boolean> {
console.log(input);
return true;
}
And it validates as expected but the problem is that the output error is not formatted properly (the extensions exception doesnt include any validation errors paased to the ArgumentValidationError constructor):
{
message: 'Argument Validation Error',
locations: [ { line: 2, column: 3 } ],
path: [ 'zodValidated' ],
extensions: {
code: 'INTERNAL_SERVER_ERROR',
stacktrace: [
'Error: Argument Validation Error',
' at C:\\Users\\angel\\...'] // Ommited rest of stacktrace for privacy
}
}
Any idea why this happens @MichalLytek?
I think that zod
is better than class-validator
, because zod can do conditional validation, while class-validator is limited in that area by only working with fields mostly
ref: https://github.com/colinhacks/zod/discussions/2099#discussioncomment-5101317
my situation: i want to validate GraphQl input objects and fields list: required fields are depending on type
property of input type
I have just tried to create a custom validator for any args passed to a graphql query or mutation:
import { createMethodDecorator, ArgumentValidationError } from 'type-graphql'; import { ValidationError } from 'class-validator'; import { z } from 'zod'; type SchemaMap = { [argName: string]: z.Schema<any> }; function convertZodErrorToClassValidatorError(zodError: z.ZodError, argName: string): ValidationError[] { return zodError.errors.map((error) => { const validationError = new ValidationError(); validationError.property = argName; validationError.constraints = { [error.code]: error.message }; return validationError; }); } export function ZodValidate(schemaMap: SchemaMap) { return createMethodDecorator(async ({ args }, next) => { for (const argName in schemaMap) { const schema = schemaMap[argName]; const argValue = args[argName]; const result = schema.safeParse(argValue); if (!result.success) { const validationErrors = convertZodErrorToClassValidatorError(result.error, argName); throw new ArgumentValidationError(validationErrors); } } return next(); }); }
Usage example:
@InputType() export class TestInput { @Field() targetAudience: string; @Field() annualLaunches: string; @Field(() => Float) employees: number; } .... const schema = z.object({ targetAudience: z.string().max(3), annualLaunches: z.string(), employees: z.number() }); @Mutation(() => Boolean) @ZodValidate({ input: schema }) async zodValidated(@Arg('input') input: TestInput): Promise<boolean> { console.log(input); return true; }
And it validates as expected but the problem is that the output error is not formatted properly (the extensions exception doesnt include any validation errors paased to the ArgumentValidationError constructor):
{ message: 'Argument Validation Error', locations: [ { line: 2, column: 3 } ], path: [ 'zodValidated' ], extensions: { code: 'INTERNAL_SERVER_ERROR', stacktrace: [ 'Error: Argument Validation Error', ' at C:\\Users\\angel\\...'] // Ommited rest of stacktrace for privacy } }
Any idea why this happens @MichalLytek?
I would like to add on top of this as I am in need of a similar configuration and eventually came to a similar solution. In my case the zod schemas not only have validation but they have certain mutation features aswell. Like z.string().toLowecase() The promblem is i cannot pass the result of zod.parse back into the next() function to be processed by the next middleware
Is your feature request related to a problem? Please describe. Since my project extensively uses zod I want to be able validate
InputType
s and maybeObjectType
s too using zod.Describe the solution you'd like something like this?
Describe alternatives you've considered Using built in
class-validator
orjoiful
as described here => https://typegraphql.com/docs/validation.html#custom-validatorAdditional context
joiful
does not seem to be well maintained.class-validator
seems not bad butzod
is already quite popular library with friendly and understandable API having high flexibility and customization. So it might be worth to integrate zod withtype-graphql
. I feel like it is already possible to do it with zod by using Extension decorator => https://typegraphql.com/docs/extensions.html and custom validation function => https://typegraphql.com/docs/validation.html#custom-validator but I did not find examples in docs how to do that. Basically how to extract extensions data in custom validation function? If it is possible, I can even maybe contribute to add examples with zod validation if needed and someone can give me direction of where to start and where to look for...So what would be the best way to integrate zod with typegraphql?