koa3-cli 1.0.5 → 1.0.7

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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 全局错误处理中间件:捕获异常、记录日志、统一错误响应
3
+ */
4
+ module.exports = function createErrorHandler(config, logger) {
5
+ return async function errorHandler(ctx, next) {
6
+ try {
7
+ await next();
8
+ } catch (err) {
9
+ ctx.status = err.status || 500;
10
+ const firstDetailMessage = err.details && err.details[0] && err.details[0].message;
11
+ ctx.body = {
12
+ success: false,
13
+ message: firstDetailMessage || err.message || 'Internal Server Error',
14
+ ...(err.details && { errors: err.details })
15
+ };
16
+
17
+ logger.error('Request failed', {
18
+ requestId: ctx.state && ctx.state.requestId,
19
+ method: ctx.method,
20
+ url: ctx.originalUrl || ctx.url,
21
+ status: ctx.status,
22
+ message: err.message,
23
+ stack: err.stack
24
+ });
25
+
26
+ ctx.app.emit('error', err, ctx);
27
+ }
28
+ };
29
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 404 兜底中间件:未匹配路由时返回统一格式
3
+ */
4
+ module.exports = async function notFound(ctx) {
5
+ if (ctx.status === 404) {
6
+ ctx.body = {
7
+ success: false,
8
+ message: 'Not Found'
9
+ };
10
+ }
11
+ };
@@ -1,27 +1,27 @@
1
- function createRequestId() {
2
- return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3
- }
4
-
5
- module.exports = function requestLogger(logger) {
6
- return async function requestLoggerMiddleware(ctx, next) {
7
- const start = Date.now();
8
- const requestId = ctx.get('x-request-id') || createRequestId();
9
-
10
- ctx.state.requestId = requestId;
11
- ctx.set('x-request-id', requestId);
12
-
13
- try {
14
- await next();
15
- } finally {
16
- const duration = Date.now() - start;
17
- logger.access({
18
- requestId,
19
- method: ctx.method,
20
- url: ctx.originalUrl || ctx.url,
21
- status: ctx.status,
22
- duration,
23
- ip: ctx.ip
24
- });
25
- }
26
- };
27
- };
1
+ function createRequestId() {
2
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
3
+ }
4
+
5
+ module.exports = function requestLogger(logger) {
6
+ return async function requestLoggerMiddleware(ctx, next) {
7
+ const start = Date.now();
8
+ const requestId = ctx.get('x-request-id') || createRequestId();
9
+
10
+ ctx.state.requestId = requestId;
11
+ ctx.set('x-request-id', requestId);
12
+
13
+ try {
14
+ await next();
15
+ } finally {
16
+ const duration = Date.now() - start;
17
+ logger.access({
18
+ requestId,
19
+ method: ctx.method,
20
+ url: ctx.originalUrl || ctx.url,
21
+ status: ctx.status,
22
+ duration,
23
+ ip: ctx.ip
24
+ });
25
+ }
26
+ };
27
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 进程级错误监听:未捕获的 Promise 与异常
3
+ */
4
+ function setupProcessEvents(logger) {
5
+ process.on('unhandledRejection', (reason) => {
6
+ logger.error('Unhandled promise rejection', reason);
7
+ });
8
+
9
+ process.on('uncaughtException', (error) => {
10
+ logger.error('Uncaught exception', error);
11
+ });
12
+ }
13
+
14
+ module.exports = setupProcessEvents;
package/app/router.js CHANGED
@@ -1,4 +1,7 @@
1
1
  const { Router } = require('@koa/router');
2
+ const { validate } = require('./lib/validator');
3
+ const userSchema = require('./schema/user');
4
+
2
5
  const router = new Router();
3
6
 
4
7
  // 加载控制器
@@ -11,9 +14,9 @@ router.get('/', homeController.index);
11
14
 
12
15
  // 用户相关路由
13
16
  router.get('/api/user', userController.list);
14
- router.get('/api/user/:id', userController.detail);
15
- router.post('/api/user', userController.create);
16
- router.put('/api/user/:id', userController.update);
17
- router.delete('/api/user/:id', userController.delete);
17
+ router.get('/api/user/:id', validate({ params: userSchema.idParam }), userController.detail);
18
+ router.post('/api/user', validate({ body: userSchema.createUserBody }), userController.create);
19
+ router.put('/api/user/:id', validate({ params: userSchema.idParam, body: userSchema.updateUserBody }), userController.update);
20
+ router.delete('/api/user/:id', validate({ params: userSchema.idParam }), userController.delete);
18
21
 
19
22
  module.exports = router;
@@ -0,0 +1,29 @@
1
+ const { Joi } = require('../lib/validator');
2
+
3
+ const idParam = Joi.object({
4
+ id: Joi.string().required().messages({ 'any.required': '用户 id 不能为空' })
5
+ });
6
+
7
+ const createUserBody = Joi.object({
8
+ name: Joi.string().trim().min(1).max(100).required().messages({
9
+ 'any.required': '用户名为必填',
10
+ 'string.empty': '用户名不能为空',
11
+ 'string.max': '用户名不能超过 100 个字符'
12
+ }),
13
+ email: Joi.string().email().allow('').optional(),
14
+ age: Joi.number().integer().min(0).max(150).optional()
15
+ }).options({ stripUnknown: true });
16
+
17
+ const updateUserBody = Joi.object({
18
+ name: Joi.string().trim().min(1).max(100).optional(),
19
+ email: Joi.string().email().allow('').optional(),
20
+ age: Joi.number().integer().min(0).max(150).optional()
21
+ }).min(1).messages({
22
+ 'object.min': '至少需要提供一个要更新的字段'
23
+ }).options({ stripUnknown: true });
24
+
25
+ module.exports = {
26
+ idParam,
27
+ createUserBody,
28
+ updateUserBody
29
+ };
package/app/setup.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 应用挂载:静态资源、视图、中间件、路由等
3
+ * 保持 app.js 只做「创建实例 + 调用 setup + 监听 + 启动」
4
+ */
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const bodyParser = require('koa-bodyparser');
8
+ const serveStatic = require('koa-static');
9
+ const views = require('@ladjs/koa-views');
10
+
11
+ const createRequestLogger = require('./middleware/requestLogger');
12
+ const createErrorHandler = require('./middleware/errorHandler');
13
+ const notFound = require('./middleware/notFound');
14
+ const middleware = require('./middleware');
15
+ const router = require('./router');
16
+
17
+ const rootDir = path.join(__dirname, '..');
18
+
19
+ function setup(app, config, logger) {
20
+ // 静态资源
21
+ if (config.static && config.static.enable !== false) {
22
+ const staticPath = path.join(rootDir, config.static.dir || 'public');
23
+ if (fs.existsSync(staticPath)) {
24
+ app.use(serveStatic(staticPath, config.static.options || {}));
25
+ }
26
+ }
27
+
28
+ // 视图
29
+ if (config.view && config.view.enable !== false) {
30
+ const viewPath = path.join(rootDir, config.view.root || 'app/view');
31
+ if (fs.existsSync(viewPath)) {
32
+ app.use(views(viewPath, config.view.options || { extension: 'ejs' }));
33
+ }
34
+ }
35
+
36
+ app.use(bodyParser(config.bodyParser || {}));
37
+ app.use(createRequestLogger(logger));
38
+
39
+ if (middleware && typeof middleware === 'function') {
40
+ app.use(middleware);
41
+ }
42
+
43
+ app.use(createErrorHandler(config, logger));
44
+ app.use(router.routes()).use(router.allowedMethods());
45
+ app.use(notFound);
46
+ }
47
+
48
+ module.exports = setup;
package/app.js CHANGED
@@ -1,119 +1,38 @@
1
- const Koa = require('koa');
2
- const bodyParser = require('koa-bodyparser');
3
- const serveStatic = require('koa-static');
4
- const views = require('@ladjs/koa-views');
5
- const path = require('path');
6
- const fs = require('fs');
7
- const { createLogger } = require('./app/lib/logger');
8
- const createRequestLogger = require('./app/middleware/requestLogger');
9
-
10
- require('dotenv').config();
11
-
12
- const env = process.env.NODE_ENV || 'development';
13
- const defaultConfig = require('./config/config.default');
14
- let envConfig = {};
15
- try {
16
- if (env === 'production') {
17
- envConfig = require('./config/config.prod');
18
- } else if (env === 'local' || env === 'development') {
19
- envConfig = require('./config/config.local');
20
- }
21
- } catch (e) {
22
- // Ignore missing env config override.
23
- }
24
- const config = Object.assign({}, defaultConfig, envConfig);
25
-
26
- const middleware = require('./app/middleware');
27
- const router = require('./app/router');
28
-
29
- const app = new Koa();
30
- const logger = createLogger({
31
- ...(config.logger || {}),
32
- appName: config.name || 'koa3-cli',
33
- cwd: __dirname
34
- });
35
-
36
- app.keys = config.keys || ['koa3-cli-secret-key'];
37
- app.context.logger = logger;
38
-
39
- if (config.static && config.static.enable !== false) {
40
- const staticPath = path.join(__dirname, config.static.dir || 'public');
41
- if (fs.existsSync(staticPath)) {
42
- app.use(serveStatic(staticPath, config.static.options || {}));
43
- }
44
- }
45
-
46
- if (config.view && config.view.enable !== false) {
47
- const viewPath = path.join(__dirname, config.view.root || 'app/view');
48
- if (fs.existsSync(viewPath)) {
49
- app.use(views(viewPath, config.view.options || {
50
- extension: 'ejs'
51
- }));
52
- }
53
- }
54
-
55
- app.use(bodyParser(config.bodyParser || {}));
56
- app.use(createRequestLogger(logger));
57
-
58
- if (middleware && typeof middleware === 'function') {
59
- app.use(middleware);
60
- }
61
-
62
- app.use(async (ctx, next) => {
63
- try {
64
- await next();
65
- } catch (err) {
66
- ctx.status = err.status || 500;
67
- ctx.body = {
68
- success: false,
69
- message: err.message || 'Internal Server Error',
70
- ...(config.env === 'development' && { stack: err.stack })
71
- };
72
-
73
- logger.error('Request failed', {
74
- requestId: ctx.state && ctx.state.requestId,
75
- method: ctx.method,
76
- url: ctx.originalUrl || ctx.url,
77
- status: ctx.status,
78
- message: err.message,
79
- stack: err.stack
80
- });
81
-
82
- ctx.app.emit('error', err, ctx);
83
- }
84
- });
85
-
86
- app.use(router.routes()).use(router.allowedMethods());
87
-
88
- app.use(async (ctx) => {
89
- if (ctx.status === 404) {
90
- ctx.body = {
91
- success: false,
92
- message: 'Not Found'
93
- };
94
- }
95
- });
96
-
97
- app.on('error', (err, ctx) => {
98
- logger.error('Server error event', {
99
- requestId: ctx && ctx.state && ctx.state.requestId,
100
- message: err.message,
101
- stack: err.stack
102
- });
103
- });
104
-
105
- process.on('unhandledRejection', (reason) => {
106
- logger.error('Unhandled promise rejection', reason);
107
- });
108
-
109
- process.on('uncaughtException', (error) => {
110
- logger.error('Uncaught exception', error);
111
- });
112
-
113
- const port = config.port || 3000;
114
- app.listen(port, () => {
115
- logger.info(`Server is running on http://localhost:${port}`);
116
- logger.info(`Environment: ${config.env}`);
117
- });
118
-
1
+ require('dotenv').config();
2
+
3
+ const Koa = require('koa');
4
+ const { loadConfig } = require('./config/loader');
5
+ const { createLogger } = require('./app/lib/logger');
6
+ const setup = require('./app/setup');
7
+ const setupProcessEvents = require('./app/processEvents');
8
+
9
+ const config = loadConfig();
10
+ const app = new Koa();
11
+ const logger = createLogger({
12
+ ...(config.logger || {}),
13
+ appName: config.name || 'koa3-cli',
14
+ cwd: __dirname
15
+ });
16
+
17
+ app.keys = config.keys || ['koa3-cli-secret-key'];
18
+ app.context.logger = logger;
19
+
20
+ setup(app, config, logger);
21
+
22
+ app.on('error', (err, ctx) => {
23
+ logger.error('Server error event', {
24
+ requestId: ctx && ctx.state && ctx.state.requestId,
25
+ message: err.message,
26
+ stack: err.stack
27
+ });
28
+ });
29
+
30
+ setupProcessEvents(logger);
31
+
32
+ const port = config.port || 3000;
33
+ app.listen(port, () => {
34
+ logger.info(`Server is running on http://localhost:${port}`);
35
+ logger.info(`Environment: ${config.env}`);
36
+ });
37
+
119
38
  module.exports = app;
@@ -1,83 +1,83 @@
1
- /**
2
- * Default config loaded in all environments.
3
- */
4
- module.exports = {
5
- // Application name
6
- name: 'koa3-cli',
7
-
8
- // Runtime env: development, production, test
9
- env: process.env.NODE_ENV || 'development',
10
-
11
- // Server port
12
- port: process.env.PORT || 3000,
13
-
14
- // Cookie signing keys
15
- keys: process.env.KEYS ? process.env.KEYS.split(',') : ['koa3-cli-secret-key'],
16
-
17
- // Static assets
18
- static: {
19
- enable: true,
20
- dir: 'public',
21
- options: {
22
- maxAge: 365 * 24 * 60 * 60 * 1000,
23
- gzip: true
24
- }
25
- },
26
-
27
- // Docs build config
28
- docs: {
29
- enable: true,
30
- buildDir: 'public/docs'
31
- },
32
-
33
- // View engine
34
- view: {
35
- enable: true,
36
- root: 'app/view',
37
- options: {
38
- extension: 'ejs',
39
- map: {
40
- html: 'ejs'
41
- }
42
- }
43
- },
44
-
45
- // bodyParser
46
- bodyParser: {
47
- enableTypes: ['json', 'form', 'text'],
48
- jsonLimit: '10mb',
49
- formLimit: '10mb'
50
- },
51
-
52
- // Database (example)
53
- database: {
54
- client: 'mysql',
55
- connection: {
56
- host: process.env.DB_HOST || 'localhost',
57
- port: process.env.DB_PORT || 3306,
58
- user: process.env.DB_USER || 'root',
59
- password: process.env.DB_PASSWORD || '',
60
- database: process.env.DB_NAME || 'test'
61
- },
62
- pool: {
63
- min: 2,
64
- max: 10
65
- }
66
- },
67
-
68
- // Redis (example)
69
- redis: {
70
- host: process.env.REDIS_HOST || 'localhost',
71
- port: process.env.REDIS_PORT || 6379,
72
- password: process.env.REDIS_PASSWORD || '',
73
- db: process.env.REDIS_DB || 0
74
- },
75
-
76
- // Logger
77
- logger: {
78
- level: process.env.LOG_LEVEL || 'info',
79
- dir: process.env.LOG_DIR || 'logs',
80
- enableConsole: process.env.LOG_ENABLE_CONSOLE !== 'false',
81
- enableFile: process.env.LOG_ENABLE_FILE !== 'false'
82
- }
1
+ /**
2
+ * Default config loaded in all environments.
3
+ */
4
+ module.exports = {
5
+ // Application name
6
+ name: 'koa3-cli',
7
+
8
+ // Runtime env: development, production, test
9
+ env: process.env.NODE_ENV || 'development',
10
+
11
+ // Server port
12
+ port: process.env.PORT || 3000,
13
+
14
+ // Cookie signing keys
15
+ keys: process.env.KEYS ? process.env.KEYS.split(',') : ['koa3-cli-secret-key'],
16
+
17
+ // Static assets
18
+ static: {
19
+ enable: true,
20
+ dir: 'public',
21
+ options: {
22
+ maxAge: 365 * 24 * 60 * 60 * 1000,
23
+ gzip: true
24
+ }
25
+ },
26
+
27
+ // Docs build config
28
+ docs: {
29
+ enable: true,
30
+ buildDir: 'public/docs'
31
+ },
32
+
33
+ // View engine
34
+ view: {
35
+ enable: true,
36
+ root: 'app/view',
37
+ options: {
38
+ extension: 'ejs',
39
+ map: {
40
+ html: 'ejs'
41
+ }
42
+ }
43
+ },
44
+
45
+ // bodyParser
46
+ bodyParser: {
47
+ enableTypes: ['json', 'form', 'text'],
48
+ jsonLimit: '10mb',
49
+ formLimit: '10mb'
50
+ },
51
+
52
+ // Database (example)
53
+ database: {
54
+ client: 'mysql',
55
+ connection: {
56
+ host: process.env.DB_HOST || 'localhost',
57
+ port: process.env.DB_PORT || 3306,
58
+ user: process.env.DB_USER || 'root',
59
+ password: process.env.DB_PASSWORD || '',
60
+ database: process.env.DB_NAME || 'test'
61
+ },
62
+ pool: {
63
+ min: 2,
64
+ max: 10
65
+ }
66
+ },
67
+
68
+ // Redis (example)
69
+ redis: {
70
+ host: process.env.REDIS_HOST || 'localhost',
71
+ port: process.env.REDIS_PORT || 6379,
72
+ password: process.env.REDIS_PASSWORD || '',
73
+ db: process.env.REDIS_DB || 0
74
+ },
75
+
76
+ // Logger
77
+ logger: {
78
+ level: process.env.LOG_LEVEL || 'info',
79
+ dir: process.env.LOG_DIR || 'logs',
80
+ enableConsole: process.env.LOG_ENABLE_CONSOLE !== 'false',
81
+ enableFile: process.env.LOG_ENABLE_FILE !== 'false'
82
+ }
83
83
  };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * 配置加载器:合并 default + 环境配置
3
+ */
4
+ const path = require('path');
5
+
6
+ const defaultConfig = require('./config.default');
7
+
8
+ function loadConfig() {
9
+ const env = process.env.NODE_ENV || 'development';
10
+ let envConfig = {};
11
+
12
+ try {
13
+ if (env === 'production') {
14
+ envConfig = require('./config.prod');
15
+ } else if (env === 'local' || env === 'development') {
16
+ envConfig = require('./config.local');
17
+ }
18
+ } catch (e) {
19
+ // 忽略缺失的环境配置文件
20
+ }
21
+
22
+ return Object.assign({}, defaultConfig, envConfig);
23
+ }
24
+
25
+ module.exports = { loadConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koa3-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Koa3脚手架",
5
5
  "main": "app.js",
6
6
  "bin": {
@@ -29,6 +29,7 @@
29
29
  "@ladjs/koa-views": "^9.0.0",
30
30
  "dotenv": "^17.3.1",
31
31
  "ejs": "^4.0.1",
32
+ "joi": "^17.13.3",
32
33
  "koa": "^3.1.2",
33
34
  "koa-bodyparser": "^4.4.1",
34
35
  "koa-cors": "^0.0.16",