befly-shared 1.1.1

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.
Files changed (76) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +442 -0
  3. package/dist/addonHelper.js +83 -0
  4. package/dist/arrayKeysToCamel.js +18 -0
  5. package/dist/arrayToTree.js +23 -0
  6. package/dist/calcPerfTime.js +13 -0
  7. package/dist/configTypes.js +1 -0
  8. package/dist/defineAddonConfig.js +40 -0
  9. package/dist/fieldClear.js +57 -0
  10. package/dist/genShortId.js +12 -0
  11. package/dist/index.js +25 -0
  12. package/dist/keysToCamel.js +21 -0
  13. package/dist/keysToSnake.js +21 -0
  14. package/dist/layouts.js +59 -0
  15. package/dist/pickFields.js +16 -0
  16. package/dist/redisKeys.js +34 -0
  17. package/dist/regex.js +200 -0
  18. package/dist/scanConfig.js +82 -0
  19. package/dist/scanFiles.js +39 -0
  20. package/dist/scanViews.js +48 -0
  21. package/dist/types.js +46 -0
  22. package/package.json +40 -0
  23. package/src/addonHelper.ts +88 -0
  24. package/src/arrayKeysToCamel.ts +18 -0
  25. package/src/arrayToTree.ts +31 -0
  26. package/src/calcPerfTime.ts +13 -0
  27. package/src/configTypes.ts +27 -0
  28. package/src/defineAddonConfig.ts +45 -0
  29. package/src/fieldClear.ts +75 -0
  30. package/src/genShortId.ts +12 -0
  31. package/src/index.ts +28 -0
  32. package/src/keysToCamel.ts +22 -0
  33. package/src/keysToSnake.ts +22 -0
  34. package/src/layouts.ts +90 -0
  35. package/src/pickFields.ts +19 -0
  36. package/src/redisKeys.ts +44 -0
  37. package/src/regex.ts +223 -0
  38. package/src/scanConfig.ts +104 -0
  39. package/src/scanFiles.ts +49 -0
  40. package/src/scanViews.ts +55 -0
  41. package/src/types.ts +338 -0
  42. package/tests/addonHelper.test.ts +55 -0
  43. package/tests/arrayKeysToCamel.test.ts +21 -0
  44. package/tests/arrayToTree.test.ts +98 -0
  45. package/tests/calcPerfTime.test.ts +19 -0
  46. package/tests/fieldClear.test.ts +39 -0
  47. package/tests/keysToCamel.test.ts +22 -0
  48. package/tests/keysToSnake.test.ts +22 -0
  49. package/tests/layouts.test.ts +93 -0
  50. package/tests/pickFields.test.ts +22 -0
  51. package/tests/regex.test.ts +308 -0
  52. package/tests/scanFiles.test.ts +58 -0
  53. package/tests/types.test.ts +283 -0
  54. package/types/addonConfigMerge.d.ts +17 -0
  55. package/types/addonHelper.d.ts +24 -0
  56. package/types/arrayKeysToCamel.d.ts +13 -0
  57. package/types/arrayToTree.d.ts +8 -0
  58. package/types/calcPerfTime.d.ts +4 -0
  59. package/types/configMerge.d.ts +49 -0
  60. package/types/configTypes.d.ts +26 -0
  61. package/types/defineAddonConfig.d.ts +19 -0
  62. package/types/fieldClear.d.ts +16 -0
  63. package/types/genShortId.d.ts +10 -0
  64. package/types/index.d.ts +22 -0
  65. package/types/keysToCamel.d.ts +10 -0
  66. package/types/keysToSnake.d.ts +10 -0
  67. package/types/layouts.d.ts +29 -0
  68. package/types/loadAndMergeConfig.d.ts +7 -0
  69. package/types/mergeConfig.d.ts +7 -0
  70. package/types/pickFields.d.ts +4 -0
  71. package/types/redisKeys.d.ts +34 -0
  72. package/types/regex.d.ts +143 -0
  73. package/types/scanConfig.d.ts +7 -0
  74. package/types/scanFiles.d.ts +12 -0
  75. package/types/scanViews.d.ts +11 -0
  76. package/types/types.d.ts +274 -0
