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 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
@@ -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
- return (value !== null &&
158
- typeof value === 'object' &&
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');
@@ -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;
@@ -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
- * Main guard API object
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;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-guard-filter",
3
- "version": "1.3.2",
3
+ "version": "1.4.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",