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,69 @@
1
+ import multer from 'multer'
2
+ import fse from 'fs-extra'
3
+ import cleanupFiles from './cleanupFiles.js'
4
+
5
+ const uploadPath = `${process.cwd()}/tmp/uploads`
6
+ await fse.ensureDir(uploadPath)
7
+ const upload = multer({ dest: 'tmp/uploads/' })
8
+
9
+ export default api => {
10
+ // 初始化上传器
11
+ let uploader
12
+ const parameters = api.parameters || {}
13
+ const uploadParameters = []
14
+ for (const name in parameters) {
15
+ if (parameters[name].type === 'file') {
16
+ uploadParameters.push({ name })
17
+ }
18
+ }
19
+ if (uploadParameters.length > 0) {
20
+ uploader = upload.fields(uploadParameters)
21
+ }
22
+
23
+ const middlewares = []
24
+
25
+ middlewares.push((req, res, next) => {
26
+ if (uploader) {
27
+ uploader(req, res, err => {
28
+ if (err) {
29
+ throw err
30
+ } else {
31
+ const files = {}
32
+ req.files = req.files || {}
33
+ for (const name in req.files) {
34
+ files[name] = files[name] || []
35
+ for (const fileIndex in req.files[name]) {
36
+ files[name].push({
37
+ name: req.files[name][fileIndex].originalname,
38
+ size: req.files[name][fileIndex].size,
39
+ mime: req.files[name][fileIndex].mimetype,
40
+ encoding: req.files[name][fileIndex].encoding,
41
+ path: `${uploadPath}/${req.files[name][fileIndex].filename}`
42
+ })
43
+ }
44
+ }
45
+ req.files = files
46
+ next()
47
+ }
48
+ })
49
+ } else {
50
+ next()
51
+ }
52
+ })
53
+
54
+ middlewares.push((req, res, next) => {
55
+ if (uploader) {
56
+ res.on('finish', async () => {
57
+ await cleanupFiles(req.files)
58
+ })
59
+
60
+ res.on('close', async () => {
61
+ await cleanupFiles(req.files)
62
+ })
63
+ }
64
+
65
+ next()
66
+ })
67
+
68
+ return middlewares
69
+ }
@@ -0,0 +1,163 @@
1
+ import { describe, it, beforeEach, afterEach, expect } from '@jest/globals'
2
+ import axios from 'axios'
3
+ import express from 'express'
4
+ import fileParser from './fileParser.js'
5
+ import FormData from 'form-data'
6
+ import http from 'http'
7
+ import fse from 'fs-extra'
8
+
9
+ const apis = [
10
+ {
11
+ route: '/upload',
12
+ parameters: {
13
+ file1: { type: 'file' },
14
+ file2: { type: 'file' }
15
+ }
16
+ },
17
+ {
18
+ route: '/test1',
19
+ parameters: {
20
+ name: { type: 'string' }
21
+ }
22
+ },
23
+ {
24
+ route: '/test2'
25
+ }
26
+ ]
27
+
28
+ describe('fileParser 中间件', () => {
29
+ let app
30
+ let server
31
+ let baseURL
32
+
33
+ beforeEach(done => {
34
+ app = express()
35
+
36
+ app.post('/upload', fileParser(apis[0]), async (req, res) => {
37
+ if (req.files) {
38
+ for (const field in req.files) {
39
+ const files = req.files[field]
40
+ // load file
41
+ if (files[0].path) files[0].data = await fse.readFile(files[0].path, 'utf-8')
42
+ }
43
+ res.status(200).send(req.files)
44
+ } else {
45
+ res.status(400).send('No files uploaded')
46
+ }
47
+ })
48
+
49
+ app.post('/test1', fileParser(apis[1]), (req, res) => {
50
+ if (!req.files) {
51
+ res.status(400).send('No files uploaded')
52
+ } else {
53
+ res.status(200).send('Files uploaded')
54
+ }
55
+ })
56
+ app.post('/test2', fileParser(apis[2]), (req, res) => {
57
+ if (!req.files) {
58
+ res.status(400).send('No files uploaded')
59
+ } else {
60
+ res.status(200).send('Files uploaded')
61
+ }
62
+ })
63
+
64
+ server = http.createServer(app).listen(() => {
65
+ const { port } = server.address()
66
+ baseURL = `http://localhost:${port}`
67
+ done()
68
+ })
69
+ })
70
+
71
+ afterEach(() => {
72
+ server.close()
73
+ })
74
+
75
+ it('应该处理已定义路由的文件上传', async () => {
76
+ const formData = new FormData()
77
+ formData.append('file1', Buffer.from('test file 1'), 'file1.txt')
78
+ formData.append('file2', Buffer.from('test file 2'), 'file2.txt')
79
+
80
+ let response
81
+ try {
82
+ response = await axios.post(`${baseURL}/upload`, formData, {
83
+ headers: formData.getHeaders()
84
+ })
85
+ } catch (e) {
86
+ console.log(e.response.status)
87
+ console.log(e.response.statusMessage)
88
+ console.log(e.response.data)
89
+ }
90
+
91
+ expect(response.status).toBe(200)
92
+ expect(Object.keys(response.data)).toEqual(['file1', 'file2'])
93
+ })
94
+
95
+ it('应该返回400当没有文件上传到/test', async () => {
96
+ let response
97
+ try {
98
+ response = await axios.post(
99
+ `${baseURL}/test1`,
100
+ {},
101
+ {
102
+ headers: { 'Content-Type': 'application/json' }
103
+ }
104
+ )
105
+ } catch (e) {
106
+ response = e.response
107
+ }
108
+
109
+ expect(response.status).toBe(400)
110
+ expect(response.data).toBe('No files uploaded')
111
+ })
112
+
113
+ it('测试无parameters参数情况', async () => {
114
+ let response
115
+ try {
116
+ response = await axios.post(
117
+ `${baseURL}/test2`,
118
+ {},
119
+ {
120
+ headers: { 'Content-Type': 'application/json' }
121
+ }
122
+ )
123
+ } catch (e) {
124
+ response = e.response
125
+ }
126
+
127
+ expect(response.status).toBe(400)
128
+ expect(response.data).toBe('No files uploaded')
129
+ })
130
+
131
+ it('应该在请求结束后清理上传的文件', async () => {
132
+ const formData = new FormData()
133
+ formData.append('file1', Buffer.from('test file 1'), 'file1.txt')
134
+ formData.append('file2', Buffer.from('test file 2'), 'file2.txt')
135
+
136
+ let response
137
+ try {
138
+ response = await axios.post(`${baseURL}/upload`, formData, {
139
+ headers: formData.getHeaders()
140
+ })
141
+ } catch (e) {
142
+ console.log(e.response.status)
143
+ console.log(e.response.statusMessage)
144
+ console.log(e.response.data)
145
+ }
146
+
147
+ expect(response.status).toBe(200)
148
+ const files = response.data
149
+ expect(files.file1[0].data).toEqual('test file 1')
150
+
151
+ expect(Object.keys(files)).toEqual(['file1', 'file2'])
152
+
153
+ // Check if files are cleaned up
154
+ let existsFile = true
155
+ for (let retries = 0; retries < 5 && existsFile; retries++) {
156
+ existsFile = await fse.exists(files.file1[0].path)
157
+ if (existsFile) {
158
+ await new Promise(resolve => setTimeout(resolve, 100))
159
+ }
160
+ }
161
+ expect(existsFile).toBeFalsy()
162
+ })
163
+ })
@@ -0,0 +1,12 @@
1
+ import bodyParser from 'body-parser'
2
+ import fileParser from './fileParser.js'
3
+ import mergeData from './mergeData.js'
4
+
5
+ const basicParsers = [
6
+ bodyParser.urlencoded({ extended: false }),
7
+ bodyParser.json(),
8
+ bodyParser.text()
9
+ ]
10
+ export default api => {
11
+ return [...basicParsers, ...fileParser(api), mergeData]
12
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'
2
+ import bodyMiddleware from './index.js'
3
+ import axios from 'axios'
4
+ import express from 'express'
5
+ import http from 'http'
6
+ import FormData from 'form-data'
7
+
8
+ const api = {
9
+ route: '/upload',
10
+ parameters: {
11
+ name: { type: 'string' },
12
+ file: { type: 'file' }
13
+ }
14
+ }
15
+
16
+ // 修改describe和it的描述为中文
17
+ describe('bodyMiddleware', () => {
18
+ let app
19
+ let server
20
+ let baseURL
21
+
22
+ beforeEach(done => {
23
+ app = express()
24
+ app.use(express.json())
25
+ app.post(api.route, bodyMiddleware(api), (req, res) => {
26
+ res.status(200).send(req.data)
27
+ })
28
+
29
+ server = http.createServer(app).listen(() => {
30
+ const { port } = server.address()
31
+ baseURL = `http://localhost:${port}`
32
+ done()
33
+ })
34
+ })
35
+
36
+ afterEach(() => {
37
+ server.close()
38
+ })
39
+
40
+ it('应该正确加载所有中间件', () => {
41
+ const middlewares = bodyMiddleware(api)
42
+ expect(middlewares).toHaveLength(6)
43
+ })
44
+
45
+ it('应该正确处理文件上传', async () => {
46
+ const formData = new FormData()
47
+ formData.append('name', 'test')
48
+ formData.append('file', Buffer.from('test file'), 'file.txt')
49
+
50
+ let response
51
+ try {
52
+ response = await axios.post(`${baseURL}${api.route}`, formData, {
53
+ headers: formData.getHeaders()
54
+ })
55
+ } catch (e) {
56
+ response = e.response
57
+ }
58
+
59
+ expect(response.status).toBe(200)
60
+ expect(response.data.name).toBe('test')
61
+ expect(response.data.file).toBeDefined()
62
+ expect(response.data.file[0].name).toBe('file.txt')
63
+ })
64
+ })
@@ -0,0 +1,4 @@
1
+ export default (req, res, next) => {
2
+ req.data = { ...req.params, ...req.query, ...req.body, ...req.files }
3
+ next()
4
+ }
@@ -0,0 +1,38 @@
1
+ import mergeData from './mergeData'
2
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals'
3
+
4
+ describe('mergeData middleware', () => {
5
+ let req, res, next
6
+
7
+ beforeEach(() => {
8
+ req = {
9
+ params: { param1: 'value1' },
10
+ query: { query1: 'value2' },
11
+ body: { body1: 'value3' },
12
+ files: { file1: 'value4' }
13
+ }
14
+ res = {}
15
+ next = jest.fn()
16
+ })
17
+
18
+ it('应该将 params、query、body 和 files 合并到 req.data 中', () => {
19
+ mergeData(req, res, next)
20
+
21
+ expect(req.data).toEqual({
22
+ param1: 'value1',
23
+ query1: 'value2',
24
+ body1: 'value3',
25
+ file1: 'value4'
26
+ })
27
+ expect(next).toHaveBeenCalled()
28
+ })
29
+
30
+ it('应该处理空的 params、query、body 和 files', () => {
31
+ req = { params: {}, query: {}, body: {}, files: {} }
32
+
33
+ mergeData(req, res, next)
34
+
35
+ expect(req.data).toEqual({})
36
+ expect(next).toHaveBeenCalled()
37
+ })
38
+ })
@@ -0,0 +1,82 @@
1
+ import getSystemLanguage from '../../system/getSystemLanguage.js'
2
+ import { convert, registerAll } from '../../i18n/index.js'
3
+ import getLogger from '../../logger/index.js'
4
+ import getError from '../../utils/getError.js'
5
+ import fse from 'fs-extra'
6
+
7
+ let sequence = 0
8
+
9
+ export default async (app, root) => {
10
+ root = root || process.cwd() + '/i18n'
11
+ const logPath = process.env.LOG_PATH || process.cwd() + '/tmp/logs/main.log'
12
+ const systemLanguage = await getSystemLanguage()
13
+ if (await fse.exists(root)) {
14
+ await registerAll(root)
15
+ }
16
+
17
+ app.namespace = namespace => {
18
+ const transform = (namespace, code, data) => {
19
+ return convert(namespace, systemLanguage, code, data)
20
+ }
21
+ const i18n = (code, data) => {
22
+ return transform(namespace, code, data)
23
+ }
24
+ const logger = getLogger({
25
+ transform,
26
+ namespace,
27
+ id: '',
28
+ path: logPath
29
+ })
30
+ const Error = getError({
31
+ namespace,
32
+ transform
33
+ })
34
+ return {
35
+ i18n,
36
+ logger,
37
+ Error
38
+ }
39
+ }
40
+
41
+ const appNamespace = app.namespace('app')
42
+ app.i18n = appNamespace.i18n
43
+ app.logger = appNamespace.logger
44
+ app.Error = appNamespace.Error
45
+
46
+ app.use((req, res, next) => {
47
+ req.sequence = sequence++
48
+ req.id = `ANONYMOUS-${req.sequence}`
49
+ const userLanguage = req.headers['accept-language'] || systemLanguage
50
+
51
+ req.namespace = namespace => {
52
+ const transform = (namespace, code, data) => {
53
+ return convert(namespace, userLanguage, code, data)
54
+ }
55
+ const i18n = (code, data) => {
56
+ return transform(namespace, code, data)
57
+ }
58
+ const logger = getLogger({
59
+ transform,
60
+ namespace,
61
+ id: req.id,
62
+ path: logPath
63
+ })
64
+ const Error = getError({
65
+ namespace,
66
+ transform
67
+ })
68
+ return {
69
+ i18n,
70
+ logger,
71
+ Error
72
+ }
73
+ }
74
+
75
+ const reqNamespace = req.namespace('req')
76
+ req.i18n = reqNamespace.i18n
77
+ req.logger = reqNamespace.logger
78
+ req.Error = reqNamespace.Error
79
+
80
+ next()
81
+ })
82
+ }
@@ -0,0 +1,75 @@
1
+ import { describe, it, beforeAll, afterAll, expect, beforeEach } from '@jest/globals'
2
+ import axios from 'axios'
3
+ import http from 'http'
4
+ import express from 'express'
5
+ import i18nMiddleware from './index.js'
6
+
7
+ describe('i18nMiddleware', () => {
8
+ let app
9
+ let server
10
+ let baseURL
11
+
12
+ beforeAll(done => {
13
+ app = express()
14
+ server = http.createServer(app)
15
+ server.listen(() => {
16
+ const { port } = server.address()
17
+ baseURL = `http://localhost:${port}`
18
+ done()
19
+ })
20
+ })
21
+
22
+ afterAll(done => {
23
+ server.close(done)
24
+ })
25
+
26
+ beforeEach(async () => {
27
+ process.env.LANGUAGE = 'en-US'
28
+ await i18nMiddleware(app)
29
+ })
30
+
31
+ it('应该设置 app.i18n、app.logger 和 app.Error', () => {
32
+ expect(app.i18n).toBeDefined()
33
+ expect(app.logger).toBeDefined()
34
+ expect(app.Error).toBeDefined()
35
+ const { i18n, logger, Error } = app.namespace('sumor_internal')
36
+ expect(i18n).toBeDefined()
37
+ expect(logger).toBeDefined()
38
+ expect(Error).toBeDefined()
39
+ const message = i18n('TEST', { value: 'value' })
40
+ expect(message).toBeDefined() // 根据你的实现添加具体的期望
41
+ expect(message).toBe('Test value')
42
+ })
43
+
44
+ it('应该为每个请求设置 req.i18n、req.logger 和 req.Error', async () => {
45
+ app.use((req, res) => {
46
+ expect(req.i18n).toBeDefined()
47
+ expect(req.logger).toBeDefined()
48
+ expect(req.Error).toBeDefined()
49
+ const { i18n, logger, Error } = req.namespace('sumor_internal')
50
+ expect(i18n).toBeDefined()
51
+ expect(logger).toBeDefined()
52
+ expect(Error).toBeDefined()
53
+ const message = i18n('TEST', { value: 'value' })
54
+ expect(message).toBeDefined() // 根据你的实现添加具体的期望
55
+ expect(message).toBe('Test value')
56
+ res.status(200).send('OK')
57
+ })
58
+
59
+ const response = await axios.get(`${baseURL}/`)
60
+ expect(response.status).toBe(200)
61
+ })
62
+
63
+ it('应该从请求头中使用正确的用户语言', async () => {
64
+ app.use((req, res) => {
65
+ const i18nMessage = req.i18n('testNamespace', 'testCode', { key: 'value' })
66
+ expect(i18nMessage).toBeDefined() // 根据你的实现添加具体的期望
67
+ res.status(200).send('OK')
68
+ })
69
+
70
+ const response = await axios.get(`${baseURL}/`, {
71
+ headers: { 'Accept-Language': 'en-US' }
72
+ })
73
+ expect(response.status).toBe(200)
74
+ })
75
+ })
@@ -0,0 +1,115 @@
1
+ import type from '../../utils/type.js'
2
+
3
+ export default class Token {
4
+ constructor(req, onChange) {
5
+ this._id = null
6
+ this._user = null
7
+ this._data = null
8
+ this._permission = {}
9
+
10
+ this._onChange = function () {
11
+ if (this._user) {
12
+ req.id = `${this._user}-${req.sequence}`
13
+ } else {
14
+ req.id = `ANONYMOUS-${req.sequence}`
15
+ }
16
+ const reqNamespace = req.namespace('req')
17
+ req.i18n = reqNamespace.i18n
18
+ req.logger = reqNamespace.logger
19
+ req.Error = reqNamespace.Error
20
+ if (onChange) {
21
+ onChange(this.id)
22
+ }
23
+ }
24
+ this._Error = req.namespace('SUMOR_TOKEN').Error
25
+
26
+ this._onChange()
27
+ }
28
+
29
+ get id() {
30
+ return this._id || ''
31
+ }
32
+
33
+ set id(id) {
34
+ this._id = id
35
+ this._onChange()
36
+ }
37
+
38
+ get data() {
39
+ return this._data || {}
40
+ }
41
+
42
+ set data(val) {
43
+ this._data = Object.assign({}, val)
44
+ this._onChange()
45
+ }
46
+
47
+ get user() {
48
+ return this._user || ''
49
+ }
50
+
51
+ set user(user) {
52
+ this._user = user
53
+ this._onChange()
54
+ }
55
+
56
+ get permission() {
57
+ return this._permission
58
+ }
59
+
60
+ set permission(val) {
61
+ const result = {}
62
+ if (typeof val === 'string') {
63
+ result[val] = []
64
+ } else if (type(val) === 'array') {
65
+ for (const item of val) {
66
+ result[item] = []
67
+ }
68
+ } else if (type(val) === 'object') {
69
+ for (const key in val) {
70
+ if (type(val[key]) === 'array') {
71
+ result[key] = val[key]
72
+ } else if (type(val[key]) === 'string') {
73
+ result[key] = [val[key]]
74
+ } else {
75
+ result[key] = []
76
+ }
77
+ }
78
+ }
79
+ this._permission = result
80
+ this._onChange()
81
+ }
82
+
83
+ has(key, value) {
84
+ let matched = false
85
+ if (this._permission[key]) {
86
+ if (value) {
87
+ if (this._permission[key].indexOf(value) >= 0) {
88
+ matched = true
89
+ }
90
+ } else {
91
+ matched = true
92
+ }
93
+ }
94
+ return matched
95
+ }
96
+
97
+ check(key, value) {
98
+ if (!this.user) {
99
+ // Check if the user is logged in
100
+ throw new this._Error('LOGIN_EXPIRED')
101
+ } else if (key) {
102
+ // Check if the user has the required permission
103
+ const lacked = !this.has(key, value)
104
+ if (lacked) {
105
+ const permission = value ? `${key}=${value}` : key
106
+ throw new this._Error('PERMISSION_DENIED', { permission })
107
+ }
108
+ }
109
+ }
110
+
111
+ destroy() {
112
+ this._id = null
113
+ this._onChange()
114
+ }
115
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, test, expect, beforeEach, jest } from '@jest/globals'
2
+ import Token from './Token'
3
+
4
+ describe('Token 类测试', () => {
5
+ let mockReq
6
+ let token
7
+
8
+ beforeEach(() => {
9
+ mockReq = {
10
+ id: '',
11
+ namespace: jest.fn().mockReturnValue({
12
+ i18n: {},
13
+ logger: {},
14
+ Error: class MockError extends Error {
15
+ constructor(code, details) {
16
+ super(code)
17
+ this.code = code
18
+ this.details = details
19
+ }
20
+ }
21
+ })
22
+ }
23
+ token = new Token(mockReq)
24
+ })
25
+
26
+ test('初始化 Token', () => {
27
+ expect(token.id).toBe('')
28
+ expect(token.user).toBe('')
29
+ expect(token.data).toEqual({})
30
+ expect(token.permission).toEqual({})
31
+ })
32
+
33
+ test('设置和获取 id', () => {
34
+ token.id = '123'
35
+ expect(token.id).toBe('123')
36
+ })
37
+
38
+ test('设置和获取 user', () => {
39
+ token.user = 'testUser'
40
+ expect(token.user).toBe('testUser')
41
+ expect(mockReq.id).toContain('testUser-')
42
+ })
43
+
44
+ test('设置和获取 data', () => {
45
+ token.data = { key: 'value' }
46
+ expect(token.data).toEqual({ key: 'value' })
47
+ })
48
+
49
+ test('设置和获取 permission', () => {
50
+ token.permission = { read: ['file1'], write: 'file2' }
51
+ expect(token.permission).toEqual({ read: ['file1'], write: ['file2'] })
52
+ })
53
+
54
+ test('检查权限', () => {
55
+ token.user = 'testUser'
56
+ token.permission = { read: ['file1'] }
57
+
58
+ expect(() => token.check('read', 'file1')).not.toThrow()
59
+ expect(() => token.check('write')).toThrowError('PERMISSION_DENIED')
60
+ })
61
+
62
+ test('销毁 Token', () => {
63
+ token.destroy()
64
+ expect(token.id).toBe('')
65
+ expect(mockReq.id).toContain('ANONYMOUS-')
66
+ })
67
+ })