@ezilemdodana/nest-mapper 1.0.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.
- package/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/constants/metadata.constants.d.ts +1 -0
- package/dist/constants/metadata.constants.js +4 -0
- package/dist/decorators/auto-map.decorator.d.ts +13 -0
- package/dist/decorators/auto-map.decorator.js +16 -0
- package/dist/errors/mapper.errors.d.ts +18 -0
- package/dist/errors/mapper.errors.js +11 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +23 -0
- package/dist/mapper.service.d.ts +48 -0
- package/dist/mapper.service.js +127 -0
- package/dist/types/auto-map.types.d.ts +14 -0
- package/dist/types/auto-map.types.js +2 -0
- package/dist/utils/path.utils.d.ts +10 -0
- package/dist/utils/path.utils.js +49 -0
- package/package.json +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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,315 @@
|
|
|
1
|
+
# Nest Mapper
|
|
2
|
+
|
|
3
|
+
A lightweight, dependency-minimal, metadata-based object mapper for NestJS.
|
|
4
|
+
Built for explicit, predictable DTO mapping without heavy libraries.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @ezilemdodana/nest-mapper reflect-metadata
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Prerequisite
|
|
17
|
+
|
|
18
|
+
Ensure `reflect-metadata` is imported **once** in your application entry point (usually `main.ts`):
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import 'reflect-metadata';
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Overview
|
|
27
|
+
|
|
28
|
+
Nest Mapper provides a simple `MapperService` and a single decorator, `@AutoMap()`, to map objects to DTOs.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Key features
|
|
33
|
+
|
|
34
|
+
- **Explicit, opt-in mapping via decorators**
|
|
35
|
+
- **Same-key mapping by default**
|
|
36
|
+
- **Support for different source keys**
|
|
37
|
+
- **Nested source paths (dot notation)**
|
|
38
|
+
- **Sync and async mapping**
|
|
39
|
+
- **Array and Promise support**
|
|
40
|
+
- **Validation for required fields**
|
|
41
|
+
- **Collect-all-errors mode**
|
|
42
|
+
- **No heavy dependencies**
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Recommended: Dependency Injection
|
|
47
|
+
|
|
48
|
+
### Inject into a service or controller
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { Injectable } from '@nestjs/common';
|
|
52
|
+
import { MapperService } from '@ezilemdodana/nest-mapper';
|
|
53
|
+
|
|
54
|
+
@Injectable()
|
|
55
|
+
export class UsersService {
|
|
56
|
+
constructor(private readonly mapper: MapperService) {}
|
|
57
|
+
|
|
58
|
+
getUserDto(entity: UserEntity): UserDto {
|
|
59
|
+
return this.mapper.map(entity as any, UserDto, {
|
|
60
|
+
requireDecorators: true,
|
|
61
|
+
validateRequired: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Register once in your module
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { Module } from '@nestjs/common';
|
|
71
|
+
import { MapperService } from '@ezilemdodana/nest-mapper';
|
|
72
|
+
|
|
73
|
+
@Module({
|
|
74
|
+
providers: [MapperService],
|
|
75
|
+
exports: [MapperService],
|
|
76
|
+
})
|
|
77
|
+
export class UsersModule {}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Alternative: Manual Instantiation (No NestJS DI)
|
|
83
|
+
|
|
84
|
+
Useful for:
|
|
85
|
+
- Scripts
|
|
86
|
+
- Tests
|
|
87
|
+
- Non-Nest environments
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { MapperService } from '@ezilemdodana/nest-mapper';
|
|
91
|
+
|
|
92
|
+
const mapper = new MapperService();
|
|
93
|
+
const dto = mapper.map(entity as any, UserDto);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Basic Usage (Same Key Mapping)
|
|
99
|
+
|
|
100
|
+
When source and destination property names are the same, use `@AutoMap()`.
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { MapperService, AutoMap } from '@ezilemdodana/nest-mapper';
|
|
104
|
+
|
|
105
|
+
class UserEntity {
|
|
106
|
+
firstName = 'Hello';
|
|
107
|
+
lastName = 'World';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class UserDto {
|
|
111
|
+
@AutoMap()
|
|
112
|
+
firstName!: string;
|
|
113
|
+
|
|
114
|
+
@AutoMap()
|
|
115
|
+
lastName!: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const mapper = new MapperService();
|
|
119
|
+
const dto = mapper.map(new UserEntity() as any, UserDto);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Mapping From a Different Source Key
|
|
125
|
+
|
|
126
|
+
If the source and destination property names differ, pass the source key to `@AutoMap()`.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { MapperService, AutoMap } from '@ezilemdodana/nest-mapper';
|
|
130
|
+
|
|
131
|
+
class UserEntity {
|
|
132
|
+
first_name = 'Hello';
|
|
133
|
+
last_name = 'World';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
class UserDto {
|
|
137
|
+
@AutoMap('first_name')
|
|
138
|
+
firstName!: string;
|
|
139
|
+
|
|
140
|
+
@AutoMap('last_name')
|
|
141
|
+
lastName!: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const mapper = new MapperService();
|
|
145
|
+
const dto = mapper.map(new UserEntity() as any, UserDto);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Nested Source Path Mapping (Dot Notation)
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { MapperService, AutoMap } from '@ezilemdodana/nest-mapper';
|
|
154
|
+
|
|
155
|
+
class UserEntity {
|
|
156
|
+
user = {
|
|
157
|
+
address: {
|
|
158
|
+
street: 'Main Road',
|
|
159
|
+
city: 'Cape Town',
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class AddressDto {
|
|
165
|
+
@AutoMap('user.address.street')
|
|
166
|
+
street!: string;
|
|
167
|
+
|
|
168
|
+
@AutoMap('user.address.city')
|
|
169
|
+
city!: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const mapper = new MapperService();
|
|
173
|
+
const dto = mapper.map(new UserEntity() as any, AddressDto);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Validation (Required Fields)
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { MapperService, AutoMap } from '@ezilemdodana/nest-mapper';
|
|
182
|
+
|
|
183
|
+
class UserEntity {
|
|
184
|
+
first_name = 'Hello';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
class UserDto {
|
|
188
|
+
@AutoMap('first_name', { required: true })
|
|
189
|
+
firstName!: string;
|
|
190
|
+
|
|
191
|
+
@AutoMap('last_name', { required: true, label: 'Surname' })
|
|
192
|
+
lastName!: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const mapper = new MapperService();
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Throw mode (default)
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { MapperValidationError } from '@ezilemdodana/nest-mapper';
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const dto = mapper.map(new UserEntity() as any, UserDto, {
|
|
205
|
+
requireDecorators: true,
|
|
206
|
+
validateRequired: true,
|
|
207
|
+
validationMode: 'throw',
|
|
208
|
+
});
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (e instanceof MapperValidationError) {
|
|
211
|
+
console.log(e.issues);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Collect-all-errors mode
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const result = mapper.mapSafe(new UserEntity() as any, UserDto, {
|
|
220
|
+
requireDecorators: true,
|
|
221
|
+
validateRequired: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
console.log(result.value);
|
|
225
|
+
console.log(result.errors);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Async Mapping
|
|
231
|
+
|
|
232
|
+
### Map a single Promise source
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const dto = await mapper.mapAsync(
|
|
236
|
+
Promise.resolve(new UserEntity() as any),
|
|
237
|
+
UserDto,
|
|
238
|
+
);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Map an array
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const dtos = mapper.mapArray(
|
|
245
|
+
[new UserEntity() as any, new UserEntity() as any],
|
|
246
|
+
UserDto,
|
|
247
|
+
);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Map async arrays
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
const dtos = await mapper.mapArrayAsync(
|
|
254
|
+
[Promise.resolve(new UserEntity() as any)],
|
|
255
|
+
UserDto,
|
|
256
|
+
);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Async safe mapping
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const result = await mapper.mapSafeAsync(
|
|
263
|
+
Promise.resolve(new UserEntity() as any),
|
|
264
|
+
UserDto,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
console.log(result.value);
|
|
268
|
+
console.log(result.errors);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Important Notes
|
|
274
|
+
|
|
275
|
+
This mapper iterates over **runtime DTO keys**.
|
|
276
|
+
Ensure all mapped properties are decorated with `@AutoMap()`.
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
class UserDto {
|
|
280
|
+
@AutoMap()
|
|
281
|
+
firstName!: string;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
If a property is not decorated and `requireDecorators` is enabled, it will not be mapped.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## API Summary
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
mapper.map(source, DestinationClass, options)
|
|
293
|
+
mapper.mapArray(sources, DestinationClass, options)
|
|
294
|
+
|
|
295
|
+
await mapper.mapAsync(sourceOrPromise, DestinationClass, options)
|
|
296
|
+
await mapper.mapArrayAsync(arrayOrPromise, DestinationClass, options)
|
|
297
|
+
|
|
298
|
+
mapper.mapSafe(source, DestinationClass, options)
|
|
299
|
+
mapper.mapArraySafe(sources, DestinationClass, options)
|
|
300
|
+
|
|
301
|
+
await mapper.mapSafeAsync(sourceOrPromise, DestinationClass, options)
|
|
302
|
+
await mapper.mapArraySafeAsync(arrayOrPromise, DestinationClass, options)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Why Nest Mapper?
|
|
308
|
+
|
|
309
|
+
- No class-transformer
|
|
310
|
+
- No implicit magic
|
|
311
|
+
- Minimal reflection
|
|
312
|
+
- Clear, maintainable DTO mapping
|
|
313
|
+
- Designed specifically for NestJS
|
|
314
|
+
|
|
315
|
+
---
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AUTO_MAP_METADATA_KEY: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AutoMapOptions } from '../types/auto-map.types';
|
|
2
|
+
/**
|
|
3
|
+
* Marks a DTO property as mappable.
|
|
4
|
+
* - @AutoMap() -> maps from same key
|
|
5
|
+
* - @AutoMap({ required: true }) -> same key + required
|
|
6
|
+
* - @AutoMap('first_name') -> maps from different key
|
|
7
|
+
* - @AutoMap('user.address.street') -> nested source path
|
|
8
|
+
* - @AutoMap('first_name', { required: true }) -> required mapping
|
|
9
|
+
*/
|
|
10
|
+
export declare function AutoMap(): PropertyDecorator;
|
|
11
|
+
export declare function AutoMap(options: AutoMapOptions): PropertyDecorator;
|
|
12
|
+
export declare function AutoMap(sourceKey: string): PropertyDecorator;
|
|
13
|
+
export declare function AutoMap(sourceKey: string, options: AutoMapOptions): PropertyDecorator;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AutoMap = AutoMap;
|
|
4
|
+
const metadata_constants_1 = require("../constants/metadata.constants");
|
|
5
|
+
function AutoMap(sourceKeyOrOptions, maybeOptions) {
|
|
6
|
+
return (target, propertyKey) => {
|
|
7
|
+
const sourceKey = typeof sourceKeyOrOptions === 'string'
|
|
8
|
+
? sourceKeyOrOptions
|
|
9
|
+
: String(propertyKey);
|
|
10
|
+
const options = typeof sourceKeyOrOptions === 'object'
|
|
11
|
+
? sourceKeyOrOptions
|
|
12
|
+
: (maybeOptions ?? {});
|
|
13
|
+
const metadata = { sourceKey, options };
|
|
14
|
+
Reflect.defineMetadata(metadata_constants_1.AUTO_MAP_METADATA_KEY, metadata, target, propertyKey);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type MapperValidationIssue = {
|
|
2
|
+
/**
|
|
3
|
+
* Destination path (e.g. "UserDto.firstName" or "UserDto.address.street")
|
|
4
|
+
*/
|
|
5
|
+
destinationPath: string;
|
|
6
|
+
/**
|
|
7
|
+
* Source path/key (e.g. "first_name" or "user.address.street")
|
|
8
|
+
*/
|
|
9
|
+
sourcePath: string;
|
|
10
|
+
/**
|
|
11
|
+
* Human-readable message
|
|
12
|
+
*/
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
export declare class MapperValidationError extends Error {
|
|
16
|
+
readonly issues: MapperValidationIssue[];
|
|
17
|
+
constructor(issues: MapperValidationIssue[]);
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MapperValidationError = void 0;
|
|
4
|
+
class MapperValidationError extends Error {
|
|
5
|
+
constructor(issues) {
|
|
6
|
+
super(issues[0]?.message ?? 'Mapper validation failed.');
|
|
7
|
+
this.name = 'MapperValidationError';
|
|
8
|
+
this.issues = issues;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.MapperValidationError = MapperValidationError;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
export * from './mapper.service';
|
|
3
|
+
export * from './decorators/auto-map.decorator';
|
|
4
|
+
export * from './constants/metadata.constants';
|
|
5
|
+
export * from './errors/mapper.errors';
|
|
6
|
+
export * from './types/auto-map.types';
|
|
7
|
+
export * from './utils/path.utils';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
require("reflect-metadata");
|
|
18
|
+
__exportStar(require("./mapper.service"), exports);
|
|
19
|
+
__exportStar(require("./decorators/auto-map.decorator"), exports);
|
|
20
|
+
__exportStar(require("./constants/metadata.constants"), exports);
|
|
21
|
+
__exportStar(require("./errors/mapper.errors"), exports);
|
|
22
|
+
__exportStar(require("./types/auto-map.types"), exports);
|
|
23
|
+
__exportStar(require("./utils/path.utils"), exports);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { MapperValidationIssue } from './errors/mapper.errors';
|
|
3
|
+
type AnyRecord = Record<string, unknown>;
|
|
4
|
+
export type MapperOptions = {
|
|
5
|
+
/**
|
|
6
|
+
* If true, throws when a destination property is not decorated with @AutoMap().
|
|
7
|
+
* This prevents “silent no mapping”.
|
|
8
|
+
*/
|
|
9
|
+
requireDecorators?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* If true, required fields produce validation issues.
|
|
12
|
+
*/
|
|
13
|
+
validateRequired?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Controls behaviour when validation issues exist:
|
|
16
|
+
* - "throw" -> throw MapperValidationError on first mapping call
|
|
17
|
+
* - "collect" -> return issues (via mapSafe / mapArraySafe) without throwing
|
|
18
|
+
*/
|
|
19
|
+
validationMode?: 'throw' | 'collect';
|
|
20
|
+
/**
|
|
21
|
+
* If true, supports destination keys like "address.street" (nested destination paths).
|
|
22
|
+
* By default, destination mapping uses the DTO property name exactly.
|
|
23
|
+
*/
|
|
24
|
+
allowNestedDestinationPaths?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export type MapperResult<T> = {
|
|
27
|
+
value: T;
|
|
28
|
+
errors: MapperValidationIssue[];
|
|
29
|
+
};
|
|
30
|
+
export declare class MapperService {
|
|
31
|
+
/**
|
|
32
|
+
* Standard mapping. Throws if validationMode is "throw" and issues exist.
|
|
33
|
+
*/
|
|
34
|
+
map<TDestination extends object>(source: AnyRecord, destinationClass: new () => TDestination, options?: MapperOptions): TDestination;
|
|
35
|
+
mapArray<TDestination extends object>(sources: AnyRecord[], destinationClass: new () => TDestination, options?: MapperOptions): TDestination[];
|
|
36
|
+
mapAsync<TDestination extends object>(source: AnyRecord | Promise<AnyRecord>, destinationClass: new () => TDestination, options?: MapperOptions): Promise<TDestination>;
|
|
37
|
+
mapArrayAsync<TDestination extends object>(sources: AnyRecord[] | Promise<AnyRecord[]> | Array<AnyRecord | Promise<AnyRecord>>, destinationClass: new () => TDestination, options?: MapperOptions): Promise<TDestination[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Safe mapping: never throws. Always returns { value, errors }.
|
|
40
|
+
* Use this for collect-all-errors mode.
|
|
41
|
+
*/
|
|
42
|
+
mapSafe<TDestination extends object>(source: AnyRecord, destinationClass: new () => TDestination, options?: MapperOptions): MapperResult<TDestination>;
|
|
43
|
+
mapArraySafe<TDestination extends object>(sources: AnyRecord[], destinationClass: new () => TDestination, options?: MapperOptions): Array<MapperResult<TDestination>>;
|
|
44
|
+
mapSafeAsync<TDestination extends object>(source: AnyRecord | Promise<AnyRecord>, destinationClass: new () => TDestination, options?: MapperOptions): Promise<MapperResult<TDestination>>;
|
|
45
|
+
mapArraySafeAsync<TDestination extends object>(sources: AnyRecord[] | Promise<AnyRecord[]> | Array<AnyRecord | Promise<AnyRecord>>, destinationClass: new () => TDestination, options?: MapperOptions): Promise<Array<MapperResult<TDestination>>>;
|
|
46
|
+
private mapInternal;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
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;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.MapperService = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
require("reflect-metadata");
|
|
12
|
+
const metadata_constants_1 = require("./constants/metadata.constants");
|
|
13
|
+
const mapper_errors_1 = require("./errors/mapper.errors");
|
|
14
|
+
const path_utils_1 = require("./utils/path.utils");
|
|
15
|
+
const defaultOptions = {
|
|
16
|
+
requireDecorators: false,
|
|
17
|
+
validateRequired: true,
|
|
18
|
+
validationMode: 'throw',
|
|
19
|
+
allowNestedDestinationPaths: false,
|
|
20
|
+
};
|
|
21
|
+
let MapperService = class MapperService {
|
|
22
|
+
/**
|
|
23
|
+
* Standard mapping. Throws if validationMode is "throw" and issues exist.
|
|
24
|
+
*/
|
|
25
|
+
map(source, destinationClass, options) {
|
|
26
|
+
const result = this.mapInternal(source, destinationClass, options);
|
|
27
|
+
if (result.errors.length && result.__cfg?.validationMode === 'throw') {
|
|
28
|
+
throw new mapper_errors_1.MapperValidationError(result.errors);
|
|
29
|
+
}
|
|
30
|
+
return result.value;
|
|
31
|
+
}
|
|
32
|
+
mapArray(sources, destinationClass, options) {
|
|
33
|
+
return sources.map((src) => this.map(src, destinationClass, options));
|
|
34
|
+
}
|
|
35
|
+
async mapAsync(source, destinationClass, options) {
|
|
36
|
+
const resolvedSource = await Promise.resolve(source);
|
|
37
|
+
return this.map(resolvedSource, destinationClass, options);
|
|
38
|
+
}
|
|
39
|
+
async mapArrayAsync(sources, destinationClass, options) {
|
|
40
|
+
const resolved = await Promise.resolve(sources);
|
|
41
|
+
const resolvedSources = await Promise.all(resolved.map((s) => Promise.resolve(s)));
|
|
42
|
+
return resolvedSources.map((src) => this.map(src, destinationClass, options));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Safe mapping: never throws. Always returns { value, errors }.
|
|
46
|
+
* Use this for collect-all-errors mode.
|
|
47
|
+
*/
|
|
48
|
+
mapSafe(source, destinationClass, options) {
|
|
49
|
+
const cfg = { ...defaultOptions, ...(options ?? {}), validationMode: 'collect' };
|
|
50
|
+
return this.mapInternal(source, destinationClass, cfg);
|
|
51
|
+
}
|
|
52
|
+
mapArraySafe(sources, destinationClass, options) {
|
|
53
|
+
return sources.map((src) => this.mapSafe(src, destinationClass, options));
|
|
54
|
+
}
|
|
55
|
+
async mapSafeAsync(source, destinationClass, options) {
|
|
56
|
+
const resolvedSource = await Promise.resolve(source);
|
|
57
|
+
return this.mapSafe(resolvedSource, destinationClass, options);
|
|
58
|
+
}
|
|
59
|
+
async mapArraySafeAsync(sources, destinationClass, options) {
|
|
60
|
+
const resolved = await Promise.resolve(sources);
|
|
61
|
+
const resolvedSources = await Promise.all(resolved.map((s) => Promise.resolve(s)));
|
|
62
|
+
return resolvedSources.map((src) => this.mapSafe(src, destinationClass, options));
|
|
63
|
+
}
|
|
64
|
+
mapInternal(source, destinationClass, options) {
|
|
65
|
+
const cfg = { ...defaultOptions, ...(options ?? {}) };
|
|
66
|
+
const instance = new destinationClass();
|
|
67
|
+
const errors = [];
|
|
68
|
+
/**
|
|
69
|
+
* IMPORTANT:
|
|
70
|
+
* This iterates over runtime keys of the DTO instance.
|
|
71
|
+
* That means your DTO fields should exist at runtime OR you should ensure
|
|
72
|
+
* @AutoMap() is applied to fields you expect to map (recommended).
|
|
73
|
+
*/
|
|
74
|
+
for (const destinationKey of Object.keys(instance)) {
|
|
75
|
+
const metadata = Reflect.getMetadata(metadata_constants_1.AUTO_MAP_METADATA_KEY, instance, destinationKey);
|
|
76
|
+
// Enforce decorator presence if requested
|
|
77
|
+
if (!metadata) {
|
|
78
|
+
if (cfg.requireDecorators) {
|
|
79
|
+
errors.push({
|
|
80
|
+
destinationPath: `${destinationClass.name}.${destinationKey}`,
|
|
81
|
+
sourcePath: destinationKey,
|
|
82
|
+
message: `Missing @AutoMap() on destination property "${destinationClass.name}.${destinationKey}".`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Same-key fallback (optional behaviour)
|
|
87
|
+
const value = source[destinationKey];
|
|
88
|
+
if (value !== undefined) {
|
|
89
|
+
instance[destinationKey] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Source can be a nested path (dot notation)
|
|
95
|
+
const sourceValue = (0, path_utils_1.getValueByPath)(source, metadata.sourceKey);
|
|
96
|
+
// Required validation
|
|
97
|
+
if (cfg.validateRequired && metadata.options.required && sourceValue === undefined) {
|
|
98
|
+
const label = metadata.options.label ? ` (${metadata.options.label})` : '';
|
|
99
|
+
errors.push({
|
|
100
|
+
destinationPath: `${destinationClass.name}.${destinationKey}`,
|
|
101
|
+
sourcePath: metadata.sourceKey,
|
|
102
|
+
message: `Required mapping missing for "${destinationClass.name}.${destinationKey}"${label} (source "${metadata.sourceKey}").`,
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Assign if present
|
|
107
|
+
if (sourceValue !== undefined) {
|
|
108
|
+
if (cfg.allowNestedDestinationPaths && destinationKey.includes('.')) {
|
|
109
|
+
// Optional: allow destinationKey to be nested (rare)
|
|
110
|
+
(0, path_utils_1.setValueByPath)(instance, destinationKey, sourceValue);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
instance[destinationKey] = sourceValue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
value: instance,
|
|
119
|
+
errors,
|
|
120
|
+
__cfg: cfg,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
exports.MapperService = MapperService;
|
|
125
|
+
exports.MapperService = MapperService = __decorate([
|
|
126
|
+
(0, common_1.Injectable)()
|
|
127
|
+
], MapperService);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type AutoMapOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* If true, a validation error is produced when the source value is missing/undefined.
|
|
4
|
+
*/
|
|
5
|
+
required?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Optional custom display label for error messages (e.g. "User First Name").
|
|
8
|
+
*/
|
|
9
|
+
label?: string;
|
|
10
|
+
};
|
|
11
|
+
export type AutoMapMetadata = {
|
|
12
|
+
sourceKey: string;
|
|
13
|
+
options: AutoMapOptions;
|
|
14
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads a nested value from an object using dot-notation path.
|
|
3
|
+
* Example: getValueByPath(obj, "user.address.street")
|
|
4
|
+
*/
|
|
5
|
+
export declare function getValueByPath(obj: unknown, path: string): unknown;
|
|
6
|
+
/**
|
|
7
|
+
* Sets a nested value on an object using dot-notation path.
|
|
8
|
+
* Example: setValueByPath(dto, "address.street", "Main Rd")
|
|
9
|
+
*/
|
|
10
|
+
export declare function setValueByPath(obj: unknown, path: string, value: unknown): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getValueByPath = getValueByPath;
|
|
4
|
+
exports.setValueByPath = setValueByPath;
|
|
5
|
+
/**
|
|
6
|
+
* Reads a nested value from an object using dot-notation path.
|
|
7
|
+
* Example: getValueByPath(obj, "user.address.street")
|
|
8
|
+
*/
|
|
9
|
+
function getValueByPath(obj, path) {
|
|
10
|
+
if (!obj || typeof obj !== 'object')
|
|
11
|
+
return undefined;
|
|
12
|
+
// Fast path for simple keys
|
|
13
|
+
if (!path.includes('.'))
|
|
14
|
+
return obj[path];
|
|
15
|
+
const parts = path.split('.').filter(Boolean);
|
|
16
|
+
let current = obj;
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
if (!current || typeof current !== 'object')
|
|
19
|
+
return undefined;
|
|
20
|
+
current = current[part];
|
|
21
|
+
}
|
|
22
|
+
return current;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Sets a nested value on an object using dot-notation path.
|
|
26
|
+
* Example: setValueByPath(dto, "address.street", "Main Rd")
|
|
27
|
+
*/
|
|
28
|
+
function setValueByPath(obj, path, value) {
|
|
29
|
+
if (!obj || typeof obj !== 'object')
|
|
30
|
+
return;
|
|
31
|
+
// Fast path for simple keys
|
|
32
|
+
if (!path.includes('.')) {
|
|
33
|
+
obj[path] = value;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const parts = path.split('.').filter(Boolean);
|
|
37
|
+
const last = parts.pop();
|
|
38
|
+
if (!last)
|
|
39
|
+
return;
|
|
40
|
+
let current = obj;
|
|
41
|
+
for (const part of parts) {
|
|
42
|
+
const next = current[part];
|
|
43
|
+
if (!next || typeof next !== 'object') {
|
|
44
|
+
current[part] = {};
|
|
45
|
+
}
|
|
46
|
+
current = current[part];
|
|
47
|
+
}
|
|
48
|
+
current[last] = value;
|
|
49
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ezilemdodana/nest-mapper",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight metadata-based mapper for NestJS",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.build.json",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"nestjs",
|
|
14
|
+
"mapper",
|
|
15
|
+
"dto",
|
|
16
|
+
"typescript",
|
|
17
|
+
"reflect-metadata"
|
|
18
|
+
],
|
|
19
|
+
"author": "Ezile Mdodana",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@nestjs/common": ">=9",
|
|
23
|
+
"reflect-metadata": ">=0.1.13"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@nestjs/common": "^10.0.0",
|
|
27
|
+
"reflect-metadata": "^0.1.13",
|
|
28
|
+
"typescript": "^5.4.0"
|
|
29
|
+
}
|
|
30
|
+
}
|