@tuoyuan/gateway-api-select-config 1.2.1

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,359 @@
1
+ /**
2
+ * 数据提取和格式化工具
3
+ */
4
+
5
+ /**
6
+ * 根据 schema 提取数据
7
+ * @param {Object} data - 原始数据
8
+ * @param {Object} schema - res_schema 配置
9
+ * @returns {any} 提取后的数据
10
+ */
11
+ export function extractDataBySchema(data, schema) {
12
+ // 安全检查
13
+ if (!schema || typeof schema !== 'object') {
14
+ return data;
15
+ }
16
+
17
+ // 优先使用 x_path 定位数据(最外层)
18
+ if (schema.x_path) {
19
+ data = getValueByPath(data, schema.x_path);
20
+ }
21
+ // 其次使用 x_value(兼容旧版)
22
+ else if (schema.x_value) {
23
+ data = getValueByPath(data, schema.x_value);
24
+ }
25
+
26
+ // 判断实际的数据类型
27
+ if (Array.isArray(data)) {
28
+ // 如果 schema 有 items,说明这是个数组 schema
29
+ if (schema.items) {
30
+ return extractArrayData(data, { type: 'array', items: schema.items });
31
+ }
32
+ } else if (schema.type === 'object' && schema.properties) {
33
+ return extractObjectData(data, schema);
34
+ }
35
+
36
+ // 如果 schema 有 format,应用格式化
37
+ if (schema.format && data !== undefined && data !== null) {
38
+ data = applyFormat(data, schema.format);
39
+ }
40
+
41
+ return data;
42
+ }
43
+
44
+ /**
45
+ * 提取数组数据
46
+ * @param {Array} data - 数组数据
47
+ * @param {Object} schema - 数组 schema
48
+ * @returns {Array} 提取后的数组
49
+ */
50
+ function extractArrayData(data, schema) {
51
+ const result = [];
52
+
53
+ if (!Array.isArray(data)) {
54
+ return [];
55
+ }
56
+
57
+ // 安全检查
58
+ if (!schema || !schema.items || typeof schema.items !== 'object') {
59
+ return data;
60
+ }
61
+
62
+ // 检查 schema 是否完整
63
+ const properties = schema.items?.properties;
64
+ const hasValidSchema = properties && Object.keys(properties).length > 0;
65
+
66
+ // 如果 schema 不完整,检查是否有顶层的 x_path 或 x_value
67
+ if (!hasValidSchema) {
68
+ // 如果 items 有 x_path,对每个元素应用路径提取
69
+ if (schema.items?.x_path) {
70
+ return data.map(item => {
71
+ let value = getValueByPath(item, schema.items.x_path);
72
+
73
+ // 应用格式化
74
+ if (schema.items.format && value !== undefined && value !== null) {
75
+ value = applyFormat(value, schema.items.format);
76
+ }
77
+
78
+ return value;
79
+ });
80
+ }
81
+
82
+ // 否则直接返回原始数据
83
+ return data;
84
+ }
85
+
86
+ // 提取每个元素
87
+ data.forEach((item) => {
88
+ const extractedItem = {};
89
+
90
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
91
+ // 安全检查 fieldSchema
92
+ if (!fieldSchema || typeof fieldSchema !== 'object') {
93
+ extractedItem[fieldName] = undefined;
94
+ continue;
95
+ }
96
+
97
+ const itemPath = fieldSchema.x_path;
98
+
99
+ if (itemPath) {
100
+ let value = getValueByPath(item, itemPath);
101
+
102
+ // 应用格式化
103
+ const format = fieldSchema.format;
104
+ if (format && value !== undefined && value !== null) {
105
+ value = applyFormat(value, format);
106
+ }
107
+
108
+ extractedItem[fieldName] = value;
109
+ } else {
110
+ extractedItem[fieldName] = undefined;
111
+ }
112
+ }
113
+
114
+ result.push(extractedItem);
115
+ });
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * 提取对象数据
122
+ * @param {Object} data - 对象数据
123
+ * @param {Object} schema - 对象 schema
124
+ * @returns {Object} 提取后的对象
125
+ */
126
+ function extractObjectData(data, schema) {
127
+ const result = {};
128
+
129
+ // 安全检查
130
+ if (!schema || typeof schema !== 'object') {
131
+ return data;
132
+ }
133
+
134
+ // 检查 schema 是否完整
135
+ const properties = schema.properties;
136
+ const hasValidSchema = properties && Object.keys(properties).length > 0;
137
+
138
+ // 如果 schema 不完整,检查是否有 x_path
139
+ if (!hasValidSchema) {
140
+ // 如果有 x_path,按路径提取
141
+ if (schema.x_path) {
142
+ let value = getValueByPath(data, schema.x_path);
143
+
144
+ // 应用格式化
145
+ if (schema.format && value !== undefined && value !== null) {
146
+ value = applyFormat(value, schema.format);
147
+ }
148
+
149
+ return value;
150
+ }
151
+
152
+ // 否则直接返回原始数据
153
+ return data;
154
+ }
155
+
156
+ for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) {
157
+ // 安全检查 fieldSchema
158
+ if (!fieldSchema || typeof fieldSchema !== 'object') {
159
+ result[fieldName] = undefined;
160
+ continue;
161
+ }
162
+
163
+ // 只有当 x_path 存在时才提取
164
+ if (fieldSchema.x_path) {
165
+ let value = getValueByPath(data, fieldSchema.x_path);
166
+
167
+ // 应用格式化
168
+ const format = fieldSchema.format;
169
+ if (format && value !== undefined && value !== null) {
170
+ value = applyFormat(value, format);
171
+ }
172
+
173
+ result[fieldName] = value;
174
+ } else {
175
+ result[fieldName] = undefined;
176
+ }
177
+ }
178
+
179
+ return result;
180
+ }
181
+
182
+ /**
183
+ * 根据路径获取值
184
+ * @param {Object} obj - 对象
185
+ * @param {String} path - 路径(支持 $ 和 @ 前缀)
186
+ * @returns {any} 值
187
+ */
188
+ function getValueByPath(obj, path) {
189
+ // 如果 path 为空,返回 undefined
190
+ if (!path || typeof path !== 'string') {
191
+ return undefined;
192
+ }
193
+
194
+ // 去掉 JSONPath 的 $ 或 @ 前缀
195
+ let cleanPath = path.replace(/^[$@]\.?/, '');
196
+
197
+ // 分割路径
198
+ const keys = cleanPath.split(/[\.\[\]]/).filter(k => k !== '' && k !== '*');
199
+
200
+ let value = obj;
201
+
202
+ for (const key of keys) {
203
+ if (value === null || value === undefined) {
204
+ return undefined;
205
+ }
206
+ value = value[key];
207
+ }
208
+
209
+ return value;
210
+ }
211
+
212
+ /**
213
+ * 应用格式化
214
+ * @param {any} value - 原始值
215
+ * @param {String} format - 格式化类型
216
+ * @returns {any} 格式化后的值
217
+ */
218
+ export function applyFormat(value, format) {
219
+ try {
220
+ switch (format) {
221
+ // 日期格式
222
+ case 'YYYY-MM-DD':
223
+ return formatDate(value, 'YYYY-MM-DD');
224
+ case 'YYYY-MM-DD HH:mm:ss':
225
+ return formatDate(value, 'YYYY-MM-DD HH:mm:ss');
226
+ case 'YYYY/MM/DD':
227
+ return formatDate(value, 'YYYY/MM/DD');
228
+ case 'timestamp':
229
+ return new Date(value).getTime();
230
+ case 'timestamp_s':
231
+ return Math.floor(new Date(value).getTime() / 1000);
232
+
233
+ // 数字格式
234
+ case 'number':
235
+ return Number(value);
236
+ case 'integer':
237
+ return Math.floor(Number(value));
238
+ case 'currency':
239
+ return '¥' + Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
240
+ case 'percent':
241
+ return (Number(value) * 100).toFixed(2) + '%';
242
+
243
+ // 字符串格式
244
+ case 'uppercase':
245
+ return String(value).toUpperCase();
246
+ case 'lowercase':
247
+ return String(value).toLowerCase();
248
+ case 'trim':
249
+ return String(value).trim();
250
+
251
+ // 通用格式
252
+ case 'string':
253
+ return String(value);
254
+ case 'boolean':
255
+ return Boolean(value);
256
+ case 'json':
257
+ return JSON.stringify(value);
258
+
259
+ default:
260
+ return value;
261
+ }
262
+ } catch (err) {
263
+ console.error('格式化失败:', err);
264
+ return value;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * 日期格式化辅助函数
270
+ * @param {any} value - 日期值
271
+ * @param {String} format - 格式
272
+ * @returns {String} 格式化后的日期字符串
273
+ */
274
+ function formatDate(value, format) {
275
+ const date = new Date(value);
276
+ if (isNaN(date.getTime())) return String(value);
277
+
278
+ const year = date.getFullYear();
279
+ const month = String(date.getMonth() + 1).padStart(2, '0');
280
+ const day = String(date.getDate()).padStart(2, '0');
281
+ const hours = String(date.getHours()).padStart(2, '0');
282
+ const minutes = String(date.getMinutes()).padStart(2, '0');
283
+ const seconds = String(date.getSeconds()).padStart(2, '0');
284
+
285
+ return format
286
+ .replace('YYYY', String(year))
287
+ .replace('MM', month)
288
+ .replace('DD', day)
289
+ .replace('HH', hours)
290
+ .replace('mm', minutes)
291
+ .replace('ss', seconds)
292
+ .replace('/', '/');
293
+ }
294
+
295
+ /**
296
+ * 执行 reqMapping 中的函数表达式
297
+ * @param {Object} reqMapping - 请求参数映射
298
+ * @param {Object} context - 上下文数据
299
+ * @returns {Object} 执行后的参数
300
+ */
301
+ export function executeReqMapping(reqMapping, context = {}) {
302
+ if (!reqMapping) return {};
303
+
304
+ const result = {};
305
+
306
+ for (const [key, value] of Object.entries(reqMapping)) {
307
+ if (typeof value === 'string') {
308
+ // 检查是否是函数表达式
309
+ if (value.includes('=>')) {
310
+ try {
311
+ // 将字符串转换为函数并执行
312
+ // eslint-disable-next-line no-new-func
313
+ const fn = new Function('ctx', `return (${value})(ctx)`);
314
+ result[key] = fn(context);
315
+ } catch (err) {
316
+ console.error(`执行函数表达式失败 [${key}]:`, err);
317
+ result[key] = value; // 失败时使用原始值
318
+ }
319
+ }
320
+ // 检查是否是纯模板变量(不带引号的):${varName}
321
+ else if (value.match(/^\$\{[^}]+\}$/)) {
322
+ const varName = value.match(/^\$\{([^}]+)\}$/)[1];
323
+ const contextValue = context[varName];
324
+ result[key] = contextValue !== undefined ? contextValue : value;
325
+ }
326
+ // 检查是否是模板字符串(带其他内容)
327
+ else if (value.includes('${')) {
328
+ try {
329
+ // 替换模板变量
330
+ let replaced = value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
331
+ return context[varName] !== undefined ? context[varName] : match;
332
+ });
333
+
334
+ // 尝试转换类型:如果替换后的值看起来像数字,转换为数字
335
+ if (replaced !== value) { // 确实发生了替换
336
+ // 如果整个字符串都是数字,转换为数字
337
+ const num = Number(replaced);
338
+ if (!isNaN(num) && replaced.trim() !== '') {
339
+ result[key] = num;
340
+ } else {
341
+ result[key] = replaced;
342
+ }
343
+ } else {
344
+ result[key] = replaced;
345
+ }
346
+ } catch (err) {
347
+ console.error(`解析模板字符串失败 [${key}]:`, err);
348
+ result[key] = value;
349
+ }
350
+ } else {
351
+ result[key] = value;
352
+ }
353
+ } else {
354
+ result[key] = value;
355
+ }
356
+ }
357
+
358
+ return result;
359
+ }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * 通用数据过滤器工具
3
+ * 提供动态、可配置的数据处理能力
4
+ */
5
+
6
+ import { applyFormat } from './dataExtractor.js';
7
+
8
+ /**
9
+ * 通用数据提取器
10
+ * 自动从响应中提取数组数据
11
+ * @param {*} response - 接口响应数据
12
+ * @param {string} path - 可选的提取路径,如 'data.list' 或 '@.data.list'
13
+ * @returns {Array} 提取出的数组数据
14
+ */
15
+ export function extractArray(response, path = null) {
16
+ // 如果指定了路径,按路径提取
17
+ if (path) {
18
+ const cleanPath = path.replace(/^@\./, ''); // 移除 @ 前缀
19
+ const keys = cleanPath.split('.');
20
+ let result = response;
21
+
22
+ for (const key of keys) {
23
+ if (result && typeof result === 'object') {
24
+ result = result[key];
25
+ } else {
26
+ result = null;
27
+ break;
28
+ }
29
+ }
30
+
31
+ if (Array.isArray(result)) {
32
+ return result;
33
+ }
34
+ }
35
+
36
+ // 如果已经是数组,直接返回
37
+ if (Array.isArray(response)) {
38
+ return response;
39
+ }
40
+
41
+ // 如果是对象,尝试提取常见的列表字段
42
+ if (typeof response === 'object' && response !== null) {
43
+ const possibleKeys = ['data', 'list', 'items', 'records', 'rows', 'result', 'content'];
44
+
45
+ for (const key of possibleKeys) {
46
+ if (Array.isArray(response[key])) {
47
+ return response[key];
48
+ }
49
+
50
+ // 递归查找一层 (如 data.list)
51
+ if (response[key] && typeof response[key] === 'object') {
52
+ for (const subKey of possibleKeys) {
53
+ if (Array.isArray(response[key][subKey])) {
54
+ return response[key][subKey];
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ // 如果找不到数组,返回包含该对象的数组
61
+ return [response];
62
+ }
63
+
64
+ // 其他情况返回空数组
65
+ return [];
66
+ }
67
+
68
+ /**
69
+ * 通用字段映射器
70
+ * 根据映射配置提取和转换字段
71
+ * @param {Object} item - 原始数据项
72
+ * @param {Object} fieldMapping - 字段映射配置 { targetField: sourceField | Function }
73
+ * @param {Object} fieldTypes - 字段类型配置 { targetField: 'string' | 'number' | 'integer' | 'boolean' | 'date' }
74
+ * @param {Object} fieldFormats - 字段格式化配置 { targetField: formatString }
75
+ * @returns {Object} 映射后的对象
76
+ */
77
+ export function mapFields(item, fieldMapping, fieldTypes = {}, fieldFormats = {}) {
78
+ if (!item || typeof item !== 'object') return item;
79
+
80
+ const result = {};
81
+
82
+ for (const [targetField, source] of Object.entries(fieldMapping)) {
83
+ let value;
84
+
85
+ // 如果是函数,执行函数
86
+ if (typeof source === 'function') {
87
+ value = source(item);
88
+ }
89
+ // 如果是字符串,从原始对象中提取
90
+ else if (typeof source === 'string') {
91
+ // 支持嵌套路径,如 'user.name'
92
+ const keys = source.split('.');
93
+ value = item;
94
+ for (const key of keys) {
95
+ if (value && typeof value === 'object') {
96
+ value = value[key];
97
+ } else {
98
+ value = undefined;
99
+ break;
100
+ }
101
+ }
102
+ }
103
+ // 如果是数组,尝试多个字段 (fallback)
104
+ else if (Array.isArray(source)) {
105
+ for (const field of source) {
106
+ const fieldValue = item[field];
107
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
108
+ value = fieldValue;
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ // 直接赋值
114
+ else {
115
+ value = source;
116
+ }
117
+
118
+ // 应用类型转换
119
+ const fieldType = fieldTypes[targetField];
120
+ if (fieldType && value !== undefined && value !== null) {
121
+ value = convertType(value, fieldType);
122
+ }
123
+
124
+ // 应用格式化
125
+ const fieldFormat = fieldFormats[targetField];
126
+ if (fieldFormat && value !== undefined && value !== null) {
127
+ value = applyFormat(value, fieldFormat);
128
+ }
129
+
130
+ result[targetField] = value;
131
+ }
132
+
133
+ return result;
134
+ }
135
+
136
+ /**
137
+ * 类型转换辅助函数
138
+ * @param {any} value - 原始值
139
+ * @param {string} type - 目标类型
140
+ * @returns {any} 转换后的值
141
+ */
142
+ function convertType(value, type) {
143
+ try {
144
+ switch (type) {
145
+ case 'string':
146
+ // 如果值是对象,不要转换(保持原样),除非配置了 json 格式化
147
+ if (typeof value === 'object' && value !== null) {
148
+ return value;
149
+ }
150
+ return String(value);
151
+ case 'number':
152
+ return Number(value);
153
+ case 'integer':
154
+ return Math.floor(Number(value));
155
+ case 'boolean':
156
+ return Boolean(value);
157
+ case 'date':
158
+ // 保持原始值,让格式化函数处理
159
+ return value;
160
+ default:
161
+ return value;
162
+ }
163
+ } catch (err) {
164
+ console.error('类型转换失败:', err);
165
+ return value;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * 通用过滤器
171
+ * 组合 extractArray 和 mapFields
172
+ * @param {*} response - 接口响应
173
+ * @param {Object} config - 过滤器配置
174
+ * @param {string} config.arrayPath - 数组提取路径
175
+ * @param {Object} config.fieldMapping - 字段映射配置
176
+ * @param {Object} config.fieldTypes - 字段类型配置
177
+ * @param {Object} config.fieldFormats - 字段格式化配置
178
+ * @param {Function} config.itemFilter - 可选的项过滤函数
179
+ * @param {Function} config.itemTransform - 可选的项转换函数
180
+ * @returns {Array} 处理后的数组
181
+ */
182
+ export function applyFilter(response, config) {
183
+ console.log('=== applyFilter 调试信息 ===');
184
+ console.log('1. 原始响应:', response);
185
+ console.log('2. 过滤器配置:', config);
186
+
187
+ // 1. 提取数组
188
+ let items = extractArray(response, config.arrayPath);
189
+ console.log('3. 提取的数组 (extractArray):', items);
190
+
191
+ // 2. 过滤项 (可选)
192
+ if (config.itemFilter && typeof config.itemFilter === 'function') {
193
+ items = items.filter(config.itemFilter);
194
+ console.log('4. 过滤后 (itemFilter):', items);
195
+ }
196
+
197
+ // 3. 映射字段 (可选)
198
+ if (config.fieldMapping) {
199
+ console.log('5. 应用字段映射:', config.fieldMapping);
200
+ items = items.map(item => mapFields(
201
+ item,
202
+ config.fieldMapping,
203
+ config.fieldTypes || {},
204
+ config.fieldFormats || {}
205
+ ));
206
+ console.log('6. 映射后的数据:', items);
207
+ } else {
208
+ console.log('5. 没有配置字段映射,返回原始数据');
209
+ }
210
+
211
+ // 4. 自定义转换 (可选)
212
+ if (config.itemTransform && typeof config.itemTransform === 'function') {
213
+ items = items.map(config.itemTransform);
214
+ console.log('7. 自定义转换后:', items);
215
+ }
216
+
217
+ console.log('8. 最终返回的数据:', items);
218
+ console.log('=== applyFilter 结束 ===');
219
+
220
+ return items;
221
+ }
222
+
223
+ // 兼容旧的简单过滤器(已废弃,保留用于兼容)
224
+ export const noticeFilter = (data) => {
225
+ const items = extractArray(data);
226
+ return items.map(item => {
227
+ if (typeof item === 'string') return item;
228
+ if (typeof item === 'object') {
229
+ return item.content || item.title || item.message || item.text || item.notice || '';
230
+ }
231
+ return String(item);
232
+ });
233
+ };
234
+
235
+ export const newsFilter = (data) => extractArray(data);
236
+ export const menuFilter = (data) => extractArray(data);
237
+ export const userFilter = (data) => data;
238
+
239
+ export const filters = {
240
+ notice: noticeFilter,
241
+ news: newsFilter,
242
+ menu: menuFilter,
243
+ user: userFilter
244
+ };
245
+
246
+ export function applyFilterByName(response, filterName) {
247
+ const filter = filters[filterName];
248
+ if (filter) {
249
+ return filter(response);
250
+ }
251
+ return extractArray(response);
252
+ }
253
+
254
+ /**
255
+ * 统一的数据获取函数
256
+ * 根据配置获取并处理数据,返回最终结果
257
+ * @param {Object} gatewayConfig - 网关配置对象(从 ApiConfig 组件保存的配置)
258
+ * @param {Function} requestFunc - 请求函数(例如 gateway-request-lib 的 request)
259
+ * @param {Object} context - 上下文数据(用于入参模板和函数)
260
+ * @returns {Promise<Array>} 处理后的数据数组
261
+ */
262
+ export async function fetchGatewayData(gatewayConfig, requestFunc, context = {}) {
263
+ if (!gatewayConfig || !gatewayConfig.apiPath) {
264
+ throw new Error('缺少必要的配置: apiPath');
265
+ }
266
+
267
+ // 1. 执行入参映射
268
+ let params = {};
269
+ if (gatewayConfig.reqMapping) {
270
+ const { executeReqMapping } = await import('./dataExtractor.js');
271
+ params = executeReqMapping(gatewayConfig.reqMapping, context);
272
+ }
273
+
274
+ // 2. 调用接口
275
+ const response = await requestFunc({
276
+ api: gatewayConfig.apiPath,
277
+ params: params,
278
+ config: {
279
+ access_token: true,
280
+ ...gatewayConfig.requestConfig
281
+ }
282
+ });
283
+
284
+ // 3. 提取业务数据
285
+ // gateway-request-lib 返回: { axiosResponse, response, data }
286
+ // 业务数据在 response 字段中
287
+ const businessData = response.response || response.data || response;
288
+
289
+ // 4. 应用过滤器
290
+ if (gatewayConfig.mode === 'filter' && gatewayConfig.filterConfig) {
291
+ return applyFilter(businessData, gatewayConfig.filterConfig);
292
+ } else if (gatewayConfig.res_schema) {
293
+ // 兼容 Schema 模式
294
+ const { extractDataBySchema } = await import('./dataExtractor.js');
295
+ return extractDataBySchema(businessData, gatewayConfig.res_schema);
296
+ } else {
297
+ // 默认: 尝试提取数组
298
+ return extractArray(businessData);
299
+ }
300
+ }