payload-guard-filter 1.3.1 → 1.4.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 +39 -293
- package/dist/core/compiler.js +54 -13
- package/dist/core/filter.d.ts +8 -29
- package/dist/core/filter.js +61 -40
- package/dist/core/types.d.ts +37 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# payload-guard
|
|
2
|
+
|
|
2
3
|
> Part of the [Professional Node.js Backend Toolkit](https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-)
|
|
4
|
+
|
|
3
5
|
<p align="center">
|
|
4
6
|
<strong>🛡️ Lightweight, zero-dependency shape-based payload filtering and sanitization</strong>
|
|
5
7
|
</p>
|
|
6
8
|
|
|
7
|
-
|
|
8
9
|
<p align="center">
|
|
9
10
|
<img src="https://img.shields.io/badge/bundle%20size-%3C8KB-brightgreen" alt="Bundle Size">
|
|
10
11
|
<img src="https://img.shields.io/badge/dependencies-0-blue" alt="Zero Dependencies">
|
|
@@ -45,10 +46,6 @@ graph LR
|
|
|
45
46
|
|
|
46
47
|
```bash
|
|
47
48
|
npm install payload-guard
|
|
48
|
-
# or
|
|
49
|
-
yarn add payload-guard
|
|
50
|
-
# or
|
|
51
|
-
pnpm add payload-guard
|
|
52
49
|
```
|
|
53
50
|
|
|
54
51
|
---
|
|
@@ -80,318 +77,67 @@ const safeData = userShape(rawData);
|
|
|
80
77
|
// Result: { id: 1, name: 'John Doe', email: 'john@example.com' }
|
|
81
78
|
```
|
|
82
79
|
|
|
83
|
-
###
|
|
80
|
+
### Advanced Validation (v1.4+)
|
|
84
81
|
|
|
85
82
|
```typescript
|
|
86
|
-
import express from 'express';
|
|
87
|
-
import { guard, guardMiddleware } from 'payload-guard';
|
|
88
|
-
|
|
89
|
-
const app = express();
|
|
90
|
-
app.use(express.json());
|
|
91
|
-
|
|
92
|
-
// Apply middleware
|
|
93
|
-
app.use(guardMiddleware({
|
|
94
|
-
sanitizeBody: true,
|
|
95
|
-
sensitiveFields: ['password', 'token'],
|
|
96
|
-
devMode: process.env.NODE_ENV === 'development',
|
|
97
|
-
}));
|
|
98
|
-
|
|
99
83
|
const userShape = guard.shape({
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
app.post('/users', (req, res) => {
|
|
106
|
-
const user = createUser(req.body);
|
|
107
|
-
res.guardJson(userShape, user); // ✅ Filtered response
|
|
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(),
|
|
108
89
|
});
|
|
109
|
-
|
|
110
|
-
app.listen(3000);
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
### Frontend Usage
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
import { validateShape } from 'payload-guard/client';
|
|
117
|
-
|
|
118
|
-
const userShape = { id: 'number', name: 'string', email: 'string' };
|
|
119
|
-
const validateUser = validateShape(userShape, { devMode: true });
|
|
120
|
-
|
|
121
|
-
// Fetch and validate
|
|
122
|
-
const user = await fetch('/api/user')
|
|
123
|
-
.then(r => r.json())
|
|
124
|
-
.then(validateUser);
|
|
125
|
-
|
|
126
|
-
// Dev mode warnings:
|
|
127
|
-
// ⚠️ Unexpected field "createdAt" in response
|
|
128
|
-
// ⚠️ Missing field "email" in response
|
|
129
90
|
```
|
|
130
91
|
|
|
131
92
|
---
|
|
132
93
|
|
|
133
94
|
## 📖 API Reference
|
|
134
95
|
|
|
135
|
-
### `guard.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
- `
|
|
151
|
-
- `
|
|
152
|
-
- `
|
|
153
|
-
|
|
154
|
-
**Field config options:**
|
|
155
|
-
```typescript
|
|
156
|
-
{
|
|
157
|
-
type: 'string',
|
|
158
|
-
required: false, // Optional field
|
|
159
|
-
default: 'value', // Default value if missing
|
|
160
|
-
}
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
### `guard.array(itemShape)`
|
|
164
|
-
|
|
165
|
-
Create an array filter.
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
const postsShape = guard.shape({
|
|
169
|
-
posts: guard.array({
|
|
170
|
-
id: 'number',
|
|
171
|
-
title: 'string',
|
|
172
|
-
}),
|
|
173
|
-
});
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### `guardMiddleware(options)`
|
|
177
|
-
|
|
178
|
-
Express middleware for automatic sanitization.
|
|
179
|
-
|
|
180
|
-
```typescript
|
|
181
|
-
app.use(guardMiddleware({
|
|
182
|
-
sanitizeBody: true, // Filter req.body
|
|
183
|
-
requestShape: userShape, // Shape for request body
|
|
184
|
-
filterResponse: true, // Auto-filter all res.json()
|
|
185
|
-
sensitiveFields: [], // Extra sensitive field names
|
|
186
|
-
devMode: false, // Enable dev warnings
|
|
187
|
-
}));
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### `res.guardJson(shape, data)`
|
|
191
|
-
|
|
192
|
-
Added by middleware — send filtered JSON response.
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
app.get('/user', (req, res) => {
|
|
196
|
-
res.guardJson(userShape, userData);
|
|
197
|
-
});
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### `validateShape(shape, options?)` (Client)
|
|
201
|
-
|
|
202
|
-
Create a validator for frontend use.
|
|
203
|
-
|
|
204
|
-
```typescript
|
|
205
|
-
import { validateShape } from 'payload-guard/client';
|
|
206
|
-
|
|
207
|
-
const validate = validateShape(userShape, {
|
|
208
|
-
devMode: true, // Log warnings
|
|
209
|
-
strict: false, // Throw on errors
|
|
210
|
-
});
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
---
|
|
214
|
-
|
|
215
|
-
## 🔒 Security
|
|
216
|
-
|
|
217
|
-
### Default Sensitive Fields
|
|
218
|
-
|
|
219
|
-
These fields are **automatically removed** from all outputs:
|
|
220
|
-
|
|
221
|
-
- `password`, `password_hash`, `password_reset_token`, `pwd`
|
|
222
|
-
- `token`, `access_token`, `refresh_token`, `auth_token`
|
|
223
|
-
- `secret`, `api_key`, `private_key`, `encryption_key`
|
|
224
|
-
- `authorization`, `auth`, `session_id`
|
|
225
|
-
- `ssn`, `credit_card`, `cvv`, `card_number`
|
|
226
|
-
|
|
227
|
-
### Add Custom Sensitive Fields
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
guard.config({
|
|
231
|
-
sensitiveFields: ['internal_id', 'admin_notes', 'salary'],
|
|
232
|
-
});
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
---
|
|
236
|
-
|
|
237
|
-
## 🏗️ Nested Objects & Arrays
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
const postShape = guard.shape({
|
|
241
|
-
id: 'number',
|
|
242
|
-
title: 'string',
|
|
243
|
-
author: guard.shape({
|
|
244
|
-
id: 'number',
|
|
245
|
-
name: 'string',
|
|
246
|
-
// author.password, author.token auto-removed!
|
|
247
|
-
}),
|
|
248
|
-
tags: guard.array({
|
|
249
|
-
id: 'number',
|
|
250
|
-
name: 'string',
|
|
251
|
-
}),
|
|
252
|
-
comments: guard.array(
|
|
253
|
-
guard.shape({
|
|
254
|
-
id: 'number',
|
|
255
|
-
text: 'string',
|
|
256
|
-
user: guard.shape({
|
|
257
|
-
id: 'number',
|
|
258
|
-
name: 'string',
|
|
259
|
-
}),
|
|
260
|
-
})
|
|
261
|
-
),
|
|
262
|
-
});
|
|
263
|
-
```
|
|
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)
|
|
264
115
|
|
|
265
116
|
---
|
|
266
117
|
|
|
267
118
|
## ⚡ Performance
|
|
268
119
|
|
|
269
|
-
|
|
|
270
|
-
|
|
271
|
-
|
|
|
272
|
-
|
|
|
273
|
-
|
|
|
274
|
-
| Bundle size | <8KB | 50KB+ | 70KB+ |
|
|
275
|
-
| Dependencies | 0 | 5+ | 10+ |
|
|
276
|
-
|
|
277
|
-
---
|
|
278
|
-
|
|
279
|
-
## 🏢 Enterprise Features (v1.2.0+)
|
|
120
|
+
| Benchmark | ops/sec | avg (ms) |
|
|
121
|
+
|-----------|---------|----------|
|
|
122
|
+
| **Small payload** (5 fields) | 449,365 | **0.0022ms** |
|
|
123
|
+
| **Medium payload** (50 posts) | 7,791 | **0.1284ms** |
|
|
124
|
+
| **Large payload** (1000 users) | 246 | **4.0724ms** |
|
|
280
125
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
### 🛡️ 1. Header Sanitization
|
|
284
|
-
Automatically remove sensitive headers like `Authorization` or `Cookie` before your business logic handles the request.
|
|
285
|
-
```ts
|
|
286
|
-
app.use(guardMiddleware({
|
|
287
|
-
sanitizeHeaders: true,
|
|
288
|
-
sensitiveHeaders: ['x-api-key', 'session-id'] // optional extras
|
|
289
|
-
}));
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### 🧠 2. Memory Safety (`maxArrayLength`)
|
|
293
|
-
Prevent DoS attacks via extremely large arrays by automatically truncating them to a safe limit.
|
|
294
|
-
```ts
|
|
295
|
-
const shape = guard.shape({ items: guard.array('string') }, { maxArrayLength: 1000 });
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### ⚡ 3. Async Safety (`maxPayloadSize`)
|
|
299
|
-
Avoid processing massive JSON payloads that could block the event loop.
|
|
300
|
-
```ts
|
|
301
|
-
app.use(guardMiddleware({
|
|
302
|
-
maxPayloadSize: 1024 * 512, // 512KB
|
|
303
|
-
skipLargePayload: true // Skips filtering if too large
|
|
304
|
-
}));
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
### ⏱️ 4. Middleware Timing Stats
|
|
308
|
-
Track exactly how much time Payload Guard adds to your request cycle.
|
|
309
|
-
```
|
|
310
|
-
[payload-guard] [POST] /api/data: reduced 15KB -> 4KB (73%) in 0.12ms
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### 📊 5. Human-Readable Metrics
|
|
314
|
-
Visibility into bandwidth savings with formatted byte sizes and percentages.
|
|
315
|
-
|
|
316
|
-
### 🛠️ 6. CLI Type Sync
|
|
317
|
-
Generate frontend TypeScript types from your backend shapes with one command.
|
|
318
|
-
```bash
|
|
319
|
-
npx payload-guard sync
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
### ⚠️ 7. Route-Aware Dev Warnings
|
|
323
|
-
Dev mode warnings now include the full method, path, and nested field locations (e.g., `user.profile.password`) for faster debugging.
|
|
324
|
-
|
|
325
|
-
### 🛣️ 8. Ignore Routes
|
|
326
|
-
Skip processing for high-volume or incompatible routes like file uploads or health checks.
|
|
327
|
-
```ts
|
|
328
|
-
app.use(guardMiddleware({ ignoreRoutes: ['/health', '/upload'] }));
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
### 🏗️ 9. Shared Schemas & Examples
|
|
332
|
-
Built-in support for reusable schemas across your monorepo and a `examples/real-world` project for reference.
|
|
333
|
-
|
|
334
|
-
### 🚀 10. Performance Benchmarks
|
|
335
|
-
A dedicated benchmark suite to verify sub-millisecond overhead. `npm run benchmark`.
|
|
126
|
+
> **Memory Usage**: ~121 MB Heap Used (Stable)
|
|
336
127
|
|
|
337
128
|
---
|
|
338
129
|
|
|
339
|
-
##
|
|
340
|
-
|
|
341
|
-
### 🛡️ Fail-Safe Mode (`failOpen`)
|
|
342
|
-
In production, we prioritize availability. If filtering fails for any reason, `failOpen: true` (default) ensures the original data is sent instead of breaking the request.
|
|
130
|
+
## 🛡️ Fail-Safe Design
|
|
343
131
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
### 🔍 Detailed Debug Logs
|
|
348
|
-
Enable `logRemovedFields: true` to see exactly which fields were stripped from your payloads:
|
|
349
|
-
`[payload-guard] /api/user Removed fields: password, token, internal_id`
|
|
132
|
+
Built for production reliability:
|
|
133
|
+
- **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.
|
|
134
|
+
- **Async-Only**: All processing is non-blocking to ensure zero impact on event loop latency.
|
|
350
135
|
|
|
351
136
|
---
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
The core is zero-dependency and framework-agnostic. Use it anywhere:
|
|
356
|
-
|
|
357
|
-
### Hono / Cloudflare Workers
|
|
358
|
-
```ts
|
|
359
|
-
app.post('/user', async (c) => {
|
|
360
|
-
const body = await c.req.json();
|
|
361
|
-
return c.json(userShape(body));
|
|
362
|
-
});
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
### Fastify
|
|
366
|
-
```ts
|
|
367
|
-
fastify.post('/user', (req, reply) => {
|
|
368
|
-
reply.send(userShape(req.body));
|
|
369
|
-
});
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
---
|
|
373
|
-
|
|
374
|
-
## 🛑 When NOT to use
|
|
375
|
-
|
|
376
|
-
Payload Guard is optimized for JSON APIs. It is **not** suitable for:
|
|
377
|
-
1. **Binary Data**: PDFs, Images, or raw Buffers.
|
|
378
|
-
2. **File Streams**: Use `multer` or similar for multipart data.
|
|
379
|
-
3. **Heavy Computation**: Don't use it on payloads > 10MB without adjusting `maxPayloadSize`.
|
|
380
|
-
|
|
381
|
-
---
|
|
382
|
-
|
|
383
|
-
## 🛠️ CLI
|
|
384
|
-
|
|
385
|
-
```bash
|
|
386
|
-
npx payload-guard init
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
Creates example files in your project:
|
|
390
|
-
- `shapes.ts` — Example shape definitions
|
|
391
|
-
- `server-example.ts` — Express middleware setup
|
|
137
|
+
### ⛑️ Maintained actively.
|
|
138
|
+
**Bug fixes usually within 24–48 hours.**
|
|
392
139
|
|
|
393
140
|
---
|
|
394
|
-
|
|
395
141
|
## 📄 License
|
|
396
142
|
|
|
397
143
|
MIT
|
package/dist/core/compiler.js
CHANGED
|
@@ -20,6 +20,55 @@ function compile(shape, opts = {}) {
|
|
|
20
20
|
if (isShapeFunction(shape)) {
|
|
21
21
|
return shape.compile;
|
|
22
22
|
}
|
|
23
|
+
// Field config or Builder
|
|
24
|
+
if (isFieldConfig(shape)) {
|
|
25
|
+
const config = shape.__isBuilder ? shape.config : shape;
|
|
26
|
+
const primType = config.type;
|
|
27
|
+
const defaultVal = config.default;
|
|
28
|
+
return (value) => {
|
|
29
|
+
let val = value == null ? (defaultVal ?? value) : (0, sanitizer_1.coercePrimitive)(primType, value);
|
|
30
|
+
// Perform validation and transformation if value is not null
|
|
31
|
+
if (val != null) {
|
|
32
|
+
// String validations
|
|
33
|
+
if (primType === 'string' && typeof val === 'string') {
|
|
34
|
+
val = (0, sanitizer_1.trimString)(val);
|
|
35
|
+
if (config.min !== undefined && val.length < config.min)
|
|
36
|
+
return defaultVal ?? undefined;
|
|
37
|
+
if (config.max !== undefined && val.length > config.max)
|
|
38
|
+
return defaultVal ?? undefined;
|
|
39
|
+
if (config.regex && !config.regex.test(val))
|
|
40
|
+
return defaultVal ?? undefined;
|
|
41
|
+
if (config.email) {
|
|
42
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
43
|
+
if (!emailRegex.test(val))
|
|
44
|
+
return defaultVal ?? undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Number validations
|
|
48
|
+
if (primType === 'number' && typeof val === 'number') {
|
|
49
|
+
if (config.min !== undefined && val < config.min)
|
|
50
|
+
return defaultVal ?? undefined;
|
|
51
|
+
if (config.max !== undefined && val > config.max)
|
|
52
|
+
return defaultVal ?? undefined;
|
|
53
|
+
}
|
|
54
|
+
// Custom validation
|
|
55
|
+
if (config.validate && !config.validate(val)) {
|
|
56
|
+
return defaultVal ?? undefined;
|
|
57
|
+
}
|
|
58
|
+
// Transformation
|
|
59
|
+
if (config.transform) {
|
|
60
|
+
try {
|
|
61
|
+
val = config.transform(val);
|
|
62
|
+
}
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (opts.dev)
|
|
65
|
+
(opts.logger ?? console.warn)('[payload-guard] transform error: ' + String(e));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return val;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
23
72
|
// If already a regular function, wrap it
|
|
24
73
|
if (typeof shape === 'function') {
|
|
25
74
|
return (value) => shape(value);
|
|
@@ -44,17 +93,6 @@ function compile(shape, opts = {}) {
|
|
|
44
93
|
return primType === 'string' ? (0, sanitizer_1.trimString)(coerced) : coerced;
|
|
45
94
|
};
|
|
46
95
|
}
|
|
47
|
-
// Field config with type
|
|
48
|
-
if (isFieldConfig(shape)) {
|
|
49
|
-
const primType = shape.type;
|
|
50
|
-
const defaultVal = shape.default;
|
|
51
|
-
return (value) => {
|
|
52
|
-
if (value == null)
|
|
53
|
-
return defaultVal ?? value;
|
|
54
|
-
const coerced = (0, sanitizer_1.coercePrimitive)(primType, value);
|
|
55
|
-
return primType === 'string' ? (0, sanitizer_1.trimString)(coerced) : coerced;
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
96
|
// Array shape
|
|
59
97
|
if (isArrayShape(shape)) {
|
|
60
98
|
const itemFn = compile(shape.item, { ...opts, _parentPath: (opts._parentPath || '') + '[]' });
|
|
@@ -154,8 +192,11 @@ function isArrayShape(value) {
|
|
|
154
192
|
* Type guard for FieldConfig
|
|
155
193
|
*/
|
|
156
194
|
function isFieldConfig(value) {
|
|
157
|
-
|
|
158
|
-
|
|
195
|
+
if (value === null)
|
|
196
|
+
return false;
|
|
197
|
+
if (typeof value === 'function' && value.__isBuilder)
|
|
198
|
+
return true;
|
|
199
|
+
return (typeof value === 'object' &&
|
|
159
200
|
!isShapeFunction(value) &&
|
|
160
201
|
'type' in value &&
|
|
161
202
|
typeof value.type === 'string');
|
package/dist/core/filter.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ShapeDescriptor, ShapeFunction, GuardConfig } from './types';
|
|
1
|
+
import { ShapeDescriptor, ShapeFunction, GuardConfig, StringBuilder, NumberBuilder, BooleanBuilder, AnyBuilder, ArrayShapeDescriptor } from './types';
|
|
2
2
|
import { defaultSensitive } from './security';
|
|
3
3
|
/**
|
|
4
4
|
* Configure global guard settings
|
|
@@ -10,39 +10,18 @@ export declare function config(opts: GuardConfig): void;
|
|
|
10
10
|
export declare function getConfig(): GuardConfig;
|
|
11
11
|
/**
|
|
12
12
|
* Build a shape filter function from a shape descriptor
|
|
13
|
-
* Returns a callable function that filters objects to match the shape
|
|
14
13
|
*/
|
|
15
14
|
export declare function buildShape<S extends ShapeDescriptor>(shape: S, opts?: GuardConfig): ShapeFunction<S>;
|
|
16
|
-
/**
|
|
17
|
-
* Main guard API object
|
|
18
|
-
*/
|
|
19
15
|
export declare const guard: {
|
|
20
|
-
/**
|
|
21
|
-
* Create a shape filter from a descriptor
|
|
22
|
-
* @example
|
|
23
|
-
* const userShape = guard.shape({ id: 'number', name: 'string' });
|
|
24
|
-
* const filtered = userShape(userData);
|
|
25
|
-
*/
|
|
26
|
-
shape: <S extends ShapeDescriptor>(descriptor: S, opts?: GuardConfig) => ShapeFunction<S>;
|
|
27
|
-
/** Compile a descriptor into a fast filter function directly */
|
|
28
|
-
compile: <S extends ShapeDescriptor>(descriptor: S, opts?: GuardConfig) => import("./types").CompiledFilter;
|
|
29
|
-
/**
|
|
30
|
-
* Create an array shape descriptor
|
|
31
|
-
* @example
|
|
32
|
-
* const usersShape = guard.shape({ users: guard.array({ id: 'number', name: 'string' }) });
|
|
33
|
-
*/
|
|
34
|
-
array: (item: ShapeDescriptor) => import("./types").ArrayShapeDescriptor;
|
|
35
|
-
/**
|
|
36
|
-
* Configure global settings
|
|
37
|
-
*/
|
|
38
16
|
config: typeof config;
|
|
39
|
-
/**
|
|
40
|
-
* Get current configuration
|
|
41
|
-
*/
|
|
42
17
|
getConfig: typeof getConfig;
|
|
43
|
-
/**
|
|
44
|
-
* Get default sensitive fields list
|
|
45
|
-
*/
|
|
46
18
|
defaultSensitive: typeof defaultSensitive;
|
|
19
|
+
shape: typeof buildShape;
|
|
20
|
+
array: (item: ShapeDescriptor) => ArrayShapeDescriptor;
|
|
21
|
+
string: () => StringBuilder;
|
|
22
|
+
number: () => NumberBuilder;
|
|
23
|
+
boolean: () => BooleanBuilder;
|
|
24
|
+
any: () => AnyBuilder;
|
|
25
|
+
compile: (descriptor: ShapeDescriptor, opts?: GuardConfig) => import("./types").CompiledFilter;
|
|
47
26
|
};
|
|
48
27
|
export default guard;
|
package/dist/core/filter.js
CHANGED
|
@@ -38,7 +38,6 @@ function getConfig() {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Build a shape filter function from a shape descriptor
|
|
41
|
-
* Returns a callable function that filters objects to match the shape
|
|
42
41
|
*/
|
|
43
42
|
function buildShape(shape, opts) {
|
|
44
43
|
const mergedOpts = {
|
|
@@ -53,59 +52,81 @@ function buildShape(shape, opts) {
|
|
|
53
52
|
logRemovedFields: opts?.logRemovedFields ?? globalConfig.logRemovedFields,
|
|
54
53
|
};
|
|
55
54
|
const compiledFn = (0, compiler_1.compile)(shape, mergedOpts);
|
|
56
|
-
// Create callable wrapper with compile method attached
|
|
57
55
|
const wrapper = ((value) => compiledFn(value));
|
|
58
56
|
wrapper.compile = compiledFn;
|
|
59
|
-
// attach extend to allow partial shape merges
|
|
60
57
|
wrapper.extend = function (extra) {
|
|
61
|
-
// shallow merge for object shapes
|
|
62
58
|
if (typeof shape === 'object' && typeof extra === 'object') {
|
|
63
59
|
const merged = { ...shape, ...extra };
|
|
64
60
|
return buildShape(merged, opts);
|
|
65
61
|
}
|
|
66
|
-
// fallback: return a wrapper that composes both
|
|
67
62
|
return buildShape({ _v: shape, _e: extra }, opts);
|
|
68
63
|
};
|
|
69
64
|
return wrapper;
|
|
70
65
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
function createBuilder(type) {
|
|
67
|
+
const config = { type };
|
|
68
|
+
const builder = (value) => {
|
|
69
|
+
// real logic is in compile()
|
|
70
|
+
return value;
|
|
71
|
+
};
|
|
72
|
+
const proto = {
|
|
73
|
+
__isBuilder: true,
|
|
74
|
+
config,
|
|
75
|
+
required() { config.required = true; return builder; },
|
|
76
|
+
optional() { config.required = false; return builder; },
|
|
77
|
+
default(val) { config.default = val; return builder; },
|
|
78
|
+
transform(fn) { config.transform = fn; return builder; },
|
|
79
|
+
validate(fn) { config.validate = fn; return builder; },
|
|
80
|
+
};
|
|
81
|
+
Object.setPrototypeOf(builder, proto);
|
|
82
|
+
if (type === 'string') {
|
|
83
|
+
builder.min = (n) => { config.min = n; return builder; };
|
|
84
|
+
builder.max = (n) => { config.max = n; return builder; };
|
|
85
|
+
builder.email = () => { config.email = true; return builder; };
|
|
86
|
+
builder.regex = (re) => { config.regex = re; return builder; };
|
|
87
|
+
builder.trim = () => {
|
|
88
|
+
const prev = config.transform;
|
|
89
|
+
config.transform = (v) => {
|
|
90
|
+
let val = typeof v === 'string' ? v.trim() : v;
|
|
91
|
+
return prev ? prev(val) : val;
|
|
92
|
+
};
|
|
93
|
+
return builder;
|
|
94
|
+
};
|
|
95
|
+
builder.toLowerCase = () => {
|
|
96
|
+
const prev = config.transform;
|
|
97
|
+
config.transform = (v) => {
|
|
98
|
+
let val = typeof v === 'string' ? v.toLowerCase() : v;
|
|
99
|
+
return prev ? prev(val) : val;
|
|
100
|
+
};
|
|
101
|
+
return builder;
|
|
102
|
+
};
|
|
103
|
+
builder.toUpperCase = () => {
|
|
104
|
+
const prev = config.transform;
|
|
105
|
+
config.transform = (v) => {
|
|
106
|
+
let val = typeof v === 'string' ? v.toUpperCase() : v;
|
|
107
|
+
return prev ? prev(val) : val;
|
|
108
|
+
};
|
|
109
|
+
return builder;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (type === 'number') {
|
|
113
|
+
builder.min = (n) => { config.min = n; return builder; };
|
|
114
|
+
builder.max = (n) => { config.max = n; return builder; };
|
|
115
|
+
builder.integer = () => { config.validate = (v) => Number.isInteger(v); return builder; };
|
|
116
|
+
builder.positive = () => { config.min = 0; return builder; };
|
|
117
|
+
}
|
|
118
|
+
return builder;
|
|
119
|
+
}
|
|
74
120
|
exports.guard = {
|
|
75
|
-
/**
|
|
76
|
-
* Create a shape filter from a descriptor
|
|
77
|
-
* @example
|
|
78
|
-
* const userShape = guard.shape({ id: 'number', name: 'string' });
|
|
79
|
-
* const filtered = userShape(userData);
|
|
80
|
-
*/
|
|
81
|
-
shape: (descriptor, opts) => buildShape(descriptor, opts),
|
|
82
|
-
/** Compile a descriptor into a fast filter function directly */
|
|
83
|
-
compile: (descriptor, opts) => (0, compiler_1.compile)(descriptor, {
|
|
84
|
-
...opts,
|
|
85
|
-
sensitive: [...(opts?.sensitiveFields || []), ...(globalConfig.sensitiveFields || [])],
|
|
86
|
-
dev: opts?.devMode ?? globalConfig.devMode,
|
|
87
|
-
strict: opts?.strict ?? globalConfig.strict,
|
|
88
|
-
redact: opts?.redact ?? globalConfig.redact,
|
|
89
|
-
logger: opts?.logger ?? globalConfig.logger,
|
|
90
|
-
maxArrayLength: opts?.maxArrayLength ?? globalConfig.maxArrayLength,
|
|
91
|
-
}),
|
|
92
|
-
/**
|
|
93
|
-
* Create an array shape descriptor
|
|
94
|
-
* @example
|
|
95
|
-
* const usersShape = guard.shape({ users: guard.array({ id: 'number', name: 'string' }) });
|
|
96
|
-
*/
|
|
97
|
-
array: (item) => (0, compiler_1.array)(item),
|
|
98
|
-
/**
|
|
99
|
-
* Configure global settings
|
|
100
|
-
*/
|
|
101
121
|
config,
|
|
102
|
-
/**
|
|
103
|
-
* Get current configuration
|
|
104
|
-
*/
|
|
105
122
|
getConfig,
|
|
106
|
-
/**
|
|
107
|
-
* Get default sensitive fields list
|
|
108
|
-
*/
|
|
109
123
|
defaultSensitive: security_1.defaultSensitive,
|
|
124
|
+
shape: buildShape,
|
|
125
|
+
array: (item) => (0, compiler_1.array)(item),
|
|
126
|
+
string: () => createBuilder('string'),
|
|
127
|
+
number: () => createBuilder('number'),
|
|
128
|
+
boolean: () => createBuilder('boolean'),
|
|
129
|
+
any: () => createBuilder('any'),
|
|
130
|
+
compile: (descriptor, opts) => (0, compiler_1.compile)(descriptor, opts),
|
|
110
131
|
};
|
|
111
132
|
exports.default = exports.guard;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -7,11 +7,47 @@ export interface FieldConfig<T extends PrimitiveType = PrimitiveType> {
|
|
|
7
7
|
type: T;
|
|
8
8
|
required?: boolean;
|
|
9
9
|
default?: InferPrimitive<T>;
|
|
10
|
+
min?: number;
|
|
11
|
+
max?: number;
|
|
12
|
+
regex?: RegExp;
|
|
13
|
+
email?: boolean;
|
|
14
|
+
transform?: (v: any) => any;
|
|
15
|
+
/** Custom validation function: return false if invalid */
|
|
16
|
+
validate?: (v: any) => boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ShapeBuilder<T extends PrimitiveType> {
|
|
19
|
+
(value: unknown): InferPrimitive<T>;
|
|
20
|
+
readonly __isBuilder: true;
|
|
21
|
+
readonly config: FieldConfig<T>;
|
|
22
|
+
required(): ShapeBuilder<T>;
|
|
23
|
+
optional(): ShapeBuilder<T>;
|
|
24
|
+
default(val: InferPrimitive<T>): ShapeBuilder<T>;
|
|
25
|
+
transform(fn: (v: InferPrimitive<T>) => any): ShapeBuilder<T>;
|
|
26
|
+
validate(fn: (v: InferPrimitive<T>) => boolean): ShapeBuilder<T>;
|
|
27
|
+
}
|
|
28
|
+
export interface StringBuilder extends ShapeBuilder<'string'> {
|
|
29
|
+
min(len: number): StringBuilder;
|
|
30
|
+
max(len: number): StringBuilder;
|
|
31
|
+
email(): StringBuilder;
|
|
32
|
+
regex(pattern: RegExp): StringBuilder;
|
|
33
|
+
trim(): StringBuilder;
|
|
34
|
+
toLowerCase(): StringBuilder;
|
|
35
|
+
toUpperCase(): StringBuilder;
|
|
36
|
+
}
|
|
37
|
+
export interface NumberBuilder extends ShapeBuilder<'number'> {
|
|
38
|
+
min(val: number): NumberBuilder;
|
|
39
|
+
max(val: number): NumberBuilder;
|
|
40
|
+
integer(): NumberBuilder;
|
|
41
|
+
positive(): NumberBuilder;
|
|
42
|
+
}
|
|
43
|
+
export interface BooleanBuilder extends ShapeBuilder<'boolean'> {
|
|
44
|
+
}
|
|
45
|
+
export interface AnyBuilder extends ShapeBuilder<'any'> {
|
|
10
46
|
}
|
|
11
47
|
export type InferPrimitive<T extends PrimitiveType> = T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : T extends 'any' ? unknown : never;
|
|
12
48
|
export type ShapeDescriptor = PrimitiveType | FieldConfig | {
|
|
13
49
|
[key: string]: ShapeDescriptor;
|
|
14
|
-
} | ArrayShapeDescriptor | CompiledFilter
|
|
50
|
+
} | ArrayShapeDescriptor | CompiledFilter | ShapeBuilder<any>;
|
|
15
51
|
export interface ArrayShapeDescriptor {
|
|
16
52
|
readonly __isArray: true;
|
|
17
53
|
readonly item: ShapeDescriptor;
|
package/package.json
CHANGED