typemold 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 +43 -0
- package/README.md +235 -0
- package/dist/cjs/decorators.js +284 -0
- package/dist/cjs/index.js +74 -0
- package/dist/cjs/mapper.js +153 -0
- package/dist/cjs/nestjs/index.js +12 -0
- package/dist/cjs/nestjs/mapper.module.js +103 -0
- package/dist/cjs/nestjs/mapper.service.js +161 -0
- package/dist/cjs/registry.js +179 -0
- package/dist/cjs/types.js +17 -0
- package/dist/cjs/utils.js +136 -0
- package/dist/esm/decorators.js +274 -0
- package/dist/esm/index.js +51 -0
- package/dist/esm/mapper.js +149 -0
- package/dist/esm/nestjs/index.js +6 -0
- package/dist/esm/nestjs/mapper.module.js +100 -0
- package/dist/esm/nestjs/mapper.service.js +125 -0
- package/dist/esm/registry.js +175 -0
- package/dist/esm/types.js +14 -0
- package/dist/esm/utils.js +127 -0
- package/dist/types/decorators.d.ts +206 -0
- package/dist/types/decorators.d.ts.map +1 -0
- package/dist/types/index.d.ts +46 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/mapper.d.ts +93 -0
- package/dist/types/mapper.d.ts.map +1 -0
- package/dist/types/nestjs/index.d.ts +7 -0
- package/dist/types/nestjs/index.d.ts.map +1 -0
- package/dist/types/nestjs/mapper.module.d.ts +89 -0
- package/dist/types/nestjs/mapper.module.d.ts.map +1 -0
- package/dist/types/nestjs/mapper.service.d.ts +80 -0
- package/dist/types/nestjs/mapper.service.d.ts.map +1 -0
- package/dist/types/registry.d.ts +60 -0
- package/dist/types/registry.d.ts.map +1 -0
- package/dist/types/types.d.ts +120 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils.d.ts +30 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/package.json +92 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @sevirial/nest-mapper - Utility Functions
|
|
4
|
+
* Helper functions for the mapping engine
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.getNestedValue = getNestedValue;
|
|
8
|
+
exports.isPlainObject = isPlainObject;
|
|
9
|
+
exports.isClassInstance = isClassInstance;
|
|
10
|
+
exports.pickKeys = pickKeys;
|
|
11
|
+
exports.omitKeys = omitKeys;
|
|
12
|
+
exports.getAllPropertyKeys = getAllPropertyKeys;
|
|
13
|
+
exports.deepClone = deepClone;
|
|
14
|
+
/**
|
|
15
|
+
* Gets a nested value from an object using dot notation path.
|
|
16
|
+
* Optimized for performance with caching of path segments.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* getNestedValue({ profile: { avatar: 'url' } }, 'profile.avatar')
|
|
20
|
+
* // Returns: 'url'
|
|
21
|
+
*/
|
|
22
|
+
const pathCache = new Map();
|
|
23
|
+
// Dangerous property names for prototype pollution
|
|
24
|
+
const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
25
|
+
function getNestedValue(obj, path) {
|
|
26
|
+
if (obj == null)
|
|
27
|
+
return undefined;
|
|
28
|
+
// Simple path - check for prototype pollution
|
|
29
|
+
if (!path.includes(".")) {
|
|
30
|
+
if (BLOCKED_KEYS.has(path))
|
|
31
|
+
return undefined;
|
|
32
|
+
return obj[path];
|
|
33
|
+
}
|
|
34
|
+
let segments = pathCache.get(path);
|
|
35
|
+
if (!segments) {
|
|
36
|
+
segments = path.split(".");
|
|
37
|
+
pathCache.set(path, segments);
|
|
38
|
+
}
|
|
39
|
+
let current = obj;
|
|
40
|
+
for (let i = 0; i < segments.length; i++) {
|
|
41
|
+
// Block prototype pollution attempts
|
|
42
|
+
if (BLOCKED_KEYS.has(segments[i]))
|
|
43
|
+
return undefined;
|
|
44
|
+
if (current == null || typeof current !== "object")
|
|
45
|
+
return undefined;
|
|
46
|
+
current = current[segments[i]];
|
|
47
|
+
}
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Checks if a value is a plain object (not an array, Date, etc.)
|
|
52
|
+
*/
|
|
53
|
+
function isPlainObject(value) {
|
|
54
|
+
if (value === null || typeof value !== "object")
|
|
55
|
+
return false;
|
|
56
|
+
const proto = Object.getPrototypeOf(value);
|
|
57
|
+
return proto === Object.prototype || proto === null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Checks if a value is a class instance (not a plain object)
|
|
61
|
+
*/
|
|
62
|
+
function isClassInstance(value) {
|
|
63
|
+
if (value === null || typeof value !== "object")
|
|
64
|
+
return false;
|
|
65
|
+
const proto = Object.getPrototypeOf(value);
|
|
66
|
+
return proto !== Object.prototype && proto !== null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Creates a shallow clone of an object with only specified keys
|
|
70
|
+
*/
|
|
71
|
+
function pickKeys(obj, keys) {
|
|
72
|
+
const result = {};
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
if (key in obj) {
|
|
75
|
+
result[key] = obj[key];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Creates a shallow clone of an object without specified keys
|
|
82
|
+
*/
|
|
83
|
+
function omitKeys(obj, keys) {
|
|
84
|
+
const keySet = new Set(keys);
|
|
85
|
+
const result = {};
|
|
86
|
+
for (const key in obj) {
|
|
87
|
+
if (Object.prototype.hasOwnProperty.call(obj, key) && !keySet.has(key)) {
|
|
88
|
+
result[key] = obj[key];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Gets all property keys including inherited ones
|
|
95
|
+
*/
|
|
96
|
+
function getAllPropertyKeys(target) {
|
|
97
|
+
const keys = new Set();
|
|
98
|
+
let current = target;
|
|
99
|
+
while (current && current !== Object.prototype) {
|
|
100
|
+
for (const key of Object.getOwnPropertyNames(current)) {
|
|
101
|
+
if (key !== "constructor") {
|
|
102
|
+
keys.add(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
current = Object.getPrototypeOf(current);
|
|
106
|
+
}
|
|
107
|
+
return Array.from(keys);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Deep clones an object (for handling circular references)
|
|
111
|
+
*/
|
|
112
|
+
function deepClone(obj, visited = new WeakMap()) {
|
|
113
|
+
if (obj === null || typeof obj !== "object")
|
|
114
|
+
return obj;
|
|
115
|
+
const objAsObject = obj;
|
|
116
|
+
if (visited.has(objAsObject))
|
|
117
|
+
return visited.get(objAsObject);
|
|
118
|
+
if (Array.isArray(obj)) {
|
|
119
|
+
const arrClone = [];
|
|
120
|
+
visited.set(objAsObject, arrClone);
|
|
121
|
+
for (let i = 0; i < obj.length; i++) {
|
|
122
|
+
arrClone[i] = deepClone(obj[i], visited);
|
|
123
|
+
}
|
|
124
|
+
return arrClone;
|
|
125
|
+
}
|
|
126
|
+
if (obj instanceof Date)
|
|
127
|
+
return new Date(obj.getTime());
|
|
128
|
+
if (obj instanceof RegExp)
|
|
129
|
+
return new RegExp(obj.source, obj.flags);
|
|
130
|
+
const clone = Object.create(Object.getPrototypeOf(obj));
|
|
131
|
+
visited.set(objAsObject, clone);
|
|
132
|
+
for (const key of Object.keys(obj)) {
|
|
133
|
+
clone[key] = deepClone(obj[key], visited);
|
|
134
|
+
}
|
|
135
|
+
return clone;
|
|
136
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sevirial/nest-mapper - Decorators
|
|
3
|
+
* Property decorators for defining mapping configurations
|
|
4
|
+
*/
|
|
5
|
+
import "reflect-metadata";
|
|
6
|
+
import { METADATA_KEYS, } from "./types";
|
|
7
|
+
/**
|
|
8
|
+
* Maps a property from a source path or using a transform function.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Direct property mapping
|
|
12
|
+
* class UserDto {
|
|
13
|
+
* @MapFrom('firstName')
|
|
14
|
+
* name: string;
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Nested path mapping
|
|
19
|
+
* class UserDto {
|
|
20
|
+
* @MapFrom('profile.avatar')
|
|
21
|
+
* avatarUrl: string;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // Transform function with typed source
|
|
26
|
+
* class UserDto {
|
|
27
|
+
* @MapFrom<User>((src) => src.age >= 18) // ← IntelliSense for src!
|
|
28
|
+
* isAdult: boolean;
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
export function MapFrom(sourcePathOrTransform) {
|
|
32
|
+
return (target, propertyKey) => {
|
|
33
|
+
const key = String(propertyKey);
|
|
34
|
+
const existingMappings = Reflect.getMetadata(METADATA_KEYS.PROPERTY_MAPPINGS, target.constructor) || new Map();
|
|
35
|
+
const existingConfig = existingMappings.get(key) || createDefaultConfig(key);
|
|
36
|
+
if (typeof sourcePathOrTransform === "function") {
|
|
37
|
+
existingConfig.source = sourcePathOrTransform;
|
|
38
|
+
existingConfig.isTransform = true;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
existingConfig.source = sourcePathOrTransform;
|
|
42
|
+
existingConfig.isTransform = false;
|
|
43
|
+
}
|
|
44
|
+
existingMappings.set(key, existingConfig);
|
|
45
|
+
Reflect.defineMetadata(METADATA_KEYS.PROPERTY_MAPPINGS, existingMappings, target.constructor);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Creates a type-safe mapping configuration with full IntelliSense support.
|
|
50
|
+
* Use this builder pattern when you need autocomplete for source paths.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* interface User {
|
|
54
|
+
* username: string;
|
|
55
|
+
* profile: { avatar: string; bio: string };
|
|
56
|
+
* }
|
|
57
|
+
*
|
|
58
|
+
* // Option 1: Define mappings with full autocomplete
|
|
59
|
+
* const toUserDto = createMapping<User, UserDto>({
|
|
60
|
+
* avatar: 'profile.avatar', // ✨ Autocomplete for paths!
|
|
61
|
+
* bio: src => src.profile.bio, // ✨ Autocomplete for transforms!
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* // Usage
|
|
65
|
+
* const dto = toUserDto(userEntity);
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Option 2: Use with Mapper
|
|
69
|
+
* const dto = Mapper.mapWith(user, toUserDto);
|
|
70
|
+
*/
|
|
71
|
+
export function createMapping(mappings) {
|
|
72
|
+
return (source) => {
|
|
73
|
+
const result = {};
|
|
74
|
+
for (const [targetKey, sourcePathOrFn] of Object.entries(mappings)) {
|
|
75
|
+
if (typeof sourcePathOrFn === "function") {
|
|
76
|
+
result[targetKey] = sourcePathOrFn(source);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
result[targetKey] = getNestedValueTyped(source, sourcePathOrFn);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Helper to get nested value with type safety
|
|
87
|
+
*/
|
|
88
|
+
function getNestedValueTyped(obj, path) {
|
|
89
|
+
if (obj == null)
|
|
90
|
+
return undefined;
|
|
91
|
+
const segments = path.split(".");
|
|
92
|
+
let current = obj;
|
|
93
|
+
for (const segment of segments) {
|
|
94
|
+
if (current == null || typeof current !== "object")
|
|
95
|
+
return undefined;
|
|
96
|
+
current = current[segment];
|
|
97
|
+
}
|
|
98
|
+
return current;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Automatically maps a property with the same name from source.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* class UserDto {
|
|
105
|
+
* @AutoMap()
|
|
106
|
+
* username: string; // Maps from source.username
|
|
107
|
+
* }
|
|
108
|
+
*/
|
|
109
|
+
export function AutoMap() {
|
|
110
|
+
return (target, propertyKey) => {
|
|
111
|
+
const key = String(propertyKey);
|
|
112
|
+
const existingMappings = Reflect.getMetadata(METADATA_KEYS.PROPERTY_MAPPINGS, target.constructor) || new Map();
|
|
113
|
+
const existingConfig = existingMappings.get(key) || createDefaultConfig(key);
|
|
114
|
+
existingConfig.source = key; // Same name mapping
|
|
115
|
+
existingConfig.isTransform = false;
|
|
116
|
+
existingMappings.set(key, existingConfig);
|
|
117
|
+
Reflect.defineMetadata(METADATA_KEYS.PROPERTY_MAPPINGS, existingMappings, target.constructor);
|
|
118
|
+
Reflect.defineMetadata(METADATA_KEYS.AUTO_MAP, true, target.constructor, key);
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Assigns a property to one or more field groups for runtime projection.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // Basic usage with strings
|
|
126
|
+
* class UserDto {
|
|
127
|
+
* @FieldGroup('minimal', 'public')
|
|
128
|
+
* @AutoMap()
|
|
129
|
+
* username: string;
|
|
130
|
+
* }
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* // Type-safe usage with const object (recommended for autocomplete!)
|
|
134
|
+
* const Groups = createFieldGroups('minimal', 'public', 'full');
|
|
135
|
+
*
|
|
136
|
+
* class UserDto {
|
|
137
|
+
* @FieldGroup(Groups.minimal, Groups.public) // ✨ Autocomplete!
|
|
138
|
+
* @AutoMap()
|
|
139
|
+
* username: string;
|
|
140
|
+
* }
|
|
141
|
+
*/
|
|
142
|
+
export function FieldGroup(...groups) {
|
|
143
|
+
return (target, propertyKey) => {
|
|
144
|
+
const key = String(propertyKey);
|
|
145
|
+
const existingMappings = Reflect.getMetadata(METADATA_KEYS.PROPERTY_MAPPINGS, target.constructor) || new Map();
|
|
146
|
+
const existingConfig = existingMappings.get(key) || createDefaultConfig(key);
|
|
147
|
+
existingConfig.groups = [...new Set([...existingConfig.groups, ...groups])];
|
|
148
|
+
existingMappings.set(key, existingConfig);
|
|
149
|
+
Reflect.defineMetadata(METADATA_KEYS.PROPERTY_MAPPINGS, existingMappings, target.constructor);
|
|
150
|
+
// Also store in field groups map for quick lookup
|
|
151
|
+
const fieldGroups = Reflect.getMetadata(METADATA_KEYS.FIELD_GROUPS, target.constructor) || new Map();
|
|
152
|
+
for (const group of groups) {
|
|
153
|
+
const groupSet = fieldGroups.get(group) || new Set();
|
|
154
|
+
groupSet.add(key);
|
|
155
|
+
fieldGroups.set(group, groupSet);
|
|
156
|
+
}
|
|
157
|
+
Reflect.defineMetadata(METADATA_KEYS.FIELD_GROUPS, fieldGroups, target.constructor);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Creates a type-safe field groups object with autocomplete support.
|
|
162
|
+
* Use this to define your groups once and get IntelliSense everywhere!
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* // Define groups once
|
|
166
|
+
* export const UserGroups = createFieldGroups('minimal', 'public', 'full');
|
|
167
|
+
*
|
|
168
|
+
* class UserDto {
|
|
169
|
+
* @FieldGroup(UserGroups.minimal, UserGroups.public) // ✨ Autocomplete!
|
|
170
|
+
* @AutoMap()
|
|
171
|
+
* username: string;
|
|
172
|
+
*
|
|
173
|
+
* @FieldGroup(UserGroups.full)
|
|
174
|
+
* @AutoMap()
|
|
175
|
+
* email: string;
|
|
176
|
+
* }
|
|
177
|
+
*
|
|
178
|
+
* // Usage with type-safety
|
|
179
|
+
* Mapper.map(user, UserDto, { group: UserGroups.minimal }); // ✨ Autocomplete!
|
|
180
|
+
*/
|
|
181
|
+
export function createFieldGroups(...groups) {
|
|
182
|
+
const result = {};
|
|
183
|
+
for (const group of groups) {
|
|
184
|
+
result[group] = group;
|
|
185
|
+
}
|
|
186
|
+
return Object.freeze(result);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Built-in field groups with autocomplete - use these directly!
|
|
190
|
+
* No need to define your own groups for common use cases.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* class UserDto {
|
|
194
|
+
* @FieldGroup(Groups.MINIMAL, Groups.PUBLIC) // ✨ Autocomplete!
|
|
195
|
+
* @AutoMap()
|
|
196
|
+
* username: string;
|
|
197
|
+
*
|
|
198
|
+
* @FieldGroup(Groups.DETAILED)
|
|
199
|
+
* @AutoMap()
|
|
200
|
+
* email: string;
|
|
201
|
+
* }
|
|
202
|
+
*
|
|
203
|
+
* // Usage
|
|
204
|
+
* Mapper.map(user, UserDto, { group: Groups.MINIMAL }); // ✨ Autocomplete!
|
|
205
|
+
*/
|
|
206
|
+
export const Groups = Object.freeze({
|
|
207
|
+
/** Minimal fields - just the essentials (e.g., id, name) */
|
|
208
|
+
MINIMAL: "minimal",
|
|
209
|
+
/** Summary fields - brief overview */
|
|
210
|
+
SUMMARY: "summary",
|
|
211
|
+
/** Public fields - safe to expose publicly */
|
|
212
|
+
PUBLIC: "public",
|
|
213
|
+
/** Private fields - internal use only */
|
|
214
|
+
PRIVATE: "private",
|
|
215
|
+
/** Detailed fields - comprehensive info */
|
|
216
|
+
DETAILED: "detailed",
|
|
217
|
+
/** Full fields - everything */
|
|
218
|
+
FULL: "full",
|
|
219
|
+
/** List view fields - for table/list displays */
|
|
220
|
+
LIST: "list",
|
|
221
|
+
/** Detail view fields - for detail pages */
|
|
222
|
+
DETAIL: "detail",
|
|
223
|
+
/** Admin fields - administrative data */
|
|
224
|
+
ADMIN: "admin",
|
|
225
|
+
/** API response fields */
|
|
226
|
+
API: "api",
|
|
227
|
+
});
|
|
228
|
+
/**
|
|
229
|
+
* Ignores a property during mapping.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* class UserDto {
|
|
233
|
+
* @Ignore()
|
|
234
|
+
* internalId: string; // Will not be mapped
|
|
235
|
+
* }
|
|
236
|
+
*/
|
|
237
|
+
export function Ignore() {
|
|
238
|
+
return (target, propertyKey) => {
|
|
239
|
+
const key = String(propertyKey);
|
|
240
|
+
const existingMappings = Reflect.getMetadata(METADATA_KEYS.PROPERTY_MAPPINGS, target.constructor) || new Map();
|
|
241
|
+
const existingConfig = existingMappings.get(key) || createDefaultConfig(key);
|
|
242
|
+
existingConfig.ignore = true;
|
|
243
|
+
existingMappings.set(key, existingConfig);
|
|
244
|
+
Reflect.defineMetadata(METADATA_KEYS.PROPERTY_MAPPINGS, existingMappings, target.constructor);
|
|
245
|
+
Reflect.defineMetadata(METADATA_KEYS.IGNORE, true, target.constructor, key);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Specifies the type for nested object mapping.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* class UserDto {
|
|
253
|
+
* @NestedType(() => AddressDto)
|
|
254
|
+
* @MapFrom('address')
|
|
255
|
+
* address: AddressDto;
|
|
256
|
+
* }
|
|
257
|
+
*/
|
|
258
|
+
export function NestedType(typeFactory) {
|
|
259
|
+
return (target, propertyKey) => {
|
|
260
|
+
Reflect.defineMetadata(METADATA_KEYS.NESTED_TYPE, typeFactory, target.constructor, String(propertyKey));
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Creates a default property mapping config
|
|
265
|
+
*/
|
|
266
|
+
function createDefaultConfig(targetKey) {
|
|
267
|
+
return {
|
|
268
|
+
targetKey,
|
|
269
|
+
source: targetKey,
|
|
270
|
+
isTransform: false,
|
|
271
|
+
groups: [],
|
|
272
|
+
ignore: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* typemold
|
|
3
|
+
* A lightweight, high-performance object mapper for TypeScript and Node.js
|
|
4
|
+
*
|
|
5
|
+
* @author Chetan Joshi
|
|
6
|
+
* @license MIT
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Basic mapping
|
|
10
|
+
* import { Mapper, MapFrom, AutoMap } from 'typemold';
|
|
11
|
+
*
|
|
12
|
+
* class UserDto {
|
|
13
|
+
* @AutoMap()
|
|
14
|
+
* username: string;
|
|
15
|
+
*
|
|
16
|
+
* @MapFrom('profile.avatar')
|
|
17
|
+
* avatar: string;
|
|
18
|
+
*
|
|
19
|
+
* @MapFrom((src) => src.age >= 18)
|
|
20
|
+
* isAdult: boolean;
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* const userDto = Mapper.map(userEntity, UserDto);
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // Runtime field projection
|
|
27
|
+
* const minimal = Mapper.map(user, UserDto, { pick: ['username', 'avatar'] });
|
|
28
|
+
* const safe = Mapper.map(user, UserDto, { omit: ['email', 'password'] });
|
|
29
|
+
* const public = Mapper.map(user, UserDto, { group: 'public' });
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // NestJS integration (optional)
|
|
33
|
+
* import { MapperModule, MapperService } from 'typemold';
|
|
34
|
+
*
|
|
35
|
+
* @Module({
|
|
36
|
+
* imports: [MapperModule.forRoot()],
|
|
37
|
+
* })
|
|
38
|
+
* export class AppModule {}
|
|
39
|
+
*/
|
|
40
|
+
// Core Mapper
|
|
41
|
+
export { Mapper } from "./mapper";
|
|
42
|
+
// Decorators
|
|
43
|
+
export { MapFrom, createMapping, AutoMap, FieldGroup, createFieldGroups, Groups, Ignore, NestedType, } from "./decorators";
|
|
44
|
+
// Types
|
|
45
|
+
export { METADATA_KEYS, } from "./types";
|
|
46
|
+
// Registry (for advanced usage)
|
|
47
|
+
export { MappingRegistry, MapperFactory } from "./registry";
|
|
48
|
+
// Utilities
|
|
49
|
+
export { getNestedValue, pickKeys, omitKeys, isPlainObject, isClassInstance, } from "./utils";
|
|
50
|
+
// NestJS Integration
|
|
51
|
+
export { MapperModule, MapperService, MAPPER_OPTIONS, } from "./nestjs";
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sevirial/nest-mapper - Core Mapper
|
|
3
|
+
* Main mapper class with static methods for easy usage
|
|
4
|
+
*/
|
|
5
|
+
import "reflect-metadata";
|
|
6
|
+
import { MapperFactory, MappingRegistry } from "./registry";
|
|
7
|
+
/**
|
|
8
|
+
* Main Mapper class - provides static methods for object mapping
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Basic usage
|
|
12
|
+
* const userDto = Mapper.map(userEntity, UserDto);
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // With field projection
|
|
16
|
+
* const minimalUser = Mapper.map(userEntity, UserDto, { pick: ['username', 'avatar'] });
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // With field groups
|
|
20
|
+
* const publicUser = Mapper.map(userEntity, UserDto, { group: 'public' });
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Array mapping
|
|
24
|
+
* const userDtos = Mapper.mapArray(users, UserDto);
|
|
25
|
+
*/
|
|
26
|
+
export class Mapper {
|
|
27
|
+
/**
|
|
28
|
+
* Maps a source object to a target DTO class
|
|
29
|
+
*
|
|
30
|
+
* @param source - Source object to map from
|
|
31
|
+
* @param targetType - Target DTO class constructor
|
|
32
|
+
* @param options - Optional mapping options for field projection
|
|
33
|
+
* @returns Mapped target object
|
|
34
|
+
*/
|
|
35
|
+
static map(source, targetType, options) {
|
|
36
|
+
if (source == null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const mapper = MapperFactory.createMapper(targetType, options);
|
|
40
|
+
const context = {
|
|
41
|
+
...this.globalContext,
|
|
42
|
+
extras: options?.extras,
|
|
43
|
+
depth: 0,
|
|
44
|
+
visited: new WeakMap(),
|
|
45
|
+
};
|
|
46
|
+
return mapper(source, context);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Maps an array of source objects to target DTOs
|
|
50
|
+
*
|
|
51
|
+
* @param sources - Array of source objects
|
|
52
|
+
* @param targetType - Target DTO class constructor
|
|
53
|
+
* @param options - Optional mapping options for field projection
|
|
54
|
+
* @returns Array of mapped target objects
|
|
55
|
+
*/
|
|
56
|
+
static mapArray(sources, targetType, options) {
|
|
57
|
+
if (!sources || !Array.isArray(sources)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const mapper = MapperFactory.createMapper(targetType, options);
|
|
61
|
+
const context = {
|
|
62
|
+
...this.globalContext,
|
|
63
|
+
extras: options?.extras,
|
|
64
|
+
depth: 0,
|
|
65
|
+
visited: new WeakMap(),
|
|
66
|
+
};
|
|
67
|
+
const result = new Array(sources.length);
|
|
68
|
+
for (let i = 0; i < sources.length; i++) {
|
|
69
|
+
result[i] =
|
|
70
|
+
sources[i] != null
|
|
71
|
+
? mapper(sources[i], context)
|
|
72
|
+
: null;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Maps source to target and returns only specified fields (shorthand)
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* const result = Mapper.pick(user, UserDto, ['username', 'avatar']);
|
|
81
|
+
*/
|
|
82
|
+
static pick(source, targetType, fields) {
|
|
83
|
+
return this.map(source, targetType, {
|
|
84
|
+
pick: fields,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Maps source to target excluding specified fields (shorthand)
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* const result = Mapper.omit(user, UserDto, ['password', 'email']);
|
|
92
|
+
*/
|
|
93
|
+
static omit(source, targetType, fields) {
|
|
94
|
+
return this.map(source, targetType, {
|
|
95
|
+
omit: fields,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Maps source using a predefined field group (shorthand)
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* const result = Mapper.group(user, UserDto, 'minimal');
|
|
103
|
+
*/
|
|
104
|
+
static group(source, targetType, groupName) {
|
|
105
|
+
return this.map(source, targetType, { group: groupName });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Creates a reusable mapper function for better performance in loops
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* const mapToUserDto = Mapper.createMapper(UserDto);
|
|
112
|
+
* const users = entities.map(mapToUserDto);
|
|
113
|
+
*/
|
|
114
|
+
static createMapper(targetType, options) {
|
|
115
|
+
const compiledMapper = MapperFactory.createMapper(targetType, options);
|
|
116
|
+
const context = {
|
|
117
|
+
...this.globalContext,
|
|
118
|
+
extras: options?.extras,
|
|
119
|
+
};
|
|
120
|
+
return (source) => compiledMapper(source, context);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Registers a type converter for automatic type transformations
|
|
124
|
+
*/
|
|
125
|
+
static registerConverter(converter) {
|
|
126
|
+
this.typeConverters.push(converter);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Sets global context that will be available to all transform functions
|
|
130
|
+
*/
|
|
131
|
+
static setGlobalContext(context) {
|
|
132
|
+
this.globalContext = { ...this.globalContext, ...context };
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Clears all cached mappers (useful for testing or hot-reload scenarios)
|
|
136
|
+
*/
|
|
137
|
+
static clearCache() {
|
|
138
|
+
MappingRegistry.clearCache();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Gets the compiled mapper for inspection (useful for debugging)
|
|
142
|
+
*/
|
|
143
|
+
static getCompiledMapper(targetType, options) {
|
|
144
|
+
const optionsKey = MappingRegistry.getOptionsKey(options);
|
|
145
|
+
return MappingRegistry.getCompiledMapper(targetType, optionsKey);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
Mapper.typeConverters = [];
|
|
149
|
+
Mapper.globalContext = {};
|