perfect-payload 1.8.0-beta.0 → 2.0.0-beta.1
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/dist/express.d.ts +16 -0
- package/dist/express.js +63 -0
- package/dist/fastify.d.ts +15 -0
- package/dist/fastify.js +59 -0
- package/dist/framework.d.ts +4 -0
- package/dist/framework.js +129 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +1238 -0
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/errors.js +1 -0
- package/dist/types/framework.d.ts +26 -0
- package/dist/types/framework.js +1 -0
- package/dist/types/options.d.ts +14 -0
- package/dist/types/options.js +1 -0
- package/dist/types/results.d.ts +19 -0
- package/dist/types/results.js +1 -0
- package/dist/types/rules.d.ts +49 -0
- package/dist/types/rules.js +1 -0
- package/package.json +25 -11
- package/express.js +0 -75
- package/fastify.js +0 -67
- package/framework.js +0 -175
- package/index.js +0 -1861
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ValidationRules } from "./rules.js";
|
|
2
|
+
import type { PerfectPayloadOptions } from "./options.js";
|
|
3
|
+
import type { ValidationError } from "./errors.js";
|
|
4
|
+
export type RequestSource = "headers" | "params" | "query" | "body";
|
|
5
|
+
export type FrameworkRule = Partial<Record<RequestSource, ValidationRules>>;
|
|
6
|
+
export type FrameworkData = Partial<Record<RequestSource, Record<string, unknown>>>;
|
|
7
|
+
export interface FrameworkConfig {
|
|
8
|
+
rule?: FrameworkRule;
|
|
9
|
+
options?: PerfectPayloadOptions;
|
|
10
|
+
}
|
|
11
|
+
export interface FrameworkValidationInput extends FrameworkConfig {
|
|
12
|
+
data?: FrameworkData;
|
|
13
|
+
}
|
|
14
|
+
export type FrameworkValidatedPayload = Partial<Record<RequestSource, Record<string, unknown>>>;
|
|
15
|
+
export interface FrameworkValidationSuccess {
|
|
16
|
+
valid: true;
|
|
17
|
+
validatedPayload: FrameworkValidatedPayload;
|
|
18
|
+
}
|
|
19
|
+
export interface FrameworkValidationFailure {
|
|
20
|
+
statusCode?: number;
|
|
21
|
+
valid: false;
|
|
22
|
+
message?: string;
|
|
23
|
+
errors: Array<string | ValidationError>;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
export type FrameworkValidationResult = FrameworkValidationSuccess | FrameworkValidationFailure;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InvalidPayloadResponse, ValidPayloadResponse } from "./results.js";
|
|
2
|
+
export type UnknownFieldsMode = "strip" | "allow" | "reject";
|
|
3
|
+
export interface PerfectPayloadOptions {
|
|
4
|
+
unknownFields?: UnknownFieldsMode;
|
|
5
|
+
prettyErrors?: boolean;
|
|
6
|
+
validPayloadResponse?: ValidPayloadResponse;
|
|
7
|
+
inValidPayloadResponse?: InvalidPayloadResponse;
|
|
8
|
+
}
|
|
9
|
+
export interface StructuredErrorOptions extends PerfectPayloadOptions {
|
|
10
|
+
prettyErrors?: false;
|
|
11
|
+
}
|
|
12
|
+
export interface PrettyErrorOptions extends PerfectPayloadOptions {
|
|
13
|
+
prettyErrors: true;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ValidationError } from "./errors.js";
|
|
2
|
+
export interface ValidPayloadResponse {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
valid: true;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface InvalidPayloadResponse {
|
|
8
|
+
statusCode: number;
|
|
9
|
+
valid: false;
|
|
10
|
+
message: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface ValidationSuccess<T = Record<string, unknown>> extends ValidPayloadResponse {
|
|
14
|
+
validatedPayload: T;
|
|
15
|
+
}
|
|
16
|
+
export interface ValidationFailure<E = ValidationError> extends InvalidPayloadResponse {
|
|
17
|
+
errors: E[];
|
|
18
|
+
}
|
|
19
|
+
export type ValidationResult<T = Record<string, unknown>, E = ValidationError> = ValidationSuccess<T> | ValidationFailure<E>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type ValidationValueType = "number" | "string" | "boolean" | "email" | "url" | "enum" | "uuid" | "uuidv1" | "uuidv3" | "uuidv4" | "uuidv5" | "objectId" | "array" | "object";
|
|
2
|
+
export type TransformFunction = (value: unknown, payload: Record<string, unknown>) => unknown;
|
|
3
|
+
export type CustomValidator = (value: unknown, payload: Record<string, unknown>) => boolean | Promise<boolean>;
|
|
4
|
+
export interface DependencyRule {
|
|
5
|
+
setDependencyRule: (value: unknown, dependencyValue: unknown) => AttributeValidationRules;
|
|
6
|
+
}
|
|
7
|
+
export type DependencyRules = Record<string, DependencyRule>;
|
|
8
|
+
export interface AttributeValidationRules {
|
|
9
|
+
mandatory?: boolean;
|
|
10
|
+
allowNull?: boolean;
|
|
11
|
+
allowEmptyObject?: boolean;
|
|
12
|
+
allowEmptyArray?: boolean;
|
|
13
|
+
type?: ValidationValueType;
|
|
14
|
+
enumValues?: unknown[];
|
|
15
|
+
path?: string;
|
|
16
|
+
mandatoryError?: string;
|
|
17
|
+
allowNullError?: string;
|
|
18
|
+
emptyObjectError?: string;
|
|
19
|
+
emptyArrayError?: string;
|
|
20
|
+
elementConstraintsError?: string;
|
|
21
|
+
regexError?: string;
|
|
22
|
+
typeError?: string;
|
|
23
|
+
minLengthError?: string;
|
|
24
|
+
maxLengthError?: string;
|
|
25
|
+
preventDecimalError?: string;
|
|
26
|
+
minError?: string;
|
|
27
|
+
maxError?: string;
|
|
28
|
+
rangeError?: string;
|
|
29
|
+
customValidatorError?: string;
|
|
30
|
+
customValidatorCode?: string;
|
|
31
|
+
minLength?: number;
|
|
32
|
+
maxLength?: number;
|
|
33
|
+
min?: number;
|
|
34
|
+
max?: number;
|
|
35
|
+
range?: string;
|
|
36
|
+
preventDecimal?: boolean;
|
|
37
|
+
minItems?: number;
|
|
38
|
+
maxItems?: number;
|
|
39
|
+
regex?: RegExp;
|
|
40
|
+
objectAttr?: ValidationRules;
|
|
41
|
+
elementConstraints?: AttributeValidationRules;
|
|
42
|
+
dependency?: DependencyRules;
|
|
43
|
+
customValidator?: CustomValidator;
|
|
44
|
+
transform?: TransformFunction;
|
|
45
|
+
trim?: boolean;
|
|
46
|
+
lowercase?: boolean;
|
|
47
|
+
uppercase?: boolean;
|
|
48
|
+
}
|
|
49
|
+
export type ValidationRules = Record<string, AttributeValidationRules>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perfect-payload",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lightweight JSON payload validation library with structured errors, nested validation, field paths, and customizable validation rules.",
|
|
6
|
-
"main": "index.js",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./express": {
|
|
14
|
+
"types": "./dist/express.d.ts",
|
|
15
|
+
"import": "./dist/express.js"
|
|
16
|
+
},
|
|
17
|
+
"./fastify": {
|
|
18
|
+
"types": "./dist/fastify.d.ts",
|
|
19
|
+
"import": "./dist/fastify.js"
|
|
20
|
+
}
|
|
11
21
|
},
|
|
12
22
|
"scripts": {
|
|
13
|
-
"test": "jest"
|
|
23
|
+
"test": "jest",
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"test:types": "tsc type-test.ts --noEmit --strict --module NodeNext --moduleResolution NodeNext --target ES2022 --ignoreConfig --skipLibCheck"
|
|
14
26
|
},
|
|
15
27
|
"files": [
|
|
16
|
-
"
|
|
17
|
-
"framework.js",
|
|
18
|
-
"express.js",
|
|
19
|
-
"fastify.js"
|
|
28
|
+
"dist"
|
|
20
29
|
],
|
|
21
30
|
"jest": {
|
|
22
31
|
"testPathIgnorePatterns": [
|
|
@@ -56,7 +65,12 @@
|
|
|
56
65
|
"devDependencies": {
|
|
57
66
|
"@babel/core": "^7.25.2",
|
|
58
67
|
"@babel/preset-env": "^7.25.4",
|
|
68
|
+
"@types/express": "^5.0.6",
|
|
69
|
+
"@types/node": "^26.6.2",
|
|
59
70
|
"babel-jest": "^29.7.0",
|
|
60
|
-
"
|
|
71
|
+
"express": "^5.2.1",
|
|
72
|
+
"fastify": "^5.12.5",
|
|
73
|
+
"jest": "^29.7.0",
|
|
74
|
+
"typescript": "^7.0.2"
|
|
61
75
|
}
|
|
62
76
|
}
|
package/express.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
validateFrameworkConfig,
|
|
3
|
-
validateFrameworkSources,
|
|
4
|
-
validateFrameworkSourcesAsync,
|
|
5
|
-
} from "./framework.js";
|
|
6
|
-
|
|
7
|
-
export function validatePayload(config = {}) {
|
|
8
|
-
const { rule, options = {} } = config;
|
|
9
|
-
|
|
10
|
-
// Validate developer configuration when middleware is created,
|
|
11
|
-
// not on every incoming request.
|
|
12
|
-
validateFrameworkConfig({
|
|
13
|
-
rule,
|
|
14
|
-
options,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
return function perfectPayloadExpressMiddleware(req, res, next) {
|
|
18
|
-
try {
|
|
19
|
-
const result = validateFrameworkSources({
|
|
20
|
-
data: {
|
|
21
|
-
headers: req.headers,
|
|
22
|
-
params: req.params,
|
|
23
|
-
query: req.query,
|
|
24
|
-
body: req.body,
|
|
25
|
-
},
|
|
26
|
-
rule,
|
|
27
|
-
options,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
if (result.valid === true) {
|
|
31
|
-
req.validatedPayload = result.validatedPayload;
|
|
32
|
-
return next();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return res.status(result.statusCode ?? 400).json(result);
|
|
36
|
-
} catch (error) {
|
|
37
|
-
return next(error);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function validatePayloadAsync(config = {}) {
|
|
43
|
-
const { rule, options = {} } = config;
|
|
44
|
-
|
|
45
|
-
// Validate developer configuration when middleware is created,
|
|
46
|
-
// not on every incoming request.
|
|
47
|
-
validateFrameworkConfig({
|
|
48
|
-
rule,
|
|
49
|
-
options,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
return async function perfectPayloadExpressAsyncMiddleware(req, res, next) {
|
|
53
|
-
try {
|
|
54
|
-
const result = await validateFrameworkSourcesAsync({
|
|
55
|
-
data: {
|
|
56
|
-
headers: req.headers,
|
|
57
|
-
params: req.params,
|
|
58
|
-
query: req.query,
|
|
59
|
-
body: req.body,
|
|
60
|
-
},
|
|
61
|
-
rule,
|
|
62
|
-
options,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
if (result.valid === true) {
|
|
66
|
-
req.validatedPayload = result.validatedPayload;
|
|
67
|
-
return next();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return res.status(result.statusCode ?? 400).json(result);
|
|
71
|
-
} catch (error) {
|
|
72
|
-
return next(error);
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
}
|
package/fastify.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
validateFrameworkConfig,
|
|
3
|
-
validateFrameworkSources,
|
|
4
|
-
validateFrameworkSourcesAsync,
|
|
5
|
-
} from "./framework.js";
|
|
6
|
-
|
|
7
|
-
export function validatePayload(config = {}) {
|
|
8
|
-
const { rule, options = {} } = config;
|
|
9
|
-
|
|
10
|
-
// Validate developer configuration when the hook is created,
|
|
11
|
-
// not on every incoming request.
|
|
12
|
-
validateFrameworkConfig({
|
|
13
|
-
rule,
|
|
14
|
-
options,
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
return async function perfectPayloadFastifyHook(request, reply) {
|
|
18
|
-
const result = validateFrameworkSources({
|
|
19
|
-
data: {
|
|
20
|
-
headers: request.headers,
|
|
21
|
-
params: request.params,
|
|
22
|
-
query: request.query,
|
|
23
|
-
body: request.body,
|
|
24
|
-
},
|
|
25
|
-
rule,
|
|
26
|
-
options,
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (result.valid === true) {
|
|
30
|
-
request.validatedPayload = result.validatedPayload;
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return reply.code(result.statusCode ?? 400).send(result);
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function validatePayloadAsync(config = {}) {
|
|
39
|
-
const { rule, options = {} } = config;
|
|
40
|
-
|
|
41
|
-
// Validate developer configuration when the hook is created,
|
|
42
|
-
// not on every incoming request.
|
|
43
|
-
validateFrameworkConfig({
|
|
44
|
-
rule,
|
|
45
|
-
options,
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
return async function perfectPayloadFastifyAsyncHook(request, reply) {
|
|
49
|
-
const result = await validateFrameworkSourcesAsync({
|
|
50
|
-
data: {
|
|
51
|
-
headers: request.headers,
|
|
52
|
-
params: request.params,
|
|
53
|
-
query: request.query,
|
|
54
|
-
body: request.body,
|
|
55
|
-
},
|
|
56
|
-
rule,
|
|
57
|
-
options,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
if (result.valid === true) {
|
|
61
|
-
request.validatedPayload = result.validatedPayload;
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return reply.code(result.statusCode ?? 400).send(result);
|
|
66
|
-
};
|
|
67
|
-
}
|
package/framework.js
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { perfectPayload, perfectPayloadAsync } from "./index.js";
|
|
2
|
-
|
|
3
|
-
const SUPPORTED_REQUEST_SOURCES = ["headers", "params", "query", "body"];
|
|
4
|
-
|
|
5
|
-
export function validateFrameworkConfig({ rule, options } = {}) {
|
|
6
|
-
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
7
|
-
throw new Error("perfect-payload:- framework rule must be an object");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const configuredSources = Object.keys(rule);
|
|
11
|
-
|
|
12
|
-
if (configuredSources.length === 0) {
|
|
13
|
-
throw new Error(
|
|
14
|
-
"perfect-payload:- framework rule must contain at least one request source",
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
for (const source of configuredSources) {
|
|
19
|
-
if (!SUPPORTED_REQUEST_SOURCES.includes(source)) {
|
|
20
|
-
throw new Error(`perfect-payload:- unsupported request source ${source}`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const sourceRule = rule[source];
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
!sourceRule ||
|
|
27
|
-
typeof sourceRule !== "object" ||
|
|
28
|
-
Array.isArray(sourceRule)
|
|
29
|
-
) {
|
|
30
|
-
throw new Error(
|
|
31
|
-
`perfect-payload:- rule for request source ${source} must be an object`,
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
options !== undefined &&
|
|
38
|
-
(options === null || typeof options !== "object" || Array.isArray(options))
|
|
39
|
-
) {
|
|
40
|
-
throw new Error("perfect-payload:- framework options must be an object");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return SUPPORTED_REQUEST_SOURCES.filter((source) =>
|
|
44
|
-
Object.prototype.hasOwnProperty.call(rule, source),
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function validateFrameworkSources({
|
|
49
|
-
data = {},
|
|
50
|
-
rule,
|
|
51
|
-
options = {},
|
|
52
|
-
} = {}) {
|
|
53
|
-
const configuredSources = validateFrameworkConfig({
|
|
54
|
-
rule,
|
|
55
|
-
options,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const validatedPayload = {};
|
|
59
|
-
const errors = [];
|
|
60
|
-
|
|
61
|
-
let invalidResponse = null;
|
|
62
|
-
|
|
63
|
-
for (const source of configuredSources) {
|
|
64
|
-
const sourceData =
|
|
65
|
-
data?.[source] !== undefined && data?.[source] !== null
|
|
66
|
-
? data[source]
|
|
67
|
-
: {};
|
|
68
|
-
|
|
69
|
-
const result = perfectPayload(sourceData, rule[source], options);
|
|
70
|
-
|
|
71
|
-
if (result.valid === true) {
|
|
72
|
-
validatedPayload[source] = result.validatedPayload;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (!invalidResponse) {
|
|
77
|
-
const { errors: _errors, ...response } = result;
|
|
78
|
-
invalidResponse = response;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
for (const error of result.errors ?? []) {
|
|
82
|
-
if (typeof error === "string") {
|
|
83
|
-
errors.push(error);
|
|
84
|
-
} else {
|
|
85
|
-
errors.push({
|
|
86
|
-
...error,
|
|
87
|
-
path: `${source}.${error.path}`,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (errors.length > 0) {
|
|
94
|
-
return {
|
|
95
|
-
...invalidResponse,
|
|
96
|
-
errors,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
valid: true,
|
|
102
|
-
validatedPayload,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function validateFrameworkSourcesAsync({
|
|
107
|
-
data = {},
|
|
108
|
-
rule,
|
|
109
|
-
options = {},
|
|
110
|
-
} = {}) {
|
|
111
|
-
const configuredSources = validateFrameworkConfig({
|
|
112
|
-
rule,
|
|
113
|
-
options,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const results = await Promise.all(
|
|
117
|
-
configuredSources.map(async (source) => {
|
|
118
|
-
const sourceData =
|
|
119
|
-
data?.[source] !== undefined && data?.[source] !== null
|
|
120
|
-
? data[source]
|
|
121
|
-
: {};
|
|
122
|
-
|
|
123
|
-
const result = await perfectPayloadAsync(
|
|
124
|
-
sourceData,
|
|
125
|
-
rule[source],
|
|
126
|
-
options,
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
source,
|
|
131
|
-
result,
|
|
132
|
-
};
|
|
133
|
-
}),
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const validatedPayload = {};
|
|
137
|
-
const errors = [];
|
|
138
|
-
|
|
139
|
-
let invalidResponse = null;
|
|
140
|
-
|
|
141
|
-
for (const { source, result } of results) {
|
|
142
|
-
if (result.valid === true) {
|
|
143
|
-
validatedPayload[source] = result.validatedPayload;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (!invalidResponse) {
|
|
148
|
-
const { errors: _errors, ...response } = result;
|
|
149
|
-
invalidResponse = response;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
for (const error of result.errors ?? []) {
|
|
153
|
-
if (typeof error === "string") {
|
|
154
|
-
errors.push(error);
|
|
155
|
-
} else {
|
|
156
|
-
errors.push({
|
|
157
|
-
...error,
|
|
158
|
-
path: `${source}.${error.path}`,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (errors.length > 0) {
|
|
165
|
-
return {
|
|
166
|
-
...invalidResponse,
|
|
167
|
-
errors,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
valid: true,
|
|
173
|
-
validatedPayload,
|
|
174
|
-
};
|
|
175
|
-
}
|