package/src/types.ts ADDED
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Befly 共享类型定义
3
+ * 这些类型可以在 core、tpl、admin 等多个包中复用
4
+ */
5
+
6
+ // ============================================
7
+ // 通用响应类型
8
+ // ============================================
9
+
10
+ /**
11
+ * API 响应结果类型
12
+ */
13
+ export interface ResponseResult<T = any> {
14
+ /** 状态码:0 表示成功,非 0 表示失败 */
15
+ code: number;
16
+ /** 响应消息 */
17
+ msg: string;
18
+ /** 响应数据 */
19
+ data?: T;
20
+ /** 错误信息(仅在失败时) */
21
+ error?: any;
22
+ }
23
+
24
+ /**
25
+ * 分页响应结果类型
26
+ */
27
+ export interface PaginatedResult<T = any> {
28
+ /** 状态码 */
29
+ code: number;
30
+ /** 响应消息 */
31
+ msg: string;
32
+ /** 数据列表 */
33
+ data: T[];
34
+ /** 总记录数 */
35
+ total: number;
36
+ /** 当前页码 */
37
+ page: number;
38
+ /** 每页数量 */
39
+ limit: number;
40
+ /** 总页数 */
41
+ pages: number;
42
+ }
43
+
44
+ /**
45
+ * 验证结果类型
46
+ */
47
+ export interface ValidationResult {
48
+ /** 验证状态:0 成功,1 失败 */
49
+ code: 0 | 1;
50
+ /** 字段错误信息 */
51
+ fields: Record<string, string>;
52
+ }
53
+
54
+ // ============================================
55
+ // HTTP 相关类型
56
+ // ============================================
57
+
58
+ /**
59
+ * HTTP 方法类型
60
+ */
61
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
62
+
63
+ /**
64
+ * 通用键值对类型
65
+ */
66
+ export type KeyValue<T = any> = Record<string, T>;
67
+
68
+ // ============================================
69
+ // 字段定义类型
70
+ // ============================================
71
+
72
+ /**
73
+ * 字段类型
74
+ */
75
+ export type FieldType = 'string' | 'number' | 'text' | 'array_string' | 'array_text';
76
+
77
+ /**
78
+ * 字段定义类型(对象格式)
79
+ */
80
+ export interface FieldDefinition {
81
+ /** 字段标签/描述 */
82
+ name: string;
83
+ /** 字段详细说明 */
84
+ detail: string;
85
+ /** 字段类型 */
86
+ type: FieldType;
87
+ /** 最小值/最小长度 */
88
+ min: number | null;
89
+ /** 最大值/最大长度 */
90
+ max: number | null;
91
+ /** 默认值 */
92
+ default: any;
93
+ /** 是否创建索引 */
94
+ index: boolean;
95
+ /** 是否唯一 */
96
+ unique: boolean;
97
+ /** 字段注释 */
98
+ comment: string;
99
+ /** 是否允许为空 */
100
+ nullable: boolean;
101
+ /** 是否无符号(仅 number 类型) */
102
+ unsigned: boolean;
103
+ /** 正则验证 */
104
+ regexp: string | null;
105
+ }
106
+
107
+ /**
108
+ * 表定义类型(对象格式)
109
+ */
110
+ export type TableDefinition = Record<string, FieldDefinition>;
111
+
112
+ // ============================================
113
+ // 用户相关类型
114
+ // ============================================
115
+
116
+ /**
117
+ * 用户信息类型
118
+ */
119
+ export interface UserInfo {
120
+ /** 用户 ID */
121
+ id: number;
122
+ /** 用户名 */
123
+ username?: string;
124
+ /** 邮箱 */
125
+ email?: string;
126
+ /** 角色代码 */
127
+ roleCode?: string;
128
+ /** 其他自定义字段 */
129
+ [key: string]: any;
130
+ }
131
+
132
+ // ============================================
133
+ // 请求上下文类型(基础版)
134
+ // ============================================
135
+
136
+ /**
137
+ * 请求上下文基础接口
138
+ * 用于跨包共享的最小上下文定义
139
+ */
140
+ export interface BaseRequestContext {
141
+ /** 请求体参数 */
142
+ body: Record<string, any>;
143
+ /** 用户信息 */
144
+ user: Record<string, any>;
145
+ /** 请求开始时间(毫秒) */
146
+ now: number;
147
+ /** 客户端 IP 地址 */
148
+ ip: string;
149
+ /** API 路由路径(如 POST/api/user/login) */
150
+ route: string;
151
+ /** 请求唯一 ID */
152
+ requestId: string;
153
+ }
154
+
155
+ // ============================================
156
+ // API 路由类型(基础版)
157
+ // ============================================
158
+
159
+ /**
160
+ * API 路由基础配置
161
+ * 用于跨包共享的最小路由定义
162
+ */
163
+ export interface BaseApiRoute {
164
+ /** 接口名称(必填) */
165
+ name: string;
166
+ /** HTTP 方法(可选,默认 POST) */
167
+ method?: HttpMethod;
168
+ /** 认证类型(可选,默认 true) */
169
+ auth?: boolean;
170
+ /** 字段定义(验证规则) */
171
+ fields?: TableDefinition;
172
+ /** 必填字段 */
173
+ required?: string[];
174
+ /** 路由路径(运行时生成) */
175
+ route?: string;
176
+ }
177
+
178
+ // ============================================
179
+ // 数据库相关类型
180
+ // ============================================
181
+
182
+ /**
183
+ * SQL 值类型
184
+ */
185
+ export type SqlValue = string | number | boolean | null | Date;
186
+
187
+ /**
188
+ * SQL 参数数组类型
189
+ */
190
+ export type SqlParams = SqlValue[];
191
+
192
+ /**
193
+ * 排序方向
194
+ */
195
+ export type OrderDirection = 'ASC' | 'DESC' | 'asc' | 'desc';
196
+
197
+ /**
198
+ * 数据库类型
199
+ */
200
+ export type DatabaseType = 'mysql' | 'postgresql' | 'sqlite';
201
+
202
+ /**
203
+ * 数据库配置类型
204
+ */
205
+ export interface DatabaseConfig {
206
+ /** 数据库类型 */
207
+ type: DatabaseType;
208
+ /** 主机地址 */
209
+ host: string;
210
+ /** 端口号 */
211
+ port: number;
212
+ /** 用户名 */
213
+ user: string;
214
+ /** 密码 */
215
+ password: string;
216
+ /** 数据库名 */
217
+ database: string;
218
+ }
219
+
220
+ /**
221
+ * Redis 配置类型
222
+ */
223
+ export interface RedisConfig {
224
+ /** 主机地址 */
225
+ host: string;
226
+ /** 端口号 */
227
+ port: number;
228
+ /** 密码 */
229
+ password?: string;
230
+ /** 数据库索引 */
231
+ db?: number;
232
+ }
233
+
234
+ // ============================================
235
+ // 菜单和权限类型
236
+ // ============================================
237
+
238
+ /**
239
+ * 菜单项类型
240
+ */
241
+ export interface MenuItem {
242
+ /** 菜单 ID */
243
+ id: number;
244
+ /** 父级 ID */
245
+ pid: number;
246
+ /** 菜单名称 */
247
+ name: string;
248
+ /** 菜单路径 */
249
+ path: string;
250
+ /** 菜单图标 */
251
+ icon?: string;
252
+ /** 排序 */
253
+ sort: number;
254
+ /** 是否隐藏 */
255
+ hidden?: boolean;
256
+ /** 子菜单 */
257
+ children?: MenuItem[];
258
+ }
259
+
260
+ /**
261
+ * 权限项类型
262
+ */
263
+ export interface PermissionItem {
264
+ /** API 路由(如 POST/api/user/list) */
265
+ route: string;
266
+ /** 权限名称 */
267
+ name: string;
268
+ }
269
+
270
+ /**
271
+ * 角色信息类型
272
+ */
273
+ export interface RoleInfo {
274
+ /** 角色 ID */
275
+ id: number;
276
+ /** 角色代码 */
277
+ code: string;
278
+ /** 角色名称 */
279
+ name: string;
280
+ /** 角色描述 */
281
+ desc?: string;
282
+ }
283
+
284
+ // ============================================
285
+ // API 响应码常量
286
+ // ============================================
287
+
288
+ /**
289
+ * API 响应码
290
+ */
291
+ export const ApiCode = {
292
+ /** 成功 */
293
+ SUCCESS: 0,
294
+ /** 通用失败 */
295
+ FAIL: 1,
296
+ /** 未授权 */
297
+ UNAUTHORIZED: 401,
298
+ /** 禁止访问 */
299
+ FORBIDDEN: 403,
300
+ /** 未找到 */
301
+ NOT_FOUND: 404,
302
+ /** 服务器错误 */
303
+ SERVER_ERROR: 500
304
+ } as const;
305
+
306
+ /**
307
+ * API 响应码类型
308
+ */
309
+ export type ApiCodeType = (typeof ApiCode)[keyof typeof ApiCode];
310
+
311
+ // ============================================
312
+ // 错误消息常量
313
+ // ============================================
314
+
315
+ /**
316
+ * 通用错误消息
317
+ */
318
+ export const ErrorMessages = {
319
+ /** 未授权 */
320
+ UNAUTHORIZED: '请先登录',
321
+ /** 禁止访问 */
322
+ FORBIDDEN: '无访问权限',
323
+ /** 未找到 */
324
+ NOT_FOUND: '资源不存在',
325
+ /** 服务器错误 */
326
+ SERVER_ERROR: '服务器错误',
327
+ /** 参数错误 */
328
+ INVALID_PARAMS: '参数错误',
329
+ /** Token 过期 */
330
+ TOKEN_EXPIRED: 'Token 已过期',
331
+ /** Token 无效 */
332
+ TOKEN_INVALID: 'Token 无效'
333
+ } as const;
334
+
335
+ /**
336
+ * 错误消息类型
337
+ */
338
+ export type ErrorMessageType = (typeof ErrorMessages)[keyof typeof ErrorMessages];
@@ -0,0 +1,55 @@
1
+ import { describe, expect, test, beforeAll, afterAll } from 'bun:test';
2
+ import { join, resolve } from 'pathe';
3
+ import { mkdirSync, rmdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
4
+ import { scanAddons, getAddonDir, addonDirExists } from '../src/addonHelper';
5
+
6
+ // Use absolute path to workspace root temp dir
7
+ const WORKSPACE_ROOT = resolve(__dirname, '../../..');
8
+ const TEST_DIR = join(WORKSPACE_ROOT, 'temp', 'test-addons-util');
9
+ const NODE_MODULES_DIR = join(TEST_DIR, 'node_modules');
10
+ const BEFLY_ADDON_DIR = join(NODE_MODULES_DIR, '@befly-addon');
11
+ // const LOCAL_ADDONS_DIR = join(TEST_DIR, 'addons');
12
+
13
+ describe('addonHelper', () => {
14
+ beforeAll(() => {
15
+ // Setup test directories
16
+ if (existsSync(TEST_DIR)) {
17
+ rmSync(TEST_DIR, { recursive: true, force: true });
18
+ }
19
+ mkdirSync(TEST_DIR, { recursive: true });
20
+ mkdirSync(BEFLY_ADDON_DIR, { recursive: true });
21
+ // mkdirSync(LOCAL_ADDONS_DIR, { recursive: true });
22
+
23
+ // Create dummy addons
24
+ mkdirSync(join(BEFLY_ADDON_DIR, 'test-addon-1'), { recursive: true });
25
+ mkdirSync(join(BEFLY_ADDON_DIR, 'test-addon-1', 'api'), { recursive: true });
26
+
27
+ mkdirSync(join(BEFLY_ADDON_DIR, 'test-addon-2'), { recursive: true });
28
+ });
29
+
30
+ afterAll(() => {
31
+ // Cleanup
32
+ rmSync(TEST_DIR, { recursive: true, force: true });
33
+ });
34
+
35
+ test('scanAddons should find addons in node_modules', () => {
36
+ const addons = scanAddons(TEST_DIR);
37
+ expect(addons).toContain('test-addon-1');
38
+ expect(addons).toContain('test-addon-2');
39
+ });
40
+
41
+ test('getAddonDir should return correct path', () => {
42
+ const dir = getAddonDir('test-addon-1', 'api', TEST_DIR);
43
+ expect(dir).toBe(join(BEFLY_ADDON_DIR, 'test-addon-1', 'api'));
44
+ });
45
+
46
+ test('addonDirExists should return true for existing dir', () => {
47
+ const exists = addonDirExists('test-addon-1', 'api', TEST_DIR);
48
+ expect(exists).toBe(true);
49
+ });
50
+
51
+ test('addonDirExists should return false for non-existing dir', () => {
52
+ const exists = addonDirExists('test-addon-1', 'non-existent', TEST_DIR);
53
+ expect(exists).toBe(false);
54
+ });
55
+ });
@@ -0,0 +1,21 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { arrayKeysToCamel } from '../src/arrayKeysToCamel';
3
+
4
+ describe('arrayKeysToCamel', () => {
5
+ test('should convert array of objects to camelCase', () => {
6
+ const input = [
7
+ { user_id: 1, user_name: 'John' },
8
+ { user_id: 2, user_name: 'Jane' }
9
+ ];
10
+ const expected = [
11
+ { userId: 1, userName: 'John' },
12
+ { userId: 2, userName: 'Jane' }
13
+ ];
14
+ expect(arrayKeysToCamel(input)).toEqual(expected);
15
+ });
16
+
17
+ test('should return original if not array', () => {
18
+ expect(arrayKeysToCamel(null as any)).toBeNull();
19
+ expect(arrayKeysToCamel({} as any)).toEqual({});
20
+ });
21
+ });
@@ -0,0 +1,98 @@
1
+ import { test, expect } from 'bun:test';
2
+ import { arrayToTree } from '../src/arrayToTree';
3
+ import type { ArrayToTreeOptions } from '../types/arrayToTree';
4
+
5
+ test('默认字段树结构', () => {
6
+ const flat = [
7
+ { id: 1, pid: 0, name: 'A' },
8
+ { id: 2, pid: 1, name: 'B' },
9
+ { id: 3, pid: 1, name: 'C' },
10
+ { id: 4, pid: 2, name: 'D' }
11
+ ];
12
+ expect(arrayToTree(flat)).toEqual([
13
+ {
14
+ id: 1,
15
+ pid: 0,
16
+ name: 'A',
17
+ children: [
18
+ {
19
+ id: 2,
20
+ pid: 1,
21
+ name: 'B',
22
+ children: [{ id: 4, pid: 2, name: 'D' }]
23
+ },
24
+ { id: 3, pid: 1, name: 'C' }
25
+ ]
26
+ }
27
+ ]);
28
+ });
29
+
30
+ test('自定义字段树结构', () => {
31
+ const custom = [
32
+ { key: 'a', parent: null, label: 'A' },
33
+ { key: 'b', parent: 'a', label: 'B' },
34
+ { key: 'c', parent: 'a', label: 'C' },
35
+ { key: 'd', parent: 'b', label: 'D' }
36
+ ];
37
+ const options: ArrayToTreeOptions<(typeof custom)[0]> = {
38
+ idField: 'key',
39
+ pidField: 'parent',
40
+ childrenField: 'nodes',
41
+ rootPid: null
42
+ };
43
+ expect(arrayToTree(custom, options)).toEqual([
44
+ {
45
+ key: 'a',
46
+ parent: null,
47
+ label: 'A',
48
+ nodes: [
49
+ {
50
+ key: 'b',
51
+ parent: 'a',
52
+ label: 'B',
53
+ nodes: [{ key: 'd', parent: 'b', label: 'D' }]
54
+ },
55
+ { key: 'c', parent: 'a', label: 'C' }
56
+ ]
57
+ }
58
+ ]);
59
+ });
60
+
61
+ test('mapFn 节点转换', () => {
62
+ const custom = [
63
+ { key: 'a', parent: null, label: 'A' },
64
+ { key: 'b', parent: 'a', label: 'B' },
65
+ { key: 'c', parent: 'a', label: 'C' },
66
+ { key: 'd', parent: 'b', label: 'D' }
67
+ ];
68
+ const options: ArrayToTreeOptions<(typeof custom)[0]> = {
69
+ idField: 'key',
70
+ pidField: 'parent',
71
+ childrenField: 'nodes',
72
+ rootPid: null,
73
+ mapFn: (node) => ({ ...node, extra: true })
74
+ };
75
+ expect(arrayToTree(custom, options)).toEqual([
76
+ {
77
+ key: 'a',
78
+ parent: null,
79
+ label: 'A',
80
+ extra: true,
81
+ nodes: [
82
+ {
83
+ key: 'b',
84
+ parent: 'a',
85
+ label: 'B',
86
+ extra: true,
87
+ nodes: [{ key: 'd', parent: 'b', label: 'D', extra: true }]
88
+ },
89
+ { key: 'c', parent: 'a', label: 'C', extra: true }
90
+ ]
91
+ }
92
+ ]);
93
+ });
94
+
95
+ test('空数组和单节点', () => {
96
+ expect(arrayToTree([], {})).toEqual([]);
97
+ expect(arrayToTree([{ id: 1, pid: 0 }], {})).toEqual([{ id: 1, pid: 0 }]);
98
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { calcPerfTime } from '../src/calcPerfTime';
3
+
4
+ describe('calcPerfTime', () => {
5
+ test('should return milliseconds for short duration', () => {
6
+ const start = Bun.nanoseconds();
7
+ // Simulate small delay
8
+ const end = start + 500_000; // 0.5ms
9
+ const result = calcPerfTime(start, end);
10
+ expect(result).toMatch(/毫秒$/);
11
+ });
12
+
13
+ test('should return seconds for long duration', () => {
14
+ const start = Bun.nanoseconds();
15
+ const end = start + 2_000_000_000; // 2s
16
+ const result = calcPerfTime(start, end);
17
+ expect(result).toMatch(/秒$/);
18
+ });
19
+ });
@@ -0,0 +1,39 @@
1
+ import { test, expect } from 'bun:test';
2
+ import { fieldClear } from '../src/fieldClear';
3
+ import type { FieldClearOptions } from '../types/fieldClear';
4
+
5
+ test('对象 pick/omit', () => {
6
+ const obj = { a: 1, b: 2, c: 3 };
7
+ const options: FieldClearOptions = { pickKeys: ['a', 'c'] };
8
+ expect(fieldClear(obj, options)).toEqual({ a: 1, c: 3 });
9
+ });
10
+
11
+ test('数组 keepValues/excludeValues', () => {
12
+ const arr = [
13
+ { a: 1, b: 2 },
14
+ { a: 3, b: 4 },
15
+ { a: 5, b: 6 }
16
+ ];
17
+ const options: FieldClearOptions = { keepValues: [1, 5] };
18
+ expect(fieldClear(arr, options)).toEqual([{ a: 1 }, { a: 5 }]);
19
+ });
20
+
21
+ test('空对象和空数组', () => {
22
+ expect(fieldClear({}, {})).toEqual({});
23
+ expect(fieldClear([], {})).toEqual([]);
24
+ });
25
+
26
+ test('原始值直接返回', () => {
27
+ expect(fieldClear(123, {})).toBe(123);
28
+ });
29
+
30
+ test('should support keepMap to force keep values', () => {
31
+ const obj = { a: 1, b: null, c: 0 };
32
+ const options: FieldClearOptions = {
33
+ excludeValues: [null, 0],
34
+ keepMap: { c: 0 }
35
+ };
36
+ // c=0 is in excludeValues but also in keepMap, so it should be kept
37
+ // b=null is in excludeValues and NOT in keepMap, so it should be excluded
38
+ expect(fieldClear(obj, options)).toEqual({ a: 1, c: 0 });
39
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { keysToCamel } from '../src/keysToCamel';
3
+
4
+ describe('keysToCamel', () => {
5
+ test('should convert keys to camelCase', () => {
6
+ const input = { user_id: 123, user_name: 'John' };
7
+ const expected = { userId: 123, userName: 'John' };
8
+ expect(keysToCamel(input)).toEqual(expected);
9
+ });
10
+
11
+ test('should handle nested objects (shallow)', () => {
12
+ const input = { user_info: { first_name: 'John' } };
13
+ const expected = { userInfo: { first_name: 'John' } };
14
+ expect(keysToCamel(input)).toEqual(expected);
15
+ });
16
+
17
+ test('should return original if not plain object', () => {
18
+ expect(keysToCamel(null as any)).toBeNull();
19
+ expect(keysToCamel(undefined as any)).toBeUndefined();
20
+ expect(keysToCamel('string' as any)).toBe('string');
21
+ });
22
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { keysToSnake } from '../src/keysToSnake';
3
+
4
+ describe('keysToSnake', () => {
5
+ test('should convert keys to snake_case', () => {
6
+ const input = { userId: 123, userName: 'John' };
7
+ const expected = { user_id: 123, user_name: 'John' };
8
+ expect(keysToSnake(input)).toEqual(expected);
9
+ });
10
+
11
+ test('should handle nested objects (shallow)', () => {
12
+ const input = { userInfo: { firstName: 'John' } };
13
+ const expected = { user_info: { firstName: 'John' } };
14
+ expect(keysToSnake(input)).toEqual(expected);
15
+ });
16
+
17
+ test('should return original if not plain object', () => {
18
+ expect(keysToSnake(null as any)).toBeNull();
19
+ expect(keysToSnake(undefined as any)).toBeUndefined();
20
+ expect(keysToSnake('string' as any)).toBe('string');
21
+ });
22
+ });