json-api-mocker 2.2.1 → 2.3.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/README.ch.md +224 -95
- package/README.md +227 -98
- package/dist/cli.js +2 -37
- package/dist/index.d.ts +1 -5
- package/dist/index.js +2 -7
- package/dist/server.d.ts +14 -17
- package/dist/server.js +103 -290
- package/dist/types.d.ts +21 -26
- package/package.json +11 -20
- package/DESIGN.md +0 -227
- package/web/assets/index-C613zJ_P.css +0 -1
- package/web/assets/index-CybI6Cd5.js +0 -25
- package/web/index.html +0 -15
- package/web/monaco-editor-worker-loader.js +0 -8
- package/web/vite.svg +0 -1
package/dist/server.js
CHANGED
@@ -4,321 +4,134 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
4
|
};
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
6
6
|
exports.MockServer = void 0;
|
7
|
+
const mockjs_1 = __importDefault(require("mockjs"));
|
7
8
|
const express_1 = __importDefault(require("express"));
|
8
9
|
const cors_1 = __importDefault(require("cors"));
|
9
|
-
const fs_1 = require("fs");
|
10
|
-
const path_1 = require("path");
|
11
|
-
const uuid_1 = require("uuid");
|
12
|
-
const mockjs_1 = __importDefault(require("mockjs"));
|
13
|
-
const multer_1 = __importDefault(require("multer"));
|
14
|
-
const path_2 = __importDefault(require("path"));
|
15
|
-
const ws_1 = require("ws");
|
16
10
|
class MockServer {
|
17
|
-
constructor(
|
18
|
-
this.logs = []; // 存储请求日志
|
19
|
-
this.MAX_LOGS = 1000; // 最大日志数量
|
20
|
-
this.clients = new Set();
|
11
|
+
constructor(config, configPath = 'data.json') {
|
21
12
|
this.app = (0, express_1.default)();
|
22
|
-
this.
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
},
|
37
|
-
filename: (req, file, cb) => {
|
38
|
-
// 生成唯一文件名
|
39
|
-
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
40
|
-
cb(null, file.fieldname + '-' + uniqueSuffix + path_2.default.extname(file.originalname));
|
41
|
-
}
|
42
|
-
});
|
43
|
-
this.upload = (0, multer_1.default)({ storage });
|
44
|
-
// 添加文件上传路由
|
45
|
-
this.setupUploadRoute();
|
46
|
-
// 使用固定的 WebSocket 端口
|
47
|
-
this.wss = new ws_1.WebSocketServer({ port: serverConfig.wsPort || 88866 });
|
48
|
-
this.wss.on('connection', (ws) => {
|
49
|
-
this.clients.add(ws);
|
50
|
-
ws.send(JSON.stringify({
|
51
|
-
type: 'init',
|
52
|
-
data: this.logs
|
53
|
-
}));
|
54
|
-
ws.on('close', () => {
|
55
|
-
this.clients.delete(ws);
|
13
|
+
this.logRequest = (req, res, next) => {
|
14
|
+
const startTime = Date.now();
|
15
|
+
const requestId = Math.random().toString(36).substring(7);
|
16
|
+
console.log(`[${new Date().toISOString()}] Request ${requestId}:`);
|
17
|
+
console.log(` Method: ${req.method}`);
|
18
|
+
console.log(` URL: ${req.url}`);
|
19
|
+
console.log(` Query Params: ${JSON.stringify(req.query)}`);
|
20
|
+
console.log(` Body: ${JSON.stringify(req.body)}`);
|
21
|
+
res.on('finish', () => {
|
22
|
+
const duration = Date.now() - startTime;
|
23
|
+
console.log(`[${new Date().toISOString()}] Response ${requestId}:`);
|
24
|
+
console.log(` Status: ${res.statusCode}`);
|
25
|
+
console.log(` Duration: ${duration}ms`);
|
26
|
+
console.log('----------------------------------------');
|
56
27
|
});
|
57
|
-
|
28
|
+
next();
|
29
|
+
};
|
30
|
+
this.config = config;
|
31
|
+
this.configPath = configPath;
|
32
|
+
this.setupMiddleware();
|
33
|
+
this.setupRoutes();
|
58
34
|
}
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
const isValid = api &&
|
68
|
-
api.id &&
|
69
|
-
api.route &&
|
70
|
-
typeof api.route.path === 'string' &&
|
71
|
-
api.route.methods &&
|
72
|
-
typeof api.route.methods === 'object';
|
73
|
-
if (!isValid) {
|
74
|
-
console.warn('Filtered out invalid API config:', api);
|
75
|
-
}
|
76
|
-
return isValid;
|
77
|
-
});
|
78
|
-
}
|
79
|
-
catch (error) {
|
80
|
-
console.error('Error loading config:', error);
|
81
|
-
return [];
|
82
|
-
}
|
35
|
+
getApp() {
|
36
|
+
return this.app;
|
37
|
+
}
|
38
|
+
setupMiddleware() {
|
39
|
+
this.app.use((0, cors_1.default)());
|
40
|
+
this.app.use(express_1.default.json());
|
41
|
+
this.app.use('/uploads', express_1.default.static('uploads'));
|
42
|
+
this.app.use(this.logRequest);
|
83
43
|
}
|
84
|
-
|
44
|
+
generateMockData(config) {
|
85
45
|
try {
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
methods: Object.entries(api.route.methods).reduce((acc, [method, config]) => ({
|
94
|
-
...acc,
|
95
|
-
[method]: {
|
96
|
-
...config,
|
97
|
-
// 解析并重新格式化 JSON,移除多余的转义
|
98
|
-
response: typeof config.response === 'string'
|
99
|
-
? JSON.stringify(JSON.parse(config.response))
|
100
|
-
: JSON.stringify(config.response),
|
101
|
-
headers: config.headers || {}
|
102
|
-
}
|
103
|
-
}), {})
|
104
|
-
}
|
105
|
-
}));
|
106
|
-
fullConfig.routes = processedRoutes;
|
107
|
-
(0, fs_1.writeFileSync)(this.configPath, JSON.stringify(fullConfig, null, 2));
|
46
|
+
if (config.mock?.enabled && config.mock.template) {
|
47
|
+
const { total, template } = config.mock;
|
48
|
+
return mockjs_1.default.mock({
|
49
|
+
[`data|${total}`]: [template]
|
50
|
+
}).data;
|
51
|
+
}
|
52
|
+
return config.response;
|
108
53
|
}
|
109
54
|
catch (error) {
|
110
|
-
console.error('Error
|
55
|
+
console.error('Error generating mock data:', error);
|
56
|
+
return config.response;
|
111
57
|
}
|
112
58
|
}
|
113
|
-
|
114
|
-
|
115
|
-
// 获取所有API配置
|
116
|
-
this.app.get('/api/_config', (req, res) => {
|
117
|
-
console.log('Sending config:', this.config);
|
118
|
-
res.json(this.config);
|
119
|
-
});
|
120
|
-
// 创建新API配置
|
121
|
-
this.app.post('/api/_config', (req, res) => {
|
122
|
-
const newApi = {
|
123
|
-
id: (0, uuid_1.v4)(),
|
124
|
-
route: req.body.route
|
125
|
-
};
|
126
|
-
this.config.push(newApi);
|
127
|
-
this.saveConfig();
|
128
|
-
res.json(newApi);
|
129
|
-
});
|
130
|
-
// 更新API配置
|
131
|
-
this.app.put('/api/_config/:id', (req, res) => {
|
132
|
-
const index = this.config.findIndex(api => api.id === req.params.id);
|
133
|
-
if (index === -1) {
|
134
|
-
res.status(404).json({ error: 'API not found' });
|
135
|
-
return;
|
136
|
-
}
|
137
|
-
this.config[index] = {
|
138
|
-
id: req.params.id,
|
139
|
-
route: req.body.route
|
140
|
-
};
|
141
|
-
this.saveConfig();
|
142
|
-
res.json(this.config[index]);
|
143
|
-
});
|
144
|
-
// 删除API配置
|
145
|
-
this.app.delete('/api/_config/:id', (req, res) => {
|
146
|
-
const index = this.config.findIndex(api => api.id === req.params.id);
|
147
|
-
if (index === -1) {
|
148
|
-
res.status(404).json({ error: 'API not found' });
|
149
|
-
return;
|
150
|
-
}
|
151
|
-
this.config.splice(index, 1);
|
152
|
-
this.saveConfig();
|
153
|
-
res.status(204).send();
|
154
|
-
});
|
155
|
-
// 添加日志接口
|
156
|
-
this.app.get('/api/_logs', (req, res) => {
|
157
|
-
const { page = 1, size = 20 } = req.query;
|
158
|
-
const start = (Number(page) - 1) * Number(size);
|
159
|
-
const end = start + Number(size);
|
160
|
-
const total = this.logs.length;
|
161
|
-
res.json({
|
162
|
-
total,
|
163
|
-
list: this.logs.slice(start, end)
|
164
|
-
});
|
165
|
-
});
|
166
|
-
// 清除日志
|
167
|
-
this.app.delete('/api/_logs', (req, res) => {
|
168
|
-
this.logs = [];
|
169
|
-
res.status(204).send();
|
170
|
-
});
|
171
|
-
}
|
172
|
-
setupMockRoutes() {
|
173
|
-
const baseProxy = this.serverConfig.baseProxy || '';
|
174
|
-
this.app.all('*', (req, res, next) => {
|
175
|
-
// 如果是配置接口,跳过日志记录
|
176
|
-
if (req.path === '/api/_config' || req.path.startsWith('/api/_config/') ||
|
177
|
-
req.path === '/api/_logs' || req.path.startsWith('/api/_logs/')) {
|
178
|
-
return next();
|
179
|
-
}
|
180
|
-
const startTime = Date.now();
|
181
|
-
const originalSend = res.send;
|
182
|
-
// 修复 this 指向问题
|
183
|
-
const self = this;
|
184
|
-
// 记录请求日志
|
185
|
-
res.send = function (body) {
|
186
|
-
const endTime = Date.now();
|
187
|
-
const duration = endTime - startTime;
|
188
|
-
const log = {
|
189
|
-
id: (0, uuid_1.v4)(),
|
190
|
-
path: req.path,
|
191
|
-
method: req.method,
|
192
|
-
timestamp: new Date().toISOString(),
|
193
|
-
status: res.statusCode,
|
194
|
-
duration,
|
195
|
-
params: {
|
196
|
-
query: req.query,
|
197
|
-
body: req.body,
|
198
|
-
params: req.params
|
199
|
-
},
|
200
|
-
requestBody: req.body,
|
201
|
-
responseBody: body
|
202
|
-
};
|
203
|
-
// 使用正确的 this 引用
|
204
|
-
self.logs.unshift(log);
|
205
|
-
if (self.logs.length > self.MAX_LOGS) {
|
206
|
-
self.logs.pop();
|
207
|
-
}
|
208
|
-
// 广播新日志
|
209
|
-
self.broadcastLog(log);
|
210
|
-
return originalSend.call(this, body);
|
211
|
-
};
|
212
|
-
next();
|
213
|
-
});
|
214
|
-
// 处理所有mock请求,但排除配置接口
|
215
|
-
this.app.all('*', (req, res, next) => {
|
59
|
+
handleRequest(config) {
|
60
|
+
return (req, res) => {
|
216
61
|
try {
|
217
|
-
|
218
|
-
if (
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
return;
|
227
|
-
}
|
228
|
-
const api = this.config.find(api => {
|
229
|
-
if (!api || !api.route || typeof api.route.path !== 'string') {
|
230
|
-
console.error('Invalid API config:', api);
|
231
|
-
return false;
|
232
|
-
}
|
233
|
-
// 移除 baseProxy 前缀再比较
|
234
|
-
const requestPath = req.path.replace(baseProxy, '');
|
235
|
-
return api.route.path === requestPath;
|
236
|
-
});
|
237
|
-
if (!api) {
|
238
|
-
res.status(404).json({ error: 'API not found' });
|
239
|
-
return;
|
240
|
-
}
|
241
|
-
if (!api.route || !api.route.methods) {
|
242
|
-
res.status(500).json({ error: 'Invalid API configuration' });
|
243
|
-
return;
|
244
|
-
}
|
245
|
-
const method = req.method.toLowerCase();
|
246
|
-
const methodConfig = api.route.methods[method];
|
247
|
-
if (!methodConfig) {
|
248
|
-
res.status(405).json({ error: 'Method not allowed' });
|
249
|
-
return;
|
250
|
-
}
|
251
|
-
if (methodConfig.delay) {
|
252
|
-
setTimeout(() => {
|
253
|
-
this.sendResponse(res, methodConfig);
|
254
|
-
}, methodConfig.delay);
|
255
|
-
}
|
256
|
-
else {
|
257
|
-
this.sendResponse(res, methodConfig);
|
62
|
+
let responseData = this.generateMockData(config);
|
63
|
+
if (config.pagination?.enabled && Array.isArray(responseData)) {
|
64
|
+
const page = parseInt(req.query.page) || 1;
|
65
|
+
const pageSize = parseInt(req.query.pageSize) || config.pagination.pageSize;
|
66
|
+
const startIndex = (page - 1) * pageSize;
|
67
|
+
const endIndex = startIndex + pageSize;
|
68
|
+
const paginatedData = responseData.slice(startIndex, endIndex);
|
69
|
+
res.header('X-Total-Count', responseData.length.toString());
|
70
|
+
responseData = paginatedData;
|
258
71
|
}
|
72
|
+
res.json(responseData);
|
259
73
|
}
|
260
74
|
catch (error) {
|
261
75
|
console.error('Error handling request:', error);
|
262
76
|
res.status(500).json({ error: 'Internal server error' });
|
263
77
|
}
|
78
|
+
};
|
79
|
+
}
|
80
|
+
setupRoutes() {
|
81
|
+
this.config.routes.forEach((route) => {
|
82
|
+
Object.entries(route.methods).forEach(([method, methodConfig]) => {
|
83
|
+
this.createRoute(route.path, method, methodConfig);
|
84
|
+
});
|
264
85
|
});
|
265
86
|
}
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
87
|
+
createRoute(path, method, config) {
|
88
|
+
const fullPath = `${this.config.server.baseProxy}${path}`;
|
89
|
+
console.log(`创建路由: ${method.toUpperCase()} ${fullPath}`);
|
90
|
+
switch (method.toLowerCase()) {
|
91
|
+
case 'get':
|
92
|
+
this.app.get(fullPath, this.handleRequest(config));
|
93
|
+
break;
|
94
|
+
case 'post':
|
95
|
+
if (path === '/upload/avatar') {
|
96
|
+
// 对于文件上传路由,使用特殊处理
|
97
|
+
this.app.post(fullPath, (req, res) => {
|
98
|
+
const mockResponse = this.generateMockData(config);
|
99
|
+
res.json(mockResponse);
|
100
|
+
});
|
101
|
+
}
|
102
|
+
else {
|
103
|
+
this.app.post(fullPath, this.handleRequest(config));
|
104
|
+
}
|
105
|
+
break;
|
106
|
+
case 'put':
|
107
|
+
this.app.put(`${fullPath}/:id`, this.handleRequest(config));
|
108
|
+
break;
|
109
|
+
case 'delete':
|
110
|
+
this.app.delete(`${fullPath}/:id`, this.handleRequest(config));
|
111
|
+
break;
|
284
112
|
}
|
285
113
|
}
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
if (!req.file) {
|
290
|
-
return res.status(400).json({ error: 'No file uploaded' });
|
291
|
-
}
|
292
|
-
// 返回文件URL
|
293
|
-
const fileUrl = `/uploads/${req.file.filename}`;
|
294
|
-
res.json({
|
295
|
-
success: true,
|
296
|
-
data: {
|
297
|
-
url: fileUrl,
|
298
|
-
filename: req.file.originalname,
|
299
|
-
size: req.file.size
|
300
|
-
}
|
301
|
-
});
|
302
|
-
});
|
303
|
-
// 提供静态文件访问
|
304
|
-
this.app.use('/uploads', express_1.default.static(path_2.default.join(process.cwd(), 'uploads')));
|
114
|
+
findRouteConfig(path, method) {
|
115
|
+
const route = this.config.routes.find(r => r.path === path);
|
116
|
+
return route?.methods[method] || null;
|
305
117
|
}
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
118
|
+
generateMockResponse(config) {
|
119
|
+
if (config.mock?.enabled && config.mock.template) {
|
120
|
+
return mockjs_1.default.mock(config.mock.template);
|
121
|
+
}
|
122
|
+
return config.response;
|
311
123
|
}
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
124
|
+
start() {
|
125
|
+
this.app.listen(this.config.server.port, () => {
|
126
|
+
console.log(`Mock 服务器已启动:`);
|
127
|
+
console.log(`- 地址: http://localhost:${this.config.server.port}`);
|
128
|
+
console.log(`- 基础路径: ${this.config.server.baseProxy}`);
|
129
|
+
console.log('可用的接口:');
|
130
|
+
this.config.routes.forEach(route => {
|
131
|
+
Object.keys(route.methods).forEach(method => {
|
132
|
+
console.log(` ${method.toUpperCase()} http://localhost:${this.config.server.port}${this.config.server.baseProxy}${route.path}`);
|
133
|
+
});
|
134
|
+
});
|
322
135
|
});
|
323
136
|
}
|
324
137
|
}
|
package/dist/types.d.ts
CHANGED
@@ -1,8 +1,23 @@
|
|
1
|
+
export interface ServerConfig {
|
2
|
+
port: number;
|
3
|
+
baseProxy: string;
|
4
|
+
}
|
5
|
+
export interface PaginationConfig {
|
6
|
+
enabled: boolean;
|
7
|
+
pageSize: number;
|
8
|
+
totalCount: number;
|
9
|
+
}
|
10
|
+
export interface MockConfig {
|
11
|
+
enabled: boolean;
|
12
|
+
total?: number;
|
13
|
+
template: Record<string, any>;
|
14
|
+
}
|
1
15
|
export interface MethodConfig {
|
2
|
-
|
3
|
-
response
|
4
|
-
|
5
|
-
|
16
|
+
type?: 'array' | 'object';
|
17
|
+
response?: any;
|
18
|
+
mock?: MockConfig;
|
19
|
+
pagination?: PaginationConfig;
|
20
|
+
requestSchema?: Record<string, string>;
|
6
21
|
}
|
7
22
|
export interface RouteConfig {
|
8
23
|
path: string;
|
@@ -10,27 +25,7 @@ export interface RouteConfig {
|
|
10
25
|
[key: string]: MethodConfig;
|
11
26
|
};
|
12
27
|
}
|
13
|
-
export interface ApiConfig {
|
14
|
-
id: string;
|
15
|
-
route: RouteConfig;
|
16
|
-
}
|
17
|
-
export interface RequestLog {
|
18
|
-
id: string;
|
19
|
-
path: string;
|
20
|
-
method: string;
|
21
|
-
timestamp: string;
|
22
|
-
status: number;
|
23
|
-
duration: number;
|
24
|
-
params: {
|
25
|
-
query: any;
|
26
|
-
body: any;
|
27
|
-
params: any;
|
28
|
-
};
|
29
|
-
requestBody?: any;
|
30
|
-
responseBody?: any;
|
31
|
-
}
|
32
28
|
export interface Config {
|
33
|
-
|
34
|
-
|
35
|
-
wsPort?: number;
|
29
|
+
server: ServerConfig;
|
30
|
+
routes: RouteConfig[];
|
36
31
|
}
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "json-api-mocker",
|
3
|
-
"version": "2.
|
4
|
-
"description": "A mock server
|
3
|
+
"version": "2.3.0",
|
4
|
+
"description": "A mock server based on JSON configuration",
|
5
5
|
"main": "dist/index.js",
|
6
6
|
"types": "dist/index.d.ts",
|
7
7
|
"bin": {
|
@@ -9,17 +9,19 @@
|
|
9
9
|
},
|
10
10
|
"files": [
|
11
11
|
"dist",
|
12
|
-
"web",
|
13
|
-
"README.ch.md",
|
14
12
|
"README.md",
|
13
|
+
"README.ch.md",
|
15
14
|
"CONFIG.md",
|
16
|
-
"CONFIG.ch.md"
|
17
|
-
"DESIGN.md"
|
15
|
+
"CONFIG.ch.md"
|
18
16
|
],
|
19
17
|
"scripts": {
|
20
18
|
"dev": "nodemon",
|
21
|
-
"build": "tsc
|
22
|
-
"
|
19
|
+
"build": "tsc",
|
20
|
+
"start": "node dist/index.js",
|
21
|
+
"test": "jest",
|
22
|
+
"test:watch": "jest --watch",
|
23
|
+
"test:coverage": "jest --coverage",
|
24
|
+
"prepublishOnly": "npm run build"
|
23
25
|
},
|
24
26
|
"keywords": [
|
25
27
|
"mock",
|
@@ -42,32 +44,21 @@
|
|
42
44
|
},
|
43
45
|
"homepage": "https://github.com/Selteve/json-api-mocker#readme",
|
44
46
|
"dependencies": {
|
45
|
-
"commander": "^8.3.0",
|
46
47
|
"cors": "^2.8.5",
|
47
48
|
"express": "^4.17.1",
|
48
49
|
"mockjs": "^1.1.0",
|
49
|
-
"multer": "^1.4.5-lts.1"
|
50
|
-
"open": "^8.4.0",
|
51
|
-
"uuid": "^9.0.0",
|
52
|
-
"ws": "^8.2.3"
|
50
|
+
"multer": "^1.4.5-lts.1"
|
53
51
|
},
|
54
52
|
"devDependencies": {
|
55
|
-
"@types/commander": "^2.12.2",
|
56
53
|
"@types/cors": "^2.8.13",
|
57
54
|
"@types/express": "^4.17.13",
|
58
|
-
"@types/fs-extra": "^11.0.4",
|
59
55
|
"@types/jest": "^27.5.2",
|
60
56
|
"@types/mockjs": "^1.0.10",
|
61
57
|
"@types/multer": "^1.4.12",
|
62
58
|
"@types/node": "^16.18.0",
|
63
|
-
"@types/open": "^6.2.1",
|
64
59
|
"@types/supertest": "^6.0.2",
|
65
|
-
"@types/uuid": "^9.0.8",
|
66
|
-
"@types/ws": "^8.5.13",
|
67
|
-
"fs-extra": "^11.2.0",
|
68
60
|
"jest": "^27.5.1",
|
69
61
|
"nodemon": "^2.0.22",
|
70
|
-
"rimraf": "^3.0.2",
|
71
62
|
"supertest": "^7.0.0",
|
72
63
|
"ts-jest": "^27.1.5",
|
73
64
|
"ts-node": "^10.9.1",
|