@naturalcycles/backend-lib 9.16.0 → 9.17.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runScript } from '@naturalcycles/nodejs-lib';
2
+ import { runScript } from '@naturalcycles/nodejs-lib/runScript';
3
3
  import { _yargs } from '@naturalcycles/nodejs-lib/yargs';
4
4
  import { deployGae } from '../deploy/deployGae.js';
5
5
  import { deployHealthCheckYargsOptions } from '../deploy/deployHealthCheck.js';
@@ -7,7 +7,7 @@ yarn deploy-health-check --url https://service-dot-yourproject.appspot.com
7
7
  --intervalSec 2
8
8
 
9
9
  */
10
- import { runScript } from '@naturalcycles/nodejs-lib';
10
+ import { runScript } from '@naturalcycles/nodejs-lib/runScript';
11
11
  import { _yargs } from '@naturalcycles/nodejs-lib/yargs';
12
12
  import { deployHealthCheck, deployHealthCheckYargsOptions } from '../deploy/deployHealthCheck.js';
13
13
  runScript(async () => {
@@ -4,7 +4,7 @@
4
4
  yarn deploy-prepare
5
5
 
6
6
  */
7
- import { runScript } from '@naturalcycles/nodejs-lib';
7
+ import { runScript } from '@naturalcycles/nodejs-lib/runScript';
8
8
  import { _yargs } from '@naturalcycles/nodejs-lib/yargs';
9
9
  import { deployPrepare, deployPrepareYargsOptions } from '../deploy/deployPrepare.js';
10
10
  runScript(async () => {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runScript } from '@naturalcycles/nodejs-lib';
2
+ import { runScript } from '@naturalcycles/nodejs-lib/runScript';
3
3
  import { _yargs } from '@naturalcycles/nodejs-lib/yargs';
4
4
  import { undeployGae } from '../deploy/deployGae.js';
5
5
  runScript(async () => {
@@ -1,9 +1,11 @@
1
+ import { _lazyValue } from '@naturalcycles/js-lib';
1
2
  import { AjvSchema } from '@naturalcycles/nodejs-lib/ajv';
2
3
  import { fs2 } from '@naturalcycles/nodejs-lib/fs2';
3
4
  import { yaml2 } from '@naturalcycles/nodejs-lib/yaml2';
4
5
  import { resourcesDir } from '../paths.cnst.js';
5
- const backendCfgSchema = AjvSchema.readJsonSync(`${resourcesDir}/backendCfg.schema.json`, {
6
- objectName: 'backend.cfg.yaml',
6
+ const getBackendCfgSchema = _lazyValue(() => {
7
+ const schemaJson = fs2.readJson(`${resourcesDir}/backendCfg.schema.json`);
8
+ return AjvSchema.create(schemaJson, { objectName: 'backend.cfg.yaml' });
7
9
  });
8
10
  export function getBackendCfg(projectDir = '.') {
9
11
  const backendCfgYamlPath = `${projectDir}/backend.cfg.yaml`;
@@ -11,6 +13,6 @@ export function getBackendCfg(projectDir = '.') {
11
13
  const backendCfg = {
12
14
  ...yaml2.readYaml(backendCfgYamlPath),
13
15
  };
14
- backendCfgSchema.validate(backendCfg);
16
+ getBackendCfgSchema().validate(backendCfg);
15
17
  return backendCfg;
16
18
  }
@@ -1,6 +1,6 @@
1
1
  import type { AjvSchema, AjvValidationError } from '@naturalcycles/nodejs-lib/ajv';
2
2
  import type { BackendRequest } from '../../server/server.model.js';
3
- import type { ReqValidationOptions } from '../joi/joiValidateRequest.js';
3
+ import { type ReqValidationOptions } from '../validateRequest.util.js';
4
4
  declare class AjvValidateRequest {
5
5
  body<T>(req: BackendRequest, schema: AjvSchema<T>, opt?: ReqValidationOptions<AjvValidationError>): T;
6
6
  query<T>(req: BackendRequest, schema: AjvSchema<T>, opt?: ReqValidationOptions<AjvValidationError>): T;
@@ -1,5 +1,4 @@
1
- import { AppError } from '@naturalcycles/js-lib/error';
2
- import { _get } from '@naturalcycles/js-lib/object';
1
+ import { handleValidationError } from '../validateRequest.util.js';
3
2
  class AjvValidateRequest {
4
3
  body(req, schema, opt = {}) {
5
4
  return this.validate(req, 'body', schema, opt);
@@ -20,53 +19,23 @@ class AjvValidateRequest {
20
19
  * Keep in mind that this will also remove all values that are not in the schema.
21
20
  */
22
21
  headers(req, schema, opt = {}) {
23
- const options = {
24
- keepOriginal: true,
22
+ return this.validate(req, 'headers', schema, {
23
+ mutate: false,
25
24
  ...opt,
26
- };
27
- return this.validate(req, 'headers', schema, options);
25
+ });
28
26
  }
29
27
  validate(req, reqProperty, schema, opt = {}) {
30
- const value = { ...req[reqProperty] }; // destructure to avoid being mutated by Ajv
31
- // It will mutate the `value`, but not the original object
32
- const error = schema.getValidationError(value, {
28
+ const { mutate = true } = opt;
29
+ const originalProperty = req[reqProperty] || {};
30
+ const item = mutate ? originalProperty : { ...originalProperty };
31
+ // Ajv mutates the input
32
+ const error = schema.getValidationError(item, {
33
33
  objectName: `request ${reqProperty}`,
34
34
  });
35
35
  if (error) {
36
- let report;
37
- if (typeof opt.report === 'boolean') {
38
- report = opt.report;
39
- }
40
- else if (typeof opt.report === 'function') {
41
- report = opt.report(error);
42
- }
43
- if (opt.redactPaths) {
44
- redact(opt.redactPaths, req[reqProperty], error);
45
- }
46
- throw new AppError(error.message, {
47
- backendResponseStatusCode: 400,
48
- report,
49
- ...error.data,
50
- });
51
- }
52
- // mutate req to replace the property with the value, converted by Joi
53
- if (!opt.keepOriginal && reqProperty !== 'query') {
54
- // query is read-only in Express 5
55
- req[reqProperty] = value;
36
+ handleValidationError(error, originalProperty, opt);
56
37
  }
57
- return value;
38
+ return item;
58
39
  }
59
40
  }
60
41
  export const ajvValidateRequest = new AjvValidateRequest();
61
- const REDACTED = 'REDACTED';
62
- /**
63
- * Mutates error
64
- */
65
- function redact(redactPaths, obj, error) {
66
- redactPaths
67
- .map(path => _get(obj, path))
68
- .filter(Boolean)
69
- .forEach(secret => {
70
- error.message = error.message.replaceAll(secret, REDACTED);
71
- });
72
- }
@@ -1,23 +1,6 @@
1
1
  import type { AnySchema, JoiValidationError } from '@naturalcycles/nodejs-lib/joi';
2
2
  import type { BackendRequest } from '../../server/server.model.js';
3
- export interface ReqValidationOptions<ERR extends Error> {
4
- /**
5
- * Pass a 'dot-paths' (e.g `pw`, or `input.pw`) that needs to be redacted from the output, in case of error.
6
- * Useful e.g to redact (prevent leaking) plaintext passwords in error messages.
7
- */
8
- redactPaths?: string[];
9
- /**
10
- * Set to true, or a function that returns true/false based on the error generated.
11
- * If true - `genericErrorHandler` will report it to errorReporter (aka Sentry).
12
- */
13
- report?: boolean | ((err: ERR) => boolean);
14
- /**
15
- * When set to true, the validated object will not be replaced with the Joi-converted value.
16
- *
17
- * The general default is `false`, with the excepction of `headers` validation, where the default is `true`.
18
- */
19
- keepOriginal?: boolean;
20
- }
3
+ import { type ReqValidationOptions } from '../validateRequest.util.js';
21
4
  declare class ValidateRequest {
22
5
  body<T>(req: BackendRequest, schema: AnySchema<T>, opt?: ReqValidationOptions<JoiValidationError>): T;
23
6
  query<T>(req: BackendRequest, schema: AnySchema<T>, opt?: ReqValidationOptions<JoiValidationError>): T;
@@ -1,6 +1,5 @@
1
- import { AppError } from '@naturalcycles/js-lib/error';
2
- import { _get } from '@naturalcycles/js-lib/object';
3
1
  import { getValidationResult } from '@naturalcycles/nodejs-lib/joi';
2
+ import { handleValidationError } from '../validateRequest.util.js';
4
3
  class ValidateRequest {
5
4
  body(req, schema, opt = {}) {
6
5
  return this.validate(req, 'body', schema, opt);
@@ -21,51 +20,27 @@ class ValidateRequest {
21
20
  * Keep in mind that this will also remove all values that are not in the schema.
22
21
  */
23
22
  headers(req, schema, opt = {}) {
24
- const options = {
25
- keepOriginal: true,
23
+ return this.validate(req, 'headers', schema, {
24
+ mutate: false,
26
25
  ...opt,
27
- };
28
- return this.validate(req, 'headers', schema, options);
26
+ });
29
27
  }
30
28
  validate(req, reqProperty, schema, opt = {}) {
31
- const { value, error } = getValidationResult(req[reqProperty] || {}, schema, `request ${reqProperty}`);
29
+ const { mutate = true } = opt;
30
+ const originalProperty = req[reqProperty] || {};
31
+ // Joi does not mutate the input
32
+ const { error, value } = getValidationResult(originalProperty, schema, `request ${reqProperty}`);
32
33
  if (error) {
33
- let report;
34
- if (typeof opt.report === 'boolean') {
35
- report = opt.report;
36
- }
37
- else if (typeof opt.report === 'function') {
38
- report = opt.report(error);
39
- }
40
34
  if (opt.redactPaths) {
41
- redact(opt.redactPaths, req[reqProperty], error);
42
35
  error.data.joiValidationErrorItems.length = 0; // clears the array
43
36
  delete error.data.annotation;
44
37
  }
45
- throw new AppError(error.message, {
46
- backendResponseStatusCode: 400,
47
- report,
48
- ...error.data,
49
- });
38
+ handleValidationError(error, originalProperty, opt);
50
39
  }
51
- // mutate req to replace the property with the value, converted by Joi
52
- if (!opt.keepOriginal && reqProperty !== 'query') {
53
- // query is read-only in Express 5
40
+ if (mutate) {
54
41
  req[reqProperty] = value;
55
42
  }
56
43
  return value;
57
44
  }
58
45
  }
59
46
  export const validateRequest = new ValidateRequest();
60
- const REDACTED = 'REDACTED';
61
- /**
62
- * Mutates error
63
- */
64
- function redact(redactPaths, obj, error) {
65
- redactPaths
66
- .map(path => _get(obj, path))
67
- .filter(Boolean)
68
- .forEach(secret => {
69
- error.message = error.message.replaceAll(secret, REDACTED);
70
- });
71
- }
@@ -0,0 +1,23 @@
1
+ import { AppError } from '@naturalcycles/js-lib/error';
2
+ export declare function handleValidationError<T, ERR extends AppError>(error: ERR, originalProperty: T, opt?: ReqValidationOptions<ERR>): never;
3
+ export interface ReqValidationOptions<ERR extends AppError> {
4
+ /**
5
+ * Pass a 'dot-paths' (e.g `pw`, or `input.pw`) that needs to be redacted from the output, in case of error.
6
+ * Useful e.g to redact (prevent leaking) plaintext passwords in error messages.
7
+ */
8
+ redactPaths?: string[];
9
+ /**
10
+ * Set to true, or a function that returns true/false based on the error generated.
11
+ * If true - `genericErrorHandler` will report it to errorReporter (aka Sentry).
12
+ */
13
+ report?: boolean | ((err: ERR) => boolean);
14
+ /**
15
+ * When set to true, the validated object will not be replaced with the Joi-converted value.
16
+ *
17
+ * Defaults to true.
18
+ * Exception is `headers` validation, where the default is `false`.
19
+ *
20
+ * To avoid mutation - shallow copy is performed.
21
+ */
22
+ mutate?: boolean;
23
+ }
@@ -0,0 +1,32 @@
1
+ import { AppError } from '@naturalcycles/js-lib/error';
2
+ import { _get } from '@naturalcycles/js-lib/object';
3
+ export function handleValidationError(error, originalProperty, opt = {}) {
4
+ // const item: T = opt.mutate ? { ...req[reqProperty] } : (req[reqProperty] || {})
5
+ let report;
6
+ if (typeof opt.report === 'boolean') {
7
+ report = opt.report;
8
+ }
9
+ else if (typeof opt.report === 'function') {
10
+ report = opt.report(error);
11
+ }
12
+ if (opt.redactPaths) {
13
+ redact(opt.redactPaths, originalProperty, error);
14
+ }
15
+ throw new AppError(error.message, {
16
+ backendResponseStatusCode: 400,
17
+ report,
18
+ ...error.data,
19
+ });
20
+ }
21
+ const REDACTED = 'REDACTED';
22
+ /**
23
+ * Mutates error
24
+ */
25
+ function redact(redactPaths, obj, error) {
26
+ redactPaths
27
+ .map(path => _get(obj, path))
28
+ .filter(Boolean)
29
+ .forEach(secret => {
30
+ error.message = error.message.replaceAll(secret, REDACTED);
31
+ });
32
+ }
@@ -1,6 +1,6 @@
1
1
  import { type ZodType, type ZodValidationError } from '@naturalcycles/js-lib/zod';
2
2
  import type { BackendRequest } from '../../server/server.model.js';
3
- import type { ReqValidationOptions } from '../joi/joiValidateRequest.js';
3
+ import { type ReqValidationOptions } from '../validateRequest.util.js';
4
4
  declare class ZodValidateRequest {
5
5
  body<T>(req: BackendRequest, schema: ZodType<T>, opt?: ReqValidationOptions<ZodValidationError>): T;
6
6
  query<T>(req: BackendRequest, schema: ZodType<T>, opt?: ReqValidationOptions<ZodValidationError>): T;
@@ -1,6 +1,5 @@
1
- import { AppError } from '@naturalcycles/js-lib/error';
2
- import { _get } from '@naturalcycles/js-lib/object';
3
1
  import { zSafeValidate } from '@naturalcycles/js-lib/zod';
2
+ import { handleValidationError } from '../validateRequest.util.js';
4
3
  class ZodValidateRequest {
5
4
  body(req, schema, opt = {}) {
6
5
  return this.validate(req, 'body', schema, opt);
@@ -21,49 +20,23 @@ class ZodValidateRequest {
21
20
  * Keep in mind that this will also remove all values that are not in the schema.
22
21
  */
23
22
  headers(req, schema, opt = {}) {
24
- const options = {
25
- keepOriginal: true,
23
+ return this.validate(req, 'headers', schema, {
24
+ mutate: false,
26
25
  ...opt,
27
- };
28
- return this.validate(req, 'headers', schema, options);
26
+ });
29
27
  }
30
28
  validate(req, reqProperty, schema, opt = {}) {
31
- const { data, error } = zSafeValidate(req[reqProperty] || {}, schema);
29
+ const { mutate = true } = opt;
30
+ const originalProperty = req[reqProperty] || {};
31
+ // Zod does not mutate the input
32
+ const { error, data } = zSafeValidate(originalProperty, schema);
32
33
  if (error) {
33
- let report;
34
- if (typeof opt.report === 'boolean') {
35
- report = opt.report;
36
- }
37
- else if (typeof opt.report === 'function') {
38
- report = opt.report(error);
39
- }
40
- if (opt.redactPaths) {
41
- redact(opt.redactPaths, req[reqProperty], error);
42
- }
43
- throw new AppError(error.message, {
44
- backendResponseStatusCode: 400,
45
- report,
46
- ...error.data,
47
- });
34
+ handleValidationError(error, originalProperty, opt);
48
35
  }
49
- // mutate req to replace the property with the value, converted by Joi
50
- if (!opt.keepOriginal && reqProperty !== 'query') {
51
- // query is read-only in Express 5
36
+ if (mutate) {
52
37
  req[reqProperty] = data;
53
38
  }
54
39
  return data;
55
40
  }
56
41
  }
57
42
  export const zodValidateRequest = new ZodValidateRequest();
58
- const REDACTED = 'REDACTED';
59
- /**
60
- * Mutates error
61
- */
62
- function redact(redactPaths, obj, error) {
63
- redactPaths
64
- .map(path => _get(obj, path))
65
- .filter(Boolean)
66
- .forEach(secret => {
67
- error.message = error.message.replaceAll(secret, REDACTED);
68
- });
69
- }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/backend-lib",
3
3
  "type": "module",
4
- "version": "9.16.0",
4
+ "version": "9.17.0",
5
5
  "peerDependencies": {
6
6
  "@sentry/node": "^9"
7
7
  },
@@ -34,6 +34,7 @@
34
34
  "exports": {
35
35
  "./cfg/tsconfig.json": "./cfg/tsconfig.json",
36
36
  ".": "./dist/index.js",
37
+ "./admin": "./dist/admin/index.js",
37
38
  "./db": "./dist/db/index.js",
38
39
  "./deploy": "./dist/deploy/index.js",
39
40
  "./express/createDefaultApp": "./dist/express/createDefaultApp.js",
@@ -61,7 +62,6 @@
61
62
  "deploy-prepare": "dist/bin/deploy-prepare.js",
62
63
  "deploy-health-check": "dist/bin/deploy-health-check.js"
63
64
  },
64
- "main": "dist/index.js",
65
65
  "types": "dist/index.d.ts",
66
66
  "publishConfig": {
67
67
  "access": "public"
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { runScript } from '@naturalcycles/nodejs-lib'
3
+ import { runScript } from '@naturalcycles/nodejs-lib/runScript'
4
4
  import { _yargs } from '@naturalcycles/nodejs-lib/yargs'
5
5
  import { deployGae } from '../deploy/deployGae.js'
6
6
  import { deployHealthCheckYargsOptions } from '../deploy/deployHealthCheck.js'
@@ -9,7 +9,7 @@ yarn deploy-health-check --url https://service-dot-yourproject.appspot.com
9
9
 
10
10
  */
11
11
 
12
- import { runScript } from '@naturalcycles/nodejs-lib'
12
+ import { runScript } from '@naturalcycles/nodejs-lib/runScript'
13
13
  import { _yargs } from '@naturalcycles/nodejs-lib/yargs'
14
14
  import { deployHealthCheck, deployHealthCheckYargsOptions } from '../deploy/deployHealthCheck.js'
15
15
 
@@ -6,7 +6,7 @@ yarn deploy-prepare
6
6
 
7
7
  */
8
8
 
9
- import { runScript } from '@naturalcycles/nodejs-lib'
9
+ import { runScript } from '@naturalcycles/nodejs-lib/runScript'
10
10
  import { _yargs } from '@naturalcycles/nodejs-lib/yargs'
11
11
  import { deployPrepare, deployPrepareYargsOptions } from '../deploy/deployPrepare.js'
12
12
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { runScript } from '@naturalcycles/nodejs-lib'
3
+ import { runScript } from '@naturalcycles/nodejs-lib/runScript'
4
4
  import { _yargs } from '@naturalcycles/nodejs-lib/yargs'
5
5
  import { undeployGae } from '../deploy/deployGae.js'
6
6
 
@@ -1,3 +1,5 @@
1
+ import { _lazyValue } from '@naturalcycles/js-lib'
2
+ import type { JsonSchema } from '@naturalcycles/js-lib/json-schema'
1
3
  import type { StringMap } from '@naturalcycles/js-lib/types'
2
4
  import { AjvSchema } from '@naturalcycles/nodejs-lib/ajv'
3
5
  import { fs2 } from '@naturalcycles/nodejs-lib/fs2'
@@ -38,12 +40,10 @@ export interface BackendCfg {
38
40
  appYamlPassEnv?: string
39
41
  }
40
42
 
41
- const backendCfgSchema = AjvSchema.readJsonSync<BackendCfg>(
42
- `${resourcesDir}/backendCfg.schema.json`,
43
- {
44
- objectName: 'backend.cfg.yaml',
45
- },
46
- )
43
+ const getBackendCfgSchema = _lazyValue(() => {
44
+ const schemaJson = fs2.readJson<JsonSchema<BackendCfg>>(`${resourcesDir}/backendCfg.schema.json`)
45
+ return AjvSchema.create(schemaJson, { objectName: 'backend.cfg.yaml' })
46
+ })
47
47
 
48
48
  export function getBackendCfg(projectDir = '.'): BackendCfg {
49
49
  const backendCfgYamlPath = `${projectDir}/backend.cfg.yaml`
@@ -54,6 +54,6 @@ export function getBackendCfg(projectDir = '.'): BackendCfg {
54
54
  ...yaml2.readYaml(backendCfgYamlPath),
55
55
  }
56
56
 
57
- backendCfgSchema.validate(backendCfg)
57
+ getBackendCfgSchema().validate(backendCfg)
58
58
  return backendCfg
59
59
  }
@@ -1,8 +1,6 @@
1
- import { AppError } from '@naturalcycles/js-lib/error'
2
- import { _get } from '@naturalcycles/js-lib/object'
3
1
  import type { AjvSchema, AjvValidationError } from '@naturalcycles/nodejs-lib/ajv'
4
2
  import type { BackendRequest } from '../../server/server.model.js'
5
- import type { ReqValidationOptions } from '../joi/joiValidateRequest.js'
3
+ import { handleValidationError, type ReqValidationOptions } from '../validateRequest.util.js'
6
4
 
7
5
  class AjvValidateRequest {
8
6
  body<T>(
@@ -43,11 +41,10 @@ class AjvValidateRequest {
43
41
  schema: AjvSchema<T>,
44
42
  opt: ReqValidationOptions<AjvValidationError> = {},
45
43
  ): T {
46
- const options: ReqValidationOptions<AjvValidationError> = {
47
- keepOriginal: true,
44
+ return this.validate(req, 'headers', schema, {
45
+ mutate: false,
48
46
  ...opt,
49
- }
50
- return this.validate(req, 'headers', schema, options)
47
+ })
51
48
  }
52
49
 
53
50
  private validate<T>(
@@ -56,53 +53,21 @@ class AjvValidateRequest {
56
53
  schema: AjvSchema<T>,
57
54
  opt: ReqValidationOptions<AjvValidationError> = {},
58
55
  ): T {
59
- const value: T = { ...req[reqProperty] } // destructure to avoid being mutated by Ajv
60
- // It will mutate the `value`, but not the original object
61
- const error = schema.getValidationError(value, {
56
+ const { mutate = true } = opt
57
+ const originalProperty = req[reqProperty] || {}
58
+ const item: T = mutate ? originalProperty : { ...originalProperty }
59
+
60
+ // Ajv mutates the input
61
+ const error = schema.getValidationError(item, {
62
62
  objectName: `request ${reqProperty}`,
63
63
  })
64
64
 
65
65
  if (error) {
66
- let report: boolean | undefined
67
- if (typeof opt.report === 'boolean') {
68
- report = opt.report
69
- } else if (typeof opt.report === 'function') {
70
- report = opt.report(error)
71
- }
72
-
73
- if (opt.redactPaths) {
74
- redact(opt.redactPaths, req[reqProperty], error)
75
- }
76
-
77
- throw new AppError(error.message, {
78
- backendResponseStatusCode: 400,
79
- report,
80
- ...error.data,
81
- })
66
+ handleValidationError(error, originalProperty, opt)
82
67
  }
83
68
 
84
- // mutate req to replace the property with the value, converted by Joi
85
- if (!opt.keepOriginal && reqProperty !== 'query') {
86
- // query is read-only in Express 5
87
- req[reqProperty] = value
88
- }
89
-
90
- return value
69
+ return item
91
70
  }
92
71
  }
93
72
 
94
73
  export const ajvValidateRequest = new AjvValidateRequest()
95
-
96
- const REDACTED = 'REDACTED'
97
-
98
- /**
99
- * Mutates error
100
- */
101
- function redact(redactPaths: string[], obj: any, error: Error): void {
102
- redactPaths
103
- .map(path => _get(obj, path) as string)
104
- .filter(Boolean)
105
- .forEach(secret => {
106
- error.message = error.message.replaceAll(secret, REDACTED)
107
- })
108
- }
@@ -1,29 +1,7 @@
1
- import { AppError } from '@naturalcycles/js-lib/error'
2
- import { _get } from '@naturalcycles/js-lib/object'
3
1
  import type { AnySchema, JoiValidationError } from '@naturalcycles/nodejs-lib/joi'
4
2
  import { getValidationResult } from '@naturalcycles/nodejs-lib/joi'
5
3
  import type { BackendRequest } from '../../server/server.model.js'
6
-
7
- export interface ReqValidationOptions<ERR extends Error> {
8
- /**
9
- * Pass a 'dot-paths' (e.g `pw`, or `input.pw`) that needs to be redacted from the output, in case of error.
10
- * Useful e.g to redact (prevent leaking) plaintext passwords in error messages.
11
- */
12
- redactPaths?: string[]
13
-
14
- /**
15
- * Set to true, or a function that returns true/false based on the error generated.
16
- * If true - `genericErrorHandler` will report it to errorReporter (aka Sentry).
17
- */
18
- report?: boolean | ((err: ERR) => boolean)
19
-
20
- /**
21
- * When set to true, the validated object will not be replaced with the Joi-converted value.
22
- *
23
- * The general default is `false`, with the excepction of `headers` validation, where the default is `true`.
24
- */
25
- keepOriginal?: boolean
26
- }
4
+ import { handleValidationError, type ReqValidationOptions } from '../validateRequest.util.js'
27
5
 
28
6
  class ValidateRequest {
29
7
  body<T>(
@@ -64,11 +42,10 @@ class ValidateRequest {
64
42
  schema: AnySchema<T>,
65
43
  opt: ReqValidationOptions<JoiValidationError> = {},
66
44
  ): T {
67
- const options: ReqValidationOptions<JoiValidationError> = {
68
- keepOriginal: true,
45
+ return this.validate(req, 'headers', schema, {
46
+ mutate: false,
69
47
  ...opt,
70
- }
71
- return this.validate(req, 'headers', schema, options)
48
+ })
72
49
  }
73
50
 
74
51
  private validate<T>(
@@ -77,35 +54,22 @@ class ValidateRequest {
77
54
  schema: AnySchema<T>,
78
55
  opt: ReqValidationOptions<JoiValidationError> = {},
79
56
  ): T {
80
- const { value, error } = getValidationResult(
81
- req[reqProperty] || {},
82
- schema,
83
- `request ${reqProperty}`,
84
- )
85
- if (error) {
86
- let report: boolean | undefined
87
- if (typeof opt.report === 'boolean') {
88
- report = opt.report
89
- } else if (typeof opt.report === 'function') {
90
- report = opt.report(error)
91
- }
57
+ const { mutate = true } = opt
58
+ const originalProperty = req[reqProperty] || {}
59
+
60
+ // Joi does not mutate the input
61
+ const { error, value } = getValidationResult(originalProperty, schema, `request ${reqProperty}`)
92
62
 
63
+ if (error) {
93
64
  if (opt.redactPaths) {
94
- redact(opt.redactPaths, req[reqProperty], error)
95
65
  error.data.joiValidationErrorItems.length = 0 // clears the array
96
66
  delete error.data.annotation
97
67
  }
98
68
 
99
- throw new AppError(error.message, {
100
- backendResponseStatusCode: 400,
101
- report,
102
- ...error.data,
103
- })
69
+ handleValidationError(error, originalProperty, opt)
104
70
  }
105
71
 
106
- // mutate req to replace the property with the value, converted by Joi
107
- if (!opt.keepOriginal && reqProperty !== 'query') {
108
- // query is read-only in Express 5
72
+ if (mutate) {
109
73
  req[reqProperty] = value
110
74
  }
111
75
 
@@ -114,17 +78,3 @@ class ValidateRequest {
114
78
  }
115
79
 
116
80
  export const validateRequest = new ValidateRequest()
117
-
118
- const REDACTED = 'REDACTED'
119
-
120
- /**
121
- * Mutates error
122
- */
123
- function redact(redactPaths: string[], obj: any, error: Error): void {
124
- redactPaths
125
- .map(path => _get(obj, path) as string)
126
- .filter(Boolean)
127
- .forEach(secret => {
128
- error.message = error.message.replaceAll(secret, REDACTED)
129
- })
130
- }
@@ -0,0 +1,65 @@
1
+ import { AppError } from '@naturalcycles/js-lib/error'
2
+ import { _get } from '@naturalcycles/js-lib/object'
3
+
4
+ export function handleValidationError<T, ERR extends AppError>(
5
+ error: ERR,
6
+ originalProperty: T,
7
+ opt: ReqValidationOptions<ERR> = {},
8
+ ): never {
9
+ // const item: T = opt.mutate ? { ...req[reqProperty] } : (req[reqProperty] || {})
10
+
11
+ let report: boolean | undefined
12
+ if (typeof opt.report === 'boolean') {
13
+ report = opt.report
14
+ } else if (typeof opt.report === 'function') {
15
+ report = opt.report(error)
16
+ }
17
+
18
+ if (opt.redactPaths) {
19
+ redact(opt.redactPaths, originalProperty, error)
20
+ }
21
+
22
+ throw new AppError(error.message, {
23
+ backendResponseStatusCode: 400,
24
+ report,
25
+ ...error.data,
26
+ })
27
+ }
28
+
29
+ const REDACTED = 'REDACTED'
30
+
31
+ /**
32
+ * Mutates error
33
+ */
34
+ function redact(redactPaths: string[], obj: any, error: Error): void {
35
+ redactPaths
36
+ .map(path => _get(obj, path) as string)
37
+ .filter(Boolean)
38
+ .forEach(secret => {
39
+ error.message = error.message.replaceAll(secret, REDACTED)
40
+ })
41
+ }
42
+
43
+ export interface ReqValidationOptions<ERR extends AppError> {
44
+ /**
45
+ * Pass a 'dot-paths' (e.g `pw`, or `input.pw`) that needs to be redacted from the output, in case of error.
46
+ * Useful e.g to redact (prevent leaking) plaintext passwords in error messages.
47
+ */
48
+ redactPaths?: string[]
49
+
50
+ /**
51
+ * Set to true, or a function that returns true/false based on the error generated.
52
+ * If true - `genericErrorHandler` will report it to errorReporter (aka Sentry).
53
+ */
54
+ report?: boolean | ((err: ERR) => boolean)
55
+
56
+ /**
57
+ * When set to true, the validated object will not be replaced with the Joi-converted value.
58
+ *
59
+ * Defaults to true.
60
+ * Exception is `headers` validation, where the default is `false`.
61
+ *
62
+ * To avoid mutation - shallow copy is performed.
63
+ */
64
+ mutate?: boolean
65
+ }
@@ -1,8 +1,6 @@
1
- import { AppError } from '@naturalcycles/js-lib/error'
2
- import { _get } from '@naturalcycles/js-lib/object'
3
1
  import { type ZodType, type ZodValidationError, zSafeValidate } from '@naturalcycles/js-lib/zod'
4
2
  import type { BackendRequest } from '../../server/server.model.js'
5
- import type { ReqValidationOptions } from '../joi/joiValidateRequest.js'
3
+ import { handleValidationError, type ReqValidationOptions } from '../validateRequest.util.js'
6
4
 
7
5
  class ZodValidateRequest {
8
6
  body<T>(
@@ -43,11 +41,10 @@ class ZodValidateRequest {
43
41
  schema: ZodType<T>,
44
42
  opt: ReqValidationOptions<ZodValidationError> = {},
45
43
  ): T {
46
- const options: ReqValidationOptions<ZodValidationError> = {
47
- keepOriginal: true,
44
+ return this.validate(req, 'headers', schema, {
45
+ mutate: false,
48
46
  ...opt,
49
- }
50
- return this.validate(req, 'headers', schema, options)
47
+ })
51
48
  }
52
49
 
53
50
  private validate<T>(
@@ -56,34 +53,21 @@ class ZodValidateRequest {
56
53
  schema: ZodType<T>,
57
54
  opt: ReqValidationOptions<ZodValidationError> = {},
58
55
  ): T {
59
- const { data, error } = zSafeValidate(
60
- req[reqProperty] || {},
56
+ const { mutate = true } = opt
57
+ const originalProperty = req[reqProperty] || {}
58
+
59
+ // Zod does not mutate the input
60
+ const { error, data } = zSafeValidate(
61
+ originalProperty,
61
62
  schema,
62
- // `request ${reqProperty}`,
63
+ // opt2?.itemName,
63
64
  )
64
65
 
65
66
  if (error) {
66
- let report: boolean | undefined
67
- if (typeof opt.report === 'boolean') {
68
- report = opt.report
69
- } else if (typeof opt.report === 'function') {
70
- report = opt.report(error)
71
- }
72
-
73
- if (opt.redactPaths) {
74
- redact(opt.redactPaths, req[reqProperty], error)
75
- }
76
-
77
- throw new AppError(error.message, {
78
- backendResponseStatusCode: 400,
79
- report,
80
- ...error.data,
81
- })
67
+ handleValidationError(error, originalProperty, opt)
82
68
  }
83
69
 
84
- // mutate req to replace the property with the value, converted by Joi
85
- if (!opt.keepOriginal && reqProperty !== 'query') {
86
- // query is read-only in Express 5
70
+ if (mutate) {
87
71
  req[reqProperty] = data
88
72
  }
89
73
 
@@ -92,17 +76,3 @@ class ZodValidateRequest {
92
76
  }
93
77
 
94
78
  export const zodValidateRequest = new ZodValidateRequest()
95
-
96
- const REDACTED = 'REDACTED'
97
-
98
- /**
99
- * Mutates error
100
- */
101
- function redact(redactPaths: string[], obj: any, error: Error): void {
102
- redactPaths
103
- .map(path => _get(obj, path) as string)
104
- .filter(Boolean)
105
- .forEach(secret => {
106
- error.message = error.message.replaceAll(secret, REDACTED)
107
- })
108
- }