befly-shared 1.2.6 → 1.2.8
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/dist/deepTransformKeys.js +139 -0
- package/dist/index.js +1 -0
- package/package.json +3 -3
- package/src/deepTransformKeys.ts +172 -0
- package/src/index.ts +1 -0
- package/tests/deepTransformKeys.test.ts +466 -0
- package/types/deepTransformKeys.d.ts +84 -0
- package/types/index.d.ts +1 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { isPlainObject } from 'es-toolkit/compat';
|
|
2
|
+
import { camelCase, snakeCase, kebabCase, pascalCase } from 'es-toolkit/string';
|
|
3
|
+
/**
|
|
4
|
+
* 深度递归遍历数据结构,转换所有键名
|
|
5
|
+
* 支持嵌套对象和数组,自动防止循环引用和栈溢出
|
|
6
|
+
*
|
|
7
|
+
* @param data - 源数据(对象、数组或其他类型)
|
|
8
|
+
* @param transformer - 转换函数或预设方式
|
|
9
|
+
* @param options - 转换选项
|
|
10
|
+
* @returns 键名转换后的新数据
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // 小驼峰
|
|
14
|
+
* deepTransformKeys({ user_id: 123, user_info: { first_name: 'John' } }, 'camel')
|
|
15
|
+
* // { userId: 123, userInfo: { firstName: 'John' } }
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // 下划线
|
|
19
|
+
* deepTransformKeys({ userId: 123, userInfo: { firstName: 'John' } }, 'snake')
|
|
20
|
+
* // { user_id: 123, user_info: { first_name: 'John' } }
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // 短横线
|
|
24
|
+
* deepTransformKeys({ userId: 123, userInfo: { firstName: 'John' } }, 'kebab')
|
|
25
|
+
* // { 'user-id': 123, 'user-info': { 'first-name': 'John' } }
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // 大驼峰
|
|
29
|
+
* deepTransformKeys({ user_id: 123 }, 'pascal')
|
|
30
|
+
* // { UserId: 123 }
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // 大写
|
|
34
|
+
* deepTransformKeys({ userId: 123 }, 'upper')
|
|
35
|
+
* // { USERID: 123 }
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // 小写
|
|
39
|
+
* deepTransformKeys({ UserId: 123 }, 'lower')
|
|
40
|
+
* // { userid: 123 }
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // 自定义转换函数
|
|
44
|
+
* deepTransformKeys({ user_id: 123 }, (key) => `prefix_${key}`)
|
|
45
|
+
* // { prefix_user_id: 123 }
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // 限制递归深度
|
|
49
|
+
* deepTransformKeys(deepData, 'camel', { maxDepth: 10 })
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // 排除特定键名
|
|
53
|
+
* deepTransformKeys({ _id: '123', user_name: 'John' }, 'camel', { excludeKeys: ['_id'] })
|
|
54
|
+
* // { _id: '123', userName: 'John' }
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // 嵌套数组和对象
|
|
58
|
+
* deepTransformKeys({
|
|
59
|
+
* user_list: [{ user_id: 1, user_tags: [{ tag_name: 'vip' }] }]
|
|
60
|
+
* }, 'camel')
|
|
61
|
+
* // { userList: [{ userId: 1, userTags: [{ tagName: 'vip' }] }] }
|
|
62
|
+
*/
|
|
63
|
+
export const deepTransformKeys = (data, transformer, options = {}) => {
|
|
64
|
+
const { maxDepth = 100, excludeKeys = [] } = options;
|
|
65
|
+
// 获取实际的转换函数
|
|
66
|
+
let transformFn;
|
|
67
|
+
if (typeof transformer === 'function') {
|
|
68
|
+
transformFn = transformer;
|
|
69
|
+
}
|
|
70
|
+
else if (transformer === 'camel') {
|
|
71
|
+
transformFn = camelCase;
|
|
72
|
+
}
|
|
73
|
+
else if (transformer === 'snake') {
|
|
74
|
+
transformFn = snakeCase;
|
|
75
|
+
}
|
|
76
|
+
else if (transformer === 'kebab') {
|
|
77
|
+
transformFn = kebabCase;
|
|
78
|
+
}
|
|
79
|
+
else if (transformer === 'pascal') {
|
|
80
|
+
transformFn = pascalCase;
|
|
81
|
+
}
|
|
82
|
+
else if (transformer === 'upper') {
|
|
83
|
+
transformFn = (key) => key.toUpperCase();
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
transformFn = (key) => key.toLowerCase();
|
|
87
|
+
}
|
|
88
|
+
// 用于检测循环引用的 WeakSet
|
|
89
|
+
const visited = new WeakSet();
|
|
90
|
+
// 创建排除键名集合,提升查找性能
|
|
91
|
+
const excludeSet = new Set(excludeKeys);
|
|
92
|
+
// 递归转换函数
|
|
93
|
+
const transform = (value, depth) => {
|
|
94
|
+
// 处理 null 和 undefined
|
|
95
|
+
if (value === null || value === undefined) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
// 处理数组
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
// 检测循环引用
|
|
101
|
+
if (visited.has(value)) {
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
visited.add(value);
|
|
105
|
+
// 检查深度限制:如果达到限制,返回原数组
|
|
106
|
+
if (maxDepth > 0 && depth >= maxDepth) {
|
|
107
|
+
visited.delete(value);
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
const result = value.map((item) => transform(item, depth + 1));
|
|
111
|
+
visited.delete(value);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
// 处理对象
|
|
115
|
+
if (isPlainObject(value)) {
|
|
116
|
+
// 检测循环引用
|
|
117
|
+
if (visited.has(value)) {
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
visited.add(value);
|
|
121
|
+
// 检查深度限制:如果达到限制,返回原对象
|
|
122
|
+
if (maxDepth > 0 && depth >= maxDepth) {
|
|
123
|
+
visited.delete(value);
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
const result = {};
|
|
127
|
+
for (const [key, val] of Object.entries(value)) {
|
|
128
|
+
// 检查是否在排除列表中
|
|
129
|
+
const transformedKey = excludeSet.has(key) ? key : transformFn(key);
|
|
130
|
+
result[transformedKey] = transform(val, depth + 1);
|
|
131
|
+
}
|
|
132
|
+
visited.delete(value);
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
// 其他类型(字符串、数字、布尔值等)直接返回
|
|
136
|
+
return value;
|
|
137
|
+
};
|
|
138
|
+
return transform(data, 0);
|
|
139
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export * from './arrayKeysToCamel.js';
|
|
|
13
13
|
export * from './arrayToTree.js';
|
|
14
14
|
export * from './calcPerfTime.js';
|
|
15
15
|
export * from './configTypes.js';
|
|
16
|
+
export * from './deepTransformKeys.js';
|
|
16
17
|
export * from './genShortId.js';
|
|
17
18
|
export * from './scanConfig.js';
|
|
18
19
|
export * from './fieldClear.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly-shared",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"typescript": "^5.7.2"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"es-toolkit": "^1.
|
|
35
|
+
"es-toolkit": "^1.43.0",
|
|
36
36
|
"merge-anything": "^6.0.6",
|
|
37
37
|
"pathe": "^2.0.3"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "e078b94f087ddf21f289b55ab86c0b8dce105807"
|
|
40
40
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { isPlainObject } from 'es-toolkit/compat';
|
|
2
|
+
import { camelCase, snakeCase, kebabCase, pascalCase } from 'es-toolkit/string';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 键名转换函数类型
|
|
6
|
+
*/
|
|
7
|
+
export type KeyTransformer = (key: string) => string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 预设的转换方式
|
|
11
|
+
* - camel: 小驼峰 user_id → userId
|
|
12
|
+
* - snake: 下划线 userId → user_id
|
|
13
|
+
* - kebab: 短横线 userId → user-id
|
|
14
|
+
* - pascal: 大驼峰 user_id → UserId
|
|
15
|
+
* - upper: 大写 userId → USERID
|
|
16
|
+
* - lower: 小写 UserId → userid
|
|
17
|
+
*/
|
|
18
|
+
export type PresetTransform = 'camel' | 'snake' | 'kebab' | 'pascal' | 'upper' | 'lower';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 转换选项
|
|
22
|
+
*/
|
|
23
|
+
export interface TransformOptions {
|
|
24
|
+
/** 最大递归深度,默认 100。设置为 0 表示不限制深度(不推荐) */
|
|
25
|
+
maxDepth?: number;
|
|
26
|
+
/** 排除的键名列表,这些键名不会被转换 */
|
|
27
|
+
excludeKeys?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 深度递归遍历数据结构,转换所有键名
|
|
32
|
+
* 支持嵌套对象和数组,自动防止循环引用和栈溢出
|
|
33
|
+
*
|
|
34
|
+
* @param data - 源数据(对象、数组或其他类型)
|
|
35
|
+
* @param transformer - 转换函数或预设方式
|
|
36
|
+
* @param options - 转换选项
|
|
37
|
+
* @returns 键名转换后的新数据
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // 小驼峰
|
|
41
|
+
* deepTransformKeys({ user_id: 123, user_info: { first_name: 'John' } }, 'camel')
|
|
42
|
+
* // { userId: 123, userInfo: { firstName: 'John' } }
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // 下划线
|
|
46
|
+
* deepTransformKeys({ userId: 123, userInfo: { firstName: 'John' } }, 'snake')
|
|
47
|
+
* // { user_id: 123, user_info: { first_name: 'John' } }
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // 短横线
|
|
51
|
+
* deepTransformKeys({ userId: 123, userInfo: { firstName: 'John' } }, 'kebab')
|
|
52
|
+
* // { 'user-id': 123, 'user-info': { 'first-name': 'John' } }
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* // 大驼峰
|
|
56
|
+
* deepTransformKeys({ user_id: 123 }, 'pascal')
|
|
57
|
+
* // { UserId: 123 }
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // 大写
|
|
61
|
+
* deepTransformKeys({ userId: 123 }, 'upper')
|
|
62
|
+
* // { USERID: 123 }
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* // 小写
|
|
66
|
+
* deepTransformKeys({ UserId: 123 }, 'lower')
|
|
67
|
+
* // { userid: 123 }
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // 自定义转换函数
|
|
71
|
+
* deepTransformKeys({ user_id: 123 }, (key) => `prefix_${key}`)
|
|
72
|
+
* // { prefix_user_id: 123 }
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* // 限制递归深度
|
|
76
|
+
* deepTransformKeys(deepData, 'camel', { maxDepth: 10 })
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* // 排除特定键名
|
|
80
|
+
* deepTransformKeys({ _id: '123', user_name: 'John' }, 'camel', { excludeKeys: ['_id'] })
|
|
81
|
+
* // { _id: '123', userName: 'John' }
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // 嵌套数组和对象
|
|
85
|
+
* deepTransformKeys({
|
|
86
|
+
* user_list: [{ user_id: 1, user_tags: [{ tag_name: 'vip' }] }]
|
|
87
|
+
* }, 'camel')
|
|
88
|
+
* // { userList: [{ userId: 1, userTags: [{ tagName: 'vip' }] }] }
|
|
89
|
+
*/
|
|
90
|
+
export const deepTransformKeys = <T = any>(data: any, transformer: KeyTransformer | PresetTransform, options: TransformOptions = {}): T => {
|
|
91
|
+
const { maxDepth = 100, excludeKeys = [] } = options;
|
|
92
|
+
|
|
93
|
+
// 获取实际的转换函数
|
|
94
|
+
let transformFn: KeyTransformer;
|
|
95
|
+
if (typeof transformer === 'function') {
|
|
96
|
+
transformFn = transformer;
|
|
97
|
+
} else if (transformer === 'camel') {
|
|
98
|
+
transformFn = camelCase;
|
|
99
|
+
} else if (transformer === 'snake') {
|
|
100
|
+
transformFn = snakeCase;
|
|
101
|
+
} else if (transformer === 'kebab') {
|
|
102
|
+
transformFn = kebabCase;
|
|
103
|
+
} else if (transformer === 'pascal') {
|
|
104
|
+
transformFn = pascalCase;
|
|
105
|
+
} else if (transformer === 'upper') {
|
|
106
|
+
transformFn = (key: string) => key.toUpperCase();
|
|
107
|
+
} else {
|
|
108
|
+
transformFn = (key: string) => key.toLowerCase();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 用于检测循环引用的 WeakSet
|
|
112
|
+
const visited = new WeakSet();
|
|
113
|
+
|
|
114
|
+
// 创建排除键名集合,提升查找性能
|
|
115
|
+
const excludeSet = new Set(excludeKeys);
|
|
116
|
+
|
|
117
|
+
// 递归转换函数
|
|
118
|
+
const transform = (value: any, depth: number): any => {
|
|
119
|
+
// 处理 null 和 undefined
|
|
120
|
+
if (value === null || value === undefined) {
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 处理数组
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
// 检测循环引用
|
|
127
|
+
if (visited.has(value)) {
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
visited.add(value);
|
|
131
|
+
|
|
132
|
+
// 检查深度限制:如果达到限制,返回原数组
|
|
133
|
+
if (maxDepth > 0 && depth >= maxDepth) {
|
|
134
|
+
visited.delete(value);
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const result = value.map((item: any) => transform(item, depth + 1));
|
|
139
|
+
visited.delete(value);
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 处理对象
|
|
144
|
+
if (isPlainObject(value)) {
|
|
145
|
+
// 检测循环引用
|
|
146
|
+
if (visited.has(value)) {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
visited.add(value);
|
|
150
|
+
|
|
151
|
+
// 检查深度限制:如果达到限制,返回原对象
|
|
152
|
+
if (maxDepth > 0 && depth >= maxDepth) {
|
|
153
|
+
visited.delete(value);
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result: any = {};
|
|
158
|
+
for (const [key, val] of Object.entries(value)) {
|
|
159
|
+
// 检查是否在排除列表中
|
|
160
|
+
const transformedKey = excludeSet.has(key) ? key : transformFn(key);
|
|
161
|
+
result[transformedKey] = transform(val, depth + 1);
|
|
162
|
+
}
|
|
163
|
+
visited.delete(value);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 其他类型(字符串、数字、布尔值等)直接返回
|
|
168
|
+
return value;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return transform(data, 0) as T;
|
|
172
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from './arrayKeysToCamel.js';
|
|
|
16
16
|
export * from './arrayToTree.js';
|
|
17
17
|
export * from './calcPerfTime.js';
|
|
18
18
|
export * from './configTypes.js';
|
|
19
|
+
export * from './deepTransformKeys.js';
|
|
19
20
|
export * from './genShortId.js';
|
|
20
21
|
export * from './scanConfig.js';
|
|
21
22
|
export * from './fieldClear.js';
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { deepTransformKeys } from '../src/deepTransformKeys';
|
|
3
|
+
|
|
4
|
+
describe('deepTransformKeys', () => {
|
|
5
|
+
describe('预设转换 - camel', () => {
|
|
6
|
+
it('应该转换简单对象的键名为小驼峰', () => {
|
|
7
|
+
const input = { user_id: 123, user_name: 'John' };
|
|
8
|
+
const expected = { userId: 123, userName: 'John' };
|
|
9
|
+
expect(deepTransformKeys(input, 'camel')).toEqual(expected);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('应该转换嵌套对象的键名为小驼峰', () => {
|
|
13
|
+
const input = {
|
|
14
|
+
user_id: 123,
|
|
15
|
+
user_info: {
|
|
16
|
+
first_name: 'John',
|
|
17
|
+
last_name: 'Doe'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const expected = {
|
|
21
|
+
userId: 123,
|
|
22
|
+
userInfo: {
|
|
23
|
+
firstName: 'John',
|
|
24
|
+
lastName: 'Doe'
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
expect(deepTransformKeys(input, 'camel')).toEqual(expected);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('预设转换 - snake', () => {
|
|
32
|
+
it('应该转换简单对象的键名为下划线', () => {
|
|
33
|
+
const input = { userId: 123, userName: 'John' };
|
|
34
|
+
const expected = { user_id: 123, user_name: 'John' };
|
|
35
|
+
expect(deepTransformKeys(input, 'snake')).toEqual(expected);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('应该转换嵌套对象的键名为下划线', () => {
|
|
39
|
+
const input = {
|
|
40
|
+
userId: 123,
|
|
41
|
+
userInfo: {
|
|
42
|
+
firstName: 'John',
|
|
43
|
+
lastName: 'Doe'
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const expected = {
|
|
47
|
+
user_id: 123,
|
|
48
|
+
user_info: {
|
|
49
|
+
first_name: 'John',
|
|
50
|
+
last_name: 'Doe'
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
expect(deepTransformKeys(input, 'snake')).toEqual(expected);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('预设转换 - kebab', () => {
|
|
58
|
+
it('应该转换简单对象的键名为短横线', () => {
|
|
59
|
+
const input = { userId: 123, userName: 'John' };
|
|
60
|
+
const expected = { 'user-id': 123, 'user-name': 'John' };
|
|
61
|
+
expect(deepTransformKeys(input, 'kebab')).toEqual(expected);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('应该转换嵌套对象的键名为短横线', () => {
|
|
65
|
+
const input = {
|
|
66
|
+
userId: 123,
|
|
67
|
+
userInfo: {
|
|
68
|
+
firstName: 'John',
|
|
69
|
+
lastName: 'Doe'
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const expected = {
|
|
73
|
+
'user-id': 123,
|
|
74
|
+
'user-info': {
|
|
75
|
+
'first-name': 'John',
|
|
76
|
+
'last-name': 'Doe'
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
expect(deepTransformKeys(input, 'kebab')).toEqual(expected);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('预设转换 - pascal', () => {
|
|
84
|
+
it('应该转换简单对象的键名为大驼峰', () => {
|
|
85
|
+
const input = { user_id: 123, user_name: 'John' };
|
|
86
|
+
const expected = { UserId: 123, UserName: 'John' };
|
|
87
|
+
expect(deepTransformKeys(input, 'pascal')).toEqual(expected);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('应该转换嵌套对象的键名为大驼峰', () => {
|
|
91
|
+
const input = {
|
|
92
|
+
user_id: 123,
|
|
93
|
+
user_info: {
|
|
94
|
+
first_name: 'John'
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const expected = {
|
|
98
|
+
UserId: 123,
|
|
99
|
+
UserInfo: {
|
|
100
|
+
FirstName: 'John'
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
expect(deepTransformKeys(input, 'pascal')).toEqual(expected);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('预设转换 - upper', () => {
|
|
108
|
+
it('应该转换简单对象的键名为大写', () => {
|
|
109
|
+
const input = { userId: 123, userName: 'John' };
|
|
110
|
+
const expected = { USERID: 123, USERNAME: 'John' };
|
|
111
|
+
expect(deepTransformKeys(input, 'upper')).toEqual(expected);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('应该转换嵌套对象的键名为大写', () => {
|
|
115
|
+
const input = {
|
|
116
|
+
userId: 123,
|
|
117
|
+
userInfo: {
|
|
118
|
+
firstName: 'John'
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const expected = {
|
|
122
|
+
USERID: 123,
|
|
123
|
+
USERINFO: {
|
|
124
|
+
FIRSTNAME: 'John'
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
expect(deepTransformKeys(input, 'upper')).toEqual(expected);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('预设转换 - lower', () => {
|
|
132
|
+
it('应该转换简单对象的键名为小写', () => {
|
|
133
|
+
const input = { UserId: 123, UserName: 'John' };
|
|
134
|
+
const expected = { userid: 123, username: 'John' };
|
|
135
|
+
expect(deepTransformKeys(input, 'lower')).toEqual(expected);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('应该转换嵌套对象的键名为小写', () => {
|
|
139
|
+
const input = {
|
|
140
|
+
UserId: 123,
|
|
141
|
+
UserInfo: {
|
|
142
|
+
FirstName: 'John'
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const expected = {
|
|
146
|
+
userid: 123,
|
|
147
|
+
userinfo: {
|
|
148
|
+
firstname: 'John'
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
expect(deepTransformKeys(input, 'lower')).toEqual(expected);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('自定义转换函数', () => {
|
|
156
|
+
it('应该使用自定义函数转换键名为大写', () => {
|
|
157
|
+
const input = { user_id: 123, user_name: 'John' };
|
|
158
|
+
const expected = { USER_ID: 123, USER_NAME: 'John' };
|
|
159
|
+
expect(deepTransformKeys(input, (key) => key.toUpperCase())).toEqual(expected);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('应该使用自定义函数转换嵌套对象', () => {
|
|
163
|
+
const input = {
|
|
164
|
+
user_id: 123,
|
|
165
|
+
user_info: {
|
|
166
|
+
first_name: 'John'
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const expected = {
|
|
170
|
+
USER_ID: 123,
|
|
171
|
+
USER_INFO: {
|
|
172
|
+
FIRST_NAME: 'John'
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
expect(deepTransformKeys(input, (key) => key.toUpperCase())).toEqual(expected);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('应该支持自定义前缀添加', () => {
|
|
179
|
+
const input = { id: 1, name: 'test' };
|
|
180
|
+
const expected = { prefix_id: 1, prefix_name: 'test' };
|
|
181
|
+
expect(deepTransformKeys(input, (key) => `prefix_${key}`)).toEqual(expected);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('数组处理', () => {
|
|
186
|
+
it('应该转换数组中对象的键名', () => {
|
|
187
|
+
const input = [
|
|
188
|
+
{ user_id: 1, user_name: 'Alice' },
|
|
189
|
+
{ user_id: 2, user_name: 'Bob' }
|
|
190
|
+
];
|
|
191
|
+
const expected = [
|
|
192
|
+
{ userId: 1, userName: 'Alice' },
|
|
193
|
+
{ userId: 2, userName: 'Bob' }
|
|
194
|
+
];
|
|
195
|
+
expect(deepTransformKeys(input, 'camel')).toEqual(expected);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('应该转换嵌套数组和对象', () => {
|
|
199
|
+
const input = {
|
|
200
|
+
user_list: [
|
|
201
|
+
{
|
|
202
|
+
user_id: 1,
|
|
203
|
+
user_tags: [
|
|
204
|
+
{ tag_name: 'vip', tag_level: 5 },
|
|
205
|
+
{ tag_name: 'active', tag_level: 3 }
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
};
|
|
210
|
+
const expected = {
|
|
211
|
+
userList: [
|
|
212
|
+
{
|
|
213
|
+
userId: 1,
|
|
214
|
+
userTags: [
|
|
215
|
+
{ tagName: 'vip', tagLevel: 5 },
|
|
216
|
+
{ tagName: 'active', tagLevel: 3 }
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
};
|
|
221
|
+
expect(deepTransformKeys(input, 'camel')).toEqual(expected);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('边界情况', () => {
|
|
226
|
+
it('应该处理 null 值', () => {
|
|
227
|
+
expect(deepTransformKeys(null, 'camel')).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('应该处理 undefined', () => {
|
|
231
|
+
expect(deepTransformKeys(undefined, 'camel')).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('应该处理基本类型', () => {
|
|
235
|
+
expect(deepTransformKeys('string', 'camel')).toBe('string');
|
|
236
|
+
expect(deepTransformKeys(123, 'camel')).toBe(123);
|
|
237
|
+
expect(deepTransformKeys(true, 'camel')).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('应该处理空对象', () => {
|
|
241
|
+
expect(deepTransformKeys({}, 'camel')).toEqual({});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('应该处理空数组', () => {
|
|
245
|
+
expect(deepTransformKeys([], 'camel')).toEqual([]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('应该处理混合类型的数组', () => {
|
|
249
|
+
const input = [{ user_id: 1 }, 'string', 123, null, { nested_obj: { deep_key: 'value' } }];
|
|
250
|
+
const expected = [{ userId: 1 }, 'string', 123, null, { nestedObj: { deepKey: 'value' } }];
|
|
251
|
+
expect(deepTransformKeys(input, 'camel')).toEqual(expected);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('应该处理深层嵌套的结构', () => {
|
|
255
|
+
const input = {
|
|
256
|
+
level_1: {
|
|
257
|
+
level_2: {
|
|
258
|
+
level_3: {
|
|
259
|
+
deep_key: 'deep_value'
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const expected = {
|
|
265
|
+
level1: {
|
|
266
|
+
level2: {
|
|
267
|
+
level3: {
|
|
268
|
+
deepKey: 'deep_value'
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
expect(deepTransformKeys(input, 'camel')).toEqual(expected);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('循环引用和深度限制', () => {
|
|
278
|
+
it('应该处理循环引用的对象', () => {
|
|
279
|
+
const obj: any = { user_id: 1, user_name: 'test' };
|
|
280
|
+
obj.self = obj; // 循环引用
|
|
281
|
+
|
|
282
|
+
const result = deepTransformKeys(obj, 'camel');
|
|
283
|
+
|
|
284
|
+
expect(result.userId).toBe(1);
|
|
285
|
+
expect(result.userName).toBe('test');
|
|
286
|
+
// 循环引用的部分应该保持原样
|
|
287
|
+
expect(result.self).toBe(obj);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('应该处理循环引用的数组', () => {
|
|
291
|
+
const arr: any = [{ user_id: 1 }];
|
|
292
|
+
arr.push(arr); // 循环引用
|
|
293
|
+
|
|
294
|
+
const result = deepTransformKeys(arr, 'camel');
|
|
295
|
+
|
|
296
|
+
expect(result[0]).toEqual({ userId: 1 });
|
|
297
|
+
// 循环引用的部分应该保持原样
|
|
298
|
+
expect(result[1]).toBe(arr);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('应该遵守 maxDepth 限制', () => {
|
|
302
|
+
const input = {
|
|
303
|
+
level_1: {
|
|
304
|
+
level_2: {
|
|
305
|
+
level_3: {
|
|
306
|
+
deep_key: 'value'
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// 限制深度为 2
|
|
313
|
+
const result = deepTransformKeys(input, 'camel', { maxDepth: 2 });
|
|
314
|
+
|
|
315
|
+
expect(result.level1).toBeDefined();
|
|
316
|
+
expect(result.level1.level2).toBeDefined();
|
|
317
|
+
// 第 3 层应该保持原样
|
|
318
|
+
expect(result.level1.level2.level_3).toBeDefined();
|
|
319
|
+
expect(result.level1.level2.level_3.deep_key).toBe('value');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('应该在深度为 1 时只转换第一层', () => {
|
|
323
|
+
const input = {
|
|
324
|
+
user_id: 1,
|
|
325
|
+
user_info: {
|
|
326
|
+
first_name: 'John'
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const result = deepTransformKeys(input, 'camel', { maxDepth: 1 });
|
|
331
|
+
|
|
332
|
+
expect(result.userId).toBe(1);
|
|
333
|
+
// 第二层应该保持原样
|
|
334
|
+
expect(result.userInfo.first_name).toBe('John');
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('maxDepth 为 0 时不限制深度', () => {
|
|
338
|
+
const input = {
|
|
339
|
+
l1: { l2: { l3: { l4: { l5: { deep_key: 'value' } } } } }
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const result = deepTransformKeys(input, 'camel', { maxDepth: 0 });
|
|
343
|
+
|
|
344
|
+
expect(result.l1.l2.l3.l4.l5.deepKey).toBe('value');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('默认 maxDepth 应该足够处理常见深度', () => {
|
|
348
|
+
// 创建 50 层深度的对象(小于默认的 100)
|
|
349
|
+
let obj: any = { deep_key: 'value' };
|
|
350
|
+
for (let i = 0; i < 49; i++) {
|
|
351
|
+
obj = { nested_obj: obj };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const result = deepTransformKeys(obj, 'camel');
|
|
355
|
+
|
|
356
|
+
// 应该能完全转换
|
|
357
|
+
let current = result;
|
|
358
|
+
for (let i = 0; i < 49; i++) {
|
|
359
|
+
expect(current.nestedObj).toBeDefined();
|
|
360
|
+
current = current.nestedObj;
|
|
361
|
+
}
|
|
362
|
+
expect(current.deepKey).toBe('value');
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('排除键名功能', () => {
|
|
367
|
+
it('应该排除指定的键名不转换', () => {
|
|
368
|
+
const input = { _id: '123', user_id: 456, user_name: 'John' };
|
|
369
|
+
const result = deepTransformKeys(input, 'camel', { excludeKeys: ['_id'] });
|
|
370
|
+
|
|
371
|
+
expect(result._id).toBe('123');
|
|
372
|
+
expect(result.userId).toBe(456);
|
|
373
|
+
expect(result.userName).toBe('John');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('应该排除嵌套对象中的指定键名', () => {
|
|
377
|
+
const input = {
|
|
378
|
+
_id: '123',
|
|
379
|
+
user_id: 456,
|
|
380
|
+
user_info: {
|
|
381
|
+
_id: '789',
|
|
382
|
+
first_name: 'John',
|
|
383
|
+
__v: 1
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const result = deepTransformKeys(input, 'camel', { excludeKeys: ['_id', '__v'] });
|
|
387
|
+
|
|
388
|
+
expect(result._id).toBe('123');
|
|
389
|
+
expect(result.userId).toBe(456);
|
|
390
|
+
expect(result.userInfo._id).toBe('789');
|
|
391
|
+
expect(result.userInfo.firstName).toBe('John');
|
|
392
|
+
expect(result.userInfo.__v).toBe(1);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('应该排除数组中对象的指定键名', () => {
|
|
396
|
+
const input = [
|
|
397
|
+
{ _id: '1', user_name: 'Alice' },
|
|
398
|
+
{ _id: '2', user_name: 'Bob' }
|
|
399
|
+
];
|
|
400
|
+
const result = deepTransformKeys(input, 'camel', { excludeKeys: ['_id'] });
|
|
401
|
+
|
|
402
|
+
expect(result[0]._id).toBe('1');
|
|
403
|
+
expect(result[0].userName).toBe('Alice');
|
|
404
|
+
expect(result[1]._id).toBe('2');
|
|
405
|
+
expect(result[1].userName).toBe('Bob');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('应该支持多个排除键名', () => {
|
|
409
|
+
const input = {
|
|
410
|
+
_id: '123',
|
|
411
|
+
__v: 1,
|
|
412
|
+
created_at: Date.now(),
|
|
413
|
+
user_id: 456,
|
|
414
|
+
user_name: 'John'
|
|
415
|
+
};
|
|
416
|
+
const result = deepTransformKeys(input, 'camel', {
|
|
417
|
+
excludeKeys: ['_id', '__v', 'created_at']
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(result._id).toBe('123');
|
|
421
|
+
expect(result.__v).toBe(1);
|
|
422
|
+
expect(result.created_at).toBe(input.created_at);
|
|
423
|
+
expect(result.userId).toBe(456);
|
|
424
|
+
expect(result.userName).toBe('John');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('排除键名不存在时应该正常转换', () => {
|
|
428
|
+
const input = { user_id: 1, user_name: 'test' };
|
|
429
|
+
const result = deepTransformKeys(input, 'camel', {
|
|
430
|
+
excludeKeys: ['_id', 'non_existent']
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
expect(result.userId).toBe(1);
|
|
434
|
+
expect(result.userName).toBe('test');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('空排除列表应该正常转换所有键', () => {
|
|
438
|
+
const input = { user_id: 1, user_name: 'test' };
|
|
439
|
+
const result = deepTransformKeys(input, 'camel', { excludeKeys: [] });
|
|
440
|
+
|
|
441
|
+
expect(result.userId).toBe(1);
|
|
442
|
+
expect(result.userName).toBe('test');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('应该结合深度限制和排除键名使用', () => {
|
|
446
|
+
const input = {
|
|
447
|
+
_id: '123',
|
|
448
|
+
user_info: {
|
|
449
|
+
_id: '456',
|
|
450
|
+
nested: {
|
|
451
|
+
deep_key: 'value'
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
const result = deepTransformKeys(input, 'camel', {
|
|
456
|
+
maxDepth: 2,
|
|
457
|
+
excludeKeys: ['_id']
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
expect(result._id).toBe('123');
|
|
461
|
+
expect(result.userInfo._id).toBe('456');
|
|
462
|
+
// 深度限制,第三层保持原样
|
|
463
|
+
expect(result.userInfo.nested.deep_key).toBe('value');
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 键名转换函数类型
|
|
3
|
+
*/
|
|
4
|
+
export type KeyTransformer = (key: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* 预设的转换方式
|
|
7
|
+
* - camel: 小驼峰 user_id → userId
|
|
8
|
+
* - snake: 下划线 userId → user_id
|
|
9
|
+
* - kebab: 短横线 userId → user-id
|
|
10
|
+
* - pascal: 大驼峰 user_id → UserId
|
|
11
|
+
* - upper: 大写 userId → USERID
|
|
12
|
+
* - lower: 小写 UserId → userid
|
|
13
|
+
*/
|
|
14
|
+
export type PresetTransform = 'camel' | 'snake' | 'kebab' | 'pascal' | 'upper' | 'lower';
|
|
15
|
+
/**
|
|
16
|
+
* 转换选项
|
|
17
|
+
*/
|
|
18
|
+
export interface TransformOptions {
|
|
19
|
+
/** 最大递归深度,默认 100。设置为 0 表示不限制深度(不推荐) */
|
|
20
|
+
maxDepth?: number;
|
|
21
|
+
/** 排除的键名列表,这些键名不会被转换 */
|
|
22
|
+
excludeKeys?: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 深度递归遍历数据结构,转换所有键名
|
|
26
|
+
* 支持嵌套对象和数组,自动防止循环引用和栈溢出
|
|
27
|
+
*
|
|
28
|
+
* @param data - 源数据(对象、数组或其他类型)
|
|
29
|
+
* @param transformer - 转换函数或预设方式
|
|
30
|
+
* @param options - 转换选项
|
|
31
|
+
* @returns 键名转换后的新数据
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // 小驼峰
|
|
35
|
+
* deepTransformKeys({ user_id: 123, user_info: { first_name: 'John' } }, 'camel')
|
|
36
|
+
* // { userId: 123, userInfo: { firstName: 'John' } }
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // 下划线
|
|
40
|
+
* deepTransformKeys({ userId: 123, userInfo: { firstName: 'John' } }, 'snake')
|
|
41
|
+
* // { user_id: 123, user_info: { first_name: 'John' } }
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // 短横线
|
|
45
|
+
* deepTransformKeys({ userId: 123, userInfo: { firstName: 'John' } }, 'kebab')
|
|
46
|
+
* // { 'user-id': 123, 'user-info': { 'first-name': 'John' } }
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // 大驼峰
|
|
50
|
+
* deepTransformKeys({ user_id: 123 }, 'pascal')
|
|
51
|
+
* // { UserId: 123 }
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // 大写
|
|
55
|
+
* deepTransformKeys({ userId: 123 }, 'upper')
|
|
56
|
+
* // { USERID: 123 }
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // 小写
|
|
60
|
+
* deepTransformKeys({ UserId: 123 }, 'lower')
|
|
61
|
+
* // { userid: 123 }
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* // 自定义转换函数
|
|
65
|
+
* deepTransformKeys({ user_id: 123 }, (key) => `prefix_${key}`)
|
|
66
|
+
* // { prefix_user_id: 123 }
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* // 限制递归深度
|
|
70
|
+
* deepTransformKeys(deepData, 'camel', { maxDepth: 10 })
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // 排除特定键名
|
|
74
|
+
* deepTransformKeys({ _id: '123', user_name: 'John' }, 'camel', { excludeKeys: ['_id'] })
|
|
75
|
+
* // { _id: '123', userName: 'John' }
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // 嵌套数组和对象
|
|
79
|
+
* deepTransformKeys({
|
|
80
|
+
* user_list: [{ user_id: 1, user_tags: [{ tag_name: 'vip' }] }]
|
|
81
|
+
* }, 'camel')
|
|
82
|
+
* // { userList: [{ userId: 1, userTags: [{ tagName: 'vip' }] }] }
|
|
83
|
+
*/
|
|
84
|
+
export declare const deepTransformKeys: <T = any>(data: any, transformer: KeyTransformer | PresetTransform, options?: TransformOptions) => T;
|
package/types/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './arrayKeysToCamel.js';
|
|
|
10
10
|
export * from './arrayToTree.js';
|
|
11
11
|
export * from './calcPerfTime.js';
|
|
12
12
|
export * from './configTypes.js';
|
|
13
|
+
export * from './deepTransformKeys.js';
|
|
13
14
|
export * from './genShortId.js';
|
|
14
15
|
export * from './scanConfig.js';
|
|
15
16
|
export * from './fieldClear.js';
|