payload-guard-filter 1.6.2 → 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 CHANGED
@@ -1,174 +1,277 @@
1
- # payload-guard
2
-
3
- > Part of the [Professional Node.js Backend Toolkit](https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-)
4
-
5
- <p align="center">
6
- <strong>🛡️ Lightweight, zero-dependency shape-based payload filtering and sanitization</strong>
7
- </p>
8
-
9
- <p align="center">
10
- <img src="https://img.shields.io/badge/bundle%20size-%3C8KB-brightgreen" alt="Bundle Size">
11
- <img src="https://img.shields.io/badge/dependencies-0-blue" alt="Zero Dependencies">
12
- <img src="https://img.shields.io/badge/TypeScript-100%25-blue" alt="TypeScript">
13
- <img src="https://img.shields.io/badge/Node.js-18%2B-green" alt="Node.js 18+">
14
- </p>
15
-
16
- ---
17
-
18
- ## 🛡️ Workflow
19
-
20
- ```mermaid
21
- graph LR
22
- A[Request] --> B(Gatekeeper)
23
- B --> C{Shape Check}
24
- C -- Valid --> D[Redact & Clean]
25
- C -- Invalid --> E[Strict Error / Fail Safe]
26
- D --> F[Secure Response]
27
- E --> F
28
- F --> G((Metrics))
29
- ```
30
-
31
- ---
32
-
33
- ## ✨ Features
34
-
35
- - **Shape-based filtering** — Define what you want, auto-remove everything else
36
- - **Sensitive field protection** — `password`, `token`, `secret` automatically removed
37
- - **Zero dependencies** — Pure TypeScript, no external packages
38
- - **Universal** — Works in Node.js, Browser, React Native
39
- - **TypeScript-first** — Full type inference from shape definitions
40
- - **Blazing fast** — Compiled schemas for production performance
41
- - **Never crashes** — Graceful failure mode, production-safe
42
-
43
- ---
44
-
45
- ## 📦 Installation
46
-
47
- ```bash
48
- npm install payload-guard
49
- ```
50
-
51
- ---
52
-
53
- ## 🚀 Quick Start
54
-
55
- ### Basic Usage
56
-
57
- ```typescript
58
- import { guard } from 'payload-guard';
59
-
60
- // Define a shape
61
- const userShape = guard.shape({
62
- id: 'number',
63
- name: 'string',
64
- email: 'string',
65
- });
66
-
67
- // Filter data
68
- const rawData = {
69
- id: 1,
70
- name: 'John Doe',
71
- email: 'john@example.com',
72
- password: 'secret123', // ❌ Will be removed
73
- internalNotes: 'VIP user', // ❌ Will be removed
74
- };
75
-
76
- const safeData = userShape(rawData);
77
- // Result: { id: 1, name: 'John Doe', email: 'john@example.com' }
78
- ```
79
-
80
- ### Advanced Validation (v1.4+)
81
-
82
- ```typescript
83
- const userShape = guard.shape({
84
- email: guard.string().email().toLowerCase().trim(),
85
- age: guard.number().min(18).max(100).default(18),
86
- tags: guard.array(guard.string().min(2)),
87
- role: guard.string().validate(v => ['admin', 'user'].includes(v)).default('user'),
88
- bio: guard.string().max(200).optional(),
89
- });
90
- ```
91
-
92
- ---
93
-
94
- ## 📖 API Reference
95
-
96
- ### `guard.string()`
97
- Creates a string builder with chained constraints:
98
- - `.min(length)` — Minimum character length
99
- - `.max(length)` — Maximum character length
100
- - `.email()` — Basic email validation
101
- - `.regex(pattern)` — Regex pattern match
102
- - `.trim()` — Auto-trim whitespace (Transformation)
103
- - `.toLowerCase()` / `.toUpperCase()` — Case transformation
104
-
105
- ### `guard.number()`
106
- - `.min(value)` / `.max(value)` — Range validation
107
- - `.integer()` — Ensure number is an integer
108
- - `.positive()` — Shortcut for `.min(0)`
109
-
110
- ### Common Builder Methods
111
- - `.required()` / `.optional()` — Toggle requirement
112
- - `.default(value)` — Value to use if field is missing or invalid
113
- - `.transform(fn)` — Custom transformation function
114
- - `.validate(fn)` — Custom validation function (return `false` to fail)
115
-
116
- ### Field Aliasing & Mapping (v1.6+)
117
-
118
- Rename fields from your database to match your frontend API:
119
-
120
- ```typescript
121
- const userShape = guard.shape({
122
- id: guard.string().from('_id'), // Map DB _id to id
123
- workTime: guard.number().from('totalWorkTimeMs').transform(v => v / 1000),
124
- name: 'string',
125
- });
126
-
127
- // Input: { _id: '123', totalWorkTimeMs: 5000, name: 'Sannu' }
128
- // Output: { id: '123', workTime: 5, name: 'Sannu' }
129
- ```
130
-
131
- ---
132
-
133
- ## 🛡️ "Never Crash" Policy
134
-
135
- Payload Guard is designed for mission-critical enterprise environments where uptime is non-negotiable.
136
-
137
- - **Circular Reference Safety**: Automatically detects and handles circular objects without infinite loops or stack overflows.
138
- - **Hook Isolation**: Custom `.transform()` and `.validate()` callbacks are wrapped in internal try/catch blocks. If your code fails, the library logs the error and safely continues using fallback values.
139
- - **Middleware Fail-Open**: If internal filtering logic hits an unexpected edge case, `failOpen: true` ensures the original request/response still reaches its destination.
140
-
141
- ---
142
-
143
- ## ⚡ Performance
144
-
145
- | Benchmark | ops/sec | avg (ms) |
146
- |-----------|---------|----------|
147
- | **Small payload** (5 fields) | 449,365 | **0.0022ms** |
148
- | **Medium payload** (50 posts) | 7,791 | **0.1284ms** |
149
- | **Large payload** (1000 users) | 246 | **4.0724ms** |
150
-
151
- > **Memory Usage**: ~121 MB Heap Used (Stable)
152
-
153
- ---
154
-
155
- ## 🛡️ Fail-Safe Design
156
-
157
- Built for production reliability:
158
- - **Isolation**: Monitoring failures **never** break your API responses. If a storage adapter or plugin crashes, the error is caught and logged, while the user's request continues normally.
159
- - **Async-Only**: All processing is non-blocking to ensure zero impact on event loop latency.
160
-
161
- ---
162
- ### ⛑️ Maintained actively.
163
- **Bug fixes usually within 24–48 hours.**
164
-
165
- ---
166
- ## 📄 License
167
-
168
- MIT
169
-
170
- ---
171
-
172
- <p align="center">
173
- Made with ❤️ for safer APIs
174
- </p>
1
+ # payload-guard
2
+
3
+ > Part of the [Professional Node.js Backend Toolkit](https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-)
4
+
5
+ <p align="center">
6
+ <strong>🛡️ Lightweight, zero-dependency shape-based payload filtering and sanitization</strong>
7
+ </p>
8
+
9
+ <p align="center">
10
+ <img src="https://img.shields.io/badge/bundle%20size-%3C8KB-brightgreen" alt="Bundle Size">
11
+ <img src="https://img.shields.io/badge/dependencies-0-blue" alt="Zero Dependencies">
12
+ <img src="https://img.shields.io/badge/TypeScript-100%25-blue" alt="TypeScript">
13
+ <img src="https://img.shields.io/badge/Node.js-18%2B-green" alt="Node.js 18+">
14
+ </p>
15
+
16
+ ---
17
+
18
+ ## 🛡️ Workflow
19
+
20
+ ```mermaid
21
+ graph LR
22
+ A[Request] --> B(Gatekeeper)
23
+ B --> C{Shape Check}
24
+ C -- Valid --> D[Redact & Clean]
25
+ C -- Invalid --> E[Strict Error / Fail Safe]
26
+ D --> F[Secure Response]
27
+ E --> F
28
+ F --> G((Metrics))
29
+ ```
30
+
31
+ ---
32
+
33
+ ## ✨ Features
34
+
35
+ - **Shape-based filtering** — Define what you want, auto-remove everything else
36
+ - **Sensitive field protection** — `password`, `token`, `secret` automatically removed
37
+ - **Zero dependencies** — Pure TypeScript, no external packages
38
+ - **Universal** — Works in Node.js, Browser, React Native
39
+ - **TypeScript-first** — Full type inference from shape definitions
40
+ - **Blazing fast** — Compiled schemas for production performance
41
+ - **Never crashes** — Graceful failure mode, production-safe
42
+
43
+ ---
44
+
45
+ ## 📦 Installation
46
+
47
+ ```bash
48
+ npm install payload-guard
49
+ ```
50
+
51
+ ---
52
+
53
+ ## 🚀 Quick Start
54
+
55
+ ### Basic Usage
56
+
57
+ ```typescript
58
+ import { guard } from 'payload-guard';
59
+
60
+ // Define a shape
61
+ const userShape = guard.shape({
62
+ id: 'number',
63
+ name: 'string',
64
+ email: 'string',
65
+ });
66
+
67
+ // Filter data
68
+ const rawData = {
69
+ id: 1,
70
+ name: 'John Doe',
71
+ email: 'john@example.com',
72
+ password: 'secret123', // ❌ Will be removed
73
+ internalNotes: 'VIP user', // ❌ Will be removed
74
+ };
75
+
76
+ const safeData = userShape(rawData);
77
+ // Result: { id: 1, name: 'John Doe', email: 'john@example.com' }
78
+ ```
79
+
80
+ ### Advanced Validation (v1.4+)
81
+
82
+ ```typescript
83
+ const userShape = guard.shape({
84
+ email: guard.string().email().toLowerCase().trim(),
85
+ age: guard.number().min(18).max(100).default(18),
86
+ tags: guard.array(guard.string().min(2)),
87
+ role: guard.string().validate(v => ['admin', 'user'].includes(v)).default('user'),
88
+ bio: guard.string().max(200).optional(),
89
+ });
90
+ ```
91
+
92
+ ### Nested Object Validation (v1.7+)
93
+
94
+ Define nested shapes for deep object validation:
95
+
96
+ ```typescript
97
+ const userShape = guard.shape({
98
+ id: 'number',
99
+ profile: {
100
+ name: 'string',
101
+ email: guard.string().email(),
102
+ age: guard.number().min(18),
103
+ address: {
104
+ street: 'string',
105
+ city: 'string',
106
+ zipCode: guard.string().regex(/^\d{5}(-\d{4})?$/),
107
+ },
108
+ },
109
+ posts: guard.array({
110
+ id: 'number',
111
+ title: 'string',
112
+ tags: guard.array('string'),
113
+ }),
114
+ });
115
+
116
+ const input = {
117
+ id: 1,
118
+ profile: {
119
+ name: 'John',
120
+ email: 'john@example.com',
121
+ age: 30,
122
+ address: {
123
+ street: '123 Main St',
124
+ city: 'NYC',
125
+ zipCode: '10001',
126
+ },
127
+ },
128
+ posts: [
129
+ { id: 1, title: 'Hello', tags: ['intro'] },
130
+ ],
131
+ };
132
+
133
+ const result = userShape(input);
134
+ // Filters all nested levels automatically
135
+ ```
136
+
137
+ ### Custom Error Messages (v1.7+)
138
+
139
+ Add per-field custom error messages:
140
+
141
+ ```typescript
142
+ const userShape = guard.shape({
143
+ email: guard
144
+ .string()
145
+ .email()
146
+ .error('Please provide a valid email address'),
147
+
148
+ username: guard
149
+ .string()
150
+ .min(5)
151
+ .error((value, field) => `${field} must be at least 5 characters`),
152
+
153
+ age: guard
154
+ .number()
155
+ .min(18)
156
+ .max(100)
157
+ .errorCodes({ min: 'AGE_TOO_YOUNG', max: 'AGE_TOO_OLD' }),
158
+ });
159
+
160
+ // Collect validation errors
161
+ const { compile } = require('payload-guard');
162
+ const errors = [];
163
+ const validator = compile(
164
+ { email: { type: 'string', email: true } },
165
+ { collectErrors: true, _errors: errors }
166
+ );
167
+
168
+ validator({ email: 'invalid' });
169
+ // errors: [{ field: 'email', message: '...', code: 'email' }]
170
+ ```
171
+
172
+ ---
173
+
174
+ ## 📖 API Reference
175
+
176
+ ### `guard.string()`
177
+ Creates a string builder with chained constraints:
178
+ - `.min(length)` — Minimum character length
179
+ - `.max(length)` — Maximum character length
180
+ - `.email()` — Basic email validation
181
+ - `.regex(pattern)` — Regex pattern match
182
+ - `.trim()` — Auto-trim whitespace (Transformation)
183
+ - `.toLowerCase()` / `.toUpperCase()` — Case transformation
184
+
185
+ ### `guard.number()`
186
+ - `.min(value)` / `.max(value)` — Range validation
187
+ - `.integer()` — Ensure number is an integer
188
+ - `.positive()` — Shortcut for `.min(0)`
189
+
190
+ ### Common Builder Methods
191
+ - `.required()` / `.optional()` — Toggle requirement
192
+ - `.default(value)` — Value to use if field is missing or invalid
193
+ - `.transform(fn)` — Custom transformation function
194
+ - `.validate(fn)` — Custom validation function (return `false` to fail)
195
+ - `.error(message)` — Custom error message (string or function) **(v1.7+)**
196
+ - `.errorCodes(codes)` — Custom error codes for validations **(v1.7+)**
197
+
198
+ ### New in v1.7+
199
+
200
+ **Nested Object Support:**
201
+ - Define nested shapes as plain objects: `{ profile: { name: 'string', email: 'string' } }`
202
+ - Arrays with nested objects: `guard.array({ id: 'number', name: 'string' })`
203
+ - Deep nesting supported at any level
204
+
205
+ **Custom Error Messages:**
206
+ - `.error('Custom message')` — Static error message
207
+ - `.error((value, field) => \`${field} is invalid\`)` — Dynamic error message
208
+ - `.errorCodes({ min: 'TOO_SHORT', email: 'BAD_EMAIL' })` — Error codes
209
+
210
+ **Error Collection:**
211
+ ```typescript
212
+ const { compile } = require('payload-guard');
213
+ const errors = [];
214
+ const validator = compile(shape, { collectErrors: true, _errors: errors });
215
+ validator(data);
216
+ // errors array populated with { field, message, code, value }
217
+ ```
218
+
219
+ ### Field Aliasing & Mapping (v1.6+)
220
+
221
+ Rename fields from your database to match your frontend API:
222
+
223
+ ```typescript
224
+ const userShape = guard.shape({
225
+ id: guard.string().from('_id'), // Map DB _id to id
226
+ workTime: guard.number().from('totalWorkTimeMs').transform(v => v / 1000),
227
+ name: 'string',
228
+ });
229
+
230
+ // Input: { _id: '123', totalWorkTimeMs: 5000, name: 'Sannu' }
231
+ // Output: { id: '123', workTime: 5, name: 'Sannu' }
232
+ ```
233
+
234
+ ---
235
+
236
+ ## 🛡️ "Never Crash" Policy
237
+
238
+ Payload Guard is designed for mission-critical enterprise environments where uptime is non-negotiable.
239
+
240
+ - **Circular Reference Safety**: Automatically detects and handles circular objects without infinite loops or stack overflows.
241
+ - **Hook Isolation**: Custom `.transform()` and `.validate()` callbacks are wrapped in internal try/catch blocks. If your code fails, the library logs the error and safely continues using fallback values.
242
+ - **Middleware Fail-Open**: If internal filtering logic hits an unexpected edge case, `failOpen: true` ensures the original request/response still reaches its destination.
243
+
244
+ ---
245
+
246
+ ## ⚡ Performance
247
+
248
+ | Benchmark | ops/sec | avg (ms) |
249
+ |-----------|---------|----------|
250
+ | **Small payload** (5 fields) | 449,365 | **0.0022ms** |
251
+ | **Medium payload** (50 posts) | 7,791 | **0.1284ms** |
252
+ | **Large payload** (1000 users) | 246 | **4.0724ms** |
253
+
254
+ > **Memory Usage**: ~121 MB Heap Used (Stable)
255
+
256
+ ---
257
+
258
+ ## 🛡️ Fail-Safe Design
259
+
260
+ Built for production reliability:
261
+ - **Isolation**: Monitoring failures **never** break your API responses. If a storage adapter or plugin crashes, the error is caught and logged, while the user's request continues normally.
262
+ - **Async-Only**: All processing is non-blocking to ensure zero impact on event loop latency.
263
+
264
+ ---
265
+ ### ⛑️ Maintained actively.
266
+ **Bug fixes usually within 24–48 hours.**
267
+
268
+ ---
269
+ ## 📄 License
270
+
271
+ MIT
272
+
273
+ ---
274
+
275
+ <p align="center">
276
+ Made with ❤️ for safer APIs
277
+ </p>
@@ -1,4 +1,4 @@
1
- import { ShapeDescriptor, ArrayShapeDescriptor, CompiledFilter } from './types';
1
+ import { ShapeDescriptor, ArrayShapeDescriptor, CompiledFilter, ValidationError } from './types';
2
2
  interface CompileOptions {
3
3
  sensitive?: string[];
4
4
  dev?: boolean;
@@ -9,6 +9,10 @@ interface CompileOptions {
9
9
  maxArrayLength?: number;
10
10
  /** Parent path for nested field warnings (internal) */
11
11
  _parentPath?: string;
12
+ /** Collect validation errors */
13
+ _errors?: ValidationError[];
14
+ /** Collect errors flag */
15
+ collectErrors?: boolean;
12
16
  }
13
17
  /**
14
18
  * Compile a shape descriptor into an optimized filter function
@@ -25,35 +25,118 @@ function compile(shape, opts = {}) {
25
25
  const config = shape.__isBuilder ? shape.config : shape;
26
26
  const primType = config.type;
27
27
  const defaultVal = config.default;
28
+ const fieldPath = opts._parentPath || 'field';
28
29
  return (value) => {
29
30
  let val = value == null ? (defaultVal ?? value) : (0, sanitizer_1.coercePrimitive)(primType, value);
31
+ let error = null;
32
+ // Helper to create error
33
+ const createError = (message, code) => {
34
+ const customMsg = typeof config.errorMessage === 'function'
35
+ ? config.errorMessage(val, fieldPath)
36
+ : config.errorMessage;
37
+ return {
38
+ field: fieldPath,
39
+ message: customMsg || message,
40
+ code: config.errorCodes?.[code] || code,
41
+ value: val,
42
+ };
43
+ };
44
+ // Required check
45
+ if (config.required && val == null) {
46
+ error = createError(`${fieldPath} is required`, 'required');
47
+ if (opts.collectErrors && opts._errors) {
48
+ opts._errors.push(error);
49
+ }
50
+ return defaultVal ?? undefined;
51
+ }
30
52
  // Perform validation and transformation if value is not null
31
53
  if (val != null) {
54
+ // Type check
55
+ if (opts.strict) {
56
+ const typeValid = (primType === 'string' && typeof val === 'string') ||
57
+ (primType === 'number' && typeof val === 'number') ||
58
+ (primType === 'boolean' && typeof val === 'boolean') ||
59
+ (primType === 'any');
60
+ if (!typeValid) {
61
+ error = createError(`${fieldPath} must be of type ${primType}`, 'type');
62
+ if (opts.collectErrors && opts._errors) {
63
+ opts._errors.push(error);
64
+ }
65
+ return defaultVal ?? undefined;
66
+ }
67
+ }
32
68
  // String validations
33
69
  if (primType === 'string' && typeof val === 'string') {
34
70
  val = (0, sanitizer_1.trimString)(val);
35
- if (config.min !== undefined && val.length < config.min)
71
+ if (config.min !== undefined && val.length < config.min) {
72
+ error = createError(`${fieldPath} must be at least ${config.min} characters`, 'min');
73
+ if (opts.collectErrors && opts._errors) {
74
+ opts._errors.push(error);
75
+ }
36
76
  return defaultVal ?? undefined;
37
- if (config.max !== undefined && val.length > config.max)
77
+ }
78
+ if (config.max !== undefined && val.length > config.max) {
79
+ error = createError(`${fieldPath} must be at most ${config.max} characters`, 'max');
80
+ if (opts.collectErrors && opts._errors) {
81
+ opts._errors.push(error);
82
+ }
38
83
  return defaultVal ?? undefined;
39
- if (config.regex && !config.regex.test(val))
84
+ }
85
+ if (config.regex && !config.regex.test(val)) {
86
+ error = createError(`${fieldPath} does not match required pattern`, 'pattern');
87
+ if (opts.collectErrors && opts._errors) {
88
+ opts._errors.push(error);
89
+ }
40
90
  return defaultVal ?? undefined;
91
+ }
41
92
  if (config.email) {
42
93
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
43
- if (!emailRegex.test(val))
94
+ if (!emailRegex.test(val)) {
95
+ error = createError(`${fieldPath} must be a valid email address`, 'email');
96
+ if (opts.collectErrors && opts._errors) {
97
+ opts._errors.push(error);
98
+ }
44
99
  return defaultVal ?? undefined;
100
+ }
45
101
  }
46
102
  }
47
103
  // Number validations
48
104
  if (primType === 'number' && typeof val === 'number') {
49
- if (config.min !== undefined && val < config.min)
105
+ if (config.min !== undefined && val < config.min) {
106
+ error = createError(`${fieldPath} must be at least ${config.min}`, 'min');
107
+ if (opts.collectErrors && opts._errors) {
108
+ opts._errors.push(error);
109
+ }
50
110
  return defaultVal ?? undefined;
51
- if (config.max !== undefined && val > config.max)
111
+ }
112
+ if (config.max !== undefined && val > config.max) {
113
+ error = createError(`${fieldPath} must be at most ${config.max}`, 'max');
114
+ if (opts.collectErrors && opts._errors) {
115
+ opts._errors.push(error);
116
+ }
52
117
  return defaultVal ?? undefined;
118
+ }
53
119
  }
54
120
  // Custom validation
55
- if (config.validate && !config.validate(val)) {
56
- return defaultVal ?? undefined;
121
+ if (config.validate) {
122
+ try {
123
+ if (!config.validate(val)) {
124
+ error = createError(`${fieldPath} failed validation`, 'custom');
125
+ if (opts.collectErrors && opts._errors) {
126
+ opts._errors.push(error);
127
+ }
128
+ return defaultVal ?? undefined;
129
+ }
130
+ }
131
+ catch (e) {
132
+ if (opts.dev)
133
+ (opts.logger ?? console.warn)('[payload-guard] validation error: ' + String(e), 'warn');
134
+ error = createError(`${fieldPath} validation threw an error`, 'custom');
135
+ if (opts.collectErrors && opts._errors) {
136
+ opts._errors.push(error);
137
+ }
138
+ return defaultVal ?? undefined;
139
+ }
57
140
  }
58
141
  // Transformation
59
142
  if (config.transform) {
@@ -97,8 +180,17 @@ function compile(shape, opts = {}) {
97
180
  if (isArrayShape(shape)) {
98
181
  const itemFn = compile(shape.item, { ...opts, _parentPath: (opts._parentPath || '') + '[]' });
99
182
  return (value) => {
100
- if (!Array.isArray(value))
183
+ if (!Array.isArray(value)) {
184
+ if (opts.collectErrors && opts._errors) {
185
+ opts._errors.push({
186
+ field: opts._parentPath || 'array',
187
+ message: `${opts._parentPath || 'array'} must be an array`,
188
+ code: 'type',
189
+ value: value,
190
+ });
191
+ }
101
192
  return [];
193
+ }
102
194
  // Memory safety: slice array if maxArrayLength is set
103
195
  let arr = value;
104
196
  if (opts.maxArrayLength && arr.length > opts.maxArrayLength) {
@@ -108,7 +200,7 @@ function compile(shape, opts = {}) {
108
200
  }
109
201
  arr = arr.slice(0, opts.maxArrayLength);
110
202
  }
111
- return arr.map(item => {
203
+ return arr.map((item, idx) => {
112
204
  try {
113
205
  return itemFn(item);
114
206
  }
@@ -121,7 +213,7 @@ function compile(shape, opts = {}) {
121
213
  }).filter(v => v !== undefined);
122
214
  };
123
215
  }
124
- // Object shape - compile each field
216
+ // Object shape - compile each field (nested object support)
125
217
  const fieldFilters = {};
126
218
  const shapeObj = shape;
127
219
  for (const key of Object.keys(shapeObj)) {
@@ -139,7 +231,33 @@ function compile(shape, opts = {}) {
139
231
  }
140
232
  continue;
141
233
  }
142
- fieldFilters[key] = compile(shapeObj[key], { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
234
+ const fieldDescriptor = shapeObj[key];
235
+ // Check if this is a FieldConfig with nested shape
236
+ if (isFieldConfig(fieldDescriptor)) {
237
+ const config = fieldDescriptor.__isBuilder
238
+ ? fieldDescriptor.config
239
+ : fieldDescriptor;
240
+ if (config.shape) {
241
+ // Nested object shape - compile both the field validator and nested shape
242
+ const fieldValidator = compile(fieldDescriptor, { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
243
+ const nestedShapeFn = compile(config.shape, { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
244
+ fieldFilters[key] = (value) => {
245
+ const validated = fieldValidator(value);
246
+ if (validated == null)
247
+ return validated;
248
+ // Apply nested shape filtering
249
+ return nestedShapeFn(validated);
250
+ };
251
+ }
252
+ else {
253
+ // Simple field - just compile the field validator
254
+ fieldFilters[key] = compile(fieldDescriptor, { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
255
+ }
256
+ }
257
+ else {
258
+ // Regular shape descriptor (primitive, nested object, array, etc.)
259
+ fieldFilters[key] = compile(fieldDescriptor, { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
260
+ }
143
261
  }
144
262
  return (value) => {
145
263
  if (value == null || typeof value !== 'object')
@@ -78,6 +78,8 @@ function createBuilder(type) {
78
78
  transform(fn) { config.transform = fn; return builder; },
79
79
  validate(fn) { config.validate = fn; return builder; },
80
80
  from(key) { config.from = key; return builder; },
81
+ error(msg) { config.errorMessage = msg; return builder; },
82
+ errorCodes(codes) { config.errorCodes = codes; return builder; },
81
83
  };
82
84
  Object.setPrototypeOf(builder, proto);
83
85
  if (type === 'string') {
@@ -3,6 +3,17 @@
3
3
  * Provides strong TypeScript inference from shape definitions
4
4
  */
5
5
  export type PrimitiveType = 'string' | 'number' | 'boolean' | 'any';
6
+ export interface ValidationError {
7
+ field: string;
8
+ message: string;
9
+ code?: string;
10
+ value?: unknown;
11
+ }
12
+ export interface ValidationResult<T = unknown> {
13
+ data: T;
14
+ errors: ValidationError[];
15
+ valid: boolean;
16
+ }
6
17
  export interface FieldConfig<T extends PrimitiveType = PrimitiveType> {
7
18
  type: T;
8
19
  required?: boolean;
@@ -16,6 +27,20 @@ export interface FieldConfig<T extends PrimitiveType = PrimitiveType> {
16
27
  from?: string;
17
28
  /** Custom validation function: return false if invalid */
18
29
  validate?: (v: any) => boolean;
30
+ /** Custom error message for validation failures */
31
+ errorMessage?: string | ((value: unknown, field: string) => string);
32
+ /** Error codes for different validation failures */
33
+ errorCodes?: {
34
+ required?: string;
35
+ type?: string;
36
+ min?: string;
37
+ max?: string;
38
+ pattern?: string;
39
+ email?: string;
40
+ custom?: string;
41
+ };
42
+ /** Nested shape for object validation */
43
+ shape?: ShapeDescriptor;
19
44
  }
20
45
  export interface ShapeBuilder<T extends PrimitiveType> {
21
46
  (value: unknown): InferPrimitive<T>;
@@ -27,6 +52,10 @@ export interface ShapeBuilder<T extends PrimitiveType> {
27
52
  transform(fn: (v: InferPrimitive<T>) => any): ShapeBuilder<T>;
28
53
  validate(fn: (v: InferPrimitive<T>) => boolean): ShapeBuilder<T>;
29
54
  from(key: string): ShapeBuilder<T>;
55
+ /** Set custom error message */
56
+ error(msg: string | ((v: unknown, field: string) => string)): ShapeBuilder<T>;
57
+ /** Set error codes */
58
+ errorCodes(codes: FieldConfig<T>['errorCodes']): ShapeBuilder<T>;
30
59
  }
31
60
  export interface StringBuilder extends ShapeBuilder<'string'> {
32
61
  min(len: number): StringBuilder;
@@ -36,18 +65,29 @@ export interface StringBuilder extends ShapeBuilder<'string'> {
36
65
  trim(): StringBuilder;
37
66
  toLowerCase(): StringBuilder;
38
67
  toUpperCase(): StringBuilder;
68
+ error(msg: string | ((v: unknown, field: string) => string)): StringBuilder;
69
+ errorCodes(codes: FieldConfig<'string'>['errorCodes']): StringBuilder;
39
70
  }
40
71
  export interface NumberBuilder extends ShapeBuilder<'number'> {
41
72
  min(val: number): NumberBuilder;
42
73
  max(val: number): NumberBuilder;
43
74
  integer(): NumberBuilder;
44
75
  positive(): NumberBuilder;
76
+ error(msg: string | ((v: unknown, field: string) => string)): NumberBuilder;
77
+ errorCodes(codes: FieldConfig<'number'>['errorCodes']): NumberBuilder;
45
78
  }
46
79
  export interface BooleanBuilder extends ShapeBuilder<'boolean'> {
80
+ error(msg: string | ((v: unknown, field: string) => string)): BooleanBuilder;
81
+ errorCodes(codes: FieldConfig<'boolean'>['errorCodes']): BooleanBuilder;
47
82
  }
48
83
  export interface AnyBuilder extends ShapeBuilder<'any'> {
84
+ error(msg: string | ((v: unknown, field: string) => string)): AnyBuilder;
85
+ errorCodes(codes: FieldConfig<'any'>['errorCodes']): AnyBuilder;
49
86
  }
50
87
  export type InferPrimitive<T extends PrimitiveType> = T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : T extends 'any' ? unknown : never;
88
+ export type InferFieldConfig<T extends FieldConfig> = T extends {
89
+ shape: infer S;
90
+ } ? InferShape<S & ShapeDescriptor> : T extends FieldConfig<infer P> ? InferPrimitive<P> : unknown;
51
91
  export type ShapeDescriptor = PrimitiveType | FieldConfig | {
52
92
  [key: string]: ShapeDescriptor;
53
93
  } | ArrayShapeDescriptor | CompiledFilter | ShapeBuilder<any>;
@@ -56,7 +96,9 @@ export interface ArrayShapeDescriptor {
56
96
  readonly item: ShapeDescriptor;
57
97
  }
58
98
  export type CompiledFilter<T = unknown> = (value: unknown) => T;
59
- export type InferShape<S extends ShapeDescriptor> = S extends PrimitiveType ? InferPrimitive<S> : S extends FieldConfig<infer T> ? InferPrimitive<T> : S extends ArrayShapeDescriptor ? InferShape<S['item']>[] : S extends CompiledFilter<infer T> ? T : S extends object ? {
99
+ export type InferShape<S extends ShapeDescriptor> = S extends PrimitiveType ? InferPrimitive<S> : S extends FieldConfig<infer T> ? InferFieldConfig<S & FieldConfig<T>> : S extends ArrayShapeDescriptor ? InferShape<S['item']>[] : S extends CompiledFilter<infer T> ? T : S extends {
100
+ [key: string]: any;
101
+ } ? {
60
102
  [K in keyof S]: InferShape<S[K] & ShapeDescriptor>;
61
103
  } : unknown;
62
104
  export interface GuardConfig {
@@ -96,6 +138,8 @@ export interface GuardConfig {
96
138
  skipNonSerializable?: boolean;
97
139
  /** Log names of fields removed during filtering (debug mode) */
98
140
  logRemovedFields?: boolean;
141
+ /** Collect validation errors instead of silently failing */
142
+ collectErrors?: boolean;
99
143
  }
100
144
  export interface GuardMiddlewareOptions extends GuardConfig {
101
145
  /** Sanitize request body */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-guard-filter",
3
- "version": "1.6.2",
3
+ "version": "1.8.0",
4
4
  "description": "Lightweight, zero-dependency shape-based payload filtering and sanitization for Node.js and browser",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -86,7 +86,7 @@
86
86
  "url": "https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-.git"
87
87
  },
88
88
  "bugs": {
89
- "url": "https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-/issues"
89
+ "url": "https://mypackagedoc.vercel.app/package/payload-guard-filter"
90
90
  },
91
- "homepage": "https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-#readme"
91
+ "homepage": "https://mypackagedoc.vercel.app/package/payload-guard-filter"
92
92
  }