whistle.mockbubu 1.0.0-dev.4 → 2.0.0-beta.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.
@@ -1,126 +1,271 @@
1
- const {
2
- addNewVersion,
3
- updateVersionContent,
4
- deleteVersion,
5
- updateVersionName,
6
- getVersions,
7
- setProperty,
8
- getPropertyAttr,
9
- removePropertyAttr,
10
- } = require('../../utils')
11
1
  const { createErrorResponse, createSuccessResponse } = require('../util')
2
+ const {
3
+ validateFilename,
4
+ validateVersionName,
5
+ validateMockData,
6
+ validate,
7
+ } = require('../validator')
12
8
 
13
9
 
14
10
  module.exports = (router) => {
15
11
  // 新增版本
16
- router.post('/cgi-bin/mockbubu/add-new-version', (ctx) => {
12
+ router.post('/cgi-bin/mockbubu/add-new-version', validate({
13
+ name: validateFilename,
14
+ versionName: validateVersionName,
15
+ content: validateMockData,
16
+ }), async (ctx) => {
17
17
  try {
18
- const { localStorage } = ctx.req
19
- const { versionName, content = {}, name } = ctx.request.body || {}
18
+ const { groupManager, storageAdapter } = ctx
19
+ const { versionName, content = {}, name, description = '' } = ctx.request.body || {}
20
+
21
+ // 获取当前组ID
22
+ const currentGroupId = await groupManager.getCurrentGroupId()
23
+
24
+ // 使用 V3 Storage 从索引查找文件ID
25
+ const v3Storage = storageAdapter.v3Storage
26
+ const index = await v3Storage.getIndex(currentGroupId)
27
+ const fileEntry = index.files.find(f => f.url === name)
28
+ if (!fileEntry) {
29
+ throw new Error('文件不存在')
30
+ }
20
31
 
21
- addNewVersion({
22
- storage: localStorage,
23
- filename: name,
24
- versionName,
32
+ // 使用 V3 Storage 创建版本
33
+ const newVersion = await v3Storage.createVersion(currentGroupId, fileEntry.id, versionName, {
25
34
  content,
35
+ description,
26
36
  })
27
37
 
28
- ctx.body = createSuccessResponse({
29
- filename: versionName,
30
- content,
31
- }, '版本创建成功')
38
+ ctx.body = createSuccessResponse(newVersion, '版本创建成功')
32
39
  } catch (error) {
40
+ console.error('[mockbubu 2025/11/19 21:30:00] 创建版本失败:', error.message)
33
41
  ctx.body = createErrorResponse(error.message)
34
42
  }
35
43
  })
36
44
 
37
45
  // 删除版本
38
- router.post('/cgi-bin/mockbubu/delete-version', (ctx) => {
46
+ router.post('/cgi-bin/mockbubu/delete-version', validate({
47
+ name: validateFilename,
48
+ }), async (ctx) => {
39
49
  try {
40
- const { localStorage } = ctx.req
41
- const { versionName, name } = ctx.request.body || {}
50
+ const { groupManager, storageAdapter } = ctx
51
+ const { versionId, name } = ctx.request.body || {}
52
+
53
+ if (!versionId) {
54
+ throw new Error('缺少版本ID参数')
55
+ }
42
56
 
43
- const mockVersion = getPropertyAttr(localStorage, name, 'mockVersion')
57
+ // 获取当前组ID
58
+ const currentGroupId = await groupManager.getCurrentGroupId()
44
59
 
45
- // 如果删除的是当前mock版本,则清除mock版本设置
46
- if (versionName === mockVersion) {
47
- removePropertyAttr(localStorage, name, 'mockVersion')
60
+ // 使用 V3 Storage 从索引查找文件ID
61
+ const v3Storage = storageAdapter.v3Storage
62
+ const index = await v3Storage.getIndex(currentGroupId)
63
+ const fileEntry = index.files.find(f => f.url === name)
64
+ if (!fileEntry) {
65
+ throw new Error('文件不存在')
48
66
  }
49
67
 
50
- deleteVersion({
51
- storage: localStorage,
52
- filename: name,
53
- versionName,
54
- })
68
+ // 获取组文件配置(检查是否是当前mock版本)
69
+ const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
70
+
71
+ // 如果删除的是当前mock版本,先获取版本name进行比较
72
+ const version = await v3Storage.getVersionById(currentGroupId, fileEntry.id, versionId)
73
+
74
+ if (version && version.name === groupConfig.mockVersion) {
75
+ // 清除mock版本设置
76
+ groupConfig.mockVersion = null
77
+ await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
78
+ }
79
+
80
+ // 使用 V3 Storage 删除版本
81
+ await v3Storage.deleteVersion(currentGroupId, fileEntry.id, versionId)
55
82
 
56
83
  ctx.body = createSuccessResponse(null, '版本删除成功')
57
84
  } catch (error) {
85
+ console.error('[mockbubu 2025/11/19 21:30:00] 删除版本失败:', error.message)
58
86
  ctx.body = createErrorResponse(error.message)
59
87
  }
60
88
  })
61
89
 
62
90
  // 更新版本内容
63
- router.post('/cgi-bin/mockbubu/update-version-content', (ctx) => {
91
+ router.post('/cgi-bin/mockbubu/update-version-content', validate({
92
+ name: validateFilename,
93
+ content: validateMockData,
94
+ }), async (ctx) => {
64
95
  try {
65
- const { localStorage } = ctx.req
66
- const { versionName, content, name } = ctx.request.body || {}
96
+ const { groupManager, storageAdapter } = ctx
97
+ const { versionId, content, name } = ctx.request.body || {}
98
+
99
+ if (!versionId) {
100
+ throw new Error('缺少版本ID参数')
101
+ }
102
+
103
+ // 获取当前组ID
104
+ const currentGroupId = await groupManager.getCurrentGroupId()
67
105
 
68
- updateVersionContent({
69
- storage: localStorage,
70
- filename: name,
71
- versionName,
106
+ // 使用 V3 Storage 从索引查找文件ID
107
+ const v3Storage = storageAdapter.v3Storage
108
+ const index = await v3Storage.getIndex(currentGroupId)
109
+ const fileEntry = index.files.find(f => f.url === name)
110
+ if (!fileEntry) {
111
+ throw new Error('文件不存在')
112
+ }
113
+
114
+ // 使用 V3 Storage 更新版本
115
+ await v3Storage.updateVersion(currentGroupId, fileEntry.id, versionId, {
72
116
  content,
73
117
  })
74
118
 
75
119
  ctx.body = createSuccessResponse(null, '版本内容更新成功')
76
120
  } catch (error) {
121
+ console.error('[mockbubu 2025/11/19 21:30:00] 更新版本内容失败:', error.message)
77
122
  ctx.body = createErrorResponse(error.message)
78
123
  }
79
124
  })
80
125
 
81
- // 更新版本名称
82
- router.post('/cgi-bin/mockbubu/update-version-name', (ctx) => {
126
+ // 获取版本列表
127
+ router.post('/cgi-bin/mockbubu/get-versions', validate({
128
+ name: validateFilename,
129
+ }), async (ctx) => {
83
130
  try {
84
- const { localStorage } = ctx.req
85
- const { versionName, name, newVersion } = ctx.request.body || {}
131
+ const { groupManager, storageAdapter } = ctx
132
+ const { name } = ctx.request.body || {}
86
133
 
87
- updateVersionName({
88
- storage: localStorage,
89
- filename: name,
90
- versionName,
91
- newVersion,
92
- })
134
+ // 获取当前组ID
135
+ const currentGroupId = await groupManager.getCurrentGroupId()
93
136
 
94
- ctx.body = createSuccessResponse(null, '版本名称更新成功')
137
+ // 使用 V3 Storage 从索引查找文件ID
138
+ const v3Storage = storageAdapter.v3Storage
139
+ const index = await v3Storage.getIndex(currentGroupId)
140
+ const fileEntry = index.files.find(f => f.url === name)
141
+ if (!fileEntry) {
142
+ throw new Error('文件不存在')
143
+ }
144
+
145
+ // 使用 V3 Storage 获取版本列表
146
+ const versions = await v3Storage.listVersions(currentGroupId, fileEntry.id)
147
+
148
+ ctx.body = createSuccessResponse(versions, '获取版本列表成功')
95
149
  } catch (error) {
150
+ console.error('[mockbubu 2025/11/19 21:30:00] 获取版本列表失败:', error.message)
96
151
  ctx.body = createErrorResponse(error.message)
97
152
  }
98
153
  })
99
154
 
100
- // 获取版本列表
101
- router.post('/cgi-bin/mockbubu/get-versions', (ctx) => {
155
+ // 设置mock的版本
156
+ router.post('/cgi-bin/mockbubu/set-mock-version', validate({
157
+ name: validateFilename,
158
+ }), async (ctx) => {
102
159
  try {
103
- const { localStorage } = ctx.req
104
- const { name } = ctx.request.body || {}
160
+ const { groupManager } = ctx
161
+ const { name, versionName } = ctx.request.body || {}
105
162
 
106
- const list = getVersions({ storage: localStorage, filename: name }) || []
163
+ // 获取当前组ID和配置
164
+ const currentGroupId = await groupManager.getCurrentGroupId()
165
+ const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
107
166
 
108
- ctx.body = createSuccessResponse(list, '获取版本列表成功')
167
+ // 更新 mockVersion (使用版本name,不是ID)
168
+ groupConfig.mockVersion = versionName || null
169
+ await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
170
+
171
+ ctx.body = createSuccessResponse(null, 'Mock版本设置成功')
109
172
  } catch (error) {
173
+ console.error('[mockbubu 2025/11/19 21:30:00] 设置Mock版本失败:', error.message)
110
174
  ctx.body = createErrorResponse(error.message)
111
175
  }
112
176
  })
113
177
 
114
- // 设置mock的版本
115
- router.post('/cgi-bin/mockbubu/set-mock-version', (ctx) => {
178
+ // 更新版本元信息(名称和描述)
179
+ router.post('/cgi-bin/mockbubu/update-version-meta', validate({
180
+ name: validateFilename,
181
+ newVersionName: validateVersionName,
182
+ }), async (ctx) => {
116
183
  try {
117
- const { localStorage } = ctx.req
118
- const { name, versionName } = ctx.request.body || {}
184
+ const { groupManager, storageAdapter } = ctx
185
+ const { name, versionId, newVersionName, description = '' } = ctx.request.body || {}
119
186
 
120
- setProperty(localStorage, name, { mockVersion: versionName })
187
+ if (!versionId) {
188
+ throw new Error('缺少版本ID参数')
189
+ }
121
190
 
122
- ctx.body = createSuccessResponse(null, 'Mock版本设置成功')
191
+ // 获取当前组ID
192
+ const currentGroupId = await groupManager.getCurrentGroupId()
193
+
194
+ // 使用 V3 Storage 从索引查找文件ID
195
+ const v3Storage = storageAdapter.v3Storage
196
+ const index = await v3Storage.getIndex(currentGroupId)
197
+ const fileEntry = index.files.find(f => f.url === name)
198
+ if (!fileEntry) {
199
+ throw new Error('文件不存在')
200
+ }
201
+
202
+ // 使用 V3 Storage 更新版本元信息
203
+ const oldVersion = await v3Storage.getVersionById(currentGroupId, fileEntry.id, versionId)
204
+ if (!oldVersion) {
205
+ throw new Error('版本不存在')
206
+ }
207
+
208
+ await v3Storage.updateVersion(currentGroupId, fileEntry.id, versionId, {
209
+ name: newVersionName,
210
+ description,
211
+ })
212
+
213
+ // 如果更新的是当前mock版本的名称,需要同步更新 mockVersion
214
+ const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
215
+ if (oldVersion.name === groupConfig.mockVersion && oldVersion.name !== newVersionName) {
216
+ groupConfig.mockVersion = newVersionName
217
+ await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
218
+ }
219
+
220
+ ctx.body = createSuccessResponse(null, '版本信息更新成功')
221
+ } catch (error) {
222
+ console.error('[mockbubu 2025/11/19 21:30:00] 更新版本元信息失败:', error.message)
223
+ ctx.body = createErrorResponse(error.message)
224
+ }
225
+ })
226
+
227
+ // 兼容旧路由:更新版本名称(已废弃)
228
+ router.post('/cgi-bin/mockbubu/update-version-name', validate({
229
+ name: validateFilename,
230
+ versionName: validateVersionName,
231
+ newVersion: validateVersionName,
232
+ }), async (ctx) => {
233
+ try {
234
+ const { groupManager, storageAdapter } = ctx
235
+ const { versionName, name, newVersion } = ctx.request.body || {}
236
+
237
+ // 获取当前组ID
238
+ const currentGroupId = await groupManager.getCurrentGroupId()
239
+
240
+ // 使用 V3 Storage 从索引查找文件ID
241
+ const v3Storage = storageAdapter.v3Storage
242
+ const index = await v3Storage.getIndex(currentGroupId)
243
+ const fileEntry = index.files.find(f => f.url === name)
244
+ if (!fileEntry) {
245
+ throw new Error('文件不存在')
246
+ }
247
+
248
+ // 使用 V3 Storage 通过名称找到版本ID
249
+ const version = await v3Storage.getVersionByName(currentGroupId, fileEntry.id, versionName)
250
+ if (!version) {
251
+ throw new Error('版本不存在')
252
+ }
253
+
254
+ // 更新版本名称
255
+ await v3Storage.updateVersion(currentGroupId, fileEntry.id, version.id, {
256
+ name: newVersion,
257
+ })
258
+
259
+ // 如果更新的是当前mock版本,需要更新mockVersion属性
260
+ const groupConfig = await groupManager.getGroupFileConfig(currentGroupId, name)
261
+ if (versionName === groupConfig.mockVersion && versionName !== newVersion) {
262
+ groupConfig.mockVersion = newVersion
263
+ await groupManager.setGroupFileConfig(currentGroupId, name, groupConfig)
264
+ }
265
+
266
+ ctx.body = createSuccessResponse(null, '版本名称更新成功')
123
267
  } catch (error) {
268
+ console.error('[mockbubu 2025/11/19 21:30:00] 更新版本名称失败:', error.message)
124
269
  ctx.body = createErrorResponse(error.message)
125
270
  }
126
271
  })
@@ -1,8 +1,7 @@
1
1
  const {
2
- getApiList,
3
- getProperty,
2
+ getGroupFileList,
4
3
  } = require('../utils')
5
- const { RangeFilterMap, RuleFilterMap, LockedFilterMap } = require('../const')
4
+ const { RangeFilterMap, RuleFilterMap, LockedFilterMap, MethodFilterMap, StatusFilterMap } = require('../const')
6
5
 
7
6
  // 统一错误响应格式
8
7
  const createErrorResponse = (message, code = 500) => ({
@@ -14,7 +13,7 @@ const createErrorResponse = (message, code = 500) => ({
14
13
  const createSuccessResponse = (data = null, message = '操作成功') => ({
15
14
  code: 200,
16
15
  msg: message,
17
- ...(data && { data }),
16
+ ...(data !== null && data !== undefined && { data }),
18
17
  })
19
18
 
20
19
  // 执行筛选
@@ -33,6 +32,13 @@ const execFilter = (result, filter) => {
33
32
  return item[key] === value
34
33
  case 'unequal':
35
34
  return item[key] !== value
35
+ case 'range':
36
+ // 范围筛选,value 是 [min, max] 数组
37
+ if (Array.isArray(value) && value.length === 2) {
38
+ const itemValue = item[key]
39
+ return itemValue >= value[0] && itemValue <= value[1]
40
+ }
41
+ break
36
42
  }
37
43
 
38
44
  return true
@@ -46,7 +52,7 @@ const handleFilterList = (list, filterOptions) => {
46
52
  if (!Array.isArray(list) || !filterOptions) return []
47
53
 
48
54
  const filters = []
49
- const { name, rule, range, ruleValue, locked } = filterOptions
55
+ const { name, rule, range, ruleValue, locked, method, status } = filterOptions
50
56
  if (name) {
51
57
  filters.push({
52
58
  key: 'name',
@@ -69,6 +75,10 @@ const handleFilterList = (list, filterOptions) => {
69
75
  RuleFilterMap[ruleValue] && filters.push(RuleFilterMap[ruleValue])
70
76
  // 锁定筛选
71
77
  LockedFilterMap[locked] && filters.push(LockedFilterMap[locked])
78
+ // HTTP Method 筛选
79
+ MethodFilterMap[method] && filters.push(MethodFilterMap[method])
80
+ // 状态码筛选
81
+ StatusFilterMap[status] && filters.push(StatusFilterMap[status])
72
82
 
73
83
  // 执行筛选
74
84
  const filteredList = filters.reduce((result, filter) => {
@@ -78,22 +88,70 @@ const handleFilterList = (list, filterOptions) => {
78
88
  return filteredList
79
89
  }
80
90
 
81
- // 获取文件列表(包含文件属性数据)
82
- const getFullDataList = (localStorage) => {
83
- // 获取接口列表
84
- const list = getApiList(localStorage) || []
85
-
86
- // 合并属性数据
87
- list.forEach((item) => {
88
- const props = getProperty(localStorage, item.name) || {}
89
- delete item.data
90
- Object.assign(item, props)
91
+ /**
92
+ * 过滤组配置,只保留组级别的字段
93
+ * 移除全局元数据字段 (query, payload, method, rule, status, pattern, ruleValue, url, date)
94
+ * 移除版本字段 (version.*, versionMeta.*) - 版本通过单独的 versions 数组导出
95
+ */
96
+ const filterGroupConfig = (groupConfig) => {
97
+ if (!groupConfig) return {}
98
+ // 保留基础配置 + 元数据字段
99
+ // 元数据字段(url, method, date, status等)对于文件列表显示是必需的
100
+ const validKeys = ['mock', 'locked', 'mockVersion', 'mockTime', 'url', 'method', 'date', 'status', 'rule', 'ruleValue']
101
+ const filtered = {}
102
+
103
+ Object.keys(groupConfig).forEach(key => {
104
+ // 仅保留基础配置字段和元数据字段
105
+ if (validKeys.includes(key)) {
106
+ filtered[key] = groupConfig[key]
107
+ }
108
+ // 版本字段通过 versions 数组单独导出,不在 groupConfig 中保留
91
109
  })
92
- return list
110
+
111
+ return filtered
112
+ }
113
+
114
+ // 获取文件列表(包含文件属性数据)
115
+ // 完全隔离架构:直接获取当前组的文件列表
116
+ const getFullDataList = async (storageAdapter, groupManager, groupId) => {
117
+ // 获取组的物理文件列表并合并配置
118
+ const list = await getGroupFileList(storageAdapter, groupId) || []
119
+ const result = []
120
+ const v3Storage = storageAdapter.getV3Storage()
121
+
122
+ // ⚠️ 强制刷新索引缓存
123
+ v3Storage.invalidateIndex(groupId)
124
+
125
+ // 获取索引以查找真实的 fileId
126
+ const index = await v3Storage.getIndex(groupId)
127
+
128
+ for (let idx = 0; idx < list.length; idx++) {
129
+ const item = list[idx]
130
+ const config = await groupManager.getGroupFileConfig(groupId, item.name)
131
+
132
+ if (config && config.url) {
133
+ // 从索引查找真实的 fileId(而不是生成)
134
+ const indexEntry = index.files.find(f => f.url === item.name)
135
+ const fileId = indexEntry ? indexEntry.id : null
136
+
137
+ result.push({
138
+ name: item.name,
139
+ id: fileId, // 添加 fileId 字段
140
+ ...config,
141
+ // ✨ Phase 3.1: 从索引获取增强字段
142
+ domain: indexEntry?.domain || null,
143
+ pathname: indexEntry?.pathname || null,
144
+ urlHash: indexEntry?.urlHash || null,
145
+ })
146
+ }
147
+ }
148
+
149
+ return result
93
150
  }
94
151
 
95
152
  exports.createErrorResponse = createErrorResponse
96
153
  exports.createSuccessResponse = createSuccessResponse
97
154
  exports.execFilter = execFilter
98
155
  exports.handleFilterList = handleFilterList
156
+ exports.filterGroupConfig = filterGroupConfig
99
157
  exports.getFullDataList = getFullDataList
@@ -0,0 +1,105 @@
1
+ /**
2
+ * 参数验证工具
3
+ */
4
+
5
+ // 验证文件名
6
+ const validateFilename = (name) => {
7
+ if (!name || typeof name !== 'string') {
8
+ return '文件名不能为空'
9
+ }
10
+ if (name.length > 2048) {
11
+ return '文件名长度不能超过 2048 字符'
12
+ }
13
+ return null
14
+ }
15
+
16
+ // 验证版本名
17
+ const validateVersionName = (versionName) => {
18
+ if (!versionName || typeof versionName !== 'string') {
19
+ return '版本名不能为空'
20
+ }
21
+ if (versionName.length > 20) {
22
+ return '版本名长度不能超过 20 字符'
23
+ }
24
+ // 不允许可能导致存储问题的特殊字符
25
+ // 允许: 中文、字母、数字、下划线、连字符、空格
26
+ // 不允许: / \ : * ? " < > | 等文件系统保留字符
27
+ // eslint-disable-next-line no-useless-escape
28
+ if (/[\/\\:*?"<>|]/.test(versionName)) {
29
+ return '版本名不能包含特殊字符 / \\ : * ? " < > |'
30
+ }
31
+ return null
32
+ }
33
+
34
+ // 验证 mock 数据内容
35
+ const validateMockData = (data) => {
36
+ // 允许 undefined (使用默认值),但不允许 null
37
+ if (data === null) {
38
+ return 'Mock 数据不能为 null'
39
+ }
40
+ // 如果 data 是 undefined,跳过大小检查(会使用默认值)
41
+ if (data === undefined) {
42
+ return null
43
+ }
44
+ // 限制大小为 5MB
45
+ const dataStr = typeof data === 'string' ? data : JSON.stringify(data)
46
+ if (dataStr.length > 5 * 1024 * 1024) {
47
+ return 'Mock 数据大小不能超过 5MB'
48
+ }
49
+ return null
50
+ }
51
+
52
+ // 验证布尔值
53
+ const validateBoolean = (value, fieldName) => {
54
+ if (typeof value !== 'boolean') {
55
+ return `${fieldName} 必须是布尔值`
56
+ }
57
+ return null
58
+ }
59
+
60
+ // 验证 ruleValue
61
+ const validateRuleValue = (ruleValue) => {
62
+ const validValues = ['pathname', 'href', 'pattern']
63
+ if (!ruleValue || !validValues.includes(ruleValue)) {
64
+ return `ruleValue 必须是 ${validValues.join(', ')} 之一`
65
+ }
66
+ return null
67
+ }
68
+
69
+ // 创建验证中间件
70
+ const validate = (schema) => {
71
+ return async (ctx, next) => {
72
+ const body = ctx.request.body || {}
73
+ const errors = []
74
+
75
+ Object.keys(schema).forEach((key) => {
76
+ const validator = schema[key]
77
+ const value = body[key]
78
+ const error = validator(value)
79
+ if (error) {
80
+ errors.push({ field: key, message: error })
81
+ }
82
+ })
83
+
84
+ if (errors.length > 0) {
85
+ ctx.status = 400
86
+ ctx.body = {
87
+ code: 400,
88
+ msg: '参数验证失败',
89
+ data: errors,
90
+ }
91
+ return
92
+ }
93
+
94
+ await next()
95
+ }
96
+ }
97
+
98
+ module.exports = {
99
+ validateFilename,
100
+ validateVersionName,
101
+ validateMockData,
102
+ validateBoolean,
103
+ validateRuleValue,
104
+ validate,
105
+ }