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/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
- return perfectPayloadStructured(
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: rowErrors,
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(${attributeValue}) found in attribute ${attributePath}`,
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(${attributeValue}) found in attribute ${attributePath}, valid values are ${allEnumValues?.join(
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(${attributeValue}) found in attribute ${attributePath}`,
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(${attributeValue}) found in attribute ${attributePath}`,
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(${attributeValue}) found in attribute ${attributePath}`,
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(${attributeValue}) found in attribute ${attributePath}`,
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(${attributeValue}) found in attribute ${attributePath}`,
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(${attributeValue}) found in attribute ${attributePath}`,
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 ${typeof attributeValue} value(${attributeValue}) found in attribute ${attributePath}, required number value`,
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}(${attributeValue})`,
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.7.0",
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": [