[FEAT] Add ajv-errors support
Information
Add ajv-errors message customization support.
Ref: https://www.npmjs.com/package/ajv-errors Related: #833
Example
Configuration:
const Ajv = require("ajv").default
const ajv = new Ajv({allErrors: true})
// Ajv option allErrors is required
require("ajv-errors")(ajv /*, {singleError: true} */)
const schema = {
type: "object",
required: ["foo"],
properties: {
foo: {type: "integer"},
},
additionalProperties: false,
errorMessage: "should be an object with an integer property foo only",
}
const validate = ajv.compile(schema)
console.log(validate({foo: "a", bar: 2})) // false
console.log(validate.errors) // processed errors
Output:
[
{
keyword: "errorMessage",
message: "should be an object with an integer property foo only",
// ...
params: {
errors: [
{keyword: "additionalProperties", dataPath: "" /* , ... */},
{keyword: "type", dataPath: ".foo" /* , ... */},
],
},
},
]
Solution
Example 1:
Expected json-schema:
{
type: "object",
required: ["foo"],
properties: {
foo: {type: "integer"},
},
additionalProperties: false,
errorMessage: {
type: "should be an object", // will not replace internal "type" error for the property "foo"
required: "should have property foo",
additionalProperties: "should not have properties other than foo",
},
}
Solution using decorator:
import {ErrorMsg} from "@tsed/ajv";
@ErrorMsg({
type: "should be an object",
required: "should have property foo",
additionalProperties: "should not have properties other than foo"
})
export class MyModel {
@Required()
@Integer()
foo: number;
}
Example 2
For keywords "required" and "dependencies" it is possible to specify different messages for different properties.
Expected json-schema:
{
type: "object",
required: ["foo", "bar"],
properties: {
foo: {type: "integer"},
bar: {type: "string"},
},
errorMessage: {
type: "should be an object", // will not replace internal "type" error for the property "foo"
required: {
foo: 'should have an integer property "foo"',
bar: 'should have a string property "bar"',
},
},
}
Solution using decorator:
import {ErrorMsg, TypeMsg, RequiredMsg} from "@tsed/ajv";
@TypeMsg("should be an object") // shortcut to ErrorMessage({type: 'msg'})
export class MyModel {
@Required()
@Integer()
@RequiredMsg('should have an integer property "foo"')
foo: number;
@Required()
@RequiredMsg('should have a string property "bar"')
bar: string;
}
Example 3 - Default message
When the value of keyword errorMessage is an object you can specify a message that will be used if any error appears that is not specified by keywords/properties/items using _ property:
const schema = {
type: "object",
required: ["foo", "bar"],
properties: {
foo: {type: "integer", minimum: 2},
bar: {type: "string", minLength: 2},
},
additionalProperties: false,
errorMessage: {
properties: {
foo: "data.foo should be integer >= 2",
bar: "data.bar should be string with length >= 2",
},
_: 'data should have properties "foo" and "bar" only',
},
}
Solution using decorator:
import {TypeMsg, RequiredMsg, DefaultMsg} from "@tsed/ajv";
@DefaultMsg('data should have properties "foo" and "bar" only') // eq: ErrorMsg({_: "message"})
export class MyModel {
@Required()
@Integer()
@RequiredMsg('should have an integer property "foo"')
foo: number;
@Required()
@RequiredMsg('should have a string property "bar"')
bar: string;
}
Acceptance criteria
- [ ] Decorators works on class and properties
- [ ] Decorators are correctly exported:
- [ ] ErrorMsg
- [ ] TypeMsg
- [ ] RequiredMsg
- [ ] DefaultMsg
- [ ] Ajv is correctly configured with ajv-errors (Edit https://github.com/TypedProject/tsed/blob/production/packages/ajv/src/services/Ajv.ts#L48)
- [ ] Documentation is up-to-date (model.md and ajv.md)
- [ ] Unit test cover correctly the decorators.
- [ ] Integration test cover the success and error cases.
@silveoj Can you review this issue. You have more experience with the ajv-errors module :). Maybe I miss something!
@flexwie This story after reviewed by @silveoj can be implemented ;). It's a complete story. Here is the Ts.ED slack to discuss: https://join.slack.com/t/tsed-framework/shared_invite/zt-ljtmbq2u-ln3FoEd4m4Oe8rIJ1~WjdQ
See you Romain
@Romakita it looks good.
- What about multiple messages in Example 2:
@Required()
@RequiredMsg('should have a string property "bar"')
@Pattern(/aaa/) // another validation
@PatternMsg('pattern should match format ...') // another decorator
bar: string;
Now @CustomKey supports one property: @CustomKey('customErrorMessage', 'not correct input property: ...')
Maybe extends it for object. It will be handly for Example 2.
export declare function CustomKey(key: string, value: any): (...args: any[]) => any;
export declare function CustomKey(obj: Record<string, any>): (...args: any[]) => any;
@CustomKey({
required: 'error msg for required',
pattern: 'error msg for pattern',
})
- I think about placeholders. It's not yet clear if they are needed at all.
AJV says us ~
pattern should match format /aaa/., If server wants to sayPlease use format /aaa/ because ...we need to paste/aaa/or constant twice.
export declare function CustomKey(obj: Record<string, (...args: any[]) => string>): (...args: any[]) => any;
@CustomKey({
pattern: (...args) => 'error msg for pattern with #{args[0] because #{args[1]}}', // but we can paste the same without method. Questionable feature.
})
- Yes multiple message will be supported.
- Placeholder is totally supported by ajv-errors, this is why I haven't added example.
const schema = {
type: "object",
properties: {
size: {
type: "number",
minimum: 4,
},
},
errorMessage: {
properties: {
size: "size should be a number bigger or equal to 4, current value is ${/size}",
},
},
}
Ts.ED:
class MyModel {
@MinLength(4)
@ErrorMsg("size should be a number bigger or equal to 4, current value is ${/size}")
size: number;
}
Adding arrow function to build message seems to be not possible if ajv-errors doesn't provide a way to do that.
Thanks for your feedback @silveoj
Romain
@silveoj PR to add @CustomKeys decorator https://github.com/TypedProject/tsed/pull/1238
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
As an idea
@Required().Error("should have required property 'foo'")
@MinLength(4).Error("size should be a number bigger or equal to 4, current value is ${/size}")
foo: number;
Ha yes it could be possible :). But they need to rework all existing decorator for that.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Thinking of contributing for the first time with this issue,
Is this issue still valid ?
Some links are broken like these ones:
- https://github.com/tsedio/tsed/blob/production/packages/ajv/src/services/Ajv.ts#L48
- https://join.slack.com/t/tsed-framework/shared_invite/zt-ljtmbq2u-ln3FoEd4m4Oe8rIJ1~WjdQ
Hello @Crunchyman-ralph The issue is always available :)
I updated the ticket description. The first solution introduce new decorator but it's not elegant rather than the second solution.
I tried to implement an util to wrap an existing decorator and add .Error. It seems to works:
export interface ErrorChainedMethods<T> {
<T>(target: Object, propertyKey?: string | symbol, descriptor?: TypedPropertyDescriptor<T>): any;
Error(msg: string): this;
}
export type ErrorChainedDecorator<Decorator extends (...args: any[]) => any> = (...args: Parameters<Decorator>) => ErrorChainedMethods<Decorator>
function withErrorMessage<Decorator extends (...args: any[]) => any>(errorKey: string, originalDecorator: Decorator): ErrorChainedDecorator<Decorator> {
const schema: any = {};
return ((...decoratorOptions: any[]) => {
const decorator = useDecorators(
originalDecorator(...decoratorOptions),
schema.message && ErrorMsg(errorKey, schema.message)
);
(decorator as any).Error = (message: string) => {
schema.message = message;
return decorator;
};
return decorator;
}) as any;
}
Then his usage with an existing decorator:
import {useDecorators} from "@tsed/core";
import {Allow} from "./allow";
import {Optional} from "./optional";
/**
* Add required annotation on Property or Parameter.
*
* The @@Required@@ decorator can be used on two cases.
*
* To decorate a parameters:
*
* ```typescript
* @Post("/")
* async method(@Required() @BodyParams("field") field: string) {}
* ```
*
* To decorate a model:
*
* ```typescript
* class Model {
* @Required()
* field: string;
* }
* ```
*
* ::: tip
* Required will throw a BadRequest when the given value is `null`, an empty string or `undefined`.
* :::
*
* ### Allow values
*
* In some case, you didn't want trigger a BadRequest when the value is an empty string for example.
* The decorator `@Allow()`, allow you to configure a value list for which there will be no exception.
*
* ```typescript
* class Model {
* @Allow("") // add automatically required flag
* field: string;
* }
* ```
*
* @decorator
* @validation
* @swagger
* @schema
* @input
*/
export const Required = withErrorMessage("required", (required: boolean = true, ...allowedRequiredValues: any[]) => {
return required ? Allow(...allowedRequiredValues) : Optional();
});
With that you are able to wrap all schema decorator that needs a .Error methods and bind it with the ajv-error :)
Tell me if you have any question. Note, I fixed the slack url ;)
See you Romain
🎉 Are you happy?
If you appreciated the support, know that it is free and is carried out on personal time ;)
A support, even a little bit makes a difference for me and continues to bring you answers!
:tada: This issue has been resolved in version 7.11.0 :tada:
The release is available on:
v7.11.0- GitHub release
Your semantic-release bot :package::rocket: