payload-guard-filter 1.3.2 → 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 +36 -0
- 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
|
@@ -77,6 +77,42 @@ const safeData = userShape(rawData);
|
|
|
77
77
|
// Result: { id: 1, name: 'John Doe', email: 'john@example.com' }
|
|
78
78
|
```
|
|
79
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
|
+
|
|
80
116
|
---
|
|
81
117
|
|
|
82
118
|
## ⚡ Performance
|
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