@nest-toolbox/response-envelope 1.8.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/LICENSE +21 -0
- package/README.md +255 -0
- package/dist/constants.d.ts +11 -0
- package/dist/constants.js +15 -0
- package/dist/constants.js.map +1 -0
- package/dist/decorators.d.ts +32 -0
- package/dist/decorators.js +40 -0
- package/dist/decorators.js.map +1 -0
- package/dist/envelope-exception.filter.d.ts +13 -0
- package/dist/envelope-exception.filter.js +117 -0
- package/dist/envelope-exception.filter.js.map +1 -0
- package/dist/helpers.d.ts +69 -0
- package/dist/helpers.js +95 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/response-envelope.interceptor.d.ts +16 -0
- package/dist/response-envelope.interceptor.js +79 -0
- package/dist/response-envelope.interceptor.js.map +1 -0
- package/dist/response-envelope.module.d.ts +44 -0
- package/dist/response-envelope.module.js +103 -0
- package/dist/response-envelope.module.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types.d.ts +75 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 bogdanlupu
|
|
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,255 @@
|
|
|
1
|
+
# @nest-toolbox/response-envelope
|
|
2
|
+
|
|
3
|
+
Standard API response envelope with interceptor, exception filter, and helpers for NestJS.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@nest-toolbox/response-envelope)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🚀 **Zero-config** — import the module and every route gets a consistent envelope
|
|
11
|
+
- 🎯 **Decorator-first** — `@SkipEnvelope()` and `@ApiMessage()` for per-route control
|
|
12
|
+
- 🛡️ **Consistent error formatting** — class-validator errors auto-parsed into structured field errors
|
|
13
|
+
- 🔄 **Idempotent** — already-wrapped responses pass through untouched
|
|
14
|
+
- ⚙️ **Configurable** — `forRoot()` / `forRootAsync()` for custom defaults
|
|
15
|
+
- 📦 **Manual helpers** — `success()`, `error()`, `paginated()` for WebSockets, queues, etc.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @nest-toolbox/response-envelope
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// app.module.ts
|
|
27
|
+
import { Module } from '@nestjs/common';
|
|
28
|
+
import { ResponseEnvelopeModule } from '@nest-toolbox/response-envelope';
|
|
29
|
+
|
|
30
|
+
@Module({
|
|
31
|
+
imports: [ResponseEnvelopeModule.forRoot()],
|
|
32
|
+
})
|
|
33
|
+
export class AppModule {}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
That's it! Every route now returns a consistent envelope. No interceptors to register, no filters to bind.
|
|
37
|
+
|
|
38
|
+
## Response Format
|
|
39
|
+
|
|
40
|
+
### Success
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"success": true,
|
|
45
|
+
"data": { "id": 1, "name": "Alice" },
|
|
46
|
+
"message": "OK",
|
|
47
|
+
"meta": {
|
|
48
|
+
"timestamp": "2025-02-05T07:32:00.000Z",
|
|
49
|
+
"path": "/api/users/1",
|
|
50
|
+
"statusCode": 200
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Paginated Success
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"success": true,
|
|
60
|
+
"data": [{ "id": 1 }, { "id": 2 }],
|
|
61
|
+
"message": "OK",
|
|
62
|
+
"meta": {
|
|
63
|
+
"timestamp": "2025-02-05T07:32:00.000Z",
|
|
64
|
+
"path": "/api/users",
|
|
65
|
+
"statusCode": 200,
|
|
66
|
+
"pagination": {
|
|
67
|
+
"page": 1,
|
|
68
|
+
"limit": 20,
|
|
69
|
+
"total": 100,
|
|
70
|
+
"totalPages": 5
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Error
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"success": false,
|
|
81
|
+
"data": null,
|
|
82
|
+
"message": "Validation failed",
|
|
83
|
+
"errors": [
|
|
84
|
+
{ "field": "email", "message": "must be a valid email" },
|
|
85
|
+
{ "field": "name", "message": "should not be empty" }
|
|
86
|
+
],
|
|
87
|
+
"meta": {
|
|
88
|
+
"timestamp": "2025-02-05T07:32:00.000Z",
|
|
89
|
+
"path": "/api/users",
|
|
90
|
+
"statusCode": 400
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API Reference
|
|
96
|
+
|
|
97
|
+
### `ResponseEnvelopeModule.forRoot(options?)`
|
|
98
|
+
|
|
99
|
+
Register the module globally with static options.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
ResponseEnvelopeModule.forRoot({
|
|
103
|
+
defaultMessage: 'Success',
|
|
104
|
+
includePath: true,
|
|
105
|
+
includeTimestamp: true,
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Option | Type | Default | Description |
|
|
110
|
+
|--------|------|---------|-------------|
|
|
111
|
+
| `defaultMessage` | `string` | `'OK'` | Default success message when `@ApiMessage()` is not used |
|
|
112
|
+
| `includePath` | `boolean` | `true` | Include the request path in response meta |
|
|
113
|
+
| `includeTimestamp` | `boolean` | `true` | Include the ISO timestamp in response meta |
|
|
114
|
+
|
|
115
|
+
### `ResponseEnvelopeModule.forRootAsync(options)`
|
|
116
|
+
|
|
117
|
+
Register the module with async factory injection.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
ResponseEnvelopeModule.forRootAsync({
|
|
121
|
+
imports: [ConfigModule],
|
|
122
|
+
useFactory: (config: ConfigService) => ({
|
|
123
|
+
defaultMessage: config.get('API_DEFAULT_MESSAGE', 'OK'),
|
|
124
|
+
includePath: config.get('API_INCLUDE_PATH') !== 'false',
|
|
125
|
+
}),
|
|
126
|
+
inject: [ConfigService],
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### `@SkipEnvelope()`
|
|
131
|
+
|
|
132
|
+
Skip the envelope for a specific route handler. The raw return value is sent as-is.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import { SkipEnvelope } from '@nest-toolbox/response-envelope';
|
|
136
|
+
|
|
137
|
+
@Controller()
|
|
138
|
+
export class AppController {
|
|
139
|
+
@Get('health')
|
|
140
|
+
@SkipEnvelope()
|
|
141
|
+
health() {
|
|
142
|
+
return { status: 'ok' }; // sent as-is, no envelope
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `@ApiMessage(message)`
|
|
148
|
+
|
|
149
|
+
Set a custom success message for a route handler.
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { ApiMessage } from '@nest-toolbox/response-envelope';
|
|
153
|
+
|
|
154
|
+
@Controller('users')
|
|
155
|
+
export class UsersController {
|
|
156
|
+
@Post()
|
|
157
|
+
@ApiMessage('User created successfully')
|
|
158
|
+
create(@Body() dto: CreateUserDto) {
|
|
159
|
+
return this.usersService.create(dto);
|
|
160
|
+
}
|
|
161
|
+
// → { success: true, data: { ... }, message: "User created successfully", meta: { ... } }
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `success(data, options?)`
|
|
166
|
+
|
|
167
|
+
Manually create a success envelope. Useful outside the HTTP interceptor (WebSocket gateways, message queues, etc.).
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { success } from '@nest-toolbox/response-envelope';
|
|
171
|
+
|
|
172
|
+
const response = success(user, {
|
|
173
|
+
message: 'User found',
|
|
174
|
+
path: '/api/users/1',
|
|
175
|
+
statusCode: 200,
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Options:** `message?: string`, `path?: string`, `statusCode?: number`, `pagination?: PaginationMeta`
|
|
180
|
+
|
|
181
|
+
### `error(message, options?)`
|
|
182
|
+
|
|
183
|
+
Manually create an error envelope.
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { error } from '@nest-toolbox/response-envelope';
|
|
187
|
+
|
|
188
|
+
const response = error('Validation failed', {
|
|
189
|
+
statusCode: 400,
|
|
190
|
+
errors: [{ field: 'email', message: 'must be a valid email' }],
|
|
191
|
+
path: '/api/users',
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Options:** `errors?: FieldError[]`, `path?: string`, `statusCode?: number`
|
|
196
|
+
|
|
197
|
+
### `paginated(data, pagination, options?)`
|
|
198
|
+
|
|
199
|
+
Create a paginated success envelope with pagination metadata.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { paginated } from '@nest-toolbox/response-envelope';
|
|
203
|
+
|
|
204
|
+
const response = paginated(users, { page: 1, limit: 20, total: 100 }, {
|
|
205
|
+
message: 'Users retrieved',
|
|
206
|
+
path: '/api/users',
|
|
207
|
+
});
|
|
208
|
+
// → meta.pagination = { page: 1, limit: 20, total: 100, totalPages: 5 }
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Pagination:** `page: number`, `limit: number`, `total: number`
|
|
212
|
+
**Options:** `message?: string`, `path?: string`, `statusCode?: number`
|
|
213
|
+
|
|
214
|
+
## Types
|
|
215
|
+
|
|
216
|
+
All types are exported from the package. See [`src/types.ts`](./src/types.ts) for full definitions.
|
|
217
|
+
|
|
218
|
+
| Type | Description |
|
|
219
|
+
|------|-------------|
|
|
220
|
+
| `ApiResponse<T>` | Standard success response envelope |
|
|
221
|
+
| `ApiErrorResponse` | Standard error response envelope |
|
|
222
|
+
| `ResponseMeta` | Metadata attached to every response (timestamp, path, statusCode) |
|
|
223
|
+
| `PaginationMeta` | Pagination info (page, limit, total, totalPages) |
|
|
224
|
+
| `FieldError` | Structured field-level error (`{ field, message }`) |
|
|
225
|
+
| `ResponseEnvelopeOptions` | Static configuration options for `forRoot()` |
|
|
226
|
+
| `ResponseEnvelopeAsyncOptions` | Async configuration options for `forRootAsync()` |
|
|
227
|
+
|
|
228
|
+
## Integration with Other Toolbox Packages
|
|
229
|
+
|
|
230
|
+
### @nest-toolbox/typeorm-paginate
|
|
231
|
+
|
|
232
|
+
The `paginated()` helper pairs naturally with [`@nest-toolbox/typeorm-paginate`](../typeorm-paginate):
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { paginate } from '@nest-toolbox/typeorm-paginate';
|
|
236
|
+
import { paginated } from '@nest-toolbox/response-envelope';
|
|
237
|
+
|
|
238
|
+
@Get()
|
|
239
|
+
async findAll(@Query() query: PaginationDto) {
|
|
240
|
+
const result = await paginate(this.userRepository, {
|
|
241
|
+
page: query.page,
|
|
242
|
+
limit: query.limit,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return paginated(result.items, {
|
|
246
|
+
page: result.meta.currentPage,
|
|
247
|
+
limit: result.meta.itemsPerPage,
|
|
248
|
+
total: result.meta.totalItems,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT © [Bogdan Lupu](https://github.com/lupu60)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants for @nest-toolbox/response-envelope
|
|
3
|
+
*/
|
|
4
|
+
/** Metadata key for @SkipEnvelope() decorator */
|
|
5
|
+
export declare const SKIP_ENVELOPE_KEY = "response-envelope:skip";
|
|
6
|
+
/** Metadata key for @ApiMessage() decorator */
|
|
7
|
+
export declare const API_MESSAGE_KEY = "response-envelope:message";
|
|
8
|
+
/** Injection token for module options */
|
|
9
|
+
export declare const RESPONSE_ENVELOPE_OPTIONS = "RESPONSE_ENVELOPE_OPTIONS";
|
|
10
|
+
/** Default success message */
|
|
11
|
+
export declare const DEFAULT_MESSAGE = "OK";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Constants for @nest-toolbox/response-envelope
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DEFAULT_MESSAGE = exports.RESPONSE_ENVELOPE_OPTIONS = exports.API_MESSAGE_KEY = exports.SKIP_ENVELOPE_KEY = void 0;
|
|
7
|
+
/** Metadata key for @SkipEnvelope() decorator */
|
|
8
|
+
exports.SKIP_ENVELOPE_KEY = 'response-envelope:skip';
|
|
9
|
+
/** Metadata key for @ApiMessage() decorator */
|
|
10
|
+
exports.API_MESSAGE_KEY = 'response-envelope:message';
|
|
11
|
+
/** Injection token for module options */
|
|
12
|
+
exports.RESPONSE_ENVELOPE_OPTIONS = 'RESPONSE_ENVELOPE_OPTIONS';
|
|
13
|
+
/** Default success message */
|
|
14
|
+
exports.DEFAULT_MESSAGE = 'OK';
|
|
15
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAEH,iDAAiD;AACpC,QAAA,iBAAiB,GAAG,wBAAwB,CAAC;AAE1D,+CAA+C;AAClC,QAAA,eAAe,GAAG,2BAA2B,CAAC;AAE3D,yCAAyC;AAC5B,QAAA,yBAAyB,GAAG,2BAA2B,CAAC;AAErE,8BAA8B;AACjB,QAAA,eAAe,GAAG,IAAI,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decorators for @nest-toolbox/response-envelope
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Skip the response envelope for a specific route handler.
|
|
6
|
+
* The raw controller return value will be sent as-is.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* @Get('health')
|
|
11
|
+
* @SkipEnvelope()
|
|
12
|
+
* health() {
|
|
13
|
+
* return { status: 'ok' };
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const SkipEnvelope: () => import("@nestjs/common").CustomDecorator<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Set a custom success message for a route handler's response envelope.
|
|
20
|
+
*
|
|
21
|
+
* @param message - The message to include in the response envelope
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* @Post()
|
|
26
|
+
* @ApiMessage('User created successfully')
|
|
27
|
+
* create(@Body() dto: CreateUserDto) {
|
|
28
|
+
* return this.usersService.create(dto);
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare const ApiMessage: (message: string) => import("@nestjs/common").CustomDecorator<string>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Decorators for @nest-toolbox/response-envelope
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ApiMessage = exports.SkipEnvelope = void 0;
|
|
7
|
+
const common_1 = require("@nestjs/common");
|
|
8
|
+
const constants_1 = require("./constants");
|
|
9
|
+
/**
|
|
10
|
+
* Skip the response envelope for a specific route handler.
|
|
11
|
+
* The raw controller return value will be sent as-is.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* @Get('health')
|
|
16
|
+
* @SkipEnvelope()
|
|
17
|
+
* health() {
|
|
18
|
+
* return { status: 'ok' };
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
const SkipEnvelope = () => (0, common_1.SetMetadata)(constants_1.SKIP_ENVELOPE_KEY, true);
|
|
23
|
+
exports.SkipEnvelope = SkipEnvelope;
|
|
24
|
+
/**
|
|
25
|
+
* Set a custom success message for a route handler's response envelope.
|
|
26
|
+
*
|
|
27
|
+
* @param message - The message to include in the response envelope
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* @Post()
|
|
32
|
+
* @ApiMessage('User created successfully')
|
|
33
|
+
* create(@Body() dto: CreateUserDto) {
|
|
34
|
+
* return this.usersService.create(dto);
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
const ApiMessage = (message) => (0, common_1.SetMetadata)(constants_1.API_MESSAGE_KEY, message);
|
|
39
|
+
exports.ApiMessage = ApiMessage;
|
|
40
|
+
//# sourceMappingURL=decorators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decorators.js","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAEH,2CAA6C;AAC7C,2CAAiE;AAEjE;;;;;;;;;;;;GAYG;AACI,MAAM,YAAY,GAAG,GAAG,EAAE,CAAC,IAAA,oBAAW,EAAC,6BAAiB,EAAE,IAAI,CAAC,CAAC;AAA1D,QAAA,YAAY,gBAA8C;AAEvE;;;;;;;;;;;;;GAaG;AACI,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,IAAA,oBAAW,EAAC,2BAAe,EAAE,OAAO,CAAC,CAAC;AAAxE,QAAA,UAAU,cAA8D"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exception filter that formats errors in the standard envelope shape.
|
|
3
|
+
*
|
|
4
|
+
* Handles HttpException (and subclasses like BadRequestException, NotFoundException)
|
|
5
|
+
* as well as unknown errors, formatting them into a consistent ApiErrorResponse.
|
|
6
|
+
*/
|
|
7
|
+
import { type ArgumentsHost, type ExceptionFilter } from '@nestjs/common';
|
|
8
|
+
import type { ResponseEnvelopeOptions } from './types';
|
|
9
|
+
export declare class EnvelopeExceptionFilter implements ExceptionFilter {
|
|
10
|
+
private readonly options?;
|
|
11
|
+
constructor(options?: ResponseEnvelopeOptions | undefined);
|
|
12
|
+
catch(exception: unknown, host: ArgumentsHost): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Exception filter that formats errors in the standard envelope shape.
|
|
4
|
+
*
|
|
5
|
+
* Handles HttpException (and subclasses like BadRequestException, NotFoundException)
|
|
6
|
+
* as well as unknown errors, formatting them into a consistent ApiErrorResponse.
|
|
7
|
+
*/
|
|
8
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
9
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
10
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
11
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
12
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
13
|
+
};
|
|
14
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
15
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
16
|
+
};
|
|
17
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
18
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.EnvelopeExceptionFilter = void 0;
|
|
22
|
+
const common_1 = require("@nestjs/common");
|
|
23
|
+
const constants_1 = require("./constants");
|
|
24
|
+
let EnvelopeExceptionFilter = class EnvelopeExceptionFilter {
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
}
|
|
28
|
+
catch(exception, host) {
|
|
29
|
+
var _a, _b, _c, _d, _e;
|
|
30
|
+
const ctx = host.switchToHttp();
|
|
31
|
+
const response = ctx.getResponse();
|
|
32
|
+
const request = ctx.getRequest();
|
|
33
|
+
const statusCode = exception instanceof common_1.HttpException ? exception.getStatus() : common_1.HttpStatus.INTERNAL_SERVER_ERROR;
|
|
34
|
+
const message = extractMessage(exception);
|
|
35
|
+
const errors = extractFieldErrors(exception);
|
|
36
|
+
const includePath = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.includePath) !== null && _b !== void 0 ? _b : true;
|
|
37
|
+
const includeTimestamp = (_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.includeTimestamp) !== null && _d !== void 0 ? _d : true;
|
|
38
|
+
const body = {
|
|
39
|
+
success: false,
|
|
40
|
+
data: null,
|
|
41
|
+
message,
|
|
42
|
+
errors,
|
|
43
|
+
meta: {
|
|
44
|
+
timestamp: includeTimestamp ? new Date().toISOString() : '',
|
|
45
|
+
path: includePath ? ((_e = request.url) !== null && _e !== void 0 ? _e : '') : '',
|
|
46
|
+
statusCode,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
response.status(statusCode).json(body);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
exports.EnvelopeExceptionFilter = EnvelopeExceptionFilter;
|
|
53
|
+
exports.EnvelopeExceptionFilter = EnvelopeExceptionFilter = __decorate([
|
|
54
|
+
(0, common_1.Catch)(),
|
|
55
|
+
(0, common_1.Injectable)(),
|
|
56
|
+
__param(0, (0, common_1.Optional)()),
|
|
57
|
+
__param(0, (0, common_1.Inject)(constants_1.RESPONSE_ENVELOPE_OPTIONS)),
|
|
58
|
+
__metadata("design:paramtypes", [Object])
|
|
59
|
+
], EnvelopeExceptionFilter);
|
|
60
|
+
/**
|
|
61
|
+
* Extract a human-readable message from the exception.
|
|
62
|
+
*/
|
|
63
|
+
function extractMessage(exception) {
|
|
64
|
+
if (exception instanceof common_1.HttpException) {
|
|
65
|
+
const response = exception.getResponse();
|
|
66
|
+
if (typeof response === 'string') {
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
if (typeof response === 'object' && response !== null) {
|
|
70
|
+
const obj = response;
|
|
71
|
+
if (typeof obj.message === 'string') {
|
|
72
|
+
return obj.message;
|
|
73
|
+
}
|
|
74
|
+
// class-validator returns message as an array
|
|
75
|
+
if (Array.isArray(obj.message) && obj.message.length > 0) {
|
|
76
|
+
return 'Validation failed';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (exception instanceof Error) {
|
|
81
|
+
return exception.message;
|
|
82
|
+
}
|
|
83
|
+
return 'Internal server error';
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Extract structured field errors from class-validator ValidationPipe responses.
|
|
87
|
+
*/
|
|
88
|
+
function extractFieldErrors(exception) {
|
|
89
|
+
if (!(exception instanceof common_1.HttpException)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const response = exception.getResponse();
|
|
93
|
+
if (typeof response !== 'object' || response === null) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const obj = response;
|
|
97
|
+
// class-validator via ValidationPipe returns { message: string[] }
|
|
98
|
+
if (Array.isArray(obj.message)) {
|
|
99
|
+
return obj.message.filter((msg) => typeof msg === 'string').map(parseValidationMessage);
|
|
100
|
+
}
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse a class-validator message like "email must be a valid email" into a FieldError.
|
|
105
|
+
* The first word is typically the field name.
|
|
106
|
+
*/
|
|
107
|
+
function parseValidationMessage(message) {
|
|
108
|
+
const spaceIndex = message.indexOf(' ');
|
|
109
|
+
if (spaceIndex === -1) {
|
|
110
|
+
return { field: 'unknown', message };
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
field: message.substring(0, spaceIndex),
|
|
114
|
+
message: message.substring(spaceIndex + 1),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=envelope-exception.filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope-exception.filter.js","sourceRoot":"","sources":["../src/envelope-exception.filter.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;;;;;;;;;;;;;AAEH,2CAA0I;AAC1I,2CAAwD;AAKjD,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAClC,YAGmB,OAAiC;QAAjC,YAAO,GAAP,OAAO,CAA0B;IACjD,CAAC;IAEJ,KAAK,CAAC,SAAkB,EAAE,IAAmB;;QAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,EAAE,CAAC;QAEjC,MAAM,UAAU,GAAG,SAAS,YAAY,sBAAa,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,mBAAU,CAAC,qBAAqB,CAAC;QAEjH,MAAM,OAAO,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAC;QAE7C,MAAM,WAAW,GAAG,MAAA,MAAA,IAAI,CAAC,OAAO,0CAAE,WAAW,mCAAI,IAAI,CAAC;QACtD,MAAM,gBAAgB,GAAG,MAAA,MAAA,IAAI,CAAC,OAAO,0CAAE,gBAAgB,mCAAI,IAAI,CAAC;QAEhE,MAAM,IAAI,GAAqB;YAC7B,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,IAAI;YACV,OAAO;YACP,MAAM;YACN,IAAI,EAAE;gBACJ,SAAS,EAAE,gBAAgB,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE;gBAC3D,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,MAAA,OAAO,CAAC,GAAG,mCAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE;gBAC5C,UAAU;aACX;SACF,CAAC;QAEF,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzC,CAAC;CACF,CAAA;AAlCY,0DAAuB;kCAAvB,uBAAuB;IAFnC,IAAA,cAAK,GAAE;IACP,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,iBAAQ,GAAE,CAAA;IACV,WAAA,IAAA,eAAM,EAAC,qCAAyB,CAAC,CAAA;;GAHzB,uBAAuB,CAkCnC;AAED;;GAEG;AACH,SAAS,cAAc,CAAC,SAAkB;IACxC,IAAI,SAAS,YAAY,sBAAa,EAAE,CAAC;QACvC,MAAM,QAAQ,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;QAEzC,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtD,MAAM,GAAG,GAAG,QAAmC,CAAC;YAChD,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;gBACpC,OAAO,GAAG,CAAC,OAAO,CAAC;YACrB,CAAC;YACD,8CAA8C;YAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACzD,OAAO,mBAAmB,CAAC;YAC7B,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,SAAS,YAAY,KAAK,EAAE,CAAC;QAC/B,OAAO,SAAS,CAAC,OAAO,CAAC;IAC3B,CAAC;IAED,OAAO,uBAAuB,CAAC;AACjC,CAAC;AAED;;GAEG;AACH,SAAS,kBAAkB,CAAC,SAAkB;IAC5C,IAAI,CAAC,CAAC,SAAS,YAAY,sBAAa,CAAC,EAAE,CAAC;QAC1C,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,QAAQ,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;IAEzC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;QACtD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,GAAG,GAAG,QAAmC,CAAC;IAEhD,mEAAmE;IACnE,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QAC/B,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IACzG,CAAC;IAED,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;GAGG;AACH,SAAS,sBAAsB,CAAC,OAAe;IAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAExC,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;IACvC,CAAC;IAED,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,UAAU,CAAC;QACvC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,UAAU,GAAG,CAAC,CAAC;KAC3C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for manually creating envelope responses.
|
|
3
|
+
*
|
|
4
|
+
* Use these when you need to construct responses outside of the interceptor,
|
|
5
|
+
* for example in WebSocket gateways or message queues.
|
|
6
|
+
*/
|
|
7
|
+
import type { ApiErrorResponse, ApiResponse, FieldError, PaginationMeta } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* Create a success envelope.
|
|
10
|
+
*
|
|
11
|
+
* @param data - The response payload
|
|
12
|
+
* @param options - Optional message, path, statusCode, and pagination
|
|
13
|
+
* @returns A typed ApiResponse envelope
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* return success(user, { message: 'User found', path: '/api/users/1' });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function success<T>(data: T, options?: {
|
|
21
|
+
message?: string;
|
|
22
|
+
path?: string;
|
|
23
|
+
statusCode?: number;
|
|
24
|
+
pagination?: PaginationMeta;
|
|
25
|
+
}): ApiResponse<T>;
|
|
26
|
+
/**
|
|
27
|
+
* Create an error envelope.
|
|
28
|
+
*
|
|
29
|
+
* @param message - The error message
|
|
30
|
+
* @param options - Optional errors array, path, and statusCode
|
|
31
|
+
* @returns A typed ApiErrorResponse envelope
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* return error('Validation failed', {
|
|
36
|
+
* statusCode: 400,
|
|
37
|
+
* errors: [{ field: 'email', message: 'must be a valid email' }],
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function error(message: string, options?: {
|
|
42
|
+
errors?: FieldError[];
|
|
43
|
+
path?: string;
|
|
44
|
+
statusCode?: number;
|
|
45
|
+
}): ApiErrorResponse;
|
|
46
|
+
/**
|
|
47
|
+
* Create a paginated success envelope.
|
|
48
|
+
*
|
|
49
|
+
* @param data - The array of items for the current page
|
|
50
|
+
* @param pagination - Pagination metadata (page, limit, total)
|
|
51
|
+
* @param options - Optional message, path, and statusCode
|
|
52
|
+
* @returns A typed ApiResponse envelope with pagination meta
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* return paginated(users, { page: 1, limit: 20, total: 100 }, {
|
|
57
|
+
* path: '/api/users',
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function paginated<T>(data: T[], pagination: {
|
|
62
|
+
page: number;
|
|
63
|
+
limit: number;
|
|
64
|
+
total: number;
|
|
65
|
+
}, options?: {
|
|
66
|
+
message?: string;
|
|
67
|
+
path?: string;
|
|
68
|
+
statusCode?: number;
|
|
69
|
+
}): ApiResponse<T[]>;
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Helper functions for manually creating envelope responses.
|
|
4
|
+
*
|
|
5
|
+
* Use these when you need to construct responses outside of the interceptor,
|
|
6
|
+
* for example in WebSocket gateways or message queues.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.success = success;
|
|
10
|
+
exports.error = error;
|
|
11
|
+
exports.paginated = paginated;
|
|
12
|
+
const constants_1 = require("./constants");
|
|
13
|
+
/**
|
|
14
|
+
* Create a success envelope.
|
|
15
|
+
*
|
|
16
|
+
* @param data - The response payload
|
|
17
|
+
* @param options - Optional message, path, statusCode, and pagination
|
|
18
|
+
* @returns A typed ApiResponse envelope
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* return success(user, { message: 'User found', path: '/api/users/1' });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
function success(data, options = {}) {
|
|
26
|
+
var _a, _b, _c;
|
|
27
|
+
const meta = {
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
path: (_a = options.path) !== null && _a !== void 0 ? _a : '',
|
|
30
|
+
statusCode: (_b = options.statusCode) !== null && _b !== void 0 ? _b : 200,
|
|
31
|
+
};
|
|
32
|
+
if (options.pagination) {
|
|
33
|
+
meta.pagination = options.pagination;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
data,
|
|
38
|
+
message: (_c = options.message) !== null && _c !== void 0 ? _c : constants_1.DEFAULT_MESSAGE,
|
|
39
|
+
meta,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create an error envelope.
|
|
44
|
+
*
|
|
45
|
+
* @param message - The error message
|
|
46
|
+
* @param options - Optional errors array, path, and statusCode
|
|
47
|
+
* @returns A typed ApiErrorResponse envelope
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* return error('Validation failed', {
|
|
52
|
+
* statusCode: 400,
|
|
53
|
+
* errors: [{ field: 'email', message: 'must be a valid email' }],
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
function error(message, options = {}) {
|
|
58
|
+
var _a, _b, _c;
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
data: null,
|
|
62
|
+
message,
|
|
63
|
+
errors: (_a = options.errors) !== null && _a !== void 0 ? _a : [],
|
|
64
|
+
meta: {
|
|
65
|
+
timestamp: new Date().toISOString(),
|
|
66
|
+
path: (_b = options.path) !== null && _b !== void 0 ? _b : '',
|
|
67
|
+
statusCode: (_c = options.statusCode) !== null && _c !== void 0 ? _c : 500,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create a paginated success envelope.
|
|
73
|
+
*
|
|
74
|
+
* @param data - The array of items for the current page
|
|
75
|
+
* @param pagination - Pagination metadata (page, limit, total)
|
|
76
|
+
* @param options - Optional message, path, and statusCode
|
|
77
|
+
* @returns A typed ApiResponse envelope with pagination meta
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* return paginated(users, { page: 1, limit: 20, total: 100 }, {
|
|
82
|
+
* path: '/api/users',
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
function paginated(data, pagination, options = {}) {
|
|
87
|
+
const totalPages = pagination.limit > 0 ? Math.ceil(pagination.total / pagination.limit) : 0;
|
|
88
|
+
return success(data, Object.assign(Object.assign({}, options), { pagination: {
|
|
89
|
+
page: pagination.page,
|
|
90
|
+
limit: pagination.limit,
|
|
91
|
+
total: pagination.total,
|
|
92
|
+
totalPages,
|
|
93
|
+
} }));
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=helpers.js.map
|