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.
- package/.eslintignore +6 -0
- package/.eslintrc.yml +4 -0
- package/.gitattributes +1 -0
- package/.github/workflows/audit.yml +22 -0
- package/.github/workflows/available.yml +22 -0
- package/.github/workflows/ci.yml +45 -0
- package/.github/workflows/coverage.yml +23 -0
- package/.github/workflows/publish.yml +31 -0
- package/.github/workflows/ut.yml +24 -0
- package/.husky/pre-commit +10 -0
- package/.husky/pre-push +10 -0
- package/.prettierrc.yml +7 -0
- package/LICENSE +21 -0
- package/demo/api/error.js +3 -0
- package/demo/api/file.js +4 -0
- package/demo/api/file.yaml +11 -0
- package/demo/api/hello.js +4 -0
- package/demo/api/hello.yaml +13 -0
- package/demo/api/login.js +4 -0
- package/demo/api/logout.js +4 -0
- package/demo/api/long.js +9 -0
- package/demo/api/long.yaml +2 -0
- package/demo/api/user/admin.js +12 -0
- package/demo/api/user/info.js +8 -0
- package/demo/config/config.yaml +2 -0
- package/demo/i18n/demo.yaml +1 -0
- package/demo/package.json +6 -0
- package/i18n/README.md +32 -0
- package/i18n/sumor_api.yaml +16 -0
- package/i18n/sumor_app.yaml +3 -0
- package/i18n/sumor_internal.yaml +1 -0
- package/i18n/sumor_token.yaml +2 -0
- package/i18nExt/sumor_api.de.yaml +3 -0
- package/i18nExt/sumor_api.en.yaml +3 -0
- package/i18nExt/sumor_api.es.yaml +3 -0
- package/i18nExt/sumor_api.fr.yaml +3 -0
- package/i18nExt/sumor_api.it.yaml +3 -0
- package/i18nExt/sumor_api.ja.yaml +3 -0
- package/i18nExt/sumor_api.ko.yaml +3 -0
- package/i18nExt/sumor_api.pt.yaml +3 -0
- package/i18nExt/sumor_api.zh-TW.yaml +3 -0
- package/i18nExt/sumor_api.zh.yaml +3 -0
- package/i18nExt/sumor_app.de.yaml +3 -0
- package/i18nExt/sumor_app.en.yaml +3 -0
- package/i18nExt/sumor_app.es.yaml +3 -0
- package/i18nExt/sumor_app.zh-TW.yaml +3 -0
- package/i18nExt/sumor_app.zh.yaml +3 -0
- package/i18nExt/sumor_internal.de.yaml +1 -0
- package/i18nExt/sumor_internal.en.yaml +1 -0
- package/i18nExt/sumor_internal.es.yaml +1 -0
- package/i18nExt/sumor_internal.zh-TW.yaml +1 -0
- package/i18nExt/sumor_internal.zh.yaml +1 -0
- package/i18nExt/sumor_token.de.yaml +2 -0
- package/i18nExt/sumor_token.es.yaml +2 -0
- package/i18nExt/sumor_token.fr.yaml +2 -0
- package/i18nExt/sumor_token.it.yaml +2 -0
- package/i18nExt/sumor_token.ja.yaml +2 -0
- package/i18nExt/sumor_token.ko.yaml +2 -0
- package/i18nExt/sumor_token.pt.yaml +2 -0
- package/i18nExt/sumor_token.ru.yaml +2 -0
- package/i18nExt/sumor_token.tr.yaml +2 -0
- package/i18nExt/sumor_token.zh-TW.yaml +2 -0
- package/i18nExt/sumor_token.zh.yaml +2 -0
- package/jest.config.json +26 -0
- package/modules/alertPage/html/template.html +162 -0
- package/modules/alertPage/html/undraw_access-denied.svg +52 -0
- package/modules/alertPage/html/undraw_alert.svg +1 -0
- package/modules/alertPage/html/undraw_celebration.svg +78 -0
- package/modules/alertPage/html/undraw_complete-form.svg +1 -0
- package/modules/alertPage/html/undraw_login.svg +1 -0
- package/modules/alertPage/index.js +46 -0
- package/modules/alertPage/index.test.js +12 -0
- package/modules/i18n/README.md +90 -0
- package/modules/i18n/convertI18nValue/README.md +31 -0
- package/modules/i18n/convertI18nValue/getI18nTemplate.js +26 -0
- package/modules/i18n/convertI18nValue/getI18nTemplate.test.js +40 -0
- package/modules/i18n/convertI18nValue/index.js +14 -0
- package/modules/i18n/convertI18nValue/index.test.js +55 -0
- package/modules/i18n/convertI18nValue/stringVariableReplace.js +13 -0
- package/modules/i18n/convertI18nValue/stringVariableReplace.test.js +39 -0
- package/modules/i18n/index.js +26 -0
- package/modules/i18n/index.test.js +13 -0
- package/modules/i18n/load/README.md +28 -0
- package/modules/i18n/load/load.js +31 -0
- package/modules/i18n/load/load.test.js +30 -0
- package/modules/i18n/registry.js +48 -0
- package/modules/i18n/registry.test.js +84 -0
- package/modules/logger/convert/parseFile.js +14 -0
- package/modules/logger/convert/parseFile.test.js +28 -0
- package/modules/logger/convert/stringifyCMD.js +48 -0
- package/modules/logger/convert/stringifyCMD.test.js +37 -0
- package/modules/logger/convert/stringifyFile.js +24 -0
- package/modules/logger/convert/stringifyFile.test.js +79 -0
- package/modules/logger/index.js +82 -0
- package/modules/logger/index.test.js +124 -0
- package/modules/logger/logFileOperator.js +50 -0
- package/modules/logger/logFileOperator.test.js +69 -0
- package/modules/middlewares/apiMiddleware/errorCatcher.js +9 -0
- package/modules/middlewares/apiMiddleware/exposeApis/index.js +82 -0
- package/modules/middlewares/apiMiddleware/index.js +111 -0
- package/modules/middlewares/apiMiddleware/index.test.js +145 -0
- package/modules/middlewares/apiMiddleware/load/index.js +35 -0
- package/modules/middlewares/apiMiddleware/load/index.test.js +30 -0
- package/modules/middlewares/apiMiddleware/metadataToSwagger.js +139 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.js +26 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/caseSensitive.test.js +36 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/convertType.js +48 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/convertType.test.js +53 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.js +31 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/defaultValue.test.js +31 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/index.js +18 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/index.test.js +40 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/precision.js +12 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/precision.test.js +33 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/trim.js +15 -0
- package/modules/middlewares/apiMiddleware/prepareData/format/trim.test.js +24 -0
- package/modules/middlewares/apiMiddleware/prepareData/index.js +29 -0
- package/modules/middlewares/apiMiddleware/prepareData/index.test.js +121 -0
- package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.js +26 -0
- package/modules/middlewares/apiMiddleware/prepareData/validate/checkLength.test.js +52 -0
- package/modules/middlewares/apiMiddleware/prepareData/validate/index.js +30 -0
- package/modules/middlewares/apiMiddleware/public/favicon.ico +0 -0
- package/modules/middlewares/apiMiddleware/response/sendError.js +57 -0
- package/modules/middlewares/apiMiddleware/response/sendError.test.js +251 -0
- package/modules/middlewares/apiMiddleware/response/sendNotFound.js +26 -0
- package/modules/middlewares/apiMiddleware/response/sendResponse.js +25 -0
- package/modules/middlewares/apiMiddleware/response/sendSuccess.js +30 -0
- package/modules/middlewares/bodyMiddleware/cleanupFiles.js +14 -0
- package/modules/middlewares/bodyMiddleware/cleanupFiles.test.js +54 -0
- package/modules/middlewares/bodyMiddleware/fileParser.js +69 -0
- package/modules/middlewares/bodyMiddleware/fileParser.test.js +163 -0
- package/modules/middlewares/bodyMiddleware/index.js +12 -0
- package/modules/middlewares/bodyMiddleware/index.test.js +64 -0
- package/modules/middlewares/bodyMiddleware/mergeData.js +4 -0
- package/modules/middlewares/bodyMiddleware/mergeData.test.js +38 -0
- package/modules/middlewares/i18nMiddleware/index.js +82 -0
- package/modules/middlewares/i18nMiddleware/index.test.js +75 -0
- package/modules/middlewares/tokenMiddleware/Token.js +115 -0
- package/modules/middlewares/tokenMiddleware/Token.test.js +67 -0
- package/modules/middlewares/tokenMiddleware/index.js +32 -0
- package/modules/middlewares/tokenMiddleware/index.test.js +115 -0
- package/modules/middlewares/tokenMiddleware/parseCookie.js +24 -0
- package/modules/middlewares/tokenMiddleware/parseCookie.test.js +49 -0
- package/modules/serve/formatConfig.js +7 -0
- package/modules/serve/formatConfig.test.js +28 -0
- package/modules/serve/index.js +30 -0
- package/modules/serve/index.test.js +69 -0
- package/modules/serve/listenApp.js +11 -0
- package/modules/system/getSystemLanguage.js +9 -0
- package/modules/system/getSystemLanguage.test.js +19 -0
- package/modules/utils/getError.js +56 -0
- package/modules/utils/getError.test.js +97 -0
- package/modules/utils/loadConfig.js +73 -0
- package/modules/utils/loadConfig.test.js +129 -0
- package/modules/utils/pathUtils.js +43 -0
- package/modules/utils/pathUtils.test.js +52 -0
- package/modules/utils/type.js +14 -0
- package/modules/utils/type.test.js +40 -0
- package/package.json +61 -1
- package/src/app.js +40 -0
- package/src/cli.js +28 -0
- package/src/index.js +3 -0
- package/test-utils/codeTest/README.md +4 -0
- package/test-utils/codeTest/custom.css +47 -0
- package/test-utils/codeTest/index.js +80 -0
- package/test-utils/codeTest/utils/calculateCoverage.js +55 -0
- package/test-utils/codeTest/utils/getTestFiles.js +20 -0
- package/test-utils/codeTest/utils/runTests.js +38 -0
- package/test-utils/codeTest/utils/startServer.js +23 -0
- package/test-utils/tmp.js +15 -0
- package/cli.js +0 -3
- package/index.es.js +0 -578
- package/template/web/AppWithFrame.vue +0 -20
- package/template/web/index.html +0 -19
- package/template/web/src/App.vue +0 -15
- package/template/web/src/entry-client.js +0 -9
- package/template/web/src/entry-server.js +0 -69
- package/template/web/src/main.js +0 -41
- 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,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
|
+
})
|