File size: 3,864 Bytes
a23aa7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import { DefinedError, ErrorObject } from 'ajv';
import { ValidationError } from './types/ValidationError';
import { filterSingleErrorPerProperty } from './lib/filter';
import { getSuggestion } from './lib/suggestions';
import { cleanAjvMessage, getLastSegment, pointerToDotNotation, safeJsonPointer } from './lib/utils';

export interface BetterAjvErrorsOptions<S = any> {
  errors: ErrorObject[] | null | undefined;
  data: any;
  schema: S;
  basePath?: string;
}

export const betterAjvErrors = <S = any>({
  errors,
  data,
  schema,
  basePath = '{base}',
}: BetterAjvErrorsOptions<S>): ValidationError[] => {
  if (!Array.isArray(errors) || errors.length === 0) {
    return [];
  }

  const definedErrors = filterSingleErrorPerProperty(errors as DefinedError[]);

  return definedErrors.map((error) => {
    const path = pointerToDotNotation(basePath + error.instancePath);
    const prop = getLastSegment(error.instancePath);
    const defaultContext = {
      errorType: error.keyword,
    };
    const defaultMessage = `${prop ? `property '${prop}'` : path} ${cleanAjvMessage(error.message as string)}`;

    let validationError: ValidationError;

    switch (error.keyword) {
      case 'additionalProperties': {
        const additionalProp = error.params.additionalProperty;
        const suggestionPointer = error.schemaPath.replace('#', '').replace('/additionalProperties', '');
        const { properties } = safeJsonPointer({
          object: schema,
          pnter: suggestionPointer,
          fallback: { properties: {} },
        });
        validationError = {
          message: `'${additionalProp}' property is not expected to be here`,
          suggestion: getSuggestion({
            value: additionalProp,
            suggestions: Object.keys(properties ?? {}),
            format: (suggestion) => `Did you mean property '${suggestion}'?`,
          }),
          path,
          context: defaultContext,
        };
        break;
      }
      case 'enum': {
        const suggestions = error.params.allowedValues.map((value) => String(value ?? ''));
        const prop = getLastSegment(error.instancePath);
        const value = safeJsonPointer({ object: data, pnter: error.instancePath, fallback: '' });
        validationError = {
          message: `'${prop}' property must be equal to one of the allowed values`,
          suggestion: getSuggestion({
            value,
            suggestions,
          }),
          path,
          context: {
            ...defaultContext,
            allowedValues: error.params.allowedValues,
          },
        };
        break;
      }
      case 'type': {
        const prop = getLastSegment(error.instancePath);
        const type = error.params.type;
        validationError = {
          message: `'${prop}' property type must be ${type}`,
          path,
          context: defaultContext,
        };
        break;
      }
      case 'required': {
        validationError = {
          message: `${path} must have required property '${error.params.missingProperty}'`,
          path,
          context: defaultContext,
        };
        break;
      }
      case 'const': {
        return {
          message: `'${prop}' property must be equal to the allowed value`,
          path,
          context: {
            ...defaultContext,
            allowedValue: error.params.allowedValue,
          },
        };
      }

      default:
        return { message: defaultMessage, path, context: defaultContext };
    }

    // Remove empty properties
    const errorEntries = Object.entries(validationError);
    for (const [key, value] of errorEntries as [keyof ValidationError, unknown][]) {
      if (value === null || value === undefined || value === '') {
        delete validationError[key];
      }
    }

    return validationError;
  });
};

export { ValidationError };