11/**
2- * validateRequestSchema — Lightweight Joi-based validation middleware factory
2+ * validateRequestSchema — Lightweight Joi-based validation middleware factory.
33 *
4- * Extracted from middleware/validation.ts to break a babel-jest CommonJS
5- * evaluation-order crash. Because this module has zero heavy dependencies
6- * (no Joi import, no express-validator, no VersionControlUtils), it can
7- * be required safely by route files that call the factory at module-load
8- * time (e.g. smartWallet.ts:24).
4+ * This module is intentionally dependency-free (no `joi`, no
5+ * `express-validator`, no `VersionControlUtils`) so it can be `require`d
6+ * by route files that call the factory at module-load time without
7+ * crashing under babel-jest's CommonJS evaluation order (the root cause
8+ * of Issue #44). Routes pass in any Joi schema object with a
9+ * `validate(value)` method; we treat it as a duck-typed `SchemaLike`.
10+ *
11+ * Behaviour:
12+ * * Validates `req.body`, `req.query`, and `req.params` against the
13+ * provided Joi schemas, aggregating every error (not just the first)
14+ * with field/message context.
15+ * * On failure, responds with the standard envelope used elsewhere in
16+ * the codebase:
17+ * { success: false, message: 'Validation failed', errors: [...] }
18+ * * On success, replaces each source with the validated (and unknown-
19+ * stripped) value before calling `next()`.
920 */
1021
1122import { Request , Response , NextFunction } from 'express' ;
1223
13- /**
14- * Minimal structural interface for a Joi-like schema object.
15- * We deliberately avoid importing Joi so this module stays lean
16- * and does not trigger the babel-jest circular-eval bug.
17- */
24+ /** Minimal structural interface for a Joi-like schema object. */
25+ type Detail = { path : Array < string | number > ; message : string ; type ?: string } ;
26+
27+ /** Minimal structural interface for a Joi-like schema object. */
1828export interface SchemaLike {
19- validate ( value : unknown ) : { error ?: { details : Array < { message : string } > } } ;
29+ validate (
30+ value : unknown ,
31+ options ?: { stripUnknown ?: boolean ; abortEarly ?: boolean }
32+ ) : {
33+ error ?: { details : Detail [ ] } ;
34+ value ?: unknown ;
35+ } ;
2036}
2137
2238export interface ValidationSchema {
@@ -25,42 +41,77 @@ export interface ValidationSchema {
2541 params ?: SchemaLike ;
2642}
2743
44+ interface NormalizedError {
45+ source : 'body' | 'query' | 'params' ;
46+ field : string ;
47+ message : string ;
48+ }
49+
50+ const VALIDATE_OPTIONS = {
51+ abortEarly : false ,
52+ stripUnknown : true ,
53+ } as const ;
54+
55+ function safePath ( path : Detail [ 'path' ] | undefined | null ) : string {
56+ if ( ! path || ! Array . isArray ( path ) || path . length === 0 ) {
57+ return '(root)' ;
58+ }
59+ return path . join ( '.' ) ;
60+ }
61+
62+ function collect ( source : 'body' | 'query' | 'params' , schema : SchemaLike , payload : unknown ) : {
63+ errors : NormalizedError [ ] ;
64+ value : unknown ;
65+ } {
66+ const result = schema . validate ( payload , VALIDATE_OPTIONS ) ;
67+ if ( ! result . error ) {
68+ return { errors : [ ] , value : result . value ?? payload } ;
69+ }
70+ const errors : NormalizedError [ ] = result . error . details . map ( ( detail ) => ( {
71+ source,
72+ field : safePath ( detail . path ) ,
73+ message : detail . message ,
74+ } ) ) ;
75+ return { errors, value : result . value ?? payload } ;
76+ }
77+
2878/**
29- * Factory that returns Express middleware which validates
30- * req.body, req. query, and/or req.params against Joi schemas.
79+ * Factory that returns Express middleware which validates req.body,
80+ * req.query, and/or req.params against Joi schemas.
3181 */
3282export function validateRequestSchema ( schema : ValidationSchema ) {
3383 return ( req : Request , res : Response , next : NextFunction ) : void => {
34- const errors : string [ ] = [ ] ;
35-
36- // Validate request body
37- if ( schema . body ) {
38- const { error } = schema . body . validate ( req . body ) ;
39- if ( error ) {
40- errors . push ( `Body: ${ error . details [ 0 ] . message } ` ) ;
41- }
42- }
43-
44- // Validate query parameters
45- if ( schema . query ) {
46- const { error } = schema . query . validate ( req . query ) ;
47- if ( error ) {
48- errors . push ( `Query: ${ error . details [ 0 ] . message } ` ) ;
49- }
50- }
84+ const sources : Array < 'body' | 'query' | 'params' > = [ 'body' , 'query' , 'params' ] ;
85+ const errors : NormalizedError [ ] = [ ] ;
5186
52- // Validate route parameters
53- if ( schema . params ) {
54- const { error } = schema . params . validate ( req . params ) ;
55- if ( error ) {
56- errors . push ( `Params: ${ error . details [ 0 ] . message } ` ) ;
87+ for ( const source of sources ) {
88+ const sub = schema [ source ] ;
89+ if ( ! sub ) continue ;
90+ const outcome = collect ( source , sub , req [ source ] ) ;
91+ if ( outcome . errors . length ) {
92+ errors . push ( ...outcome . errors ) ;
5793 }
94+ // Replace the request source with the validated/sanitized value.
95+ // Request.body, Request.query, Request.params are all `any` on
96+ // Express's Request, so we use an explicit switch rather than
97+ // a `Record<string, unknown>` cast (which trips TS2352).
98+ // Replace the request source with the validated/sanitized value.
99+ // Express types `req.query` as `ParsedQs` and `req.params` as
100+ // `ParamsDictionary` (both stricter than `unknown`), so we widen
101+ // locally — the value has already been validated/stripped by Joi
102+ // in collect() above.
103+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104+ req . query = ( source === 'query' ? outcome . value : req . query ) as any ;
105+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106+ req . params = ( source === 'params' ? outcome . value : req . params ) as any ;
107+ req . body = source === 'body' ? outcome . value : req . body ;
58108 }
59109
60110 if ( errors . length > 0 ) {
61111 res . status ( 400 ) . json ( {
62- error : 'Validation failed' ,
63- details : errors ,
112+ success : false ,
113+ message : 'Validation failed' ,
114+ errors,
64115 } ) ;
65116 return ;
66117 }
0 commit comments