sumor 1.3.0 → 2.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.
Files changed (179) hide show
  1. package/.eslintignore +6 -0
  2. package/.eslintrc.yml +4 -0
  3. package/.gitattributes +1 -0
  4. package/.github/workflows/audit.yml +22 -0
  5. package/.github/workflows/available.yml +22 -0
  6. package/.github/workflows/ci.yml +45 -0
  7. package/.github/workflows/coverage.yml +23 -0
  8. package/.github/workflows/publish.yml +31 -0
  9. package/.github/workflows/ut.yml +24 -0
  10. package/.husky/pre-commit +10 -0
  11. package/.husky/pre-push +10 -0
  12. package/.prettierrc.yml +7 -0
  13. package/LICENSE +21 -0
  14. package/demo/api/error.js +3 -0
  15. package/demo/api/file.js +4 -0
  16. package/demo/api/file.yaml +11 -0
  17. package/demo/api/hello.js +4 -0
  18. package/demo/api/hello.yaml +13 -0
  19. package/demo/api/login.js +4 -0
  20. package/demo/api/logout.js +4 -0
  21. package/demo/api/long.js +9 -0
  22. package/demo/api/long.yaml +2 -0
  23. package/demo/api/user/admin.js +12 -0
  24. package/demo/api/user/info.js +8 -0
  25. package/demo/config/config.yaml +2 -0
  26. package/demo/i18n/demo.yaml +1 -0
  27. package/demo/package.json +6 -0
  28. package/i18n/README.md +32 -0
  29. package/i18n/sumor_api.yaml +16 -0
  30. package/i18n/sumor_app.yaml +3 -0
  31. package/i18n/sumor_internal.yaml +1 -0
  32. package/i18n/sumor_token.yaml +2 -0
  33. package/i18nExt/sumor_api.de.yaml +3 -0
  34. package/i18nExt/sumor_api.en.yaml +3 -0
  35. package/i18nExt/sumor_api.es.yaml +3 -0
  36. package/i18nExt/sumor_api.fr.yaml +3 -0
  37. package/i18nExt/sumor_api.it.yaml +3 -0
  38. package/i18nExt/sumor_api.ja.yaml +3 -0
  39. package/i18nExt/sumor_api.ko.yaml +3 -0
  40. package/i18nExt/sumor_api.pt.yaml +3 -0
  41. package/i18nExt/sumor_api.zh-TW.yaml +3 -0
  42. package/i18nExt/sumor_api.zh.yaml +3 -0
  43. package/i18nExt/sumor_app.de.yaml +3 -0
  44. package/i18nExt/sumor_app.en.yaml +3 -0
  45. package/i18nExt/sumor_app.es.yaml +3 -0
  46. package/i18nExt/sumor_app.zh-TW.yaml +3 -0
  47. package/i18nExt/sumor_app.zh.yaml +3 -0
  48. package/i18nExt/sumor_internal.de.yaml +1 -0
  49. package/i18nExt/sumor_internal.en.yaml +1 -0
  50. package/i18nExt/sumor_internal.es.yaml +1 -0
  51. package/i18nExt/sumor_internal.zh-TW.yaml +1 -0
  52. package/i18nExt/sumor_internal.zh.yaml +1 -0
  53. package/i18nExt/sumor_token.de.yaml +2 -0
  54. package/i18nExt/sumor_token.es.yaml +2 -0
  55. package/i18nExt/sumor_token.fr.yaml +2 -0
  56. package/i18nExt/sumor_token.it.yaml +2 -0
  57. package/i18nExt/sumor_token.ja.yaml +2 -0
  58. package/i18nExt/sumor_token.ko.yaml +2 -0
  59. package/i18nExt/sumor_token.pt.yaml +2 -0
  60. package/i18nExt/sumor_token.ru.yaml +2 -0
  61. package/i18nExt/sumor_token.tr.yaml +2 -0
  62. package/i18nExt/sumor_token.zh-TW.yaml +2 -0
  63. package/i18nExt/sumor_token.zh.yaml +2 -0
  64. package/jest.config.json +26 -0
  65. package/modules/alertPage/html/template.html +162 -0
  66. package/modules/alertPage/html/undraw_access-denied.svg +52 -0
  67. package/modules/alertPage/html/undraw_alert.svg +1 -0
  68. package/modules/alertPage/html/undraw_celebration.svg +78 -0
  69. package/modules/alertPage/html/undraw_complete-form.svg +1 -0
  70. package/modules/alertPage/html/undraw_login.svg +1 -0
  71. package/modules/alertPage/index.js +46 -0
  72. package/modules/alertPage/index.test.js +12 -0
  73. package/modules/i18n/README.md +90 -0
  74. package/modules/i18n/convertI18nValue/README.md +31 -0
  75. package/modules/i18n/convertI18nValue/getI18nTemplate.js +26 -0
  76. package/modules/i18n/convertI18nValue/getI18nTemplate.test.js +40 -0
  77. package/modules/i18n/convertI18nValue/index.js +14 -0
  78. package/modules/i18n/convertI18nValue/index.test.js +55 -0
  79. package/modules/i18n/convertI18nValue/stringVariableReplace.js +13 -0
  80. package/modules/i18n/convertI18nValue/stringVariableReplace.test.js +39 -0
  81. package/modules/i18n/index.js +26 -0
  82. package/modules/i18n/index.test.js +13 -0
  83. package/modules/i18n/load/README.md +28 -0
  84. package/modules/i18n/load/load.js +31 -0
  85. package/modules/i18n/load/load.test.js +30 -0
  86. package/modules/i18n/registry.js +48 -0
  87. package/modules/i18n/registry.test.js +84 -0
  88. package/modules/logger/convert/parseFile.js +14 -0
  89. package/modules/logger/convert/parseFile.test.js +28 -0
  90. package/modules/logger/convert/stringifyCMD.js +48 -0
  91. package/modules/logger/convert/stringifyCMD.test.js +37 -0
  92. package/modules/logger/convert/stringifyFile.js +24 -0
  93. package/modules/logger/convert/stringifyFile.test.js +79 -0
  94. package/modules/logger/index.js +82 -0
  95. package/modules/logger/index.test.js +124 -0
  96. package/modules/logger/logFileOperator.js +50 -0
  97. package/modules/logger/logFileOperator.test.js +69 -0
  98. package/modules/middlewares/apiMiddleware/errorCatcher.js +9 -0
  99. package/modules/middlewares/apiMiddleware/exposeApis/index.js +82 -0
  100. package/modules/middlewares/apiMiddleware/index.js +111 -0
  101. package/modules/middlewares/apiMiddleware/index.test.js +145 -0
  102. package/modules/middlewares/apiMiddleware/load/index.js +35 -0
  103. package/modules/middlewares/apiMiddleware/load/index.test.js +30 -0
  104. package/modules/middlewares/apiMiddleware/metadataToSwagger.js +139 -0
  105. package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.js +26 -0
  106. package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.test.js +36 -0
  107. package/modules/middlewares/apiMiddleware/prepareData/format/convertType.js +48 -0
  108. package/modules/middlewares/apiMiddleware/prepareData/format/convertType.test.js +53 -0
  109. package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.js +31 -0
  110. package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.test.js +31 -0
  111. package/modules/middlewares/apiMiddleware/prepareData/format/index.js +18 -0
  112. package/modules/middlewares/apiMiddleware/prepareData/format/index.test.js +40 -0
  113. package/modules/middlewares/apiMiddleware/prepareData/format/precision.js +12 -0
  114. package/modules/middlewares/apiMiddleware/prepareData/format/precision.test.js +33 -0
  115. package/modules/middlewares/apiMiddleware/prepareData/format/trim.js +15 -0
  116. package/modules/middlewares/apiMiddleware/prepareData/format/trim.test.js +24 -0
  117. package/modules/middlewares/apiMiddleware/prepareData/index.js +29 -0
  118. package/modules/middlewares/apiMiddleware/prepareData/index.test.js +121 -0
  119. package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.js +26 -0
  120. package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.test.js +52 -0
  121. package/modules/middlewares/apiMiddleware/prepareData/validate/index.js +30 -0
  122. package/modules/middlewares/apiMiddleware/public/favicon.ico +0 -0
  123. package/modules/middlewares/apiMiddleware/response/sendError.js +57 -0
  124. package/modules/middlewares/apiMiddleware/response/sendError.test.js +251 -0
  125. package/modules/middlewares/apiMiddleware/response/sendNotFound.js +26 -0
  126. package/modules/middlewares/apiMiddleware/response/sendResponse.js +25 -0
  127. package/modules/middlewares/apiMiddleware/response/sendSuccess.js +30 -0
  128. package/modules/middlewares/bodyMiddleware/cleanupFiles.js +14 -0
  129. package/modules/middlewares/bodyMiddleware/cleanupFiles.test.js +54 -0
  130. package/modules/middlewares/bodyMiddleware/fileParser.js +69 -0
  131. package/modules/middlewares/bodyMiddleware/fileParser.test.js +163 -0
  132. package/modules/middlewares/bodyMiddleware/index.js +12 -0
  133. package/modules/middlewares/bodyMiddleware/index.test.js +64 -0
  134. package/modules/middlewares/bodyMiddleware/mergeData.js +4 -0
  135. package/modules/middlewares/bodyMiddleware/mergeData.test.js +38 -0
  136. package/modules/middlewares/i18nMiddleware/index.js +82 -0
  137. package/modules/middlewares/i18nMiddleware/index.test.js +75 -0
  138. package/modules/middlewares/tokenMiddleware/Token.js +115 -0
  139. package/modules/middlewares/tokenMiddleware/Token.test.js +67 -0
  140. package/modules/middlewares/tokenMiddleware/index.js +32 -0
  141. package/modules/middlewares/tokenMiddleware/index.test.js +115 -0
  142. package/modules/middlewares/tokenMiddleware/parseCookie.js +24 -0
  143. package/modules/middlewares/tokenMiddleware/parseCookie.test.js +49 -0
  144. package/modules/serve/formatConfig.js +7 -0
  145. package/modules/serve/formatConfig.test.js +28 -0
  146. package/modules/serve/index.js +30 -0
  147. package/modules/serve/index.test.js +69 -0
  148. package/modules/serve/listenApp.js +11 -0
  149. package/modules/system/getSystemLanguage.js +9 -0
  150. package/modules/system/getSystemLanguage.test.js +19 -0
  151. package/modules/utils/getError.js +56 -0
  152. package/modules/utils/getError.test.js +97 -0
  153. package/modules/utils/loadConfig.js +73 -0
  154. package/modules/utils/loadConfig.test.js +129 -0
  155. package/modules/utils/pathUtils.js +43 -0
  156. package/modules/utils/pathUtils.test.js +52 -0
  157. package/modules/utils/type.js +14 -0
  158. package/modules/utils/type.test.js +40 -0
  159. package/package.json +61 -1
  160. package/src/app.js +40 -0
  161. package/src/cli.js +28 -0
  162. package/src/index.js +3 -0
  163. package/test-utils/codeTest/README.md +4 -0
  164. package/test-utils/codeTest/custom.css +47 -0
  165. package/test-utils/codeTest/index.js +80 -0
  166. package/test-utils/codeTest/utils/calculateCoverage.js +55 -0
  167. package/test-utils/codeTest/utils/getTestFiles.js +20 -0
  168. package/test-utils/codeTest/utils/runTests.js +38 -0
  169. package/test-utils/codeTest/utils/startServer.js +23 -0
  170. package/test-utils/tmp.js +15 -0
  171. package/cli.js +0 -3
  172. package/index.es.js +0 -578
  173. package/template/web/AppWithFrame.vue +0 -20
  174. package/template/web/index.html +0 -19
  175. package/template/web/src/App.vue +0 -15
  176. package/template/web/src/entry-client.js +0 -9
  177. package/template/web/src/entry-server.js +0 -69
  178. package/template/web/src/main.js +0 -41
  179. package/template/web/src/style.scss +0 -389
