@naturalcycles/backend-lib 9.42.4 → 9.44.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.
@@ -33,6 +33,10 @@ export async function createDefaultApp(cfg) {
33
33
  // accepts application/json
34
34
  app.use(express.json({
35
35
  limit: '1mb',
36
+ verify(req, _res, buf) {
37
+ // Store the raw Buffer body
38
+ req.rawBody = buf;
39
+ },
36
40
  ...cfg.bodyParserJsonOptions,
37
41
  }));
38
42
  app.use(express.urlencoded({
@@ -44,6 +48,10 @@ export async function createDefaultApp(cfg) {
44
48
  app.use(express.raw({
45
49
  // inflate: true, // default is `true`
46
50
  limit: '1mb',
51
+ verify(req, _res, buf) {
52
+ // Store the raw Buffer body
53
+ req.rawBody = buf;
54
+ },
47
55
  ...cfg.bodyParserRawOptions,
48
56
  }));
49
57
  app.use(cookieParser());
@@ -18,6 +18,15 @@ export interface BackendRequest extends Request {
18
18
  * Only used for request logging purposes.
19
19
  */
20
20
  userId?: string;
21
+ /**
22
+ * Raw Buffer of the `req.body`, before it's stringified and json-parsed.
23
+ * Useful for when something mutates `req.body` json (e.g j validation), and you
24
+ * want access to the original input.
25
+ *
26
+ * For `req.rawBody` to exist - you need to use `createDefaultApp`, or use the
27
+ * `verify` option of the json parser (copy-paste it from `createDefaultApp`).
28
+ */
29
+ rawBody?: Buffer;
21
30
  /**
22
31
  * Set by requestTimeoutMiddleware.
23
32
  * Can be used to cancel/override the timeout.
@@ -1,9 +1,8 @@
1
- import { _deepCopy } from '@naturalcycles/js-lib/object';
2
1
  import { AjvSchema, getCoercingAjv, } from '@naturalcycles/nodejs-lib/ajv';
3
2
  import { handleValidationError } from '../validateRequest.util.js';
4
3
  class AjvValidateRequest {
5
4
  body(req, schema, opt = {}) {
6
- return this.validate(req, 'body', schema, opt);
5
+ return this.validate(req, 'body', schema, req.rawBody ? () => JSON.parse(req.rawBody.toString()) : undefined, opt);
7
6
  }
8
7
  /**
9
8
  * Query validation uses type coercion (unlike body validation),
@@ -12,7 +11,11 @@ class AjvValidateRequest {
12
11
  * Coercion mutates the input, even if the end result is that the input failed the validation.
13
12
  */
14
13
  query(req, schema, opt = {}) {
15
- return this.validate(req, 'query', schema, { coerceTypes: true, ...opt });
14
+ const originalQuery = JSON.stringify(req.query);
15
+ return this.validate(req, 'query', schema, () => JSON.parse(originalQuery), {
16
+ coerceTypes: true,
17
+ ...opt,
18
+ });
16
19
  }
17
20
  /**
18
21
  * Params validation uses type coercion (unlike body validation),
@@ -21,7 +24,11 @@ class AjvValidateRequest {
21
24
  * Coercion mutates the input, even if the end result is that the input failed the validation.
22
25
  */
23
26
  params(req, schema, opt = {}) {
24
- return this.validate(req, 'params', schema, { coerceTypes: true, ...opt });
27
+ const originalParams = JSON.stringify(req.params);
28
+ return this.validate(req, 'params', schema, () => JSON.parse(originalParams), {
29
+ coerceTypes: true,
30
+ ...opt,
31
+ });
25
32
  }
26
33
  /**
27
34
  * Does NOT mutate `req.headers`,
@@ -32,18 +39,20 @@ class AjvValidateRequest {
32
39
  * there may be additional consumers for `req.headers` (e.g middlewares, etc).
33
40
  */
34
41
  headers(req, schema, opt = {}) {
35
- const originalHeaders = _deepCopy(req.headers);
36
- const headers = this.validate(req, 'headers', schema, opt);
37
- req.headers = originalHeaders;
38
- return headers;
42
+ return this.validate(req, 'headers', schema, undefined, {
43
+ mutateInput: false,
44
+ ...opt,
45
+ });
39
46
  }
40
- validate(req, reqProperty, schema, opt = {}) {
47
+ validate(req, reqProperty, schema, getOriginalInput, opt = {}) {
41
48
  const input = req[reqProperty] || {};
42
- const { coerceTypes } = opt;
49
+ const { coerceTypes, mutateInput } = opt;
43
50
  const ajv = coerceTypes ? getCoercingAjv() : undefined;
44
51
  const ajvSchema = AjvSchema.create(schema, { ajv });
45
52
  const [error, output] = ajvSchema.getValidationResult(input, {
46
53
  inputName: `request.${reqProperty}`,
54
+ getOriginalInput,
55
+ mutateInput,
47
56
  });
48
57
  if (error) {
49
58
  handleValidationError(error, input, opt);
@@ -20,4 +20,14 @@ export interface ReqValidationOptions<ERR extends AppError> {
20
20
  * typically: request path params and request query params.
21
21
  */
22
22
  coerceTypes?: boolean;
23
+ /**
24
+ * Default value depends on the implementation and the object.
25
+ * Joi and Zod do not mutate input (even if you pass `mutateInput: true` - that feature is not supported).
26
+ * AJV by default does mutate the input, but depending on the property:
27
+ *
28
+ * body - will be mutated, by req.rawBody will be used (if available) for error message snippet
29
+ * params, query - will NOT mutate by default (unless you pass `mutateInput: true`)
30
+ * headers - will NOT mutate by default
31
+ */
32
+ mutateInput?: boolean;
23
33
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.42.4",
4
+ "version": "9.44.0",
5
5
  "peerDependencies": {
6
6
  "@sentry/node": "^10"
7
7
  },
@@ -10,7 +10,11 @@ import { logMiddleware } from '../server/logMiddleware.js'
10
10
  import { methodOverrideMiddleware } from '../server/methodOverrideMiddleware.js'
11
11
  import { notFoundMiddleware } from '../server/notFoundMiddleware.js'
12
12
  import { requestTimeoutMiddleware } from '../server/requestTimeoutMiddleware.js'
13
- import type { BackendApplication, BackendRequestHandler } from '../server/server.model.js'
13
+ import type {
14
+ BackendApplication,
15
+ BackendRequest,
16
+ BackendRequestHandler,
17
+ } from '../server/server.model.js'
14
18
  import { simpleRequestLoggerMiddleware } from '../server/simpleRequestLoggerMiddleware.js'
15
19
 
16
20
  const isTest = process.env['APP_ENV'] === 'test'
@@ -53,6 +57,10 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
53
57
  app.use(
54
58
  express.json({
55
59
  limit: '1mb',
60
+ verify(req: BackendRequest, _res, buf) {
61
+ // Store the raw Buffer body
62
+ req.rawBody = buf
63
+ },
56
64
  ...cfg.bodyParserJsonOptions,
57
65
  }),
58
66
  )
@@ -70,6 +78,10 @@ export async function createDefaultApp(cfg: DefaultAppCfg): Promise<BackendAppli
70
78
  express.raw({
71
79
  // inflate: true, // default is `true`
72
80
  limit: '1mb',
81
+ verify(req: BackendRequest, _res, buf) {
82
+ // Store the raw Buffer body
83
+ req.rawBody = buf
84
+ },
73
85
  ...cfg.bodyParserRawOptions,
74
86
  }),
75
87
  )
@@ -20,6 +20,15 @@ export interface BackendRequest extends Request {
20
20
  * Only used for request logging purposes.
21
21
  */
22
22
  userId?: string
23
+ /**
24
+ * Raw Buffer of the `req.body`, before it's stringified and json-parsed.
25
+ * Useful for when something mutates `req.body` json (e.g j validation), and you
26
+ * want access to the original input.
27
+ *
28
+ * For `req.rawBody` to exist - you need to use `createDefaultApp`, or use the
29
+ * `verify` option of the json parser (copy-paste it from `createDefaultApp`).
30
+ */
31
+ rawBody?: Buffer
23
32
 
24
33
  /**
25
34
  * Set by requestTimeoutMiddleware.
@@ -1,4 +1,3 @@
1
- import { _deepCopy } from '@naturalcycles/js-lib/object'
2
1
  import {
3
2
  AjvSchema,
4
3
  type AjvValidationError,
@@ -14,7 +13,13 @@ class AjvValidateRequest {
14
13
  schema: SchemaHandledByAjv<IN, OUT>,
15
14
  opt: ReqValidationOptions<AjvValidationError> = {},
16
15
  ): OUT {
17
- return this.validate(req, 'body', schema, opt)
16
+ return this.validate(
17
+ req,
18
+ 'body',
19
+ schema,
20
+ req.rawBody ? () => JSON.parse(req.rawBody!.toString()) : undefined,
21
+ opt,
22
+ )
18
23
  }
19
24
 
20
25
  /**
@@ -28,7 +33,11 @@ class AjvValidateRequest {
28
33
  schema: SchemaHandledByAjv<IN, OUT>,
29
34
  opt: ReqValidationOptions<AjvValidationError> = {},
30
35
  ): OUT {
31
- return this.validate(req, 'query', schema, { coerceTypes: true, ...opt })
36
+ const originalQuery = JSON.stringify(req.query)
37
+ return this.validate(req, 'query', schema, () => JSON.parse(originalQuery), {
38
+ coerceTypes: true,
39
+ ...opt,
40
+ })
32
41
  }
33
42
 
34
43
  /**
@@ -42,7 +51,11 @@ class AjvValidateRequest {
42
51
  schema: SchemaHandledByAjv<IN, OUT>,
43
52
  opt: ReqValidationOptions<AjvValidationError> = {},
44
53
  ): OUT {
45
- return this.validate(req, 'params', schema, { coerceTypes: true, ...opt })
54
+ const originalParams = JSON.stringify(req.params)
55
+ return this.validate(req, 'params', schema, () => JSON.parse(originalParams), {
56
+ coerceTypes: true,
57
+ ...opt,
58
+ })
46
59
  }
47
60
 
48
61
  /**
@@ -58,26 +71,29 @@ class AjvValidateRequest {
58
71
  schema: SchemaHandledByAjv<IN, OUT>,
59
72
  opt: ReqValidationOptions<AjvValidationError> = {},
60
73
  ): OUT {
61
- const originalHeaders = _deepCopy(req.headers)
62
- const headers = this.validate(req, 'headers', schema, opt)
63
- req.headers = originalHeaders
64
- return headers
74
+ return this.validate(req, 'headers', schema, undefined, {
75
+ mutateInput: false,
76
+ ...opt,
77
+ })
65
78
  }
66
79
 
67
80
  private validate<IN, OUT>(
68
81
  req: BackendRequest,
69
82
  reqProperty: 'body' | 'params' | 'query' | 'headers',
70
83
  schema: SchemaHandledByAjv<IN, OUT>,
84
+ getOriginalInput?: () => IN,
71
85
  opt: ReqValidationOptions<AjvValidationError> = {},
72
86
  ): OUT {
73
87
  const input: IN = req[reqProperty] || {}
74
88
 
75
- const { coerceTypes } = opt
89
+ const { coerceTypes, mutateInput } = opt
76
90
  const ajv = coerceTypes ? getCoercingAjv() : undefined
77
91
  const ajvSchema = AjvSchema.create(schema, { ajv })
78
92
 
79
93
  const [error, output] = ajvSchema.getValidationResult(input, {
80
94
  inputName: `request.${reqProperty}`,
95
+ getOriginalInput,
96
+ mutateInput,
81
97
  })
82
98
 
83
99
  if (error) {
@@ -69,4 +69,15 @@ export interface ReqValidationOptions<ERR extends AppError> {
69
69
  * typically: request path params and request query params.
70
70
  */
71
71
  coerceTypes?: boolean
72
+
73
+ /**
74
+ * Default value depends on the implementation and the object.
75
+ * Joi and Zod do not mutate input (even if you pass `mutateInput: true` - that feature is not supported).
76
+ * AJV by default does mutate the input, but depending on the property:
77
+ *
78
+ * body - will be mutated, by req.rawBody will be used (if available) for error message snippet
79
+ * params, query - will NOT mutate by default (unless you pass `mutateInput: true`)
80
+ * headers - will NOT mutate by default
81
+ */
82
+ mutateInput?: boolean
72
83
  }