@tsed/ajv 8.0.0 → 8.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -11
- package/src/decorators/formats.ts +24 -0
- package/src/decorators/keyword.ts +26 -0
- package/src/errors/AjvValidationError.ts +11 -0
- package/src/index.ts +14 -0
- package/src/interfaces/FormatsMethods.ts +5 -0
- package/src/interfaces/IAjvSettings.ts +23 -0
- package/src/interfaces/KeywordMethods.ts +23 -0
- package/src/interfaces/interfaces.ts +10 -0
- package/src/services/Ajv.spec.ts +39 -0
- package/src/services/Ajv.ts +84 -0
- package/src/services/AjvService.spec.ts +47 -0
- package/src/services/AjvService.ts +116 -0
- package/src/utils/defaultErrorFormatter.spec.ts +73 -0
- package/src/utils/defaultErrorFormatter.ts +33 -0
- package/src/utils/getPath.ts +13 -0
package/package.json
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
"name": "@tsed/ajv",
|
|
3
3
|
"description": "AJV package for Ts.ED framework",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "8.0.
|
|
5
|
+
"version": "8.0.2",
|
|
6
6
|
"source": "./src/index.ts",
|
|
7
7
|
"main": "./lib/esm/index.js",
|
|
8
8
|
"module": "./lib/esm/index.js",
|
|
9
9
|
"typings": "./lib/types/index.d.ts",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
|
+
"@tsed/source": "./src/index.ts",
|
|
12
13
|
"types": "./lib/types/index.d.ts",
|
|
13
14
|
"import": "./lib/esm/index.js",
|
|
14
15
|
"default": "./lib/esm/index.js"
|
|
@@ -27,22 +28,22 @@
|
|
|
27
28
|
"tslib": "2.7.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@tsed/barrels": "8.0.
|
|
31
|
-
"@tsed/core": "8.0.
|
|
32
|
-
"@tsed/di": "8.0.
|
|
33
|
-
"@tsed/exceptions": "8.0.
|
|
34
|
-
"@tsed/schema": "8.0.
|
|
35
|
-
"@tsed/typescript": "8.0.
|
|
31
|
+
"@tsed/barrels": "8.0.2",
|
|
32
|
+
"@tsed/core": "8.0.2",
|
|
33
|
+
"@tsed/di": "8.0.2",
|
|
34
|
+
"@tsed/exceptions": "8.0.2",
|
|
35
|
+
"@tsed/schema": "8.0.2",
|
|
36
|
+
"@tsed/typescript": "8.0.2",
|
|
36
37
|
"ajv": "^8.17.1",
|
|
37
38
|
"eslint": "9.12.0",
|
|
38
39
|
"typescript": "5.4.5",
|
|
39
40
|
"vitest": "2.1.2"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
|
-
"@tsed/core": "8.0.
|
|
43
|
-
"@tsed/di": "8.0.
|
|
44
|
-
"@tsed/exceptions": "8.0.
|
|
45
|
-
"@tsed/schema": "8.0.
|
|
43
|
+
"@tsed/core": "8.0.2",
|
|
44
|
+
"@tsed/di": "8.0.2",
|
|
45
|
+
"@tsed/exceptions": "8.0.2",
|
|
46
|
+
"@tsed/schema": "8.0.2",
|
|
46
47
|
"ajv": ">=8.9.0",
|
|
47
48
|
"ajv-errors": ">=3.0.0"
|
|
48
49
|
},
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {StoreSet, useDecorators} from "@tsed/core";
|
|
2
|
+
import {Injectable} from "@tsed/di";
|
|
3
|
+
import {AsyncFormatDefinition, FormatDefinition} from "ajv";
|
|
4
|
+
|
|
5
|
+
export type FormatsOptions = Omit<FormatDefinition<any>, "validate" | "compare"> | Omit<AsyncFormatDefinition<any>, "validate" | "compare">;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a new custom formats validator
|
|
9
|
+
* @param name
|
|
10
|
+
* @param options
|
|
11
|
+
* @decorator
|
|
12
|
+
* @ajv
|
|
13
|
+
*/
|
|
14
|
+
export function Formats(name: string, options: FormatsOptions = {}): ClassDecorator {
|
|
15
|
+
return useDecorators(
|
|
16
|
+
Injectable({
|
|
17
|
+
type: "ajv:formats"
|
|
18
|
+
}),
|
|
19
|
+
StoreSet("ajv:formats", {
|
|
20
|
+
name,
|
|
21
|
+
options
|
|
22
|
+
})
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {StoreSet, useDecorators} from "@tsed/core";
|
|
2
|
+
import {Injectable} from "@tsed/di";
|
|
3
|
+
import {JsonSchema} from "@tsed/schema";
|
|
4
|
+
import {AnySchemaObject, KeywordDefinition} from "ajv";
|
|
5
|
+
|
|
6
|
+
export type KeywordOptions = Partial<Omit<KeywordDefinition, "metaSchema">> & {
|
|
7
|
+
metaSchema?: AnySchemaObject | JsonSchema;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create new keyword custom validator
|
|
12
|
+
* @param options
|
|
13
|
+
* @decorator
|
|
14
|
+
* @ajv
|
|
15
|
+
*/
|
|
16
|
+
export function Keyword(options: KeywordOptions): ClassDecorator {
|
|
17
|
+
return useDecorators(
|
|
18
|
+
Injectable({
|
|
19
|
+
type: "ajv:keyword"
|
|
20
|
+
}),
|
|
21
|
+
StoreSet("ajv:keyword", {
|
|
22
|
+
...options,
|
|
23
|
+
metaSchema: options.metaSchema && options.metaSchema.toJSON ? options.metaSchema.toJSON() : options.metaSchema
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {BadRequest} from "@tsed/exceptions";
|
|
2
|
+
|
|
3
|
+
export class AjvValidationError extends BadRequest {
|
|
4
|
+
public name: string = "AJV_VALIDATION_ERROR";
|
|
5
|
+
public errors: any[];
|
|
6
|
+
|
|
7
|
+
constructor(message: string, errors: any[] = []) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.errors = errors;
|
|
10
|
+
}
|
|
11
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Automatically generated by @tsed/barrels.
|
|
3
|
+
*/
|
|
4
|
+
export * from "./decorators/formats.js";
|
|
5
|
+
export * from "./decorators/keyword.js";
|
|
6
|
+
export * from "./errors/AjvValidationError.js";
|
|
7
|
+
export * from "./interfaces/FormatsMethods.js";
|
|
8
|
+
export * from "./interfaces/IAjvSettings.js";
|
|
9
|
+
export * from "./interfaces/interfaces.js";
|
|
10
|
+
export * from "./interfaces/KeywordMethods.js";
|
|
11
|
+
export * from "./services/Ajv.js";
|
|
12
|
+
export * from "./services/AjvService.js";
|
|
13
|
+
export * from "./utils/defaultErrorFormatter.js";
|
|
14
|
+
export * from "./utils/getPath.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {ErrorObject, Options} from "ajv";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
export interface AjvErrorObject extends ErrorObject {
|
|
7
|
+
modelName: string;
|
|
8
|
+
collectionName?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
export type ErrorFormatter = (error: AjvErrorObject) => string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
export interface IAjvSettings extends Options {
|
|
20
|
+
Ajv?: any;
|
|
21
|
+
errorFormatter?: ErrorFormatter;
|
|
22
|
+
returnsCoercedValues?: boolean;
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {AnySchemaObject, KeywordCxt, SchemaObjCxt, ValidateFunction} from "ajv";
|
|
2
|
+
|
|
3
|
+
export interface AjvDataValidationCtx<T extends string | number = string | number> {
|
|
4
|
+
dataPath: string;
|
|
5
|
+
parentData: {[K in T]: any}; // object or array
|
|
6
|
+
parentDataProperty: T; // string or number
|
|
7
|
+
rootData: Record<string, any> | any[];
|
|
8
|
+
dynamicAnchors: {[Ref in string]?: ValidateFunction};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AjvDataValidateFunction {
|
|
12
|
+
(...args: Parameters<ValidateFunction>): boolean | Promise<any>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AjvSchemaValidateFunction {
|
|
16
|
+
(schema: any, data: any, parentSchema?: AnySchemaObject, dataCxt?: AjvDataValidationCtx): boolean | Promise<any>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface KeywordMethods {
|
|
20
|
+
code?: (cxt: KeywordCxt, ruleType?: string) => void;
|
|
21
|
+
compile?: (schema: any, parentSchema: AnySchemaObject, it: SchemaObjCxt) => AjvDataValidateFunction;
|
|
22
|
+
validate?: AjvSchemaValidateFunction | AjvDataValidateFunction;
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
2
|
+
import {Ajv} from "ajv";
|
|
3
|
+
|
|
4
|
+
describe("Ajv", () => {
|
|
5
|
+
beforeEach(() => PlatformTest.create());
|
|
6
|
+
afterEach(() => PlatformTest.reset());
|
|
7
|
+
it("should create a new Ajv instance", async () => {
|
|
8
|
+
const ajv = await PlatformTest.invoke<Ajv>(Ajv);
|
|
9
|
+
|
|
10
|
+
expect(ajv).toBeInstanceOf(Ajv);
|
|
11
|
+
expect(
|
|
12
|
+
ajv.validate(
|
|
13
|
+
{
|
|
14
|
+
type: "object"
|
|
15
|
+
},
|
|
16
|
+
{}
|
|
17
|
+
)
|
|
18
|
+
).toEqual(true);
|
|
19
|
+
|
|
20
|
+
ajv.validate(
|
|
21
|
+
{
|
|
22
|
+
type: "object"
|
|
23
|
+
},
|
|
24
|
+
[]
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(ajv.errors).toEqual([
|
|
28
|
+
{
|
|
29
|
+
instancePath: "",
|
|
30
|
+
keyword: "type",
|
|
31
|
+
message: "must be object",
|
|
32
|
+
params: {
|
|
33
|
+
type: "object"
|
|
34
|
+
},
|
|
35
|
+
schemaPath: "#/type"
|
|
36
|
+
}
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {cleanObject} from "@tsed/core";
|
|
2
|
+
import {constant, inject, injectable, injector, InjectorService, ProviderScope} from "@tsed/di";
|
|
3
|
+
import {Ajv, Format, KeywordDefinition, Options, Vocabulary} from "ajv";
|
|
4
|
+
import AjvErrors from "ajv-errors";
|
|
5
|
+
import AjvFormats from "ajv-formats";
|
|
6
|
+
|
|
7
|
+
import {FormatsMethods} from "../interfaces/FormatsMethods.js";
|
|
8
|
+
import {IAjvSettings} from "../interfaces/IAjvSettings.js";
|
|
9
|
+
|
|
10
|
+
function getHandler(key: string, service: any) {
|
|
11
|
+
if (service[key]) {
|
|
12
|
+
return service[key].bind(service);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getKeywordProviders() {
|
|
17
|
+
return injector().getProviders("ajv:keyword");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function bindKeywords(): Vocabulary {
|
|
21
|
+
return getKeywordProviders().map((provider) => {
|
|
22
|
+
const options = provider.store.get<Omit<KeywordDefinition, "compile">>("ajv:keyword", {})!;
|
|
23
|
+
const service = inject(provider.token);
|
|
24
|
+
|
|
25
|
+
return <KeywordDefinition>cleanObject({
|
|
26
|
+
coerceTypes: "array",
|
|
27
|
+
...options,
|
|
28
|
+
validate: getHandler("validate", service),
|
|
29
|
+
compile: getHandler("compile", service),
|
|
30
|
+
code: getHandler("code", service),
|
|
31
|
+
macro: getHandler("macro", service)
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getFormatsProviders() {
|
|
37
|
+
return injector().getProviders("ajv:formats");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getFormats(): {name: string; options: Format}[] {
|
|
41
|
+
return getFormatsProviders().map((provider) => {
|
|
42
|
+
const {name, options} = provider.store.get<any>("ajv:formats", {})!;
|
|
43
|
+
const service = inject<FormatsMethods<any>>(provider.token);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
name,
|
|
47
|
+
options: {
|
|
48
|
+
...options,
|
|
49
|
+
validate: service.validate.bind(service),
|
|
50
|
+
compare: service.compare?.bind(service)
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
injectable(Ajv)
|
|
57
|
+
.scope(ProviderScope.SINGLETON)
|
|
58
|
+
.factory(() => {
|
|
59
|
+
const {errorFormatter, keywords = [], ...props} = constant<IAjvSettings>("ajv") || {};
|
|
60
|
+
const options: Options = {
|
|
61
|
+
verbose: false,
|
|
62
|
+
coerceTypes: true,
|
|
63
|
+
strict: false,
|
|
64
|
+
keywords: [...keywords, ...bindKeywords()],
|
|
65
|
+
discriminator: true,
|
|
66
|
+
allErrors: true,
|
|
67
|
+
...props
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
const ajv = new Ajv(options);
|
|
72
|
+
|
|
73
|
+
// add support for custom error messages
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
AjvErrors(ajv);
|
|
76
|
+
// @ts-ignore
|
|
77
|
+
AjvFormats(ajv as any);
|
|
78
|
+
|
|
79
|
+
getFormats().forEach(({name, options}) => {
|
|
80
|
+
ajv.addFormat(name, options);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return ajv;
|
|
84
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {catchAsyncError} from "@tsed/core";
|
|
2
|
+
import {PlatformTest} from "@tsed/platform-http/testing";
|
|
3
|
+
import {object, string} from "@tsed/schema";
|
|
4
|
+
|
|
5
|
+
import {AjvService} from "./AjvService.js";
|
|
6
|
+
|
|
7
|
+
describe("AjvService", () => {
|
|
8
|
+
beforeEach(() => PlatformTest.create());
|
|
9
|
+
afterEach(() => PlatformTest.reset());
|
|
10
|
+
|
|
11
|
+
it("should use the function api as schema", async () => {
|
|
12
|
+
const ajvService = PlatformTest.get<AjvService>(AjvService);
|
|
13
|
+
|
|
14
|
+
const error: any = await catchAsyncError(() => ajvService.validate("test", string().minLength(5)));
|
|
15
|
+
expect(error.errors).toEqual([
|
|
16
|
+
{
|
|
17
|
+
data: "test",
|
|
18
|
+
dataPath: "",
|
|
19
|
+
instancePath: "",
|
|
20
|
+
keyword: "minLength",
|
|
21
|
+
message: "must NOT have fewer than 5 characters",
|
|
22
|
+
params: {
|
|
23
|
+
limit: 5
|
|
24
|
+
},
|
|
25
|
+
schemaPath: "#/minLength"
|
|
26
|
+
}
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should use the function api as schema (required)", async () => {
|
|
31
|
+
const ajvService = PlatformTest.get<AjvService>(AjvService);
|
|
32
|
+
|
|
33
|
+
const error: any = await catchAsyncError(() => ajvService.validate({}, object({test: string().required()})));
|
|
34
|
+
expect(error.errors).toEqual([
|
|
35
|
+
{
|
|
36
|
+
dataPath: ".test",
|
|
37
|
+
instancePath: "",
|
|
38
|
+
keyword: "required",
|
|
39
|
+
message: "must have required property 'test'",
|
|
40
|
+
params: {
|
|
41
|
+
missingProperty: "test"
|
|
42
|
+
},
|
|
43
|
+
schemaPath: "#/required"
|
|
44
|
+
}
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import "./Ajv.js";
|
|
2
|
+
|
|
3
|
+
import {deepClone, getValue, nameOf, prototypeOf, setValue, Type} from "@tsed/core";
|
|
4
|
+
import {constant, inject, injectable} from "@tsed/di";
|
|
5
|
+
import {getJsonSchema, JsonEntityStore, JsonSchema, JsonSchemaObject} from "@tsed/schema";
|
|
6
|
+
import {Ajv, ErrorObject} from "ajv";
|
|
7
|
+
|
|
8
|
+
import {AjvValidationError} from "../errors/AjvValidationError.js";
|
|
9
|
+
import {AjvErrorObject, ErrorFormatter} from "../interfaces/IAjvSettings.js";
|
|
10
|
+
import {defaultErrorFormatter} from "../utils/defaultErrorFormatter.js";
|
|
11
|
+
import {getPath} from "../utils/getPath.js";
|
|
12
|
+
|
|
13
|
+
export interface AjvValidateOptions extends Record<string, any> {
|
|
14
|
+
schema?: JsonSchema | Partial<JsonSchemaObject>;
|
|
15
|
+
type?: Type<any> | any;
|
|
16
|
+
collectionType?: Type<any> | any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class AjvService {
|
|
20
|
+
readonly name = "ajv";
|
|
21
|
+
protected errorFormatter = constant<ErrorFormatter>("ajv.errorFormatter", defaultErrorFormatter);
|
|
22
|
+
protected returnsCoercedValues = constant<boolean>("ajv.returnsCoercedValues");
|
|
23
|
+
protected ajv = inject(Ajv);
|
|
24
|
+
|
|
25
|
+
async validate(value: any, options: AjvValidateOptions | JsonSchema): Promise<any> {
|
|
26
|
+
let {schema: defaultSchema, type, collectionType, ...additionalOptions} = this.mapOptions(options);
|
|
27
|
+
|
|
28
|
+
const schema = defaultSchema || getJsonSchema(type, {...additionalOptions, customKeys: true});
|
|
29
|
+
|
|
30
|
+
if (schema) {
|
|
31
|
+
const localValue = this.returnsCoercedValues ? value : deepClone(value);
|
|
32
|
+
const validate = this.ajv.compile(schema);
|
|
33
|
+
|
|
34
|
+
const valid = await validate(localValue);
|
|
35
|
+
const {errors} = validate;
|
|
36
|
+
|
|
37
|
+
if (!valid && errors) {
|
|
38
|
+
throw this.mapErrors(errors, {
|
|
39
|
+
type,
|
|
40
|
+
collectionType,
|
|
41
|
+
async: true,
|
|
42
|
+
value: localValue
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.returnsCoercedValues) {
|
|
47
|
+
return localValue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected mapOptions(options: AjvValidateOptions | JsonSchema): AjvValidateOptions {
|
|
55
|
+
if (options instanceof JsonSchema) {
|
|
56
|
+
return {
|
|
57
|
+
schema: options.toJSON({customKeys: true})
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return options;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected mapErrors(errors: ErrorObject[], options: any) {
|
|
65
|
+
const {type, collectionType, value} = options;
|
|
66
|
+
|
|
67
|
+
const message = errors
|
|
68
|
+
.map((error: AjvErrorObject) => {
|
|
69
|
+
if (collectionType) {
|
|
70
|
+
error.collectionName = nameOf(collectionType);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const dataPath = getPath(error);
|
|
74
|
+
|
|
75
|
+
if (!error.data) {
|
|
76
|
+
if (dataPath) {
|
|
77
|
+
error.data = getValue(value, dataPath.replace(/^\./, ""));
|
|
78
|
+
} else if (error.schemaPath !== "#/required") {
|
|
79
|
+
error.data = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (dataPath && dataPath.match(/pwd|password|mdp|secret/)) {
|
|
84
|
+
error.data = "[REDACTED]";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (type) {
|
|
88
|
+
error.modelName = nameOf(type);
|
|
89
|
+
error.message = this.mapClassError(error, type);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.errorFormatter.call(this, error, {});
|
|
93
|
+
})
|
|
94
|
+
.join("\n");
|
|
95
|
+
|
|
96
|
+
return new AjvValidationError(message, errors);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected mapClassError(error: AjvErrorObject, targetType: Type<any>) {
|
|
100
|
+
const propertyKey = getValue(error, "params.missingProperty");
|
|
101
|
+
|
|
102
|
+
if (propertyKey) {
|
|
103
|
+
const store = JsonEntityStore.from<JsonEntityStore>(prototypeOf(targetType), propertyKey);
|
|
104
|
+
|
|
105
|
+
if (store) {
|
|
106
|
+
setValue(error, "params.missingProperty", store.name || propertyKey);
|
|
107
|
+
|
|
108
|
+
return error.message?.replace(`'${propertyKey}'`, `'${store.name || propertyKey}'`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return error.message;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
injectable(AjvService).type("validator:service");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {defaultErrorFormatter} from "./defaultErrorFormatter.js";
|
|
2
|
+
|
|
3
|
+
describe("defaultErrorFormatter", () => {
|
|
4
|
+
it("should extract data and format error (data = undefined)", () => {
|
|
5
|
+
expect(
|
|
6
|
+
defaultErrorFormatter({
|
|
7
|
+
keyword: "maximum",
|
|
8
|
+
dataPath: "/length",
|
|
9
|
+
schemaPath: "#/properties/length/maximum",
|
|
10
|
+
params: {comparison: "<=", limit: 250},
|
|
11
|
+
message: "should be <= 250",
|
|
12
|
+
data: undefined,
|
|
13
|
+
modelName: "RandomStringModel"
|
|
14
|
+
} as any)
|
|
15
|
+
).toEqual('RandomStringModel.length should be <= 250. Given value: "undefined"');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should extract data and format error (modelName = '' & collectionName)", () => {
|
|
19
|
+
expect(
|
|
20
|
+
defaultErrorFormatter({
|
|
21
|
+
keyword: "maximum",
|
|
22
|
+
dataPath: "/0",
|
|
23
|
+
schemaPath: "#/properties/length/maximum",
|
|
24
|
+
params: {comparison: "<=", limit: 250},
|
|
25
|
+
message: "should be <= 250",
|
|
26
|
+
data: undefined,
|
|
27
|
+
modelName: undefined,
|
|
28
|
+
collectionName: "Array"
|
|
29
|
+
} as any)
|
|
30
|
+
).toEqual('[0] should be <= 250. Given value: "undefined"');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should extract data and format error (modelName = '')", () => {
|
|
34
|
+
expect(
|
|
35
|
+
defaultErrorFormatter({
|
|
36
|
+
keyword: "maximum",
|
|
37
|
+
dataPath: "/length",
|
|
38
|
+
schemaPath: "#/properties/length/maximum",
|
|
39
|
+
params: {comparison: "<=", limit: 250},
|
|
40
|
+
message: "should be <= 250",
|
|
41
|
+
data: undefined,
|
|
42
|
+
modelName: undefined
|
|
43
|
+
} as any)
|
|
44
|
+
).toEqual('Value.length should be <= 250. Given value: "undefined"');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should extract data and format error", () => {
|
|
48
|
+
expect(
|
|
49
|
+
defaultErrorFormatter({
|
|
50
|
+
keyword: "maximum",
|
|
51
|
+
dataPath: "/length",
|
|
52
|
+
schemaPath: "#/properties/length/maximum",
|
|
53
|
+
params: {comparison: "<=", limit: 250},
|
|
54
|
+
message: "should be <= 250",
|
|
55
|
+
data: 251,
|
|
56
|
+
modelName: "RandomStringModel"
|
|
57
|
+
} as any)
|
|
58
|
+
).toEqual("RandomStringModel.length should be <= 250. Given value: 251");
|
|
59
|
+
});
|
|
60
|
+
it("should extract data and format error(2)", () => {
|
|
61
|
+
expect(
|
|
62
|
+
defaultErrorFormatter({
|
|
63
|
+
instancePath: "/length",
|
|
64
|
+
schemaPath: "#/properties/length/maximum",
|
|
65
|
+
keyword: "maximum",
|
|
66
|
+
params: {comparison: "<=", limit: 250},
|
|
67
|
+
message: "must be <= 250",
|
|
68
|
+
data: {length: 251},
|
|
69
|
+
modelName: "RandomStringModel"
|
|
70
|
+
} as any)
|
|
71
|
+
).toEqual('RandomStringModel.length must be <= 250. Given value: {"length":251}');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {AjvErrorObject} from "../interfaces/IAjvSettings.js";
|
|
2
|
+
import {getInstancePath, getPath} from "./getPath.js";
|
|
3
|
+
|
|
4
|
+
export function defaultErrorFormatter(error: AjvErrorObject & {dataPath: string}) {
|
|
5
|
+
const value = JSON.stringify(error.data === undefined ? "undefined" : error.data);
|
|
6
|
+
const join = (list: any[]): string => list.filter(Boolean).join("").trim();
|
|
7
|
+
|
|
8
|
+
const [, indexPath, ...paths] = getInstancePath(error).split(".");
|
|
9
|
+
const deepPaths = paths.length ? "." + paths.join(".") : "";
|
|
10
|
+
|
|
11
|
+
error.dataPath = getPath(error);
|
|
12
|
+
|
|
13
|
+
if (error.collectionName) {
|
|
14
|
+
const modelName = error.modelName || "";
|
|
15
|
+
|
|
16
|
+
switch (error.collectionName) {
|
|
17
|
+
case "Array":
|
|
18
|
+
return join([`${modelName}[${indexPath}]${deepPaths}`, ` ${error.message}. Given value: ${value}`]);
|
|
19
|
+
case "Map":
|
|
20
|
+
return join([`Map<${indexPath}, ${modelName}>${deepPaths}`, ` ${error.message}. Given value: ${value}`]);
|
|
21
|
+
case "Set":
|
|
22
|
+
return join([`Set<${indexPath}, ${modelName}>${deepPaths}`, ` ${error.message}. Given value: ${value}`]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return join([
|
|
27
|
+
!error.modelName && "Value",
|
|
28
|
+
`${error.modelName || ""}`,
|
|
29
|
+
(!error?.params?.missingProperty && error.dataPath) ||
|
|
30
|
+
(error?.params?.missingProperty && (error.instancePath || "").replace(/\//g, ".")),
|
|
31
|
+
` ${error.message}. Given value: ${value}`
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {getValue} from "@tsed/core";
|
|
2
|
+
|
|
3
|
+
export function getPath(error: any) {
|
|
4
|
+
if (error.params && error.params.missingProperty) {
|
|
5
|
+
return `.${error.params.missingProperty}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return getInstancePath(error);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getInstancePath(error: any) {
|
|
12
|
+
return getValue(error, "dataPath", getValue(error, "instancePath", "")).replace(/\//gi, ".");
|
|
13
|
+
}
|