tsed icon indicating copy to clipboard operation
tsed copied to clipboard

[FEAT] Add ajv-errors support

Open Romakita opened this issue 4 years ago • 8 comments

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.

Romakita avatar Feb 06 '21 09:02 Romakita

@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 avatar Feb 06 '21 09:02 Romakita

@Romakita it looks good.

  1. 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',
})
  1. 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 say Please 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.
})

silveoj avatar Feb 07 '21 14:02 silveoj

  1. Yes multiple message will be supported.
  2. 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

Romakita avatar Feb 07 '21 16:02 Romakita

@silveoj PR to add @CustomKeys decorator https://github.com/TypedProject/tsed/pull/1238

Romakita avatar Feb 08 '21 08:02 Romakita

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.

stale[bot] avatar May 15 '21 21:05 stale[bot]

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;

IARKI avatar Oct 04 '21 17:10 IARKI

Ha yes it could be possible :). But they need to rework all existing decorator for that.

Romakita avatar Oct 04 '21 17:10 Romakita

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.

stale[bot] avatar Mar 10 '22 22:03 stale[bot]

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

Crunchyman-ralph avatar Nov 24 '22 14:11 Crunchyman-ralph

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

Romakita avatar Nov 25 '22 08:11 Romakita

🎉 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!

github opencollective

github-actions[bot] avatar Jan 15 '23 18:01 github-actions[bot]

:tada: This issue has been resolved in version 7.11.0 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

Romakita avatar Jan 15 '23 18:01 Romakita