@@ -0,0 +1,35 @@
1
+ import path from 'path'
2
+ import { glob } from 'glob'
3
+ import { getRelativePath } from '../../../utils/pathUtils.js'
4
+ import loadConfig from '../../../utils/loadConfig.js'
5
+ import { pathToFileURL } from 'url'
6
+
7
+ export default async apiRoot => {
8
+ // 解析 apiRoot 的绝对路径
9
+ const absoluteApiRoot = path.resolve(apiRoot)
10
+
11
+ // 使用 glob 扫描 apiRoot 目录中的 .js 文件
12
+ const jsFiles = glob.sync(`${absoluteApiRoot}/**/*.js`)
13
+
14
+ const apis = {}
15
+ for (const file of jsFiles) {
16
+ const relativePath = getRelativePath(apiRoot, file)
17
+ const route = '/' + relativePath
18
+
19
+ const config = await loadConfig(file.split('.')[0])
20
+ let callback
21
+ try {
22
+ const program = await import(pathToFileURL(file))
23
+ callback = program.default
24
+ } catch (e) {
25
+ callback = null
26
+ }
27
+
28
+ apis[route] = {
29
+ ...config,
30
+ callback
31
+ }
32
+ }
33
+
34
+ return apis
35
+ }
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import load from './index.js'
3
+ import fse from 'fs-extra'
4
+ import path from 'path'
5
+ import tmp from '../../../../test-utils/tmp.js'
6
+
7
+ describe('load', () => {
8
+ it('应该加载指定目录下的所有.js文件', async () => {
9
+ const tmpDir = await tmp('apiLoader')
10
+ await fse.writeFile(path.join(tmpDir, 'test1.js'), 'syntax error')
11
+ await fse.writeFile(path.join(tmpDir, 'test2.js'), 'export default ()=>{return "test2";}')
12
+ await fse.writeFile(path.join(tmpDir, 'test2.json'), '{"name":"Test 2"}')
13
+ await fse.writeFile(path.join(tmpDir, 'test4.ts'), 'export default ()=>{return "test4";}')
14
+
15
+ await fse.ensureDir(tmpDir + '/deep')
16
+ await fse.writeFile(
17
+ path.join(tmpDir, 'deep', 'test3.js'),
18
+ 'export default ()=>{return "test3";}'
19
+ )
20
+
21
+ const apis = await load(tmpDir)
22
+ expect(Object.keys(apis).length).toBe(3)
23
+ expect(Object.keys(apis).sort()).toStrictEqual(['/deep/test3', '/test1', '/test2'])
24
+ expect(apis['/test2'].name).toBe('Test 2')
25
+
26
+ // 检查加载失败的文件
27
+ expect(apis['/test1'].callback).toBeNull()
28
+ expect(apis['/test2'].callback()).toBe('test2')
29
+ })
30
+ })
@@ -0,0 +1,139 @@
1
+ /**
2
+ * 将API元数据转换为Swagger UI格式
3
+ * @param {Object} metadata - API元数据对象
4
+ * @returns {Object} Swagger UI格式的API文档
5
+ */
6
+ export default function convertToSwagger(metadata) {
7
+ const swaggerDoc = {
8
+ openapi: '3.0.0',
9
+ info: {
10
+ title: metadata.name,
11
+ version: '1.0.0',
12
+ description: metadata.desc || '接口文档'
13
+ },
14
+ tags: [
15
+ {
16
+ name: '接口列表',
17
+ description: `共${Object.keys(metadata.apis).length}个接口`
18
+ }
19
+ ],
20
+ paths: {},
21
+ components: {
22
+ schemas: {}
23
+ }
24
+ }
25
+
26
+ // 遍历所有API端点
27
+ Object.entries(metadata.apis)
28
+ .sort(([a], [b]) => a.localeCompare(b))
29
+ .forEach(([path, apiInfo]) => {
30
+ const swaggerPath = {}
31
+
32
+ let methods = apiInfo.methods
33
+ if (methods.includes('POST')) {
34
+ methods = ['POST']
35
+ } else if (methods.includes('GET')) {
36
+ methods = ['GET']
37
+ }
38
+
39
+ // 处理每个HTTP方法
40
+ methods.forEach(method => {
41
+ const methodLower = method.toLowerCase()
42
+ swaggerPath[methodLower] = {
43
+ tags: ['接口列表'],
44
+ summary: apiInfo.name || path,
45
+ description: apiInfo.desc || '',
46
+ parameters: [],
47
+ responses: {
48
+ 200: {
49
+ description: '成功响应',
50
+ content: {
51
+ 'application/json': {
52
+ schema: {
53
+ type: 'object'
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ // 处理路径参数
62
+ if (path.includes(':')) {
63
+ const pathParams = path.match(/:[^/]+/g) || []
64
+ pathParams.forEach(param => {
65
+ const paramName = param.slice(1)
66
+ const paramInfo = apiInfo.parameters[paramName]
67
+ if (paramInfo) {
68
+ swaggerPath[methodLower].parameters.push({
69
+ name: paramName,
70
+ in: 'path',
71
+ required: paramInfo.required,
72
+ schema: {
73
+ type: paramInfo.type,
74
+ description: paramInfo.desc
75
+ },
76
+ description: paramInfo.name
77
+ })
78
+ }
79
+ })
80
+ }
81
+
82
+ // 处理查询参数
83
+ Object.entries(apiInfo.parameters).forEach(([paramName, paramInfo]) => {
84
+ if (!path.includes(`:${paramName}`) && paramInfo.type !== 'file') {
85
+ swaggerPath[methodLower].parameters.push({
86
+ name: paramName,
87
+ in: 'query',
88
+ required: paramInfo.required,
89
+ schema: {
90
+ type: paramInfo.type,
91
+ description: paramInfo.desc
92
+ },
93
+ description: paramInfo.name
94
+ })
95
+ }
96
+ })
97
+
98
+ // 处理文件上传
99
+ if (method === 'POST' && Object.values(apiInfo.parameters).some(p => p.type === 'file')) {
100
+ swaggerPath[methodLower].requestBody = {
101
+ content: {
102
+ 'multipart/form-data': {
103
+ schema: {
104
+ type: 'object',
105
+ properties: {}
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ // 添加文件参数到schema
112
+ Object.entries(apiInfo.parameters).forEach(([paramName, paramInfo]) => {
113
+ if (paramInfo.type === 'file') {
114
+ swaggerPath[methodLower].requestBody.content['multipart/form-data'].schema.properties[
115
+ paramName
116
+ ] = {
117
+ type: 'array',
118
+ items: {
119
+ type: 'string',
120
+ format: 'binary'
121
+ },
122
+ description: paramInfo.desc || paramInfo.name
123
+ }
124
+ }
125
+ })
126
+ }
127
+ })
128
+
129
+ // 将处理后的路径添加到swagger文档中
130
+ swaggerDoc.paths[path] = swaggerPath
131
+ })
132
+
133
+ return swaggerDoc
134
+ }
135
+
136
+ // 使用示例
137
+ // import metadata from './sample-metadata.json' assert { type: 'json' };
138
+ // const swaggerDoc = convertToSwagger(metadata);
139
+ // console.log(JSON.stringify(swaggerDoc, null, 2));
@@ -0,0 +1,26 @@
1
+ export default (parameter, value) => {
2
+ const needLowerCase = parameter.toLowerCase === true
3
+ const needUpperCase = parameter.toUpperCase === true
4
+
5
+ if (value !== null && needLowerCase) {
6
+ switch (parameter.type) {
7
+ case 'string':
8
+ value = value.toLowerCase()
9
+ break
10
+ default:
11
+ break
12
+ }
13
+ }
14
+
15
+ if (value !== null && needUpperCase) {
16
+ switch (parameter.type) {
17
+ case 'string':
18
+ value = value.toUpperCase()
19
+ break
20
+ default:
21
+ break
22
+ }
23
+ }
24
+
25
+ return value
26
+ }
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import caseSensitive from './caseSensitive'
3
+
4
+ describe('区分大小写', () => {
5
+ it('当参数.toLowerCase 为 true 时,应将字符串转换为小写', () => {
6
+ const result = caseSensitive({ toLowerCase: true, type: 'string' }, 'Hello World')
7
+ expect(result).toBe('hello world')
8
+ })
9
+
10
+ it('当参数.toUpperCase 为 true 时,应将字符串转换为大写', () => {
11
+ const result = caseSensitive({ toUpperCase: true, type: 'string' }, 'Hello World')
12
+ expect(result).toBe('HELLO WORLD')
13
+ })
14
+
15
+ it('当参数.type 不是字符串时,应返回原始值', () => {
16
+ const result = caseSensitive({ toLowerCase: true, type: 'number' }, 12345)
17
+ expect(result).toBe(12345)
18
+ })
19
+
20
+ it('当值为 null 时,应返回 null', () => {
21
+ const result = caseSensitive({ toLowerCase: true, type: 'string' }, null)
22
+ expect(result).toBeNull()
23
+ })
24
+
25
+ it('当 toLowerCase 和 toUpperCase 同时为 true 时,应优先处理为大写', () => {
26
+ const result = caseSensitive(
27
+ {
28
+ toLowerCase: true,
29
+ toUpperCase: true,
30
+ type: 'string'
31
+ },
32
+ 'Hello World'
33
+ )
34
+ expect(result).toBe('HELLO WORLD') // 大写优先
35
+ })
36
+ })
@@ -0,0 +1,48 @@
1
+ export default (type, value) => {
2
+ if (value === null) return value
3
+
4
+ switch (type) {
5
+ case 'string':
6
+ if (typeof value !== 'string') {
7
+ return JSON.stringify(value)
8
+ } else {
9
+ return String(value)
10
+ }
11
+ case 'number':
12
+ if (isNaN(Number(value))) {
13
+ throw new Error('INVALID')
14
+ }
15
+ return Number(value)
16
+ case 'boolean':
17
+ return Boolean(value)
18
+ case 'array':
19
+ if (typeof value === 'string') {
20
+ try {
21
+ return JSON.parse(value)
22
+ } catch {
23
+ throw new Error('INVALID')
24
+ }
25
+ } else if (!Array.isArray(value)) {
26
+ throw new Error('INVALID')
27
+ }
28
+ return value
29
+ case 'object':
30
+ if (typeof value === 'string') {
31
+ try {
32
+ return JSON.parse(value)
33
+ } catch {
34
+ throw new Error('INVALID')
35
+ }
36
+ } else if (typeof value !== 'object') {
37
+ throw new Error('INVALID')
38
+ }
39
+ return value
40
+ case 'file':
41
+ if (!Array.isArray(value)) {
42
+ throw new Error('INVALID')
43
+ }
44
+ return value
45
+ default:
46
+ return value
47
+ }
48
+ }
@@ -0,0 +1,53 @@
1
+ import { describe, test, expect } from '@jest/globals'
2
+ import convertType from './convertType.js'
3
+
4
+ describe('参数类型转换', () => {
5
+ test('应转换为字符串', () => {
6
+ expect(convertType('string', 123)).toBe('123')
7
+ expect(convertType('string', true)).toBe('true')
8
+ })
9
+
10
+ test('应转换为数字', () => {
11
+ expect(convertType('number', '123')).toBe(123)
12
+ expect(() => convertType('number', 'abc')).toThrow('INVALID')
13
+ })
14
+
15
+ test('应转换为布尔值', () => {
16
+ expect(convertType('boolean', 1)).toBe(true)
17
+ expect(convertType('boolean', 0)).toBe(false)
18
+ })
19
+
20
+ test('应转换为数组', () => {
21
+ expect(convertType('array', '[1,2,3]')).toEqual([1, 2, 3])
22
+ expect(convertType('array', [1, 2, 3])).toEqual([1, 2, 3])
23
+ expect(() => convertType('array', 'not an array')).toThrow('INVALID')
24
+ expect(() => convertType('array', 123)).toThrow('INVALID')
25
+ })
26
+
27
+ test('对象类型传入数组应报错', () => {
28
+ expect(() => convertType('array', { key: 'value' })).toThrow('INVALID')
29
+ })
30
+
31
+ test('应转换为对象', () => {
32
+ expect(convertType('object', { key: 'value' })).toEqual({ key: 'value' })
33
+ expect(convertType('object', '{"key":"value"}')).toEqual({ key: 'value' })
34
+ expect(() => convertType('object', 'not an object')).toThrow('INVALID')
35
+ expect(() => convertType('object', 123)).toThrow('INVALID')
36
+ })
37
+
38
+ test('对于未知类型应返回原值', () => {
39
+ expect(convertType('unknown', 'value')).toBe('value')
40
+ })
41
+
42
+ test('对于 null 值应返回 null', () => {
43
+ expect(convertType('string', null)).toBeNull()
44
+ })
45
+
46
+ test('应转换为文件类型', () => {
47
+ expect(() => convertType('file', 'not an array')).toThrow('INVALID')
48
+ expect(() => convertType('file', 123)).toThrow('INVALID')
49
+ expect(() => convertType('file', {})).toThrow('INVALID')
50
+ expect(convertType('file', [])).toEqual([])
51
+ expect(convertType('file', [{ path: '' }, { path: '' }])).toEqual([{ path: '' }, { path: '' }])
52
+ })
53
+ })
@@ -0,0 +1,31 @@
1
+ export default (parameter, value) => {
2
+ if (value === undefined) {
3
+ value = null
4
+ }
5
+ if (parameter.type !== 'string' && value === '') {
6
+ value = null
7
+ }
8
+
9
+ const defaultValue = parameter.default
10
+ const hasDefault = defaultValue !== undefined && defaultValue !== null
11
+
12
+ if (hasDefault) {
13
+ if (value === '' && parameter.type === 'string') {
14
+ value = null
15
+ }
16
+ if (value === null) {
17
+ switch (parameter.type) {
18
+ case 'string':
19
+ value = defaultValue
20
+ break
21
+ case 'number':
22
+ value = defaultValue
23
+ break
24
+ default:
25
+ break
26
+ }
27
+ }
28
+ }
29
+
30
+ return value
31
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, test, expect } from '@jest/globals'
2
+ import defaultValue from './defaultValue.js'
3
+
4
+ describe('defaultValue 函数', () => {
5
+ test('当值为 undefined 时应返回 null', () => {
6
+ expect(defaultValue({ type: 'string' }, undefined)).toBeNull()
7
+ })
8
+
9
+ test('当值为空字符串且类型为非字符串时应返回 null', () => {
10
+ expect(defaultValue({ type: 'number' }, '')).toBeNull()
11
+ })
12
+
13
+ test('当有默认值且值为 null 时应返回默认值', () => {
14
+ expect(defaultValue({ type: 'string', default: 'default' }, null)).toBe('default')
15
+ expect(defaultValue({ type: 'number', default: 42 }, null)).toBe(42)
16
+ })
17
+
18
+ test('当值为空字符串且类型为字符串时应返回默认值', () => {
19
+ expect(defaultValue({ type: 'string', default: 'default' }, '')).toBe('default')
20
+ })
21
+
22
+ test('当无默认值时应保持原值', () => {
23
+ expect(defaultValue({ type: 'string' }, 'value')).toBe('value')
24
+ expect(defaultValue({ type: 'number' }, 123)).toBe(123)
25
+ })
26
+
27
+ test('当默认值为 null 或 undefined 时应保持原值', () => {
28
+ expect(defaultValue({ type: 'string', default: null }, null)).toBeNull()
29
+ expect(defaultValue({ type: 'string', default: undefined }, null)).toBeNull()
30
+ })
31
+ })
@@ -0,0 +1,18 @@
1
+ import convertType from './convertType.js'
2
+ import defaultValue from './defaultValue.js'
3
+ import trim from './trim.js'
4
+ import caseSensitive from './caseSensitive.js'
5
+ import precision from './precision.js'
6
+
7
+ export default (parameter, value) => {
8
+ value = defaultValue(parameter, value)
9
+
10
+ // 类型强制转换
11
+ value = convertType(parameter.type, value)
12
+
13
+ value = trim(parameter, value)
14
+ value = caseSensitive(parameter, value)
15
+ value = precision(parameter, value)
16
+
17
+ return value
18
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import format from './index.js'
3
+
4
+ describe('格式化', () => {
5
+ it('当值为 undefined 时,应应用默认值', () => {
6
+ const parameter = { type: 'string', default: 'defaultValue' }
7
+ const result = format(parameter, undefined)
8
+ expect(result).toBe('defaultValue')
9
+ })
10
+
11
+ it('当 trim 不为 false 时,应去除字符串两端的空格', () => {
12
+ const parameter = { type: 'string', trim: true }
13
+ const result = format(parameter, ' test ')
14
+ expect(result).toBe('test')
15
+ })
16
+
17
+ it('当 toLowerCase 为 true 时,应将字符串转换为小写', () => {
18
+ const parameter = { type: 'string', toLowerCase: true }
19
+ const result = format(parameter, 'TEST')
20
+ expect(result).toBe('test')
21
+ })
22
+
23
+ it('当 toUpperCase 为 true 时,应将字符串转换为大写', () => {
24
+ const parameter = { type: 'string', toUpperCase: true }
25
+ const result = format(parameter, 'test')
26
+ expect(result).toBe('TEST')
27
+ })
28
+
29
+ it('当指定小数位数时,应处理数字精度', () => {
30
+ const parameter = { type: 'number', decimal: 2 }
31
+ const result = format(parameter, 123.456)
32
+ expect(result).toBe(123.46)
33
+ })
34
+
35
+ it('当没有匹配规则时,应保持原值不变', () => {
36
+ const parameter = { type: 'string' }
37
+ const result = format(parameter, 'unchanged')
38
+ expect(result).toBe('unchanged')
39
+ })
40
+ })
@@ -0,0 +1,12 @@
1
+ export default (parameter, value) => {
2
+ if (
3
+ parameter.type === 'number' &&
4
+ parameter.decimal !== undefined &&
5
+ parameter.decimal !== null &&
6
+ typeof value === 'number'
7
+ ) {
8
+ value = Number(value.toFixed(parameter.decimal))
9
+ }
10
+
11
+ return value
12
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+
3
+ import precision from './precision'
4
+
5
+ describe('precision', () => {
6
+ it('should round a number to the specified decimal places', () => {
7
+ const parameter = { type: 'number', decimal: 2 }
8
+ const result = precision(parameter, 3.14159)
9
+ expect(result).toBe(3.14)
10
+ })
11
+
12
+ it('should return the original number if decimal is not specified', () => {
13
+ const parameter = { type: 'number' }
14
+ const result = precision(parameter, 3.14159)
15
+ expect(result).toBe(3.14159)
16
+ })
17
+
18
+ it('should handle null or undefined decimal gracefully', () => {
19
+ const parameter = { type: 'number', decimal: null }
20
+ const result = precision(parameter, 3.14159)
21
+ expect(result).toBe(3.14159)
22
+
23
+ parameter.decimal = undefined
24
+ const result2 = precision(parameter, 3.14159)
25
+ expect(result2).toBe(3.14159)
26
+ })
27
+
28
+ it('should return the original value if it is not a number', () => {
29
+ const parameter = { type: 'number', decimal: 2 }
30
+ const result = precision(parameter, 'not-a-number')
31
+ expect(result).toBe('not-a-number')
32
+ })
33
+ })
@@ -0,0 +1,15 @@
1
+ export default (parameter, value) => {
2
+ const needTrim = parameter.trim !== false
3
+
4
+ if (value !== null && needTrim) {
5
+ switch (parameter.type) {
6
+ case 'string':
7
+ value = value.trim()
8
+ break
9
+ default:
10
+ break
11
+ }
12
+ }
13
+
14
+ return value
15
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from '@jest/globals'
2
+ import trim from './trim'
3
+
4
+ describe('去除空格', () => {
5
+ it('应去除字符串两端的空格', () => {
6
+ const parameter = { type: 'string', trim: true }
7
+ expect(trim(parameter, ' hello ')).toBe('hello')
8
+ })
9
+
10
+ it('当 trim 设置为 false 时,不应去除空格', () => {
11
+ const parameter = { type: 'string', trim: false }
12
+ expect(trim(parameter, ' hello ')).toBe(' hello ')
13
+ })
14
+
15
+ it('对于非字符串类型的值,应返回原始值', () => {
16
+ const parameter = { type: 'number', trim: true }
17
+ expect(trim(parameter, 123)).toBe(123)
18
+ })
19
+
20
+ it('应优雅地处理 null 值', () => {
21
+ const parameter = { type: 'string', trim: true }
22
+ expect(trim(parameter, null)).toBe(null)
23
+ })
24
+ })
@@ -0,0 +1,29 @@
1
+ import format from './format/index.js'
2
+ import validate from './validate/index.js'
3
+
4
+ export default (req, apiKey, parameters) => {
5
+ parameters = parameters || {}
6
+ const data = req.data || {}
7
+
8
+ const { Error } = req.namespace('SUMOR_API')
9
+ const { Error: APIError } = req.namespace('API')
10
+
11
+ const errors = []
12
+
13
+ for (const key in parameters) {
14
+ const parameter = parameters[key] || {}
15
+ data[key] = format(parameter, data[key])
16
+
17
+ const parameterErrors = validate(Error, APIError, apiKey, parameter, data[key])
18
+
19
+ if (parameterErrors.length) {
20
+ errors.push(new Error('API_PARAMETER_NOT_VALID', { key }, parameterErrors))
21
+ }
22
+ }
23
+
24
+ if (errors.length) {
25
+ throw new Error('API_PARAMETERS_NOT_VALID', {}, errors)
26
+ }
27
+
28
+ return data
29
+ }