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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sevirial/nest-mapper - NestJS MapperModule
|
|
3
|
+
* Dynamic module for NestJS integration
|
|
4
|
+
*/
|
|
5
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
6
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
8
|
+
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;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
var MapperModule_1;
|
|
12
|
+
import { Module, } from "@nestjs/common";
|
|
13
|
+
import { MapperService, MAPPER_OPTIONS, } from "./mapper.service";
|
|
14
|
+
/**
|
|
15
|
+
* NestJS Module for @sevirial/nest-mapper
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Basic usage (global by default)
|
|
19
|
+
* @Module({
|
|
20
|
+
* imports: [MapperModule.forRoot()],
|
|
21
|
+
* })
|
|
22
|
+
* export class AppModule {}
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // With options
|
|
26
|
+
* @Module({
|
|
27
|
+
* imports: [
|
|
28
|
+
* MapperModule.forRoot({
|
|
29
|
+
* enableValidation: true,
|
|
30
|
+
* converters: [myDateConverter],
|
|
31
|
+
* }),
|
|
32
|
+
* ],
|
|
33
|
+
* })
|
|
34
|
+
* export class AppModule {}
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Async configuration
|
|
38
|
+
* @Module({
|
|
39
|
+
* imports: [
|
|
40
|
+
* MapperModule.forRootAsync({
|
|
41
|
+
* imports: [ConfigModule],
|
|
42
|
+
* useFactory: (config: ConfigService) => ({
|
|
43
|
+
* enableValidation: config.get('ENABLE_VALIDATION'),
|
|
44
|
+
* }),
|
|
45
|
+
* inject: [ConfigService],
|
|
46
|
+
* }),
|
|
47
|
+
* ],
|
|
48
|
+
* })
|
|
49
|
+
* export class AppModule {}
|
|
50
|
+
*/
|
|
51
|
+
let MapperModule = MapperModule_1 = class MapperModule {
|
|
52
|
+
/**
|
|
53
|
+
* Configure the mapper module with static options
|
|
54
|
+
*/
|
|
55
|
+
static forRoot(options) {
|
|
56
|
+
const isGlobal = options?.isGlobal ?? true;
|
|
57
|
+
const optionsProvider = {
|
|
58
|
+
provide: MAPPER_OPTIONS,
|
|
59
|
+
useValue: options || {},
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
module: MapperModule_1,
|
|
63
|
+
global: isGlobal,
|
|
64
|
+
providers: [optionsProvider, MapperService],
|
|
65
|
+
exports: [MapperService],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Configure the mapper module with async options (factory pattern)
|
|
70
|
+
*/
|
|
71
|
+
static forRootAsync(options) {
|
|
72
|
+
const isGlobal = options.isGlobal ?? true;
|
|
73
|
+
const asyncOptionsProvider = {
|
|
74
|
+
provide: MAPPER_OPTIONS,
|
|
75
|
+
useFactory: options.useFactory,
|
|
76
|
+
inject: options.inject || [],
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
module: MapperModule_1,
|
|
80
|
+
global: isGlobal,
|
|
81
|
+
imports: options.imports || [],
|
|
82
|
+
providers: [asyncOptionsProvider, MapperService],
|
|
83
|
+
exports: [MapperService],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* For feature modules that need the mapper (when not using isGlobal)
|
|
88
|
+
*/
|
|
89
|
+
static forFeature() {
|
|
90
|
+
return {
|
|
91
|
+
module: MapperModule_1,
|
|
92
|
+
providers: [MapperService],
|
|
93
|
+
exports: [MapperService],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
MapperModule = MapperModule_1 = __decorate([
|
|
98
|
+
Module({})
|
|
99
|
+
], MapperModule);
|
|
100
|
+
export { MapperModule };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sevirial/nest-mapper - NestJS MapperService
|
|
3
|
+
* Injectable service for NestJS dependency injection
|
|
4
|
+
*/
|
|
5
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
6
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
7
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
8
|
+
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;
|
|
9
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
10
|
+
};
|
|
11
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
12
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
13
|
+
};
|
|
14
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
15
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
16
|
+
};
|
|
17
|
+
import { Injectable, Inject, Optional } from "@nestjs/common";
|
|
18
|
+
import { Mapper } from "../mapper";
|
|
19
|
+
/**
|
|
20
|
+
* Injection token for MapperModule options
|
|
21
|
+
*/
|
|
22
|
+
export const MAPPER_OPTIONS = Symbol("MAPPER_OPTIONS");
|
|
23
|
+
/**
|
|
24
|
+
* Injectable mapper service for NestJS
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* @Injectable()
|
|
28
|
+
* export class UserService {
|
|
29
|
+
* constructor(private readonly mapper: MapperService) {}
|
|
30
|
+
*
|
|
31
|
+
* async getUser(id: string): Promise<UserDto> {
|
|
32
|
+
* const user = await this.userRepo.findOne(id);
|
|
33
|
+
* return this.mapper.map(user, UserDto);
|
|
34
|
+
* }
|
|
35
|
+
* }
|
|
36
|
+
*/
|
|
37
|
+
let MapperService = class MapperService {
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.validator = null;
|
|
40
|
+
this.options = options || {};
|
|
41
|
+
// Register custom converters if provided
|
|
42
|
+
if (this.options.converters) {
|
|
43
|
+
for (const converter of this.options.converters) {
|
|
44
|
+
Mapper.registerConverter(converter);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Set global context if extras provided
|
|
48
|
+
if (this.options.globalExtras) {
|
|
49
|
+
Mapper.setGlobalContext({ extras: this.options.globalExtras });
|
|
50
|
+
}
|
|
51
|
+
// Try to load class-validator if validation is enabled
|
|
52
|
+
if (this.options.enableValidation) {
|
|
53
|
+
this.initializeValidator();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Lazily loads class-validator for hybrid integration
|
|
58
|
+
*/
|
|
59
|
+
async initializeValidator() {
|
|
60
|
+
try {
|
|
61
|
+
this.validator = await import("class-validator");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
console.warn("[@sevirial/nest-mapper] class-validator not found. Validation will be skipped.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Maps a source object to a target DTO
|
|
69
|
+
*/
|
|
70
|
+
map(source, targetType, options) {
|
|
71
|
+
return Mapper.map(source, targetType, options);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Maps an array of source objects to target DTOs
|
|
75
|
+
*/
|
|
76
|
+
mapArray(sources, targetType, options) {
|
|
77
|
+
return Mapper.mapArray(sources, targetType, options);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Maps and validates the result using class-validator (if enabled)
|
|
81
|
+
*
|
|
82
|
+
* @throws ValidationError[] if validation fails
|
|
83
|
+
*/
|
|
84
|
+
async mapAndValidate(source, targetType, options) {
|
|
85
|
+
const result = this.map(source, targetType, options);
|
|
86
|
+
if (this.validator && this.options.enableValidation) {
|
|
87
|
+
const errors = await this.validator.validate(result);
|
|
88
|
+
if (errors.length > 0) {
|
|
89
|
+
throw errors;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Pick specific fields (shorthand)
|
|
96
|
+
*/
|
|
97
|
+
pick(source, targetType, fields) {
|
|
98
|
+
return Mapper.pick(source, targetType, fields);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Omit specific fields (shorthand)
|
|
102
|
+
*/
|
|
103
|
+
omit(source, targetType, fields) {
|
|
104
|
+
return Mapper.omit(source, targetType, fields);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Use a field group (shorthand)
|
|
108
|
+
*/
|
|
109
|
+
group(source, targetType, groupName) {
|
|
110
|
+
return Mapper.group(source, targetType, groupName);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Creates a reusable mapper function
|
|
114
|
+
*/
|
|
115
|
+
createMapper(targetType, options) {
|
|
116
|
+
return Mapper.createMapper(targetType, options);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
MapperService = __decorate([
|
|
120
|
+
Injectable(),
|
|
121
|
+
__param(0, Optional()),
|
|
122
|
+
__param(0, Inject(MAPPER_OPTIONS)),
|
|
123
|
+
__metadata("design:paramtypes", [Object])
|
|
124
|
+
], MapperService);
|
|
125
|
+
export { MapperService };
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sevirial/nest-mapper - Mapping Registry
|
|
3
|
+
* Singleton registry for storing and caching mapping configurations
|
|
4
|
+
*/
|
|
5
|
+
import "reflect-metadata";
|
|
6
|
+
import { METADATA_KEYS, } from "./types";
|
|
7
|
+
import { getNestedValue } from "./utils";
|
|
8
|
+
/**
|
|
9
|
+
* Global mapping registry - singleton pattern for performance
|
|
10
|
+
*/
|
|
11
|
+
class MappingRegistryClass {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.registry = new Map();
|
|
14
|
+
this.compiledMappers = new WeakMap();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Gets or creates a registry entry for a target DTO class
|
|
18
|
+
*/
|
|
19
|
+
getEntry(targetType) {
|
|
20
|
+
let entry = this.registry.get(targetType);
|
|
21
|
+
if (!entry) {
|
|
22
|
+
entry = this.createEntryFromMetadata(targetType);
|
|
23
|
+
this.registry.set(targetType, entry);
|
|
24
|
+
}
|
|
25
|
+
return entry;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates a mapping entry by reading decorator metadata
|
|
29
|
+
*/
|
|
30
|
+
createEntryFromMetadata(targetType) {
|
|
31
|
+
const propertyMappings = Reflect.getMetadata(METADATA_KEYS.PROPERTY_MAPPINGS, targetType) ||
|
|
32
|
+
new Map();
|
|
33
|
+
const fieldGroups = Reflect.getMetadata(METADATA_KEYS.FIELD_GROUPS, targetType) || new Map();
|
|
34
|
+
return {
|
|
35
|
+
targetType,
|
|
36
|
+
propertyConfigs: propertyMappings,
|
|
37
|
+
fieldGroups,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Gets a compiled mapper for the given target type and options signature
|
|
42
|
+
*/
|
|
43
|
+
getCompiledMapper(targetType, optionsKey = "default") {
|
|
44
|
+
const typeMappers = this.compiledMappers.get(targetType);
|
|
45
|
+
return typeMappers?.get(optionsKey);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Stores a compiled mapper for reuse
|
|
49
|
+
*/
|
|
50
|
+
setCompiledMapper(targetType, optionsKey, mapper) {
|
|
51
|
+
let typeMappers = this.compiledMappers.get(targetType);
|
|
52
|
+
if (!typeMappers) {
|
|
53
|
+
typeMappers = new Map();
|
|
54
|
+
this.compiledMappers.set(targetType, typeMappers);
|
|
55
|
+
}
|
|
56
|
+
typeMappers.set(optionsKey, mapper);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Clears all cached mappers (useful for testing)
|
|
60
|
+
*/
|
|
61
|
+
clearCache() {
|
|
62
|
+
this.registry.clear();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Generates a cache key for mapping options
|
|
66
|
+
*/
|
|
67
|
+
getOptionsKey(options) {
|
|
68
|
+
if (!options)
|
|
69
|
+
return "default";
|
|
70
|
+
if (options.pick)
|
|
71
|
+
return `pick:${options.pick.sort().join(",")}`;
|
|
72
|
+
if (options.omit)
|
|
73
|
+
return `omit:${options.omit.sort().join(",")}`;
|
|
74
|
+
if (options.group)
|
|
75
|
+
return `group:${options.group}`;
|
|
76
|
+
return "default";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Singleton instance
|
|
81
|
+
*/
|
|
82
|
+
export const MappingRegistry = new MappingRegistryClass();
|
|
83
|
+
/**
|
|
84
|
+
* Mapper Factory - Creates optimized mapping functions
|
|
85
|
+
*/
|
|
86
|
+
export class MapperFactory {
|
|
87
|
+
/**
|
|
88
|
+
* Creates a compiled mapper for the given target type with optional field projection
|
|
89
|
+
*/
|
|
90
|
+
static createMapper(targetType, options) {
|
|
91
|
+
const entry = MappingRegistry.getEntry(targetType);
|
|
92
|
+
const optionsKey = MappingRegistry.getOptionsKey(options);
|
|
93
|
+
// Check cache first
|
|
94
|
+
let compiledMapper = MappingRegistry.getCompiledMapper(targetType, optionsKey);
|
|
95
|
+
if (compiledMapper) {
|
|
96
|
+
return compiledMapper;
|
|
97
|
+
}
|
|
98
|
+
// Determine which properties to include
|
|
99
|
+
const propertiesToMap = this.getPropertiesToMap(entry, options);
|
|
100
|
+
// Build the compiled mapper
|
|
101
|
+
compiledMapper = this.buildMapper(targetType, entry, propertiesToMap);
|
|
102
|
+
// Cache for reuse
|
|
103
|
+
MappingRegistry.setCompiledMapper(targetType, optionsKey, compiledMapper);
|
|
104
|
+
return compiledMapper;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Determines which properties to map based on options
|
|
108
|
+
*/
|
|
109
|
+
static getPropertiesToMap(entry, options) {
|
|
110
|
+
const allConfigs = Array.from(entry.propertyConfigs.values()).filter((config) => !config.ignore);
|
|
111
|
+
if (!options) {
|
|
112
|
+
return allConfigs;
|
|
113
|
+
}
|
|
114
|
+
// Field group selection
|
|
115
|
+
if (options.group) {
|
|
116
|
+
const groupFields = entry.fieldGroups.get(options.group);
|
|
117
|
+
if (groupFields) {
|
|
118
|
+
return allConfigs.filter((config) => groupFields.has(config.targetKey));
|
|
119
|
+
}
|
|
120
|
+
return []; // Group not found, return empty
|
|
121
|
+
}
|
|
122
|
+
// Pick specific fields
|
|
123
|
+
if (options.pick && options.pick.length > 0) {
|
|
124
|
+
const pickSet = new Set(options.pick.map(String));
|
|
125
|
+
return allConfigs.filter((config) => pickSet.has(config.targetKey));
|
|
126
|
+
}
|
|
127
|
+
// Omit specific fields
|
|
128
|
+
if (options.omit && options.omit.length > 0) {
|
|
129
|
+
const omitSet = new Set(options.omit.map(String));
|
|
130
|
+
return allConfigs.filter((config) => !omitSet.has(config.targetKey));
|
|
131
|
+
}
|
|
132
|
+
return allConfigs;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Builds an optimized mapping function
|
|
136
|
+
*/
|
|
137
|
+
static buildMapper(targetType, entry, properties) {
|
|
138
|
+
// Separate transform functions from path mappings for optimization
|
|
139
|
+
const pathMappings = [];
|
|
140
|
+
const transformMappings = [];
|
|
141
|
+
for (const prop of properties) {
|
|
142
|
+
if (prop.isTransform && typeof prop.source === "function") {
|
|
143
|
+
transformMappings.push({
|
|
144
|
+
target: prop.targetKey,
|
|
145
|
+
transform: prop.source,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
pathMappings.push({
|
|
150
|
+
target: prop.targetKey,
|
|
151
|
+
source: prop.source,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Return optimized mapper function
|
|
156
|
+
return (source, context) => {
|
|
157
|
+
if (source == null) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
// Create new instance or plain object based on target type
|
|
161
|
+
const result = new targetType();
|
|
162
|
+
// Apply path mappings (optimized)
|
|
163
|
+
for (let i = 0; i < pathMappings.length; i++) {
|
|
164
|
+
const mapping = pathMappings[i];
|
|
165
|
+
result[mapping.target] = getNestedValue(source, mapping.source);
|
|
166
|
+
}
|
|
167
|
+
// Apply transform mappings
|
|
168
|
+
for (let i = 0; i < transformMappings.length; i++) {
|
|
169
|
+
const mapping = transformMappings[i];
|
|
170
|
+
result[mapping.target] = mapping.transform(source, context);
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sevirial/nest-mapper - Type Definitions
|
|
3
|
+
* Core types for the high-performance object mapper
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Metadata key constants
|
|
7
|
+
*/
|
|
8
|
+
export const METADATA_KEYS = {
|
|
9
|
+
PROPERTY_MAPPINGS: Symbol("sevirial:property-mappings"),
|
|
10
|
+
FIELD_GROUPS: Symbol("sevirial:field-groups"),
|
|
11
|
+
AUTO_MAP: Symbol("sevirial:auto-map"),
|
|
12
|
+
IGNORE: Symbol("sevirial:ignore"),
|
|
13
|
+
NESTED_TYPE: Symbol("sevirial:nested-type"),
|
|
14
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sevirial/nest-mapper - Utility Functions
|
|
3
|
+
* Helper functions for the mapping engine
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Gets a nested value from an object using dot notation path.
|
|
7
|
+
* Optimized for performance with caching of path segments.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* getNestedValue({ profile: { avatar: 'url' } }, 'profile.avatar')
|
|
11
|
+
* // Returns: 'url'
|
|
12
|
+
*/
|
|
13
|
+
const pathCache = new Map();
|
|
14
|
+
// Dangerous property names for prototype pollution
|
|
15
|
+
const BLOCKED_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
16
|
+
export function getNestedValue(obj, path) {
|
|
17
|
+
if (obj == null)
|
|
18
|
+
return undefined;
|
|
19
|
+
// Simple path - check for prototype pollution
|
|
20
|
+
if (!path.includes(".")) {
|
|
21
|
+
if (BLOCKED_KEYS.has(path))
|
|
22
|
+
return undefined;
|
|
23
|
+
return obj[path];
|
|
24
|
+
}
|
|
25
|
+
let segments = pathCache.get(path);
|
|
26
|
+
if (!segments) {
|
|
27
|
+
segments = path.split(".");
|
|
28
|
+
pathCache.set(path, segments);
|
|
29
|
+
}
|
|
30
|
+
let current = obj;
|
|
31
|
+
for (let i = 0; i < segments.length; i++) {
|
|
32
|
+
// Block prototype pollution attempts
|
|
33
|
+
if (BLOCKED_KEYS.has(segments[i]))
|
|
34
|
+
return undefined;
|
|
35
|
+
if (current == null || typeof current !== "object")
|
|
36
|
+
return undefined;
|
|
37
|
+
current = current[segments[i]];
|
|
38
|
+
}
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Checks if a value is a plain object (not an array, Date, etc.)
|
|
43
|
+
*/
|
|
44
|
+
export function isPlainObject(value) {
|
|
45
|
+
if (value === null || typeof value !== "object")
|
|
46
|
+
return false;
|
|
47
|
+
const proto = Object.getPrototypeOf(value);
|
|
48
|
+
return proto === Object.prototype || proto === null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Checks if a value is a class instance (not a plain object)
|
|
52
|
+
*/
|
|
53
|
+
export function isClassInstance(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
|
+
* Creates a shallow clone of an object with only specified keys
|
|
61
|
+
*/
|
|
62
|
+
export function pickKeys(obj, keys) {
|
|
63
|
+
const result = {};
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
if (key in obj) {
|
|
66
|
+
result[key] = obj[key];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Creates a shallow clone of an object without specified keys
|
|
73
|
+
*/
|
|
74
|
+
export function omitKeys(obj, keys) {
|
|
75
|
+
const keySet = new Set(keys);
|
|
76
|
+
const result = {};
|
|
77
|
+
for (const key in obj) {
|
|
78
|
+
if (Object.prototype.hasOwnProperty.call(obj, key) && !keySet.has(key)) {
|
|
79
|
+
result[key] = obj[key];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Gets all property keys including inherited ones
|
|
86
|
+
*/
|
|
87
|
+
export function getAllPropertyKeys(target) {
|
|
88
|
+
const keys = new Set();
|
|
89
|
+
let current = target;
|
|
90
|
+
while (current && current !== Object.prototype) {
|
|
91
|
+
for (const key of Object.getOwnPropertyNames(current)) {
|
|
92
|
+
if (key !== "constructor") {
|
|
93
|
+
keys.add(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
current = Object.getPrototypeOf(current);
|
|
97
|
+
}
|
|
98
|
+
return Array.from(keys);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Deep clones an object (for handling circular references)
|
|
102
|
+
*/
|
|
103
|
+
export function deepClone(obj, visited = new WeakMap()) {
|
|
104
|
+
if (obj === null || typeof obj !== "object")
|
|
105
|
+
return obj;
|
|
106
|
+
const objAsObject = obj;
|
|
107
|
+
if (visited.has(objAsObject))
|
|
108
|
+
return visited.get(objAsObject);
|
|
109
|
+
if (Array.isArray(obj)) {
|
|
110
|
+
const arrClone = [];
|
|
111
|
+
visited.set(objAsObject, arrClone);
|
|
112
|
+
for (let i = 0; i < obj.length; i++) {
|
|
113
|
+
arrClone[i] = deepClone(obj[i], visited);
|
|
114
|
+
}
|
|
115
|
+
return arrClone;
|
|
116
|
+
}
|
|
117
|
+
if (obj instanceof Date)
|
|
118
|
+
return new Date(obj.getTime());
|
|
119
|
+
if (obj instanceof RegExp)
|
|
120
|
+
return new RegExp(obj.source, obj.flags);
|
|
121
|
+
const clone = Object.create(Object.getPrototypeOf(obj));
|
|
122
|
+
visited.set(objAsObject, clone);
|
|
123
|
+
for (const key of Object.keys(obj)) {
|
|
124
|
+
clone[key] = deepClone(obj[key], visited);
|
|
125
|
+
}
|
|
126
|
+
return clone;
|
|
127
|
+
}
|