@lenne.tech/nest-server 11.34.0 → 11.34.1
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/FRAMEWORK-API.md +1 -1
- package/dist/core/common/helpers/validation-message.helper.d.ts +3 -0
- package/dist/core/common/helpers/validation-message.helper.js +41 -0
- package/dist/core/common/helpers/validation-message.helper.js.map +1 -0
- package/dist/core/common/pipes/map-and-validate.pipe.js +16 -6
- package/dist/core/common/pipes/map-and-validate.pipe.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/migration-guides/11.34.0-to-11.34.1.md +132 -0
- package/package.json +1 -1
- package/src/core/common/helpers/validation-message.helper.ts +83 -0
- package/src/core/common/pipes/map-and-validate.pipe.ts +25 -7
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Migration Guide: 11.34.0 → 11.34.1
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None in signatures. One **client-visible text** change: validation error messages are now rendered the way class-validator itself renders them (§1, §2) |
|
|
8
|
+
| **Bugfixes** | A custom `message` in `ValidationOptions` is no longer ignored (§1). Default messages no longer reach API clients with raw placeholders such as `$constraint1` (§2) |
|
|
9
|
+
| **Migration Effort** | Nothing to configure. Read §1 and §2 if any test — or any frontend — asserts on exact validation error strings. Read §3 before you put `$value` in a custom message |
|
|
10
|
+
|
|
11
|
+
`MapAndValidatePipe` re-implements class-validator's validation executor, so it also has to render
|
|
12
|
+
error messages the way that executor does. It did neither: it built every message from the
|
|
13
|
+
constraint's `defaultMessage()` alone, and substituted only `$property`.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick Migration
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm update @lenne.tech/nest-server@11.34.1
|
|
21
|
+
pnpm run build
|
|
22
|
+
pnpm test
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
No configuration change is required, and no source change is required in a typical project.
|
|
26
|
+
|
|
27
|
+
**Vendor-mode projects: this release ADDS a file, so the sync is atomic.**
|
|
28
|
+
`src/core/common/pipes/map-and-validate.pipe.ts` now imports the new
|
|
29
|
+
`src/core/common/helpers/validation-message.helper.ts`. Taking the pipe without the helper does not
|
|
30
|
+
fail a rights check or a test — it fails the build. Sync both files together.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1. A custom `message` in `ValidationOptions` is now honored
|
|
35
|
+
|
|
36
|
+
Previously the pipe built the message from the constraint's own `defaultMessage()` and never looked
|
|
37
|
+
at `metadata.message`, so the message you wrote was silently discarded and the client received the
|
|
38
|
+
generic default instead.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
class UpdateUserInput {
|
|
42
|
+
@IsIn(['admin'], { each: true, message: 'roles may only contain assignable roles (admin)' })
|
|
43
|
+
@IsOptional()
|
|
44
|
+
roles?: string[];
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| | Before | After |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Response | `each value in roles must be one of the following values: $constraint1` | `roles may only contain assignable roles (admin)` |
|
|
51
|
+
|
|
52
|
+
Custom messages now take precedence over the constraint's default — the same order class-validator's
|
|
53
|
+
own executor uses. The function form (`message: (args) => string`) works too, and its result is
|
|
54
|
+
interpolated afterwards.
|
|
55
|
+
|
|
56
|
+
**What you may notice:** a custom message that was written but never delivered starts being
|
|
57
|
+
delivered. If a frontend or a test was written against the *default* text your project was
|
|
58
|
+
accidentally serving, it now sees the text the decorator actually asks for.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 2. Default messages no longer leak raw placeholders
|
|
63
|
+
|
|
64
|
+
Only `$property` was substituted, so every other token reached the client verbatim:
|
|
65
|
+
|
|
66
|
+
| | Before | After |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Response | `each value in roles must be one of the following values: $constraint1` | `each value in roles must be one of the following values: alpha, beta` |
|
|
69
|
+
|
|
70
|
+
All four special tokens are now interpolated — `$constraint1..N`, `$value`, `$property` and
|
|
71
|
+
`$target` — rendered exactly as class-validator renders them (arrays joined with `, `, symbols by
|
|
72
|
+
their description).
|
|
73
|
+
|
|
74
|
+
**Scope of the text change:** 44 of class-validator's built-in messages carry a `$constraint` token,
|
|
75
|
+
so those 44 change text. None of the built-ins uses `$value` or `$target`, so no built-in message
|
|
76
|
+
changes *shape* — they stop showing a placeholder and start showing the value it stood for.
|
|
77
|
+
|
|
78
|
+
**What to check:** any test or frontend string comparison that asserts on a full validation message.
|
|
79
|
+
A test that was pinned to the broken output (`…: $constraint1`) fails and should be updated to the
|
|
80
|
+
interpolated text.
|
|
81
|
+
|
|
82
|
+
This also applies to `@IsDefined`, which since class-validator 0.13 is the one built-in that does not
|
|
83
|
+
register through `ValidateBy` and therefore took a separate code path with the same two gaps.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. Security: `$value` echoes the submitted value, and validation errors are not scrubbed
|
|
88
|
+
|
|
89
|
+
`$value` now works — which means a custom message containing it renders the value the client
|
|
90
|
+
submitted, straight back into the error response.
|
|
91
|
+
|
|
92
|
+
**Validation errors bypass the secret-field filtering you may be assuming.**
|
|
93
|
+
`security.secretFields` is applied by `CheckSecurityInterceptor` on the **response** path, whereas
|
|
94
|
+
the `BadRequestException` raised by the pipe goes to the exception filter and is forwarded verbatim.
|
|
95
|
+
The guard inside the renderer is a **type** guard (`boolean | number | string`), not a secrecy guard
|
|
96
|
+
— it will happily echo a password or a token.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// DANGEROUS — the rejected password is echoed back in the error response
|
|
100
|
+
@MinLength(12, { message: 'password "$value" is too short' })
|
|
101
|
+
password: string;
|
|
102
|
+
|
|
103
|
+
// SAFE — describe the rule, never the submitted value
|
|
104
|
+
@MinLength(12, { message: 'password must be at least $constraint1 characters' })
|
|
105
|
+
password: string;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
No built-in message uses `$value`, so reaching this is opt-in: it requires a custom message that
|
|
109
|
+
names the token. Note that class-validator's own `validate()` behaves identically — this is not a
|
|
110
|
+
new exposure introduced by the framework, but it becomes reachable in projects where `$value`
|
|
111
|
+
previously did nothing.
|
|
112
|
+
|
|
113
|
+
**Action:** grep your input classes for `$value` in custom messages and confirm none of them sits on
|
|
114
|
+
a credential, token, or otherwise sensitive field.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Under the hood
|
|
119
|
+
|
|
120
|
+
The token renderer is a port of class-validator internals
|
|
121
|
+
(`ValidationUtils.replaceMessageSpecialTokens`, `constraintToString`), which are not part of its
|
|
122
|
+
public API. Because vendor-mode projects resolve their own class-validator, that port can meet a
|
|
123
|
+
version this framework never installed. It is therefore pinned by an invariant test that compares it
|
|
124
|
+
against the installed implementation over a fixture table, so a divergence surfaces as a failing
|
|
125
|
+
test rather than as two renderers disagreeing about one decorator.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Module Documentation
|
|
130
|
+
|
|
131
|
+
- Request lifecycle and the validation pipe: [`docs/REQUEST-LIFECYCLE.md`](../docs/REQUEST-LIFECYCLE.md)
|
|
132
|
+
- Field decorators and validation: `src/core/common/decorators/unified-field.decorator.ts`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.34.
|
|
3
|
+
"version": "11.34.1",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ValidationArguments } from 'class-validator';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders a single constraint for use in an error message.
|
|
5
|
+
*
|
|
6
|
+
* Mirror of class-validator's internal `constraintToString`
|
|
7
|
+
* (`class-validator/cjs/validation/ValidationUtils`), which is NOT part of its public API.
|
|
8
|
+
*
|
|
9
|
+
* See {@link replaceMessageSpecialTokens} for why this lives here and what holds it in sync.
|
|
10
|
+
*/
|
|
11
|
+
export function constraintToString(constraint: unknown): string {
|
|
12
|
+
if (Array.isArray(constraint)) {
|
|
13
|
+
return constraint.join(', ');
|
|
14
|
+
}
|
|
15
|
+
if (typeof constraint === 'symbol') {
|
|
16
|
+
constraint = constraint.description;
|
|
17
|
+
}
|
|
18
|
+
return `${constraint}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolves a message (string or function) and interpolates the special tokens
|
|
23
|
+
* $constraint1..N, $value, $property and $target.
|
|
24
|
+
*
|
|
25
|
+
* Mirror of class-validator's internal `ValidationUtils.replaceMessageSpecialTokens`
|
|
26
|
+
* (`class-validator/cjs/validation/ValidationUtils`), which is NOT part of its public API.
|
|
27
|
+
* `MapAndValidatePipe` re-implements the validation executor and therefore has to render messages
|
|
28
|
+
* exactly the way class-validator's own `validate()` would — otherwise the same decorator produces
|
|
29
|
+
* two different strings depending on which of the two ran.
|
|
30
|
+
*
|
|
31
|
+
* WHY IT IS NOT BARREL-EXPORTED
|
|
32
|
+
* Deliberately absent from `src/index.ts`. It exists to reproduce a dependency's internal
|
|
33
|
+
* behaviour, so it has to stay free to follow that dependency; exporting it would owe consumers
|
|
34
|
+
* backward compatibility for a surface we do not control. Vendor-mode consumers resolve their own
|
|
35
|
+
* class-validator, so the copy in `src/core/` can meet a version this repo never installed —
|
|
36
|
+
* `tests/unit/validation-message-mirror.spec.ts` compares it against the INSTALLED implementation
|
|
37
|
+
* over a fixture table so a divergence surfaces as a failing test rather than as two renderers of
|
|
38
|
+
* one contract.
|
|
39
|
+
*
|
|
40
|
+
* SECURITY — `$value` echoes the SUBMITTED value back to the client, and validation errors are
|
|
41
|
+
* never scrubbed: `security.secretFields` is applied by `CheckSecurityInterceptor` on the RESPONSE
|
|
42
|
+
* path, whereas the `BadRequestException` raised from the pipe goes to the exception filter and is
|
|
43
|
+
* forwarded verbatim. The check below is a TYPE guard (boolean | number | string), not a secrecy
|
|
44
|
+
* guard — it happily admits a password or a token. No built-in message uses `$value`, so reaching
|
|
45
|
+
* it is opt-in: never put `$value` in a custom message on a sensitive field.
|
|
46
|
+
*/
|
|
47
|
+
export function replaceMessageSpecialTokens(
|
|
48
|
+
message: ((args: ValidationArguments) => string) | string,
|
|
49
|
+
validationArguments: ValidationArguments,
|
|
50
|
+
): string {
|
|
51
|
+
let messageString = '';
|
|
52
|
+
if (typeof message === 'function') {
|
|
53
|
+
messageString = message(validationArguments);
|
|
54
|
+
} else if (typeof message === 'string') {
|
|
55
|
+
messageString = message;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (messageString && Array.isArray(validationArguments.constraints)) {
|
|
59
|
+
validationArguments.constraints.forEach((constraint, index) => {
|
|
60
|
+
messageString = messageString.replace(
|
|
61
|
+
new RegExp(`\\$constraint${index + 1}`, 'g'),
|
|
62
|
+
constraintToString(constraint),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
messageString &&
|
|
69
|
+
validationArguments.value !== undefined &&
|
|
70
|
+
validationArguments.value !== null &&
|
|
71
|
+
['boolean', 'number', 'string'].includes(typeof validationArguments.value)
|
|
72
|
+
) {
|
|
73
|
+
messageString = messageString.replace(/\$value/g, `${validationArguments.value}`);
|
|
74
|
+
}
|
|
75
|
+
if (messageString) {
|
|
76
|
+
messageString = messageString.replace(/\$property/g, validationArguments.property);
|
|
77
|
+
}
|
|
78
|
+
if (messageString) {
|
|
79
|
+
messageString = messageString.replace(/\$target/g, validationArguments.targetName);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return messageString;
|
|
83
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
maxLength,
|
|
21
21
|
min,
|
|
22
22
|
minLength,
|
|
23
|
+
ValidationArguments,
|
|
23
24
|
ValidationError,
|
|
24
25
|
} from 'class-validator';
|
|
25
26
|
import { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata';
|
|
@@ -27,6 +28,7 @@ import { inspect } from 'util';
|
|
|
27
28
|
|
|
28
29
|
import { getUnifiedFieldKeys, nestedTypeRegistry } from '../decorators/unified-field.decorator';
|
|
29
30
|
import { isBasicType } from '../helpers/input.helper';
|
|
31
|
+
import { replaceMessageSpecialTokens } from '../helpers/validation-message.helper';
|
|
30
32
|
import { ConfigService } from '../services/config.service';
|
|
31
33
|
import { ErrorCode } from '../../modules/error-code/error-codes';
|
|
32
34
|
|
|
@@ -268,18 +270,21 @@ async function validateWithInheritance(object: any, originalPlainValue: any): Pr
|
|
|
268
270
|
isValid = validationResult instanceof Promise ? await validationResult : validationResult;
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
// Get
|
|
273
|
+
// Get message and constraint name if validation failed
|
|
272
274
|
if (!isValid) {
|
|
273
275
|
// Use metadata.name for the constraint key (e.g., "isEmail", "isString")
|
|
274
276
|
const constraintName = metadata.name || 'customValidation';
|
|
275
277
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
278
|
+
// A custom message from ValidationOptions takes precedence over the
|
|
279
|
+
// constraint's default message — same order as class-validator's executor
|
|
280
|
+
let messageTemplate: string | ((args: ValidationArguments) => string) | undefined =
|
|
281
|
+
metadata.message as string | ((args: ValidationArguments) => string) | undefined;
|
|
282
|
+
if (!messageTemplate && typeof constraintInstance.defaultMessage === 'function') {
|
|
283
|
+
messageTemplate = constraintInstance.defaultMessage(validationArgs);
|
|
282
284
|
}
|
|
285
|
+
errorMessage = messageTemplate
|
|
286
|
+
? replaceMessageSpecialTokens(messageTemplate, validationArgs)
|
|
287
|
+
: `${propertyName} failed custom validation`;
|
|
283
288
|
|
|
284
289
|
// Add to constraints with the proper name
|
|
285
290
|
propertyError.constraints[constraintName] = errorMessage;
|
|
@@ -578,6 +583,19 @@ async function validateWithInheritance(object: any, originalPlainValue: any): Pr
|
|
|
578
583
|
|
|
579
584
|
// Add constraint violation if validation failed
|
|
580
585
|
if (!isValid) {
|
|
586
|
+
// A custom message from ValidationOptions takes precedence over the built-in message
|
|
587
|
+
if (metadata.message) {
|
|
588
|
+
errorMessage = replaceMessageSpecialTokens(
|
|
589
|
+
metadata.message as string | ((args: ValidationArguments) => string),
|
|
590
|
+
{
|
|
591
|
+
constraints: metadata.constraints || [],
|
|
592
|
+
object: tempInstance,
|
|
593
|
+
property: propertyName,
|
|
594
|
+
targetName: targetClass.name,
|
|
595
|
+
value: propertyValue,
|
|
596
|
+
},
|
|
597
|
+
);
|
|
598
|
+
}
|
|
581
599
|
propertyError.constraints[constraintType] = errorMessage;
|
|
582
600
|
}
|
|
583
601
|
}
|