perfect-payload 1.4.0-beta.0 → 1.5.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/README.md CHANGED
@@ -4,14 +4,16 @@ A lightweight JavaScript payload validation utility for validating API
4
4
  and JSON payloads with simple rule-based configuration.
5
5
 
6
6
  `perfect-payload` supports structured validation errors, nested field
7
- paths, synchronous custom validators, and synchronous payload
8
- transformation/sanitization while keeping the validation schema simple.
7
+ paths, synchronous custom validators, synchronous payload
8
+ transformation/sanitization, array size constraints, and deeply nested
9
+ array/object validation while keeping the validation schema simple.
9
10
 
10
11
  ## Quick Links
11
12
 
12
13
  - [Installation](#installation)
13
14
  - [Basic Usage](#basic-usage)
14
15
  - [Validation Rules](#validation-rules)
16
+ - [Array Size and Nested Validation](#array-size-and-nested-validation)
15
17
  - [Transformations and
16
18
  Sanitization](#transformations-and-sanitization)
17
19
  - [Custom Validators](#customvalidator)
@@ -293,6 +295,68 @@ Error code: `EMPTY_ARRAY_NOT_ALLOWED`
293
295
 
294
296
  ---
295
297
 
298
+ ### `minItems`
299
+
300
+ Defines the minimum number of items required in an array.
301
+
302
+ Default: Not applied when omitted.
303
+
304
+ ```js
305
+ const rules = {
306
+ tags: {
307
+ type: "array",
308
+ minItems: 2,
309
+ },
310
+ };
311
+ ```
312
+
313
+ An array with fewer than 2 items returns `MIN_ITEMS`.
314
+
315
+ ```js
316
+ {
317
+ path: "tags",
318
+ code: "MIN_ITEMS",
319
+ message: "Attribute tags must contain at least 2 item(s)"
320
+ }
321
+ ```
322
+
323
+ `minItems` is enforced even when `allowEmptyArray: true` is set. For example, `minItems: 2` still rejects `[]`.
324
+
325
+ Error code: `MIN_ITEMS`
326
+
327
+ ---
328
+
329
+ ### `maxItems`
330
+
331
+ Defines the maximum number of items allowed in an array.
332
+
333
+ Default: Not applied when omitted.
334
+
335
+ ```js
336
+ const rules = {
337
+ tags: {
338
+ type: "array",
339
+ maxItems: 5,
340
+ },
341
+ };
342
+ ```
343
+
344
+ An array with more than 5 items returns `MAX_ITEMS`.
345
+
346
+ ```js
347
+ {
348
+ path: "tags",
349
+ code: "MAX_ITEMS",
350
+ message: "Attribute tags must contain at most 5 item(s)"
351
+ }
352
+ ```
353
+
354
+ `minItems` and `maxItems` can be used together.
355
+
356
+ Error code: `MAX_ITEMS`
357
+
358
+ ---
359
+
296
360
  ### `type`
297
361
 
298
362
  Validates the expected data type.
@@ -415,6 +479,8 @@ INVALID_UUID_V5
415
479
  INVALID_OBJECT_ID
416
480
  ```
417
481
 
482
+ For `type: "number"`, `NaN` is rejected as `INVALID_TYPE`.
483
+
418
484
  ---
419
485
 
420
486
  ### `regex`
@@ -731,6 +797,103 @@ Example error:
731
797
 
732
798
  ---
733
799
 
800
+ ## Array Size and Nested Validation
801
+
802
+ `perfectPayload()` supports array size constraints and recursive validation of arrays and objects at multiple depths. Array indexes and nested object keys are preserved in structured error paths.
803
+
804
+ ### Array size constraints
805
+
806
+ Use `minItems` and `maxItems` with `type: "array"`:
807
+
808
+ ```js
809
+ const rules = {
810
+ products: {
811
+ type: "array",
812
+ minItems: 1,
813
+ maxItems: 3,
814
+ elementConstraints: {
815
+ type: "object",
816
+ objectAttr: {
817
+ productId: { mandatory: true, type: "string" },
818
+ quantity: { mandatory: true, type: "number", min: 1 },
819
+ },
820
+ },
821
+ },
822
+ };
823
+ ```
824
+
825
+ If the array is empty, `minItems` reports the array path itself:
826
+
827
+ ```js
828
+ {
829
+ path: "products",
830
+ code: "MIN_ITEMS",
831
+ message: "Attribute products must contain at least 1 item(s)"
832
+ }
833
+ ```
834
+
835
+ ### Arrays of objects
836
+
837
+ `elementConstraints` can contain `objectAttr`, allowing every object in an array to use a nested schema. An invalid quantity in the second product is reported as:
838
+
839
+ ```text
840
+ products[1].quantity
841
+ ```
842
+
843
+ ### Deeply nested arrays and objects
844
+
845
+ `objectAttr` and `elementConstraints` can be combined recursively:
846
+
847
+ ```js
848
+ const rules = {
849
+ orders: {
850
+ type: "array",
851
+ minItems: 1,
852
+ maxItems: 2,
853
+ elementConstraints: {
854
+ type: "object",
855
+ objectAttr: {
856
+ orderId: { mandatory: true, type: "string" },
857
+ items: {
858
+ mandatory: true,
859
+ type: "array",
860
+ minItems: 1,
861
+ maxItems: 2,
862
+ elementConstraints: {
863
+ type: "object",
864
+ objectAttr: {
865
+ productId: { mandatory: true, type: "string" },
866
+ quantity: { mandatory: true, type: "number", min: 1 },
867
+ },
868
+ },
869
+ },
870
+ },
871
+ },
872
+ },
873
+ };
874
+ ```
875
+
876
+ A deep validation failure preserves the complete indexed path, for example:
877
+
878
+ ```text
879
+ orders[1].items[2].quantity
880
+ ```
881
+
882
+ Array constraints work at nested levels too. A nested array can report paths such as:
883
+
884
+ ```text
885
+ orders[1].items
886
+ ```
887
+
888
+ Nested arrays are supported and every array index is preserved:
889
+
890
+ ```text
891
+ matrix[1][1]
892
+ matrix[1][1][1]
893
+ ```
894
+
895
+ Transformations applied inside nested objects or array elements are preserved in `validatedPayload`, while the original input remains unchanged.
896
+
734
897
  ### Transformations and Sanitization
735
898
 
736
899
  `perfectPayload()` can transform a field before its validation rules
@@ -922,9 +1085,17 @@ not passed to transformation functions; null handling remains controlled
922
1085
  by `allowNull`.
923
1086
 
924
1087
  **Important:** `transform` is synchronous. A non-function transformer,
925
- an `async` transformer, or a transformer that returns a Promise is not
926
- supported and throws an error. Exceptions thrown inside the transformer
927
- propagate to the caller.
1088
+ an `async` transformer, a transformer that returns a Promise, or a
1089
+ transformer that returns `undefined` is not supported and throws an error.
1090
+ Returning `null`, `""`, `0`, or `false` is allowed; the transformed value is
1091
+ then processed by the normal validation rules. Exceptions thrown inside the
1092
+ transformer propagate to the caller.
1093
+
1094
+ For example, returning `undefined` throws:
1095
+
1096
+ ```text
1097
+ perfect-payload:- transform must not return undefined for attribute username
1098
+ ```
928
1099
 
929
1100
  ### `customValidator`
930
1101
 
@@ -1051,6 +1222,10 @@ EMPTY_OBJECT_NOT_ALLOWED
1051
1222
 
1052
1223
  EMPTY_ARRAY_NOT_ALLOWED
1053
1224
 
1225
+ MIN_ITEMS
1226
+
1227
+ MAX_ITEMS
1228
+
1054
1229
  INVALID_ARRAY_ELEMENT
1055
1230
 
1056
1231
  REGEX_MISMATCH
@@ -1086,6 +1261,8 @@ MIN_VALUE
1086
1261
  MAX_VALUE
1087
1262
 
1088
1263
  OUT_OF_RANGE
1264
+
1265
+ CUSTOM_VALIDATION_FAILED
1089
1266
  ```
1090
1267
 
1091
1268
  These codes are designed for programmatic handling while `message`
@@ -1733,6 +1910,8 @@ marks[1]
1733
1910
  marks[2]
1734
1911
  ```
1735
1912
 
1913
+ Array-level constraints such as `minItems` and `maxItems` report the path of the array itself. For nested arrays, the complete parent path is retained, for example `orders[1].items`.
1914
+
1736
1915
  ### Nested Fields Inside Arrays
1737
1916
 
1738
1917
  Paths can also identify fields inside array elements.
package/index.js CHANGED
@@ -633,6 +633,12 @@ function perfectPayloadStructured(
633
633
  );
634
634
  }
635
635
 
636
+ if (transformedValue === undefined) {
637
+ throw new Error(
638
+ `perfect-payload:- transform must not return undefined for attribute ${attributePath}`,
639
+ );
640
+ }
641
+
636
642
  attributeValue = transformedValue;
637
643
  }
638
644
  // TRANSFORMATIONS END
@@ -803,6 +809,45 @@ function perfectPayloadStructured(
803
809
 
804
810
  break;
805
811
 
812
+ case "minItems":
813
+ if (
814
+ addNextError &&
815
+ attrExist &&
816
+ attributeValue !== null &&
817
+ isArray(attributeValue) &&
818
+ attributeValue.length < attributeRules[ruleName]
819
+ ) {
820
+ addStructuredError(
821
+ rowErrors,
822
+ attributePath,
823
+ "MIN_ITEMS",
824
+ `Attribute ${attributePath} must contain at least ${attributeRules[ruleName]} item(s)`,
825
+ );
826
+
827
+ addNextError = false;
828
+ }
829
+
830
+ break;
831
+
832
+ case "maxItems":
833
+ if (
834
+ addNextError &&
835
+ attrExist &&
836
+ attributeValue !== null &&
837
+ isArray(attributeValue) &&
838
+ attributeValue.length > attributeRules[ruleName]
839
+ ) {
840
+ addStructuredError(
841
+ rowErrors,
842
+ attributePath,
843
+ "MAX_ITEMS",
844
+ `Attribute ${attributePath} must contain at most ${attributeRules[ruleName]} item(s)`,
845
+ );
846
+
847
+ addNextError = false;
848
+ }
849
+
850
+ break;
806
851
  // ==================================================
807
852
  // REGEX
808
853
  // ==================================================
@@ -837,7 +882,10 @@ function perfectPayloadStructured(
837
882
 
838
883
  switch (expectedType) {
839
884
  case "number":
840
- if (!isNumber(attributeValue)) {
885
+ if (
886
+ !isNumber(attributeValue) ||
887
+ Number.isNaN(attributeValue)
888
+ ) {
841
889
  addStructuredError(
842
890
  rowErrors,
843
891
  attributePath,
@@ -854,7 +902,6 @@ function perfectPayloadStructured(
854
902
  }
855
903
 
856
904
  break;
857
-
858
905
  case "string":
859
906
  if (!isString(attributeValue)) {
860
907
  addStructuredError(
package/package.json CHANGED
@@ -1,12 +1,22 @@
1
1
  {
2
2
  "name": "perfect-payload",
3
- "version": "1.4.0-beta.0",
3
+ "version": "1.5.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
6
  "main": "index.js",
7
7
  "scripts": {
8
8
  "test": "jest"
9
9
  },
10
+ "files": [
11
+ "index.js"
12
+ ],
13
+ "jest": {
14
+ "testPathIgnorePatterns": [
15
+ "/node_modules/",
16
+ "/test.js",
17
+ "/testingV1.js"
18
+ ]
19
+ },
10
20
  "repository": {
11
21
  "type": "git",
12
22
  "url": "git+https://github.com/kiranpoojary/perfect-payload.git"
package/.babelrc DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "presets": ["@babel/preset-env"]
3
- }
4
-
@@ -1,332 +0,0 @@
1
- name: Stage npm package
2
-
3
- on:
4
- push:
5
- # Beta:
6
- # Run for pushes to every branch except main.
7
- branches:
8
- - "**"
9
- - "!main"
10
-
11
- # Production:
12
- # Run only when a version tag is pushed.
13
- # Examples: v1.3.0, v1.3.1, v2.0.0
14
- tags:
15
- - "v*.*.*"
16
-
17
- permissions:
18
- contents: read
19
- id-token: write
20
-
21
- jobs:
22
- # ==========================================================
23
- # 1. PACKAGE INFO
24
- # ==========================================================
25
- package-info:
26
- name: Package Info
27
- runs-on: ubuntu-latest
28
-
29
- outputs:
30
- name: ${{ steps.package.outputs.name }}
31
- version: ${{ steps.package.outputs.version }}
32
- release_type: ${{ steps.package.outputs.release_type }}
33
-
34
- steps:
35
- - name: Checkout repository
36
- uses: actions/checkout@v7
37
-
38
- - name: Read package information
39
- id: package
40
- shell: bash
41
- run: |
42
- NAME=$(node -p "require('./package.json').name")
43
- VERSION=$(node -p "require('./package.json').version")
44
-
45
- echo "name=$NAME" >> "$GITHUB_OUTPUT"
46
- echo "version=$VERSION" >> "$GITHUB_OUTPUT"
47
-
48
- if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
49
- RELEASE_TYPE="production"
50
- else
51
- RELEASE_TYPE="beta"
52
- fi
53
-
54
- echo "release_type=$RELEASE_TYPE" >> "$GITHUB_OUTPUT"
55
-
56
- echo "======================================"
57
- echo "PACKAGE INFORMATION"
58
- echo "======================================"
59
- echo "Package: $NAME"
60
- echo "Version: $VERSION"
61
- echo "Release type: $RELEASE_TYPE"
62
- echo "Git ref: $GITHUB_REF_NAME"
63
- echo "Git ref type: $GITHUB_REF_TYPE"
64
- echo "======================================"
65
-
66
- # ==========================================================
67
- # 2. VALIDATE RELEASE
68
- # ==========================================================
69
- validate-release:
70
- name: Validate Release
71
- needs: package-info
72
- runs-on: ubuntu-latest
73
-
74
- steps:
75
- - name: Validate branch / tag and version
76
- shell: bash
77
- run: |
78
- VERSION="${{ needs.package-info.outputs.version }}"
79
-
80
- echo "Version: $VERSION"
81
- echo "Ref: $GITHUB_REF_NAME"
82
- echo "Ref type: $GITHUB_REF_TYPE"
83
-
84
- # --------------------------------------------------
85
- # PRODUCTION
86
- # Must come from a Git tag.
87
- #
88
- # Tag: v1.3.0
89
- # package version: 1.3.0
90
- # --------------------------------------------------
91
- if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
92
-
93
- TAG="$GITHUB_REF_NAME"
94
-
95
- if [[ "$VERSION" == *"-beta"* ]]; then
96
- echo "::error::Production releases cannot contain -beta."
97
- echo "::error::Current package version: $VERSION"
98
- exit 1
99
- fi
100
-
101
- EXPECTED_TAG="v$VERSION"
102
-
103
- if [[ "$TAG" != "$EXPECTED_TAG" ]]; then
104
- echo "::error::Git tag does not match package.json version."
105
- echo "::error::Tag: $TAG"
106
- echo "::error::Expected tag: $EXPECTED_TAG"
107
- exit 1
108
- fi
109
-
110
- echo "Valid production release."
111
- echo "Tag $TAG matches package version $VERSION."
112
-
113
- # --------------------------------------------------
114
- # BETA
115
- # Any non-main branch.
116
- #
117
- # Example:
118
- # feature/custom-validator
119
- # package version: 1.3.0-beta.0
120
- # --------------------------------------------------
121
- else
122
-
123
- if [[ "$GITHUB_REF_NAME" == "main" ]]; then
124
- echo "::error::Production releases from main pushes are disabled."
125
- echo "::error::Create a Git tag to release production."
126
- exit 1
127
- fi
128
-
129
- if [[ "$VERSION" != *"-beta"* ]]; then
130
- echo "::error::Non-main branches must contain a beta version."
131
- echo "::error::Current version: $VERSION"
132
- echo "::error::Expected something like 1.3.0-beta.0"
133
- exit 1
134
- fi
135
-
136
- echo "Valid beta release."
137
- echo "Branch: $GITHUB_REF_NAME"
138
- echo "Version: $VERSION"
139
-
140
- fi
141
-
142
- # ==========================================================
143
- # 3. INSTALL DEPENDENCIES
144
- # ==========================================================
145
- install:
146
- name: Install Dependencies
147
- needs:
148
- - package-info
149
- - validate-release
150
-
151
- runs-on: ubuntu-latest
152
-
153
- steps:
154
- - name: Checkout repository
155
- uses: actions/checkout@v7
156
-
157
- - name: Setup Node.js
158
- uses: actions/setup-node@v7
159
- with:
160
- node-version: 24
161
- registry-url: https://registry.npmjs.org
162
- package-manager-cache: false
163
-
164
- - name: Setup npm
165
- run: npm install --global npm@11.19.1
166
-
167
- - name: Verify Node and npm versions
168
- run: |
169
- node -v
170
- npm -v
171
-
172
- - name: Install dependencies
173
- run: npm ci
174
-
175
- # ==========================================================
176
- # 4. RUN TESTS
177
- # ==========================================================
178
- test:
179
- name: Run Tests
180
- needs: install
181
- runs-on: ubuntu-latest
182
-
183
- steps:
184
- - name: Checkout repository
185
- uses: actions/checkout@v7
186
-
187
- - name: Setup Node.js
188
- uses: actions/setup-node@v7
189
- with:
190
- node-version: 24
191
- package-manager-cache: false
192
-
193
- - name: Install dependencies
194
- run: npm ci
195
-
196
- - name: Run tests
197
- run: npm test -- --passWithNoTests
198
-
199
- # ==========================================================
200
- # 5. CHECK NPM VERSION
201
- # ==========================================================
202
- check-npm:
203
- name: Check npm Version
204
- needs:
205
- - package-info
206
- - validate-release
207
- - test
208
-
209
- runs-on: ubuntu-latest
210
-
211
- steps:
212
- - name: Setup Node.js
213
- uses: actions/setup-node@v7
214
- with:
215
- node-version: 24
216
- registry-url: https://registry.npmjs.org
217
- package-manager-cache: false
218
-
219
- - name: Check whether version already exists
220
- shell: bash
221
- run: |
222
- NAME="${{ needs.package-info.outputs.name }}"
223
- VERSION="${{ needs.package-info.outputs.version }}"
224
-
225
- echo "Checking npm for $NAME@$VERSION..."
226
-
227
- if npm view "$NAME@$VERSION" version >/dev/null 2>&1; then
228
- echo "::error::$NAME@$VERSION is already published."
229
- echo "::error::Use a new package version."
230
- exit 1
231
- fi
232
-
233
- echo "$NAME@$VERSION is not published."
234
- echo "Release is ready to stage."
235
-
236
- # ==========================================================
237
- # 6. STAGE NPM PACKAGE
238
- #
239
- # This environment can optionally have a GitHub
240
- # Required Reviewer configured.
241
- #
242
- # npm itself still requires approval + 2FA after staging.
243
- # ==========================================================
244
- stage-npm:
245
- name: Stage npm Package
246
- needs:
247
- - package-info
248
- - check-npm
249
-
250
- runs-on: ubuntu-latest
251
-
252
- environment:
253
- name: npm-stage
254
-
255
- steps:
256
- - name: Checkout repository
257
- uses: actions/checkout@v7
258
-
259
- - name: Setup Node.js
260
- uses: actions/setup-node@v7
261
- with:
262
- node-version: 24
263
- registry-url: https://registry.npmjs.org
264
- package-manager-cache: false
265
-
266
- - name: Setup npm
267
- run: npm install --global npm@11.19.1
268
-
269
- - name: Show release information
270
- shell: bash
271
- run: |
272
- echo "======================================"
273
- echo "READY TO STAGE"
274
- echo "======================================"
275
- echo "Package: ${{ needs.package-info.outputs.name }}"
276
- echo "Version: ${{ needs.package-info.outputs.version }}"
277
- echo "Type: ${{ needs.package-info.outputs.release_type }}"
278
- echo "Ref: $GITHUB_REF_NAME"
279
- echo "======================================"
280
-
281
- # ------------------------------------------------------
282
- # BETA
283
- # Triggered only from non-main branch pushes.
284
- # ------------------------------------------------------
285
- - name: Stage Beta Package
286
- if: >
287
- github.ref_type == 'branch' &&
288
- needs.package-info.outputs.release_type == 'beta'
289
- run: |
290
- echo "Staging beta package..."
291
- echo "${{ needs.package-info.outputs.name }}@${{ needs.package-info.outputs.version }}"
292
-
293
- npm stage publish --tag beta
294
-
295
- # ------------------------------------------------------
296
- # PRODUCTION
297
- # Triggered ONLY from Git tags.
298
- # ------------------------------------------------------
299
- - name: Stage Production Package
300
- if: >
301
- github.ref_type == 'tag' &&
302
- needs.package-info.outputs.release_type == 'production'
303
- run: |
304
- echo "Staging production package..."
305
- echo "${{ needs.package-info.outputs.name }}@${{ needs.package-info.outputs.version }}"
306
-
307
- npm stage publish
308
-
309
- # ==========================================================
310
- # 7. COMPLETION
311
- # ==========================================================
312
- complete:
313
- name: npm Package Staged
314
- needs:
315
- - package-info
316
- - stage-npm
317
-
318
- runs-on: ubuntu-latest
319
-
320
- steps:
321
- - name: Staging completed
322
- run: |
323
- echo "======================================"
324
- echo "NPM STAGING COMPLETED"
325
- echo "======================================"
326
- echo "Package: ${{ needs.package-info.outputs.name }}"
327
- echo "Version: ${{ needs.package-info.outputs.version }}"
328
- echo "Type: ${{ needs.package-info.outputs.release_type }}"
329
- echo ""
330
- echo "The package has been staged on npm."
331
- echo "Review and approve it on npm with 2FA."
332
- echo "======================================"