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,82 @@
1
+ import stringifyCMD from './convert/stringifyCMD.js'
2
+ import stringifyFile from './convert/stringifyFile.js'
3
+ import { cacheWriteFile } from './logFileOperator.js'
4
+
5
+ function getLogger({ namespace = 'default', transform = () => {}, id = '', path: logPath = '' }) {
6
+ // 确保命名空间是小写,避免大小写问题
7
+ namespace = namespace.toLowerCase()
8
+
9
+ const showInConsole = (level, message) => {
10
+ console.log(
11
+ stringifyCMD({
12
+ level,
13
+ namespace,
14
+ id
15
+ }),
16
+ message
17
+ )
18
+ }
19
+
20
+ const convertToFileString = (level, message, code, data) => {
21
+ let messageString = ''
22
+ if (message !== undefined && message !== null) {
23
+ switch (typeof message) {
24
+ case 'object':
25
+ try {
26
+ messageString = JSON.stringify(message)
27
+ } catch (e) {
28
+ messageString = '[object Object]'
29
+ }
30
+ break
31
+ case 'function':
32
+ messageString = '[Function: ' + (message.name || 'anonymous') + ']'
33
+ break
34
+ case 'symbol':
35
+ messageString = message.toString()
36
+ break
37
+ default:
38
+ messageString = String(message)
39
+ }
40
+ } else {
41
+ messageString = ''
42
+ }
43
+ return stringifyFile({
44
+ level,
45
+ message: messageString,
46
+ code,
47
+ data,
48
+ namespace,
49
+ id
50
+ })
51
+ }
52
+
53
+ function asyncLog(level, code, data) {
54
+ let message
55
+ if (typeof code === 'string') {
56
+ message = transform(namespace, code, data)
57
+ }
58
+ let actualCode, actualMessage
59
+ if (message) {
60
+ actualCode = code
61
+ actualMessage = message
62
+ } else {
63
+ actualCode = null
64
+ actualMessage = code
65
+ }
66
+ const fileString = convertToFileString(level, actualMessage, actualCode, data)
67
+ showInConsole(level, actualMessage)
68
+ if (logPath) {
69
+ cacheWriteFile(logPath, fileString)
70
+ }
71
+ }
72
+
73
+ return {
74
+ trace: asyncLog.bind(null, 'trace'),
75
+ debug: asyncLog.bind(null, 'debug'),
76
+ info: asyncLog.bind(null, 'info'),
77
+ warn: asyncLog.bind(null, 'warn'),
78
+ error: asyncLog.bind(null, 'error')
79
+ }
80
+ }
81
+
82
+ export default getLogger
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'
2
+ import getLogger from './index.js'
3
+ import { setCacheInterval } from './logFileOperator.js'
4
+ import fse from 'fs-extra'
5
+
6
+ const logFolder = `${process.cwd()}/tmp/logger`
7
+
8
+ describe('日志器', () => {
9
+ beforeAll(async () => {
10
+ setCacheInterval(50)
11
+ await fse.ensureDir(logFolder)
12
+ })
13
+ afterAll(async () => {
14
+ await fse.remove(logFolder)
15
+ })
16
+
17
+ it('记录日志消息到控制台和文件', async () => {
18
+ const logFile = `${logFolder}/default.log`
19
+ const logger = getLogger({
20
+ path: logFile
21
+ })
22
+
23
+ logger.info('测试消息')
24
+ await new Promise(resolve => setTimeout(resolve, 100))
25
+
26
+ const logContent = await fse.readFile(logFile, 'utf-8')
27
+ expect(logContent).toContain('default')
28
+ expect(logContent).toContain(encodeURIComponent('测试消息'))
29
+ expect(logContent).toContain('info')
30
+ })
31
+
32
+ it('消息不是字符串时,应该转换为字符串', async () => {
33
+ const logFile = `${logFolder}/nonStringMessage.log`
34
+ const logger = getLogger({
35
+ path: logFile
36
+ })
37
+
38
+ logger.info({ key: 'value' })
39
+ logger.info([1, 2, 3])
40
+ logger.info(() => 'function')
41
+ logger.info(Symbol('symbol'))
42
+ await new Promise(resolve => setTimeout(resolve, 100))
43
+
44
+ const logContent = await fse.readFile(logFile, 'utf-8')
45
+ expect(logContent).toContain('default')
46
+ expect(logContent).toContain(encodeURIComponent('"key":"value"'))
47
+ expect(logContent).toContain(encodeURIComponent('[1,2,3]'))
48
+ expect(logContent).toContain(encodeURIComponent('[Function: anonymous]'))
49
+ expect(logContent).toContain(encodeURIComponent('Symbol(symbol)'))
50
+ })
51
+
52
+ it('应该处理不同的日志级别', async () => {
53
+ const logFile = `${logFolder}/level.log`
54
+ const logger = getLogger({
55
+ namespace: 'test',
56
+ path: logFile
57
+ })
58
+ logger.error('错误信息')
59
+ logger.warn('警告信息')
60
+ logger.debug('调试信息')
61
+ await new Promise(resolve => setTimeout(resolve, 100))
62
+
63
+ const logContent = await fse.readFile(logFile, 'utf-8')
64
+ expect(logContent).toContain('test')
65
+ expect(logContent).toContain(encodeURIComponent('错误信息'))
66
+ expect(logContent).toContain('error')
67
+ expect(logContent).toContain(encodeURIComponent('警告信息'))
68
+ expect(logContent).toContain('warn')
69
+ expect(logContent).toContain(encodeURIComponent('调试信息'))
70
+ expect(logContent).toContain('debug')
71
+ })
72
+
73
+ it('测试消息转换', async () => {
74
+ const logFile = `${logFolder}/transform.log`
75
+ const logger = getLogger({
76
+ namespace: 'transform',
77
+ path: logFile,
78
+ transform: (namespace, code, data) =>
79
+ `转换后的消息: ${namespace} ${code} ${JSON.stringify(data)}`
80
+ })
81
+
82
+ logger.info('测试消息', { key: 'value' })
83
+ await new Promise(resolve => setTimeout(resolve, 100))
84
+ const logContent = await fse.readFile(logFile, 'utf-8')
85
+ expect(logContent).toContain('transform')
86
+ expect(logContent).toContain(
87
+ encodeURIComponent('转换后的消息: transform 测试消息 {"key":"value"}')
88
+ )
89
+ expect(logContent).toContain('info')
90
+ })
91
+
92
+ it('对象无法序列化时,应该记录为[object Object]', async () => {
93
+ const logFile = `${logFolder}/unserializableObject.log`
94
+ const logger = getLogger({
95
+ path: logFile
96
+ })
97
+
98
+ const circularObj = {}
99
+ circularObj.self = circularObj
100
+
101
+ logger.info(circularObj)
102
+ await new Promise(resolve => setTimeout(resolve, 100))
103
+
104
+ const logContent = await fse.readFile(logFile, 'utf-8')
105
+ expect(logContent).toContain('default')
106
+ expect(logContent).toContain(encodeURIComponent('[object Object]'))
107
+ expect(logContent).toContain('info')
108
+ })
109
+
110
+ it('消息为null时,应该记录为空字符串', async () => {
111
+ const logFile = `${logFolder}/nullMessage.log`
112
+ const logger = getLogger({
113
+ path: logFile
114
+ })
115
+
116
+ logger.info(null)
117
+ await new Promise(resolve => setTimeout(resolve, 100))
118
+
119
+ const logContent = await fse.readFile(logFile, 'utf-8')
120
+ expect(logContent).toContain('default')
121
+ expect(logContent).toContain(encodeURIComponent(''))
122
+ expect(logContent).toContain('info')
123
+ })
124
+ })
@@ -0,0 +1,50 @@
1
+ import fse from 'fs-extra'
2
+ import { setTimeout } from 'timers'
3
+
4
+ const cache = {}
5
+ let cacheInterval = 500
6
+
7
+ const writeFile = async (fileName, logContent) => {
8
+ await fse.ensureFile(fileName)
9
+ await fse.appendFile(fileName, logContent + '\n')
10
+ }
11
+
12
+ const setTimer = fileName => {
13
+ return setTimeout(async () => {
14
+ const fileCache = cache[fileName]
15
+ if (fileCache) {
16
+ const logContent = fileCache.cache.join('\n')
17
+ fileCache.cache = []
18
+ fileCache.busy = true
19
+ await writeFile(fileName, logContent)
20
+ fileCache.busy = false
21
+
22
+ if (fileCache.cache.length === 0) {
23
+ cache[fileName] = {
24
+ timer: null,
25
+ busy: false,
26
+ cache: []
27
+ }
28
+ } else {
29
+ fileCache.timer = setTimer(fileName)
30
+ }
31
+ }
32
+ }, cacheInterval)
33
+ }
34
+
35
+ const cacheWriteFile = (fileName, logContent) => {
36
+ if (!cache[fileName] || !cache[fileName].timer) {
37
+ cache[fileName] = {
38
+ busy: false,
39
+ timer: setTimer(fileName),
40
+ cache: []
41
+ }
42
+ }
43
+ cache[fileName].cache.push(logContent)
44
+ }
45
+
46
+ const setCacheInterval = interval => {
47
+ cacheInterval = interval
48
+ }
49
+
50
+ export { writeFile, cacheWriteFile, setCacheInterval }
@@ -0,0 +1,69 @@
1
+ // logFileOperator.test.ts
2
+ // 此文件用于测试 logFileOperator.ts 中的 writeFile 和 cacheWriteFile 方法
3
+ import { describe, it, expect, beforeAll } from '@jest/globals'
4
+ import { writeFile, cacheWriteFile, setCacheInterval } from './logFileOperator.js'
5
+ import fse from 'fs-extra'
6
+ import path from 'path'
7
+ import tmp from '../../test-utils/tmp.js'
8
+
9
+ describe('测试 logFileOperator', () => {
10
+ let tempFolder
11
+
12
+ beforeAll(async () => {
13
+ tempFolder = await tmp('logFileOperator')
14
+ })
15
+
16
+ it('测试 writeFile 方法', async () => {
17
+ setCacheInterval(50)
18
+ const tempFile = path.join(tempFolder, 'fileOperator1.log')
19
+ const log = '测试写入日志'
20
+ await writeFile(tempFile, log)
21
+ const content = await fse.readFile(tempFile, 'utf8')
22
+ // 检查日志内容是否正确写入
23
+ expect(content.trim()).toBe(log)
24
+ })
25
+
26
+ it('测试 cacheWriteFile 方法', async () => {
27
+ setCacheInterval(50)
28
+ const tempFile = path.join(tempFolder, 'fileOperator2.log')
29
+ const log1 = '缓存日志1'
30
+ const log2 = '缓存日志2'
31
+
32
+ await cacheWriteFile(tempFile, log1)
33
+ await cacheWriteFile(tempFile, log2)
34
+
35
+ // 延时等待写入完成
36
+ await new Promise(resolve => setTimeout(resolve, 100))
37
+
38
+ const content = await fse.readFile(tempFile, 'utf8')
39
+ // 检查文件中是否包含所有缓存日志
40
+ expect(content).toContain(log1)
41
+ expect(content).toContain(log2)
42
+ })
43
+
44
+ it('测试 setCacheInterval 功能', async () => {
45
+ const tempFile = path.join(tempFolder, 'fileOperator3.log')
46
+ // 设置新的缓存间隔
47
+ setCacheInterval(100)
48
+
49
+ const logA = '日志A'
50
+ const logB = '日志B'
51
+
52
+ await cacheWriteFile(tempFile, logA)
53
+ await cacheWriteFile(tempFile, logB)
54
+
55
+ await new Promise(resolve => setTimeout(resolve, 50))
56
+
57
+ const exists1 = await fse.exists(tempFile)
58
+ expect(exists1).toBe(false)
59
+
60
+ // 等待缓存间隔时间到达
61
+ await new Promise(resolve => setTimeout(resolve, 100))
62
+
63
+ const exists2 = await fse.exists(tempFile)
64
+ expect(exists2).toBe(true)
65
+ const content2 = await fse.readFile(tempFile, 'utf8')
66
+ expect(content2).toContain(logA)
67
+ expect(content2).toContain(logB)
68
+ })
69
+ })
@@ -0,0 +1,9 @@
1
+ export default middleware => {
2
+ return async (req, res, next) => {
3
+ try {
4
+ await middleware(req, res, next)
5
+ } catch (e) {
6
+ next(e, req, res, next)
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,82 @@
1
+ import { register } from '../../../i18n/index.js'
2
+
3
+ const hasFileParameter = parameters => {
4
+ let hasFile = false
5
+ for (const param in parameters) {
6
+ if (parameters[param].type === 'file') {
7
+ hasFile = true
8
+ break
9
+ }
10
+ }
11
+ return hasFile
12
+ }
13
+
14
+ export default async (apis, logger) => {
15
+ const exposeApis = {}
16
+ const i18n = {
17
+ origin: {}
18
+ }
19
+ for (const path in apis) {
20
+ const apiInfo = apis[path]
21
+
22
+ apiInfo.key = path
23
+ apiInfo.name = apiInfo.name || ''
24
+ apiInfo.desc = apiInfo.desc || ''
25
+ apiInfo.parameters = apiInfo.parameters || {}
26
+ const hasFile = hasFileParameter(apiInfo.parameters)
27
+ const defaultMethods = hasFile ? ['POST', 'PUT'] : ['GET', 'POST', 'PUT', 'DELETE']
28
+ if (apiInfo.methods) {
29
+ // 检查是否包含不合要求的方法,包含则抛出警告日志,并删除该方法
30
+ const invalidMethods = apiInfo.methods.filter(method => !defaultMethods.includes(method))
31
+ if (invalidMethods.length > 0) {
32
+ logger.warn('API_METHOD_NOT_ALLOWED', {
33
+ path,
34
+ methods: invalidMethods.join(',')
35
+ })
36
+ apiInfo.methods = apiInfo.methods.filter(method => defaultMethods.includes(method))
37
+ }
38
+ } else {
39
+ apiInfo.methods = defaultMethods
40
+ }
41
+ apiInfo.callback = apiInfo.callback || null
42
+
43
+ for (const parameter in apiInfo.parameters) {
44
+ const parameterInfo = apiInfo.parameters[parameter] || {}
45
+ parameterInfo.key = parameter
46
+ parameterInfo.type = parameterInfo.type || 'string'
47
+ parameterInfo.required = parameterInfo.required === true
48
+ parameterInfo.name = parameterInfo.name || ''
49
+ parameterInfo.desc = parameterInfo.desc || ''
50
+ parameterInfo.length = parameterInfo.length || null
51
+ i18n.origin[`parameter.${parameter}.name@${path}`] = parameterInfo.name
52
+ i18n.origin[`parameter.${parameter}.desc@${path}`] = parameterInfo.desc
53
+ parameterInfo.rules = parameterInfo.rules || {}
54
+
55
+ for (const rule in parameterInfo.rules) {
56
+ const ruleInfo = parameterInfo.rules[rule]
57
+ i18n.origin[`parameter.${parameter}.rules.${rule}@${path}`] = ruleInfo.message
58
+ }
59
+ }
60
+
61
+ const prefix = '/api'
62
+ const listenPaths = Array.isArray(apiInfo.path)
63
+ ? apiInfo.path
64
+ : [apiInfo.path || `${prefix}${path}`] // Renamed listenPath to path
65
+
66
+ for (const apiPath of listenPaths) {
67
+ // 处理国际化原始数据
68
+ i18n.origin[`name@${path}`] = apiInfo.name
69
+ exposeApis[apiPath] = {
70
+ key: apiInfo.key,
71
+ name: apiInfo.name,
72
+ desc: apiInfo.desc,
73
+ parameters: apiInfo.parameters,
74
+ methods: apiInfo.methods,
75
+ callback: apiInfo.callback
76
+ }
77
+ }
78
+ }
79
+
80
+ await register('API', i18n)
81
+ return exposeApis
82
+ }
@@ -0,0 +1,111 @@
1
+ import load from './load/index.js'
2
+ import bodyMiddleware from '../bodyMiddleware/index.js'
3
+ import sendSuccess from './response/sendSuccess.js'
4
+ import sendNotFound from './response/sendNotFound.js'
5
+ import sendError from './response/sendError.js'
6
+ import errorCatcher from './errorCatcher.js'
7
+ import prepareData from './prepareData/index.js'
8
+ import getExposeApis from './exposeApis/index.js'
9
+ import { data as i18nData } from '../../i18n/index.js'
10
+ import metadataToSwagger from './metadataToSwagger.js'
11
+ import swaggerUi from 'swagger-ui-express'
12
+ import path from 'path'
13
+ import { fileURLToPath } from 'url'
14
+ import express from 'express'
15
+
16
+ const __filename = fileURLToPath(import.meta.url)
17
+ const __dirname = path.dirname(__filename)
18
+
19
+ export default apiRoot => {
20
+ return async app => {
21
+ const appNamespace = app.namespace('SUMOR_API')
22
+ const apis = await load(apiRoot)
23
+ const exposeApis = await getExposeApis(apis, appNamespace.logger)
24
+ const prefix = '/api'
25
+ for (const path in exposeApis) {
26
+ const apiInfo = exposeApis[path]
27
+
28
+ let middlewares = [...bodyMiddleware(apiInfo)]
29
+ const callback = apiInfo.callback
30
+
31
+ middlewares.push(async (req, res, next) => {
32
+ const reqNamespace = req.namespace('SUMOR_API')
33
+ if (callback) {
34
+ const dataString = JSON.stringify(req.data)
35
+ reqNamespace.logger.trace('API_CALLED', {
36
+ path: req.path,
37
+ data: dataString
38
+ })
39
+ req.data = prepareData(req, apiInfo.key, apiInfo.parameters)
40
+ const result = await callback(req, res)
41
+ sendSuccess(req, res, result)
42
+ } else {
43
+ // 接口程序加载失败,不可执行
44
+ throw new reqNamespace.Error('API_PROGRAM_WRONG')
45
+ }
46
+ })
47
+
48
+ // 捕捉中间件错误
49
+ middlewares = middlewares.map(errorCatcher)
50
+
51
+ middlewares.push((err, req, res, next) => {
52
+ sendError(req, res, err)
53
+ })
54
+
55
+ // 监听API请求
56
+ for (const method of apiInfo.methods) {
57
+ app[method.toLowerCase()](path, middlewares)
58
+ }
59
+
60
+ if (apiInfo.callback) {
61
+ appNamespace.logger.debug('API_REGISTERED', {
62
+ path,
63
+ name: apiInfo.name ? ' - ' + apiInfo.name : ''
64
+ })
65
+ } else {
66
+ appNamespace.logger.error('API_REGISTERED_FAILED', { path })
67
+ }
68
+ }
69
+
70
+ app.all(`/sumor/i18n`, (req, res) => {
71
+ sendSuccess(req, res, i18nData)
72
+ })
73
+
74
+ const metadata = {
75
+ name: '轻呈云应用',
76
+ desc: '',
77
+ apis: {}
78
+ }
79
+ if (app.sumor && app.sumor.config) {
80
+ metadata.name = app.sumor.config.name
81
+ metadata.desc = app.sumor.config.desc
82
+ }
83
+ for (const path in exposeApis) {
84
+ const result = Object.assign({}, exposeApis[path])
85
+ delete result.callback
86
+ metadata.apis[path] = result
87
+ }
88
+ app.all(`/sumor/metadata`, (req, res) => {
89
+ sendSuccess(req, res, metadata)
90
+ })
91
+
92
+ // 添加 Swagger UI
93
+ app.use('/api-docs', express.static(__dirname + '/public'))
94
+ app.use(
95
+ '/api-docs',
96
+ swaggerUi.serve,
97
+ swaggerUi.setup(metadataToSwagger(metadata), {
98
+ customCss: '.swagger-ui .topbar, .version-stamp { display: none !important }',
99
+ customSiteTitle: metadata.name,
100
+ customfavIcon: './favicon.ico'
101
+ })
102
+ )
103
+
104
+ app.get('/api', (req, res) => {
105
+ res.redirect('/api-docs')
106
+ })
107
+
108
+ // 监听prefix,让没匹配到的接口,返回报错页面
109
+ app.all(`${prefix}/*`, sendNotFound)
110
+ }
111
+ }
@@ -0,0 +1,145 @@
1
+ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'
2
+ import axios from 'axios'
3
+ import express from 'express'
4
+ import http from 'http'
5
+ import fse from 'fs-extra'
6
+ import tmp from '../../../test-utils/tmp.js'
7
+ import apiMiddleware from './index.js'
8
+ import i18nMiddleware from '../i18nMiddleware/index.js'
9
+
10
+ const tmpFolder = await tmp('apiMiddleware')
11
+
12
+ describe('API Middleware', () => {
13
+ let app
14
+ let server
15
+ let baseURL
16
+
17
+ beforeAll(async () => {
18
+ app = express()
19
+ app.use(express.json())
20
+
21
+ const apiPath = `${tmpFolder}/api`
22
+ await fse.ensureDir(apiPath)
23
+ const files = {
24
+ 'hello.js':
25
+ 'export default (req, res) => { const name = req.data.name || "World"; return `Hello, ${name}!`; }', // eslint-disable-line
26
+ 'hello.json': JSON.stringify({
27
+ name: 'Hello API',
28
+ path: ['/api/hello', '/api/hello/:name'],
29
+ parameters: {
30
+ name: {
31
+ type: 'string',
32
+ required: true,
33
+ description: 'Name of the person to greet'
34
+ }
35
+ }
36
+ }),
37
+ 'syntaxError.js': 'syntax error`' // eslint-disable-line
38
+ }
39
+ for (const [fileName, content] of Object.entries(files)) {
40
+ await fse.writeFile(`${apiPath}/${fileName}`, content)
41
+ }
42
+
43
+ await i18nMiddleware(app, tmpFolder + '/i18n')
44
+ await apiMiddleware(apiPath)(app)
45
+
46
+ await new Promise(resolve => {
47
+ server = http.createServer(app).listen(() => {
48
+ const { port } = server.address()
49
+ baseURL = `http://localhost:${port}`
50
+ resolve()
51
+ })
52
+ })
53
+ })
54
+
55
+ afterAll(done => {
56
+ server.close(done)
57
+ })
58
+
59
+ it('验证参数传递正常', async () => {
60
+ // 测试缺少必填参数
61
+ let error
62
+ try {
63
+ await axios.get(`${baseURL}/api/hello`)
64
+ } catch (e) {
65
+ error = e
66
+ }
67
+ expect(error).toBeDefined()
68
+ expect(error.response.status).toBe(400)
69
+ expect(error.response.data.code).toBe('API_PARAMETERS_NOT_VALID')
70
+ expect(error.response.data.errors[0].code).toBe('API_PARAMETER_NOT_VALID')
71
+ expect(error.response.data.errors[0].errors[0].code).toBe('API_PARAMETER_REQUIRED')
72
+
73
+ // 测试params
74
+ const response1 = await axios.get(`${baseURL}/api/hello/Alice`)
75
+ expect(response1.status).toBe(200)
76
+ expect(response1.data.data).toBe('Hello, Alice!')
77
+
78
+ // 测试query
79
+ const response2 = await axios.get(`${baseURL}/api/hello?name=Bob`)
80
+ expect(response2.status).toBe(200)
81
+ expect(response2.data.data).toBe('Hello, Bob!')
82
+
83
+ // 测试body
84
+ const response3 = await axios.post(`${baseURL}/api/hello`, { name: 'Charlie' })
85
+ expect(response3.status).toBe(200)
86
+ expect(response3.data.data).toBe('Hello, Charlie!')
87
+
88
+ // 测试HTML响应
89
+ const response4 = await axios.get(`${baseURL}/api/hello`, {
90
+ params: { name: 'John' },
91
+ headers: {
92
+ Accept: 'text/html'
93
+ }
94
+ })
95
+ expect(response4.status).toBe(200)
96
+ expect(response4.data).toContain('Hello, John!</pre>')
97
+ })
98
+
99
+ it('验证错误处理', async () => {
100
+ let error
101
+ try {
102
+ await axios.get(`${baseURL}/api/syntaxError`)
103
+ } catch (e) {
104
+ error = e
105
+ }
106
+
107
+ expect(error).toBeDefined()
108
+ expect(error.response.status).toBe(500)
109
+ expect(error.response.data.code).toBe('API_PROGRAM_WRONG')
110
+ })
111
+
112
+ it('获取API元数据', async () => {
113
+ const response = await axios.get(`${baseURL}/sumor/metadata`)
114
+ expect(response.status).toBe(200)
115
+ expect(response.data.code).toBe('OK')
116
+ expect(response.data.data.apis['/api/hello']).toBeDefined()
117
+ })
118
+
119
+ it('未找到接口', async () => {
120
+ let error
121
+ try {
122
+ await axios.get(`${baseURL}/api/nonexistent`)
123
+ } catch (e) {
124
+ error = e
125
+ }
126
+ expect(error).toBeDefined()
127
+ expect(error.response.status).toBe(404)
128
+ expect(error.response.data.code).toBe('API_NOT_FOUND')
129
+ })
130
+
131
+ it('长文本处理', async () => {
132
+ const longText = 'a'.repeat(10000) + 'DEMO'
133
+ const response = await axios.post(
134
+ `${baseURL}/api/hello`,
135
+ { name: longText },
136
+ {
137
+ headers: {
138
+ Accept: 'text/html'
139
+ }
140
+ }
141
+ )
142
+ expect(response.status).toBe(200)
143
+ expect(response.data).toContain('DEMO')
144
+ })
145
+ })