@idirdev/formguard 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020-2026 idirdev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # formguard
2
+
3
+ > **[EN]** Lightweight form validation and sanitization library for Node.js — 30+ built-in rules, fluent schema builder, i18n messages and HTML sanitizers.
4
+ > **[FR]** Bibliothèque légère de validation et sanitisation de formulaires pour Node.js — plus de 30 règles intégrées, constructeur de schéma fluent, messages i18n et sanitiseurs HTML.
5
+
6
+ ---
7
+
8
+ ## Features / Fonctionnalités
9
+
10
+ **[EN]**
11
+ - 30+ built-in validation rules: required, email, url, minLength, maxLength, integer, numeric, phone, creditCard, uuid, ip, date, slug, strongPassword, oneOf, matches and more
12
+ - Fluent schema builder with method chaining (`createSchema().field().required().email()...`)
13
+ - Pipe-syntax string rules: `"required|email|maxLength:255"`
14
+ - Sanitizers: `escapeHtml`, `stripTags`, `slugify`, and generic `sanitize`
15
+ - Multi-locale error messages with `setLocale` / `registerLocale`
16
+ - `abortEarly` option to stop on first error per field
17
+ - Nested field support via dot notation (`"address.city"`)
18
+ - Returns `{ valid, errors }` — easy to plug into any HTTP framework
19
+
20
+ **[FR]**
21
+ - Plus de 30 règles de validation intégrées : required, email, url, minLength, maxLength, integer, numeric, phone, creditCard, uuid, ip, date, slug, strongPassword, oneOf, matches et plus encore
22
+ - Constructeur de schéma fluent avec chaînage de méthodes (`createSchema().field().required().email()...`)
23
+ - Règles de chaîne avec syntaxe pipe : `"required|email|maxLength:255"`
24
+ - Sanitiseurs : `escapeHtml`, `stripTags`, `slugify` et `sanitize` générique
25
+ - Messages d'erreur multi-locales avec `setLocale` / `registerLocale`
26
+ - Option `abortEarly` pour s'arrêter à la première erreur par champ
27
+ - Support des champs imbriqués via la notation pointée (`"address.city"`)
28
+ - Retourne `{ valid, errors }` — facile à brancher sur n'importe quel framework HTTP
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ npm install @idirdev/formguard
36
+ ```
37
+
38
+ ---
39
+
40
+ ## API (Programmatic) / API (Programmation)
41
+
42
+ ### Object-rules style / Style règles objet
43
+
44
+ ```js
45
+ const { validate, sanitize, escapeHtml, slugify } = require('@idirdev/formguard');
46
+
47
+ const body = { email: 'alice@example.com', age: '17', bio: '<script>bad</script>' };
48
+
49
+ const result = validate(body, {
50
+ email: 'required|email',
51
+ age: 'required|integer|min:18',
52
+ bio: 'required|maxLength:500',
53
+ });
54
+
55
+ console.log(result.valid); // false
56
+ console.log(result.errors.age); // ['age must be at least 18']
57
+
58
+ // Sanitize before storing / Sanitiser avant de stocker
59
+ const cleanBio = escapeHtml(body.bio); // &lt;script&gt;bad&lt;/script&gt;
60
+ const tag = slugify('Hello World!'); // 'hello-world'
61
+ ```
62
+
63
+ ### Fluent schema builder / Constructeur de schéma fluent
64
+
65
+ ```js
66
+ const { createSchema, setLocale } = require('@idirdev/formguard');
67
+
68
+ setLocale('fr'); // switch to French messages / passer aux messages français
69
+
70
+ const schema = createSchema()
71
+ .field('username').required().minLength(3).maxLength(32)
72
+ .field('email').required().email()
73
+ .field('password').required().strongPassword()
74
+ .field('age').required().integer().min(13).max(120);
75
+
76
+ const { valid, errors } = schema.validate({
77
+ username: 'id',
78
+ email: 'not-an-email',
79
+ password: 'weak',
80
+ age: 10,
81
+ });
82
+
83
+ // errors.username → ['username must be at least 3 characters']
84
+ // errors.email → ['email must be a valid email address']
85
+ // errors.password → ['password must be at least 8 characters with uppercase, lowercase, digit and symbol']
86
+ // errors.age → ['age must be at least 13']
87
+ ```
88
+
89
+ ### Express middleware example / Exemple middleware Express
90
+
91
+ ```js
92
+ app.post('/register', (req, res) => {
93
+ const { valid, errors } = validate(req.body, {
94
+ email: 'required|email',
95
+ password: 'required|strongPassword',
96
+ name: 'required|minLength:2|maxLength:80',
97
+ });
98
+ if (!valid) return res.status(422).json({ errors });
99
+ // proceed...
100
+ });
101
+ ```
102
+
103
+ ---
104
+
105
+ ## License
106
+
107
+ MIT — idirdev
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@idirdev/formguard",
3
+ "version": "1.0.0",
4
+ "description": "Form validation and sanitization library with pipe-syntax rules and locale support.",
5
+ "main": "src/index.js",
6
+ "author": "idirdev",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/idirdev/formguard.git"
11
+ },
12
+ "keywords": [],
13
+ "engines": {
14
+ "node": ">=16.0.0"
15
+ },
16
+ "scripts": {
17
+ "test": "node --test tests/"
18
+ },
19
+ "homepage": "https://github.com/idirdev/formguard",
20
+ "bugs": {
21
+ "url": "https://github.com/idirdev/formguard/issues"
22
+ }
23
+ }
package/src/index.js ADDED
@@ -0,0 +1,355 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module formguard
5
+ * @description Form validation and sanitization library.
6
+ * Supports pipe-syntax rules, built-in validators, locale messages,
7
+ * reusable schemas, and common sanitizers.
8
+ * @author idirdev
9
+ */
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Locale messages
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const MESSAGES = {
16
+ en: {
17
+ required: 'The {field} field is required.',
18
+ email: 'The {field} field must be a valid email address.',
19
+ url: 'The {field} field must be a valid URL.',
20
+ numeric: 'The {field} field must be a number.',
21
+ alpha: 'The {field} field must contain only letters.',
22
+ alphanumeric: 'The {field} field must contain only letters and numbers.',
23
+ min: 'The {field} field must be at least {arg}.',
24
+ max: 'The {field} field must not exceed {arg}.',
25
+ minLength: 'The {field} field must be at least {arg} characters.',
26
+ maxLength: 'The {field} field must not exceed {arg} characters.',
27
+ regex: 'The {field} field format is invalid.',
28
+ in: 'The {field} field must be one of: {arg}.',
29
+ },
30
+ fr: {
31
+ required: 'Le champ {field} est obligatoire.',
32
+ email: 'Le champ {field} doit contenir une adresse e-mail valide.',
33
+ url: 'Le champ {field} doit contenir une URL valide.',
34
+ numeric: 'Le champ {field} doit être un nombre.',
35
+ alpha: 'Le champ {field} ne doit contenir que des lettres.',
36
+ alphanumeric: 'Le champ {field} ne doit contenir que des lettres et des chiffres.',
37
+ min: 'Le champ {field} doit être supérieur ou égal à {arg}.',
38
+ max: 'Le champ {field} ne doit pas dépasser {arg}.',
39
+ minLength: 'Le champ {field} doit comporter au moins {arg} caractères.',
40
+ maxLength: 'Le champ {field} ne doit pas dépasser {arg} caractères.',
41
+ regex: 'Le format du champ {field} est invalide.',
42
+ in: 'Le champ {field} doit être l\'une des valeurs suivantes : {arg}.',
43
+ },
44
+ };
45
+
46
+ let _locale = 'en';
47
+
48
+ /**
49
+ * Sets the locale for error messages.
50
+ * @param {'en'|'fr'} locale - The locale code.
51
+ * @returns {void}
52
+ * @throws {Error} If the locale is not supported.
53
+ * @example
54
+ * setLocale('fr');
55
+ */
56
+ function setLocale(locale) {
57
+ if (!MESSAGES[locale]) throw new Error('Unsupported locale: ' + locale + '. Supported: ' + Object.keys(MESSAGES).join(', '));
58
+ _locale = locale;
59
+ }
60
+
61
+ /**
62
+ * Returns the current locale code.
63
+ * @returns {string}
64
+ */
65
+ function getLocale() {
66
+ return _locale;
67
+ }
68
+
69
+ /** @private */
70
+ function _msg(rule, field, arg) {
71
+ const tpl = (MESSAGES[_locale] || MESSAGES.en)[rule] || 'The {field} field is invalid.';
72
+ const argStr = Array.isArray(arg) ? arg.join(', ') : String(arg !== undefined ? arg : '');
73
+ return tpl.replace('{field}', field).replace('{arg}', argStr);
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Built-in validators
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /**
81
+ * Map of built-in validator functions.
82
+ * Each validator receives (value, arg?) and returns true if valid.
83
+ * @type {Object.<string, function(*, *=): boolean>}
84
+ */
85
+ const VALIDATORS = {
86
+ required(value) {
87
+ if (value === null || value === undefined) return false;
88
+ if (typeof value === 'string') return value.trim().length > 0;
89
+ if (Array.isArray(value)) return value.length > 0;
90
+ return true;
91
+ },
92
+ email(value) {
93
+ if (!value && value !== 0) return true; // skip if empty (use required for that)
94
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value));
95
+ },
96
+ url(value) {
97
+ if (!value) return true;
98
+ try { new URL(String(value)); return true; } catch { return false; }
99
+ },
100
+ numeric(value) {
101
+ if (value === null || value === undefined || value === '') return true;
102
+ return !isNaN(Number(value)) && String(value).trim() !== '';
103
+ },
104
+ alpha(value) {
105
+ if (!value && value !== 0) return true;
106
+ return /^[a-zA-Z]+$/.test(String(value));
107
+ },
108
+ alphanumeric(value) {
109
+ if (!value && value !== 0) return true;
110
+ return /^[a-zA-Z0-9]+$/.test(String(value));
111
+ },
112
+ min(value, arg) {
113
+ if (value === null || value === undefined || value === '') return true;
114
+ return Number(value) >= Number(arg);
115
+ },
116
+ max(value, arg) {
117
+ if (value === null || value === undefined || value === '') return true;
118
+ return Number(value) <= Number(arg);
119
+ },
120
+ minLength(value, arg) {
121
+ if (!value) return true;
122
+ return String(value).length >= Number(arg);
123
+ },
124
+ maxLength(value, arg) {
125
+ if (!value) return true;
126
+ return String(value).length <= Number(arg);
127
+ },
128
+ regex(value, arg) {
129
+ if (!value) return true;
130
+ const re = arg instanceof RegExp ? arg : new RegExp(arg);
131
+ return re.test(String(value));
132
+ },
133
+ in(value, arg) {
134
+ if (value === null || value === undefined || value === '') return true;
135
+ const list = Array.isArray(arg) ? arg : String(arg).split(',').map(s => s.trim());
136
+ return list.includes(String(value));
137
+ },
138
+ };
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Rule parsing
142
+ // ---------------------------------------------------------------------------
143
+
144
+ /**
145
+ * Parses a pipe-syntax rule string into an array of { rule, arg } objects.
146
+ * @param {string|string[]} rules - Pipe-delimited rule string like "required|email|min:3|max:100",
147
+ * or an array of rule strings.
148
+ * @returns {Array<{ rule: string, arg: string|undefined }>}
149
+ * @example
150
+ * _parseRules('required|minLength:3|max:100');
151
+ * // => [{ rule: 'required' }, { rule: 'minLength', arg: '3' }, { rule: 'max', arg: '100' }]
152
+ */
153
+ function _parseRules(rules) {
154
+ const list = Array.isArray(rules) ? rules : String(rules).split('|');
155
+ return list
156
+ .map(r => r.trim())
157
+ .filter(Boolean)
158
+ .map(r => {
159
+ const idx = r.indexOf(':');
160
+ if (idx === -1) return { rule: r, arg: undefined };
161
+ return { rule: r.slice(0, idx).trim(), arg: r.slice(idx + 1).trim() };
162
+ });
163
+ }
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // Core validation
167
+ // ---------------------------------------------------------------------------
168
+
169
+ /**
170
+ * Validates a single value against a pipe-syntax rule string (or array).
171
+ * @param {*} value - The value to validate.
172
+ * @param {string|string[]} rules - Pipe-delimited rules, e.g. "required|email|maxLength:200".
173
+ * @param {string} [field='value'] - Field name for error messages.
174
+ * @returns {{ valid: boolean, errors: string[] }}
175
+ * @example
176
+ * validateField('', 'required|email');
177
+ * // => { valid: false, errors: ['The value field is required.'] }
178
+ */
179
+ function validateField(value, rules, field = 'value') {
180
+ const parsed = _parseRules(rules);
181
+ const errors = [];
182
+ for (const { rule, arg } of parsed) {
183
+ const fn = VALIDATORS[rule];
184
+ if (!fn) continue;
185
+ const ok = fn(value, arg);
186
+ if (!ok) errors.push(_msg(rule, field, arg));
187
+ }
188
+ return { valid: errors.length === 0, errors };
189
+ }
190
+
191
+ /**
192
+ * Validates a data object against a schema.
193
+ * @param {object} data - Plain object of field values.
194
+ * @param {Object.<string, string|string[]>} schema - Map of field name to pipe-syntax rules.
195
+ * @returns {{ valid: boolean, errors: Array<{ field: string, messages: string[] }> }}
196
+ * @throws {TypeError} If data or schema are not objects.
197
+ * @example
198
+ * const result = validate({ email: 'bad' }, { email: 'required|email' });
199
+ * // => { valid: false, errors: [{ field: 'email', messages: ['...'] }] }
200
+ */
201
+ function validate(data, schema) {
202
+ if (typeof data !== 'object' || data === null) throw new TypeError('data must be a non-null object');
203
+ if (typeof schema !== 'object' || schema === null) throw new TypeError('schema must be a non-null object');
204
+
205
+ const errors = [];
206
+ for (const [field, rules] of Object.entries(schema)) {
207
+ const value = data[field] !== undefined ? data[field] : null;
208
+ const { valid, errors: fieldErrors } = validateField(value, rules, field);
209
+ if (!valid) errors.push({ field, messages: fieldErrors });
210
+ }
211
+ return { valid: errors.length === 0, errors };
212
+ }
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Reusable schemas
216
+ // ---------------------------------------------------------------------------
217
+
218
+ /**
219
+ * Creates a reusable schema object with a validate() method bound to the definition.
220
+ * @param {Object.<string, string|string[]>} definition - Schema definition (field -> rules).
221
+ * @returns {{ validate: function(object): { valid: boolean, errors: Array } }}
222
+ * @example
223
+ * const loginSchema = createSchema({ email: 'required|email', password: 'required|minLength:8' });
224
+ * const result = loginSchema.validate({ email: 'x@y.com', password: 'secret123' });
225
+ */
226
+ function createSchema(definition) {
227
+ if (typeof definition !== 'object' || definition === null) throw new TypeError('definition must be a non-null object');
228
+ return {
229
+ /** @type {Object.<string, string|string[]>} */
230
+ definition,
231
+ /**
232
+ * Validates data against this schema.
233
+ * @param {object} data
234
+ * @returns {{ valid: boolean, errors: Array }}
235
+ */
236
+ validate(data) {
237
+ return validate(data, this.definition);
238
+ },
239
+ };
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Sanitizers
244
+ // ---------------------------------------------------------------------------
245
+
246
+ /**
247
+ * Escapes HTML special characters to prevent XSS.
248
+ * @param {string} str - Input string.
249
+ * @returns {string} Escaped string.
250
+ * @example
251
+ * escapeHtml('<script>alert(1)</script>'); // '&lt;script&gt;alert(1)&lt;/script&gt;'
252
+ */
253
+ function escapeHtml(str) {
254
+ if (typeof str !== 'string') return String(str !== undefined && str !== null ? str : '');
255
+ return str
256
+ .replace(/&/g, '&amp;')
257
+ .replace(/</g, '&lt;')
258
+ .replace(/>/g, '&gt;')
259
+ .replace(/"/g, '&quot;')
260
+ .replace(/'/g, '&#x27;');
261
+ }
262
+
263
+ /**
264
+ * Strips all HTML tags from a string.
265
+ * @param {string} str - Input string, possibly containing HTML.
266
+ * @returns {string} Plain text with tags removed.
267
+ * @example
268
+ * stripTags('<b>Hello</b> <i>world</i>'); // 'Hello world'
269
+ */
270
+ function stripTags(str) {
271
+ if (typeof str !== 'string') return String(str !== undefined && str !== null ? str : '');
272
+ return str.replace(/<[^>]*>/g, '');
273
+ }
274
+
275
+ /**
276
+ * Converts a string into a URL-safe slug.
277
+ * @param {string} str - Input string.
278
+ * @returns {string} Lowercase, hyphen-separated slug.
279
+ * @example
280
+ * slugify('Hello World!'); // 'hello-world'
281
+ */
282
+ function slugify(str) {
283
+ if (typeof str !== 'string') return '';
284
+ return str
285
+ .toLowerCase()
286
+ .normalize('NFD')
287
+ .replace(/[\u0300-\u036f]/g, '')
288
+ .replace(/[^a-z0-9\s-]/g, '')
289
+ .trim()
290
+ .replace(/[\s-]+/g, '-');
291
+ }
292
+
293
+ /**
294
+ * Trims whitespace from both ends of a string.
295
+ * @param {string} str - Input string.
296
+ * @returns {string} Trimmed string.
297
+ * @example
298
+ * trim(' hello '); // 'hello'
299
+ */
300
+ function trim(str) {
301
+ if (str === null || str === undefined) return '';
302
+ return String(str).trim();
303
+ }
304
+
305
+ /**
306
+ * Normalises an email address (lowercase, trimmed).
307
+ * @param {string} str - Raw email string.
308
+ * @returns {string} Normalised email.
309
+ * @example
310
+ * normalizeEmail(' User@Example.COM '); // 'user@example.com'
311
+ */
312
+ function normalizeEmail(str) {
313
+ if (typeof str !== 'string') return '';
314
+ return str.trim().toLowerCase();
315
+ }
316
+
317
+ /**
318
+ * Sanitizes a data object according to a map of sanitizer names per field.
319
+ * Supported sanitizers: 'trim', 'escape', 'stripTags', 'slugify', 'normalizeEmail'.
320
+ * @param {object} data - Raw field values.
321
+ * @param {Object.<string, string|string[]>} rules - Map of field name to sanitizer name(s).
322
+ * @returns {object} New object with sanitized values (original is not mutated).
323
+ * @example
324
+ * sanitize({ name: ' <b>Alice</b> ' }, { name: ['trim', 'stripTags'] });
325
+ * // => { name: 'Alice' }
326
+ */
327
+ function sanitize(data, rules) {
328
+ if (typeof data !== 'object' || data === null) return {};
329
+ const sanitizers = { trim, escape: escapeHtml, stripTags, slugify, normalizeEmail };
330
+ const out = Object.assign({}, data);
331
+ for (const [field, fieldRules] of Object.entries(rules)) {
332
+ const list = Array.isArray(fieldRules) ? fieldRules : [fieldRules];
333
+ let val = out[field];
334
+ for (const name of list) {
335
+ if (sanitizers[name]) val = sanitizers[name](val);
336
+ }
337
+ out[field] = val;
338
+ }
339
+ return out;
340
+ }
341
+
342
+ module.exports = {
343
+ setLocale,
344
+ getLocale,
345
+ validate,
346
+ validateField,
347
+ createSchema,
348
+ sanitize,
349
+ escapeHtml,
350
+ stripTags,
351
+ slugify,
352
+ trim,
353
+ normalizeEmail,
354
+ VALIDATORS,
355
+ };
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+ let locale = 'en';
3
+ const locales = {
4
+ en: { required: '{field} is required', email: '{field} must be a valid email', minLength: '{field} must be at least {0} characters', maxLength: '{field} must be at most {0} characters', min: '{field} must be at least {0}', max: '{field} must be at most {0}', integer: '{field} must be an integer', strongPassword: '{field} must have 8+ chars with upper, lower, number, special', default: '{field} is invalid' },
5
+ fr: { required: '{field} est requis', email: '{field} doit etre un email valide', minLength: '{field} doit contenir au moins {0} caracteres', integer: '{field} doit etre un entier', strongPassword: '{field}: 8+ car. maj, min, chiffre, special', default: '{field} est invalide' }
6
+ };
7
+ function getMessage(rule, field, params = []) { const msgs = locales[locale] || locales.en; let tpl = msgs[rule] || msgs.default; tpl = tpl.replace('{field}', field || 'Field'); params.forEach((p, i) => { tpl = tpl.replace('{' + i + '}', String(p)); }); return tpl; }
8
+ function setLocale(l) { locale = l; }
9
+ function registerLocale(n, m) { locales[n] = { ...locales.en, ...m }; }
10
+ module.exports = { getMessage, setLocale, registerLocale };
@@ -0,0 +1,118 @@
1
+ 'use strict';
2
+
3
+ const { validate } = require('./validator');
4
+
5
+ /**
6
+ * Express middleware factory for request validation.
7
+ *
8
+ * Usage:
9
+ * const { formguard } = require('@idirdev/formguard/middleware');
10
+ *
11
+ * app.post('/register', formguard({
12
+ * email: ['required', 'email'],
13
+ * password: ['required', { rule: 'minLength', param: 8 }]
14
+ * }), (req, res) => { ... });
15
+ *
16
+ * @param {Object} rules - Validation rules object
17
+ * @param {Object} [options] - Options
18
+ * @param {string} [options.source='body'] - Request property to validate ('body', 'query', 'params')
19
+ * @param {string} [options.locale='en'] - Locale for error messages
20
+ * @param {Object} [options.messages] - Custom message overrides
21
+ * @param {number} [options.statusCode=422] - HTTP status code on validation failure
22
+ * @param {boolean} [options.abortEarly=false] - Stop on first error
23
+ * @param {Object} [options.labels] - Human-readable field labels
24
+ * @param {Function} [options.onError] - Custom error handler (err, req, res, next)
25
+ * @returns {Function} Express middleware
26
+ */
27
+ function formguard(rules, options = {}) {
28
+ const {
29
+ source = 'body',
30
+ locale = 'en',
31
+ messages: customMessages,
32
+ statusCode = 422,
33
+ abortEarly = false,
34
+ labels,
35
+ onError
36
+ } = options;
37
+
38
+ return function formguardMiddleware(req, res, next) {
39
+ const data = req[source] || {};
40
+
41
+ const result = validate(data, rules, {
42
+ locale,
43
+ messages: customMessages,
44
+ abortEarly,
45
+ labels
46
+ });
47
+
48
+ if (result.valid) {
49
+ return next();
50
+ }
51
+
52
+ if (onError) {
53
+ return onError(result.errors, req, res, next);
54
+ }
55
+
56
+ return res.status(statusCode).json({
57
+ success: false,
58
+ message: locale === 'fr' ? 'Erreurs de validation' : 'Validation failed',
59
+ errors: result.errors
60
+ });
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Express middleware that validates and attaches sanitized data to req.validated.
66
+ * Combines validation with the sanitizers module.
67
+ */
68
+ function formguardSanitized(rules, sanitizerMap = {}, options = {}) {
69
+ const { source = 'body', ...validateOptions } = options;
70
+
71
+ let sanitizers;
72
+ try {
73
+ sanitizers = require('./sanitizers');
74
+ } catch (e) {
75
+ sanitizers = null;
76
+ }
77
+
78
+ return function formguardSanitizedMiddleware(req, res, next) {
79
+ const data = req[source] || {};
80
+
81
+ // Apply sanitizers first
82
+ const sanitized = {};
83
+ for (const [field, value] of Object.entries(data)) {
84
+ if (sanitizerMap[field] && sanitizers) {
85
+ let val = value;
86
+ const fns = Array.isArray(sanitizerMap[field]) ? sanitizerMap[field] : [sanitizerMap[field]];
87
+ for (const fn of fns) {
88
+ if (typeof fn === 'string' && sanitizers[fn]) {
89
+ val = sanitizers[fn](val);
90
+ } else if (typeof fn === 'function') {
91
+ val = fn(val);
92
+ }
93
+ }
94
+ sanitized[field] = val;
95
+ } else {
96
+ sanitized[field] = value;
97
+ }
98
+ }
99
+
100
+ const result = validate(sanitized, rules, validateOptions);
101
+
102
+ if (result.valid) {
103
+ req.validated = sanitized;
104
+ return next();
105
+ }
106
+
107
+ const statusCode = options.statusCode || 422;
108
+ const locale = validateOptions.locale || 'en';
109
+
110
+ return res.status(statusCode).json({
111
+ success: false,
112
+ message: locale === 'fr' ? 'Erreurs de validation' : 'Validation failed',
113
+ errors: result.errors
114
+ });
115
+ };
116
+ }
117
+
118
+ module.exports = { formguard, formguardSanitized };