befly-shared 1.3.0 → 1.3.2

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/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "befly-shared",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "private": false,
5
5
  "description": "Befly 纯函数与纯常量共享包(不包含 Node/浏览器特定实现)",
6
6
  "license": "Apache-2.0",
7
7
  "author": "chensuiyi <bimostyle@qq.com>",
8
8
  "files": [
9
9
  "package.json",
10
- "utils"
10
+ "utils/"
11
11
  ],
12
12
  "type": "module",
13
13
  "exports": {
@@ -19,5 +19,5 @@
19
19
  "engines": {
20
20
  "bun": ">=1.3.0"
21
21
  },
22
- "gitHead": "123125b9d7a6c26879a5b5c9d3ac05e2082141c6"
22
+ "gitHead": "7dd54bc1ed484139fa52ce5408f1a6c166e2a928"
23
23
  }
@@ -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
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 生成短 ID
3
+ * 由时间戳(base36)+ 随机字符组成,约 13 位
4
+ * - 前 8 位:时间戳(可排序)
5
+ * - 后 5 位:随机字符(防冲突)
6
+ * @returns 短 ID 字符串
7
+ * @example
8
+ * genShortId() // "lxyz1a2b3c4"
9
+ */
10
+ export function genShortId(): string {
11
+ return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
12
+ }
@@ -1,127 +0,0 @@
1
- export type BuildTreeByParentPathOptions = {
2
- pathKey?: string;
3
- parentPathKey?: string;
4
- childrenKey?: string;
5
- normalize?: (value: unknown) => string;
6
- };
7
-
8
- export type BuildTreeByParentPathResult<T extends Record<string, any>> = {
9
- flat: Array<T>;
10
- tree: Array<T>;
11
- map: Map<string, T>;
12
- };
13
-
14
- /**
15
- * 将一维数组按 { path, parentPath } 组装为树形结构(纯函数 / 无副作用)。
16
- *
17
- * - 默认字段:path / parentPath / children
18
- * - parentPath 为空字符串或父节点不存在时,视为根节点
19
- * - 内部会 clone 一份节点对象,并写入 children: []
20
- * - 默认自带递归排序:按 sort 升序;sort 缺省/非法或 < 1 视为 999999;sort 相同按 path 自然序
21
- */
22
- export function buildTreeByParentPath<T extends Record<string, any>>(items: T[], options?: BuildTreeByParentPathOptions): BuildTreeByParentPathResult<T> {
23
- const pathKey = typeof options?.pathKey === "string" && options.pathKey.length > 0 ? options.pathKey : "path";
24
- const parentPathKey = typeof options?.parentPathKey === "string" && options.parentPathKey.length > 0 ? options.parentPathKey : "parentPath";
25
- const childrenKey = typeof options?.childrenKey === "string" && options.childrenKey.length > 0 ? options.childrenKey : "children";
26
-
27
- const normalize =
28
- typeof options?.normalize === "function"
29
- ? options.normalize
30
- : (value: unknown) => {
31
- return typeof value === "string" ? value : "";
32
- };
33
-
34
- const map = new Map<string, T>();
35
- const flat: T[] = [];
36
-
37
- const safeItems = Array.isArray(items) ? items : [];
38
-
39
- for (const item of safeItems) {
40
- const rawPath = item ? item[pathKey] : undefined;
41
- const rawParentPath = item ? item[parentPathKey] : undefined;
42
-
43
- const normalizedPath = normalize(rawPath);
44
- const normalizedParentPath = normalize(rawParentPath);
45
-
46
- const nextNode = Object.assign({}, item) as T;
47
- (nextNode as any)[pathKey] = normalizedPath;
48
- (nextNode as any)[parentPathKey] = normalizedParentPath;
49
- (nextNode as any)[childrenKey] = [];
50
-
51
- flat.push(nextNode);
52
-
53
- if (typeof normalizedPath === "string" && normalizedPath.length > 0) {
54
- map.set(normalizedPath, nextNode);
55
- }
56
- }
57
-
58
- const tree: T[] = [];
59
- for (const node of flat) {
60
- const parentPath = (node as any)[parentPathKey];
61
-
62
- if (typeof parentPath === "string" && parentPath.length > 0) {
63
- const parent = map.get(parentPath);
64
- if (parent && Array.isArray((parent as any)[childrenKey])) {
65
- (parent as any)[childrenKey].push(node);
66
- continue;
67
- }
68
- }
69
-
70
- tree.push(node);
71
- }
72
-
73
- const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
74
-
75
- const getSortValue = (node: T): number => {
76
- const raw = node ? (node as any).sort : undefined;
77
- if (typeof raw !== "number") {
78
- return 999999;
79
- }
80
-
81
- if (!Number.isFinite(raw)) {
82
- return 999999;
83
- }
84
-
85
- if (raw < 1) {
86
- return 999999;
87
- }
88
-
89
- return raw;
90
- };
91
-
92
- const compareNode = (a: T, b: T): number => {
93
- const aSort = getSortValue(a);
94
- const bSort = getSortValue(b);
95
-
96
- if (aSort !== bSort) {
97
- return aSort - bSort;
98
- }
99
-
100
- const aPath = a ? (a as any)[pathKey] : "";
101
- const bPath = b ? (b as any)[pathKey] : "";
102
- return collator.compare(typeof aPath === "string" ? aPath : "", typeof bPath === "string" ? bPath : "");
103
- };
104
-
105
- const sortTreeInPlace = (nodes: Array<T>): void => {
106
- if (!Array.isArray(nodes) || nodes.length <= 1) {
107
- return;
108
- }
109
-
110
- nodes.sort(compareNode);
111
-
112
- for (const node of nodes) {
113
- const children = node ? (node as any)[childrenKey] : undefined;
114
- if (Array.isArray(children) && children.length > 0) {
115
- sortTreeInPlace(children);
116
- }
117
- }
118
- };
119
-
120
- sortTreeInPlace(tree);
121
-
122
- return {
123
- flat: flat,
124
- tree: tree,
125
- map: map
126
- };
127
- }