perfect-payload 1.7.0 → 1.8.0
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/README.md +903 -337
- package/express.js +75 -0
- package/fastify.js +67 -0
- package/framework.js +175 -0
- package/index.js +38 -15
- package/package.json +10 -2
package/express.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
}
|
package/index.js
CHANGED
|
@@ -526,7 +526,7 @@ export function perfectPayload(
|
|
|
526
526
|
) {
|
|
527
527
|
const {
|
|
528
528
|
unknownFields = "strip",
|
|
529
|
-
|
|
529
|
+
prettyErrors = false,
|
|
530
530
|
validPayloadResponse = {
|
|
531
531
|
statusCode: 200,
|
|
532
532
|
valid: true,
|
|
@@ -544,8 +544,10 @@ export function perfectPayload(
|
|
|
544
544
|
`perfect-payload:- unknownFields must be one of strip, allow, reject`,
|
|
545
545
|
);
|
|
546
546
|
}
|
|
547
|
-
|
|
548
|
-
|
|
547
|
+
if (typeof prettyErrors !== "boolean") {
|
|
548
|
+
throw new Error(`perfect-payload:- prettyErrors must be a boolean`);
|
|
549
|
+
}
|
|
550
|
+
const validationResult = perfectPayloadStructured(
|
|
549
551
|
data,
|
|
550
552
|
dataValidationRule,
|
|
551
553
|
validPayloadResponse,
|
|
@@ -555,6 +557,15 @@ export function perfectPayload(
|
|
|
555
557
|
unknownFields,
|
|
556
558
|
},
|
|
557
559
|
);
|
|
560
|
+
|
|
561
|
+
if (prettyErrors && validationResult?.errors?.length) {
|
|
562
|
+
return {
|
|
563
|
+
...validationResult,
|
|
564
|
+
errors: validationResult.errors.map((error) => error.message),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return validationResult;
|
|
558
569
|
}
|
|
559
570
|
|
|
560
571
|
export async function perfectPayloadAsync(
|
|
@@ -574,6 +585,7 @@ export async function perfectPayloadAsync(
|
|
|
574
585
|
message: "One or more attribute values are invalid",
|
|
575
586
|
},
|
|
576
587
|
unknownFields = "strip",
|
|
588
|
+
prettyErrors = false,
|
|
577
589
|
} = options ?? {};
|
|
578
590
|
|
|
579
591
|
if (!["strip", "allow", "reject"].includes(unknownFields)) {
|
|
@@ -581,7 +593,9 @@ export async function perfectPayloadAsync(
|
|
|
581
593
|
`perfect-payload:- unknownFields must be one of strip, allow, reject`,
|
|
582
594
|
);
|
|
583
595
|
}
|
|
584
|
-
|
|
596
|
+
if (typeof prettyErrors !== "boolean") {
|
|
597
|
+
throw new Error(`perfect-payload:- prettyErrors must be a boolean`);
|
|
598
|
+
}
|
|
585
599
|
const validationResult = perfectPayloadStructured(
|
|
586
600
|
data,
|
|
587
601
|
dataValidationRule,
|
|
@@ -595,6 +609,13 @@ export async function perfectPayloadAsync(
|
|
|
595
609
|
);
|
|
596
610
|
|
|
597
611
|
if (validationResult?.errors?.length) {
|
|
612
|
+
if (prettyErrors) {
|
|
613
|
+
return {
|
|
614
|
+
...validationResult,
|
|
615
|
+
errors: validationResult.errors.map((error) => error.message),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
598
619
|
return validationResult;
|
|
599
620
|
}
|
|
600
621
|
|
|
@@ -606,7 +627,9 @@ export async function perfectPayloadAsync(
|
|
|
606
627
|
if (rowErrors.length > 0) {
|
|
607
628
|
return {
|
|
608
629
|
...inValidPayloadResponse,
|
|
609
|
-
errors:
|
|
630
|
+
errors: prettyErrors
|
|
631
|
+
? rowErrors.map((error) => error.message)
|
|
632
|
+
: rowErrors,
|
|
610
633
|
};
|
|
611
634
|
}
|
|
612
635
|
|
|
@@ -1050,7 +1073,7 @@ function perfectPayloadStructured(
|
|
|
1050
1073
|
attributePath,
|
|
1051
1074
|
"INVALID_URL",
|
|
1052
1075
|
attributeRules?.["typeError"] ||
|
|
1053
|
-
`Invalid URL format
|
|
1076
|
+
`Invalid URL format for attribute ${attributePath}`,
|
|
1054
1077
|
);
|
|
1055
1078
|
|
|
1056
1079
|
addNextError = false;
|
|
@@ -1067,7 +1090,7 @@ function perfectPayloadStructured(
|
|
|
1067
1090
|
attributePath,
|
|
1068
1091
|
"INVALID_ENUM",
|
|
1069
1092
|
attributeRules?.["typeError"] ||
|
|
1070
|
-
`Invalid value
|
|
1093
|
+
`Invalid value for attribute ${attributePath}, valid values are ${allEnumValues?.join(
|
|
1071
1094
|
", ",
|
|
1072
1095
|
)}`,
|
|
1073
1096
|
);
|
|
@@ -1085,7 +1108,7 @@ function perfectPayloadStructured(
|
|
|
1085
1108
|
attributePath,
|
|
1086
1109
|
"INVALID_UUID",
|
|
1087
1110
|
attributeRules?.["typeError"] ||
|
|
1088
|
-
`Invalid UUID
|
|
1111
|
+
`Invalid UUID for attribute ${attributePath}`,
|
|
1089
1112
|
);
|
|
1090
1113
|
|
|
1091
1114
|
addNextError = false;
|
|
@@ -1100,7 +1123,7 @@ function perfectPayloadStructured(
|
|
|
1100
1123
|
attributePath,
|
|
1101
1124
|
"INVALID_UUID_V1",
|
|
1102
1125
|
attributeRules?.["typeError"] ||
|
|
1103
|
-
`Invalid v1 UUID
|
|
1126
|
+
`Invalid v1 UUID for attribute ${attributePath}`,
|
|
1104
1127
|
);
|
|
1105
1128
|
|
|
1106
1129
|
addNextError = false;
|
|
@@ -1115,7 +1138,7 @@ function perfectPayloadStructured(
|
|
|
1115
1138
|
attributePath,
|
|
1116
1139
|
"INVALID_UUID_V3",
|
|
1117
1140
|
attributeRules?.["typeError"] ||
|
|
1118
|
-
`Invalid v3 UUID
|
|
1141
|
+
`Invalid v3 UUID for attribute ${attributePath}`,
|
|
1119
1142
|
);
|
|
1120
1143
|
|
|
1121
1144
|
addNextError = false;
|
|
@@ -1130,7 +1153,7 @@ function perfectPayloadStructured(
|
|
|
1130
1153
|
attributePath,
|
|
1131
1154
|
"INVALID_UUID_V4",
|
|
1132
1155
|
attributeRules?.["typeError"] ||
|
|
1133
|
-
`Invalid v4 UUID
|
|
1156
|
+
`Invalid v4 UUID for attribute ${attributePath}`,
|
|
1134
1157
|
);
|
|
1135
1158
|
|
|
1136
1159
|
addNextError = false;
|
|
@@ -1145,7 +1168,7 @@ function perfectPayloadStructured(
|
|
|
1145
1168
|
attributePath,
|
|
1146
1169
|
"INVALID_UUID_V5",
|
|
1147
1170
|
attributeRules?.["typeError"] ||
|
|
1148
|
-
`Invalid v5 UUID
|
|
1171
|
+
`Invalid v5 UUID for attribute ${attributePath}`,
|
|
1149
1172
|
);
|
|
1150
1173
|
|
|
1151
1174
|
addNextError = false;
|
|
@@ -1160,7 +1183,7 @@ function perfectPayloadStructured(
|
|
|
1160
1183
|
attributePath,
|
|
1161
1184
|
"INVALID_OBJECT_ID",
|
|
1162
1185
|
attributeRules?.["typeError"] ||
|
|
1163
|
-
`Invalid ObjectId
|
|
1186
|
+
`Invalid ObjectId for attribute ${attributePath}`,
|
|
1164
1187
|
);
|
|
1165
1188
|
|
|
1166
1189
|
addNextError = false;
|
|
@@ -1295,7 +1318,7 @@ function perfectPayloadStructured(
|
|
|
1295
1318
|
rowErrors,
|
|
1296
1319
|
attributePath,
|
|
1297
1320
|
"INVALID_TYPE",
|
|
1298
|
-
`Invalid
|
|
1321
|
+
`Invalid type for attribute ${attributePath}, required number value`,
|
|
1299
1322
|
);
|
|
1300
1323
|
|
|
1301
1324
|
addNextError = false;
|
|
@@ -1305,7 +1328,7 @@ function perfectPayloadStructured(
|
|
|
1305
1328
|
attributePath,
|
|
1306
1329
|
"DECIMAL_NOT_ALLOWED",
|
|
1307
1330
|
attributeRules?.["preventDecimalError"] ||
|
|
1308
|
-
`Decimal value not allowed in attribute ${attributePath}
|
|
1331
|
+
`Decimal value not allowed in attribute ${attributePath}`,
|
|
1309
1332
|
);
|
|
1310
1333
|
|
|
1311
1334
|
addNextError = false;
|
package/package.json
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perfect-payload",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lightweight JSON payload validation library with structured errors, nested validation, field paths, and customizable validation rules.",
|
|
6
6
|
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./express": "./express.js",
|
|
10
|
+
"./fastify": "./fastify.js"
|
|
11
|
+
},
|
|
7
12
|
"scripts": {
|
|
8
13
|
"test": "jest"
|
|
9
14
|
},
|
|
10
15
|
"files": [
|
|
11
|
-
"index.js"
|
|
16
|
+
"index.js",
|
|
17
|
+
"framework.js",
|
|
18
|
+
"express.js",
|
|
19
|
+
"fastify.js"
|
|
12
20
|
],
|
|
13
21
|
"jest": {
|
|
14
22
|
"testPathIgnorePatterns": [
|