midway-fatcms 0.0.6 → 0.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.
- package/.qoder/skills/midway-fatcms-crud/SKILL.md +375 -0
- package/.qoder/skills/midway-fatcms-crud/examples.md +990 -0
- package/.qoder/skills/midway-fatcms-crud/reference.md +568 -0
- package/dist/libs/crud-pro/models/RequestModel.d.ts +0 -36
- package/dist/libs/crud-pro/models/RequestModel.js +2 -99
- package/dist/libs/crud-pro/utils/OrderByUtils.d.ts +70 -0
- package/dist/libs/crud-pro/utils/OrderByUtils.js +158 -0
- package/dist/libs/crud-sharding/ShardingCrudPro.js +15 -39
- package/dist/libs/crud-sharding/ShardingMerger.d.ts +2 -2
- package/dist/libs/crud-sharding/ShardingMerger.js +11 -9
- package/dist/service/curd/README.md +101 -2
- package/package.json +2 -1
- package/src/libs/crud-pro/models/RequestModel.ts +2 -108
- package/src/libs/crud-pro/utils/OrderByUtils.ts +169 -0
- package/src/libs/crud-sharding/ShardingCrudPro.ts +55 -76
- package/src/libs/crud-sharding/ShardingMerger.ts +14 -12
- package/src/service/curd/README.md +101 -2
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.RequestModel = void 0;
|
|
4
4
|
const _ = require("lodash");
|
|
5
5
|
const MixinUtils_1 = require("../utils/MixinUtils");
|
|
6
|
+
const OrderByUtils_1 = require("../utils/OrderByUtils");
|
|
6
7
|
const exceptions_1 = require("../exceptions");
|
|
7
8
|
const defaultConfigs_1 = require("../defaultConfigs");
|
|
8
9
|
class RequestModel {
|
|
@@ -10,109 +11,11 @@ class RequestModel {
|
|
|
10
11
|
Object.assign(this, req);
|
|
11
12
|
this.visitor = visitor;
|
|
12
13
|
this.columns = MixinUtils_1.MixinUtils.parseColumns(req.columns);
|
|
13
|
-
this.orderBys =
|
|
14
|
+
this.orderBys = OrderByUtils_1.OrderByUtils.parseOrderBys(req.orderBy);
|
|
14
15
|
const limitOffset = this.parseOffsetList(req);
|
|
15
16
|
this.limit = limitOffset.limit;
|
|
16
17
|
this.offset = limitOffset.offset;
|
|
17
18
|
}
|
|
18
|
-
/**
|
|
19
|
-
* 解析 orderBy 参数为 IOrderByItem 数组
|
|
20
|
-
*
|
|
21
|
-
* 支持的格式:
|
|
22
|
-
* 1. 字符串格式(逗号分隔多个字段):
|
|
23
|
-
* - 标准 SQL 格式:'created_at DESC, amount ASC'
|
|
24
|
-
* - 简写 +/- 格式:'created_at-, amount+'('-' 表示 DESC,'+' 或省略表示 ASC)
|
|
25
|
-
* - 默认升序:'order_id'(无后缀时默认为 ASC)
|
|
26
|
-
*
|
|
27
|
-
* 2. 数组格式:
|
|
28
|
-
* - 纯对象数组:[{ fieldName: 'created_at', orderType: 'desc' }]
|
|
29
|
-
* - 混合数组1(字符串+对象):['order_id', { fieldName: 'amount', orderType: 'asc' }]
|
|
30
|
-
* - 混合数组2(字符串+对象):['order_id+', { fieldName: 'amount', orderType: 'asc' }]
|
|
31
|
-
* - 混合数组3(字符串+对象):['order_id DESC', { fieldName: 'amount', orderType: 'asc' }]
|
|
32
|
-
* 注:数组中的字符串元素支持标准 SQL 格式和简写格式,与字符串参数格式一致。
|
|
33
|
-
*
|
|
34
|
-
* SQL 注入防护:
|
|
35
|
-
* - 所有字段名必须通过 MixinUtils.isValidFieldName() 校验
|
|
36
|
-
* - 只允许 ASC/DESC 作为排序方向
|
|
37
|
-
*
|
|
38
|
-
* @param orderByStr 排序参数,可以是字符串或数组
|
|
39
|
-
* @returns IOrderByItem[] 解析后的排序项数组
|
|
40
|
-
*/
|
|
41
|
-
parseOrderBys(orderByStr) {
|
|
42
|
-
if (MixinUtils_1.MixinUtils.isEmpty(orderByStr)) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
// 数组格式:支持字符串和对象混合
|
|
46
|
-
if (Array.isArray(orderByStr)) {
|
|
47
|
-
return orderByStr
|
|
48
|
-
.map(item => this.parseOrderByItem(item))
|
|
49
|
-
.filter((o) => !!o);
|
|
50
|
-
}
|
|
51
|
-
// 字符串格式:逗号分隔多个字段
|
|
52
|
-
return orderByStr
|
|
53
|
-
.split(',')
|
|
54
|
-
.map(s => s.trim())
|
|
55
|
-
.filter(s => !!s)
|
|
56
|
-
.map(item => this.parseOrderByString(item))
|
|
57
|
-
.filter((o) => !!o);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* 解析单个排序项(数组元素)
|
|
61
|
-
*/
|
|
62
|
-
parseOrderByItem(item) {
|
|
63
|
-
// 字符串格式:解析标准 SQL 格式或简写格式
|
|
64
|
-
if (typeof item === 'string') {
|
|
65
|
-
return this.parseOrderByString(item);
|
|
66
|
-
}
|
|
67
|
-
// 对象格式:提取 fieldName 和 orderType
|
|
68
|
-
const { fieldName, orderType = 'asc' } = item || {};
|
|
69
|
-
if (!fieldName) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
this.validateFieldName(fieldName, fieldName);
|
|
73
|
-
return { fieldName, orderType };
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* 解析字符串格式的排序项(支持标准 SQL 和简写格式)
|
|
77
|
-
*/
|
|
78
|
-
parseOrderByString(orderByStr) {
|
|
79
|
-
let orderType = 'asc';
|
|
80
|
-
let fieldName = orderByStr;
|
|
81
|
-
// 检查是否为空格分隔的标准 SQL 格式(如 'created_at DESC')
|
|
82
|
-
const spaceIndex = orderByStr.lastIndexOf(' ');
|
|
83
|
-
if (spaceIndex > 0) {
|
|
84
|
-
const beforeSpace = orderByStr.substring(0, spaceIndex).trim();
|
|
85
|
-
const afterSpace = orderByStr.substring(spaceIndex + 1).trim().toUpperCase();
|
|
86
|
-
if (afterSpace === 'ASC' || afterSpace === 'DESC') {
|
|
87
|
-
fieldName = beforeSpace;
|
|
88
|
-
orderType = afterSpace.toLowerCase();
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
throw new exceptions_1.CommonException(exceptions_1.Exceptions.REQUEST_MODEL_PARSE_ORDER_BY_FAILED, orderByStr);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
else if (orderByStr.endsWith('+')) {
|
|
95
|
-
// 简写格式:+ 表示升序
|
|
96
|
-
fieldName = orderByStr.slice(0, -1);
|
|
97
|
-
orderType = 'asc';
|
|
98
|
-
}
|
|
99
|
-
else if (orderByStr.endsWith('-')) {
|
|
100
|
-
// 简写格式:- 表示降序
|
|
101
|
-
fieldName = orderByStr.slice(0, -1);
|
|
102
|
-
orderType = 'desc';
|
|
103
|
-
}
|
|
104
|
-
fieldName = fieldName.trim();
|
|
105
|
-
this.validateFieldName(fieldName, orderByStr);
|
|
106
|
-
return { fieldName, orderType };
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* SQL 注入防护:校验字段名格式
|
|
110
|
-
*/
|
|
111
|
-
validateFieldName(fieldName, originalValue) {
|
|
112
|
-
if (MixinUtils_1.MixinUtils.isEmpty(fieldName) || !MixinUtils_1.MixinUtils.isValidFieldName(fieldName)) {
|
|
113
|
-
throw new exceptions_1.CommonException(exceptions_1.Exceptions.REQUEST_MODEL_PARSE_ORDER_BY_FAILED, originalValue);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
19
|
parseOffsetList(req) {
|
|
117
20
|
const { limit, offset, pageSize, pageNo } = req;
|
|
118
21
|
const limitOffset = {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { IOrderByItem } from '../interfaces';
|
|
2
|
+
/**
|
|
3
|
+
* OrderBy 解析工具类
|
|
4
|
+
*
|
|
5
|
+
* 提供统一的 orderBy 参数解析功能,支持多种格式:
|
|
6
|
+
* 1. 字符串格式(逗号分隔多个字段):
|
|
7
|
+
* - 标准 SQL 格式:'created_at DESC, amount ASC'
|
|
8
|
+
* - 简写 +/- 格式:'created_at-, amount+'('-' 表示 DESC,'+' 或省略表示 ASC)
|
|
9
|
+
* - 默认升序:'order_id'(无后缀时默认为 ASC)
|
|
10
|
+
*
|
|
11
|
+
* 2. 数组格式:
|
|
12
|
+
* - 纯对象数组:[{ fieldName: 'created_at', orderType: 'desc' }]
|
|
13
|
+
* - 混合数组(字符串+对象):['order_id+', { fieldName: 'amount', orderType: 'asc' }]
|
|
14
|
+
*
|
|
15
|
+
* SQL 注入防护:
|
|
16
|
+
* - 所有字段名必须通过 MixinUtils.isValidFieldName() 校验
|
|
17
|
+
* - 只允许 ASC/DESC 作为排序方向
|
|
18
|
+
*/
|
|
19
|
+
export declare class OrderByUtils {
|
|
20
|
+
/**
|
|
21
|
+
* 解析 orderBy 参数为 IOrderByItem 数组
|
|
22
|
+
*
|
|
23
|
+
* @param orderByStr 排序参数,可以是字符串或数组
|
|
24
|
+
* @returns IOrderByItem[] 解析后的排序项数组
|
|
25
|
+
*/
|
|
26
|
+
static parseOrderBys(orderByStr: any): IOrderByItem[];
|
|
27
|
+
/**
|
|
28
|
+
* 获取第一个排序项
|
|
29
|
+
*
|
|
30
|
+
* @param orderByStr 排序参数
|
|
31
|
+
* @returns IOrderByItem | null 第一个排序项,无则返回 null
|
|
32
|
+
*/
|
|
33
|
+
static getFirstOrderBy(orderByStr: any): IOrderByItem | null;
|
|
34
|
+
/**
|
|
35
|
+
* 判断是否为 ASC 升序排序
|
|
36
|
+
*
|
|
37
|
+
* @param orderByStr 排序参数
|
|
38
|
+
* @returns true 表示 ASC,false 表示 DESC 或无排序
|
|
39
|
+
*/
|
|
40
|
+
static isFirstOrderByAsc(orderByStr: any): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* 判断是否为 DESC 降序排序
|
|
43
|
+
*
|
|
44
|
+
* @param orderByStr 排序参数
|
|
45
|
+
* @returns true 表示 DESC,false 表示 ASC 或无排序
|
|
46
|
+
*/
|
|
47
|
+
static isFirstOrderByDesc(orderByStr: any): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* 判断排序字段是否匹配指定的时间字段
|
|
50
|
+
*
|
|
51
|
+
* 用于分表查询时校验排序字段是否为分表字段
|
|
52
|
+
*
|
|
53
|
+
* @param orderByStr 排序参数
|
|
54
|
+
* @param timeColumn 时间字段名
|
|
55
|
+
* @returns true 表示匹配,false 表示不匹配或无排序
|
|
56
|
+
*/
|
|
57
|
+
static isOrderByTimeColumn(orderByStr: any, timeColumn: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* 解析单个排序项(数组元素)
|
|
60
|
+
*/
|
|
61
|
+
private static parseOrderByItem;
|
|
62
|
+
/**
|
|
63
|
+
* 解析字符串格式的排序项(支持标准 SQL 和简写格式)
|
|
64
|
+
*/
|
|
65
|
+
private static parseOrderByString;
|
|
66
|
+
/**
|
|
67
|
+
* SQL 注入防护:校验字段名格式
|
|
68
|
+
*/
|
|
69
|
+
private static validateFieldName;
|
|
70
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OrderByUtils = void 0;
|
|
4
|
+
const MixinUtils_1 = require("./MixinUtils");
|
|
5
|
+
const exceptions_1 = require("../exceptions");
|
|
6
|
+
/**
|
|
7
|
+
* OrderBy 解析工具类
|
|
8
|
+
*
|
|
9
|
+
* 提供统一的 orderBy 参数解析功能,支持多种格式:
|
|
10
|
+
* 1. 字符串格式(逗号分隔多个字段):
|
|
11
|
+
* - 标准 SQL 格式:'created_at DESC, amount ASC'
|
|
12
|
+
* - 简写 +/- 格式:'created_at-, amount+'('-' 表示 DESC,'+' 或省略表示 ASC)
|
|
13
|
+
* - 默认升序:'order_id'(无后缀时默认为 ASC)
|
|
14
|
+
*
|
|
15
|
+
* 2. 数组格式:
|
|
16
|
+
* - 纯对象数组:[{ fieldName: 'created_at', orderType: 'desc' }]
|
|
17
|
+
* - 混合数组(字符串+对象):['order_id+', { fieldName: 'amount', orderType: 'asc' }]
|
|
18
|
+
*
|
|
19
|
+
* SQL 注入防护:
|
|
20
|
+
* - 所有字段名必须通过 MixinUtils.isValidFieldName() 校验
|
|
21
|
+
* - 只允许 ASC/DESC 作为排序方向
|
|
22
|
+
*/
|
|
23
|
+
class OrderByUtils {
|
|
24
|
+
/**
|
|
25
|
+
* 解析 orderBy 参数为 IOrderByItem 数组
|
|
26
|
+
*
|
|
27
|
+
* @param orderByStr 排序参数,可以是字符串或数组
|
|
28
|
+
* @returns IOrderByItem[] 解析后的排序项数组
|
|
29
|
+
*/
|
|
30
|
+
static parseOrderBys(orderByStr) {
|
|
31
|
+
if (MixinUtils_1.MixinUtils.isEmpty(orderByStr)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
// 数组格式:支持字符串和对象混合
|
|
35
|
+
if (Array.isArray(orderByStr)) {
|
|
36
|
+
return orderByStr
|
|
37
|
+
.map(item => this.parseOrderByItem(item))
|
|
38
|
+
.filter((o) => !!o);
|
|
39
|
+
}
|
|
40
|
+
// 字符串格式:逗号分隔多个字段
|
|
41
|
+
return orderByStr
|
|
42
|
+
.split(',')
|
|
43
|
+
.map(s => s.trim())
|
|
44
|
+
.filter(s => !!s)
|
|
45
|
+
.map(item => this.parseOrderByString(item))
|
|
46
|
+
.filter((o) => !!o);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 获取第一个排序项
|
|
50
|
+
*
|
|
51
|
+
* @param orderByStr 排序参数
|
|
52
|
+
* @returns IOrderByItem | null 第一个排序项,无则返回 null
|
|
53
|
+
*/
|
|
54
|
+
static getFirstOrderBy(orderByStr) {
|
|
55
|
+
const orderBys = this.parseOrderBys(orderByStr);
|
|
56
|
+
return orderBys.length > 0 ? orderBys[0] : null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 判断是否为 ASC 升序排序
|
|
60
|
+
*
|
|
61
|
+
* @param orderByStr 排序参数
|
|
62
|
+
* @returns true 表示 ASC,false 表示 DESC 或无排序
|
|
63
|
+
*/
|
|
64
|
+
static isFirstOrderByAsc(orderByStr) {
|
|
65
|
+
const first = this.getFirstOrderBy(orderByStr);
|
|
66
|
+
if (!first) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
return first.orderType.toUpperCase() === 'ASC';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 判断是否为 DESC 降序排序
|
|
73
|
+
*
|
|
74
|
+
* @param orderByStr 排序参数
|
|
75
|
+
* @returns true 表示 DESC,false 表示 ASC 或无排序
|
|
76
|
+
*/
|
|
77
|
+
static isFirstOrderByDesc(orderByStr) {
|
|
78
|
+
const first = this.getFirstOrderBy(orderByStr);
|
|
79
|
+
if (!first) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return first.orderType.toUpperCase() === 'DESC';
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 判断排序字段是否匹配指定的时间字段
|
|
86
|
+
*
|
|
87
|
+
* 用于分表查询时校验排序字段是否为分表字段
|
|
88
|
+
*
|
|
89
|
+
* @param orderByStr 排序参数
|
|
90
|
+
* @param timeColumn 时间字段名
|
|
91
|
+
* @returns true 表示匹配,false 表示不匹配或无排序
|
|
92
|
+
*/
|
|
93
|
+
static isOrderByTimeColumn(orderByStr, timeColumn) {
|
|
94
|
+
const first = this.getFirstOrderBy(orderByStr);
|
|
95
|
+
if (!first) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return first.fieldName === timeColumn;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 解析单个排序项(数组元素)
|
|
102
|
+
*/
|
|
103
|
+
static parseOrderByItem(item) {
|
|
104
|
+
// 字符串格式:解析标准 SQL 格式或简写格式
|
|
105
|
+
if (typeof item === 'string') {
|
|
106
|
+
return this.parseOrderByString(item);
|
|
107
|
+
}
|
|
108
|
+
// 对象格式:提取 fieldName 和 orderType
|
|
109
|
+
const { fieldName, orderType = 'asc' } = item || {};
|
|
110
|
+
if (!fieldName) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
this.validateFieldName(fieldName, fieldName);
|
|
114
|
+
return { fieldName, orderType };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 解析字符串格式的排序项(支持标准 SQL 和简写格式)
|
|
118
|
+
*/
|
|
119
|
+
static parseOrderByString(orderByStr) {
|
|
120
|
+
let orderType = 'asc';
|
|
121
|
+
let fieldName = orderByStr;
|
|
122
|
+
// 检查是否为空格分隔的标准 SQL 格式(如 'created_at DESC')
|
|
123
|
+
const spaceIndex = orderByStr.lastIndexOf(' ');
|
|
124
|
+
if (spaceIndex > 0) {
|
|
125
|
+
const beforeSpace = orderByStr.substring(0, spaceIndex).trim();
|
|
126
|
+
const afterSpace = orderByStr.substring(spaceIndex + 1).trim().toUpperCase();
|
|
127
|
+
if (afterSpace === 'ASC' || afterSpace === 'DESC') {
|
|
128
|
+
fieldName = beforeSpace;
|
|
129
|
+
orderType = afterSpace.toLowerCase();
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
throw new exceptions_1.CommonException(exceptions_1.Exceptions.REQUEST_MODEL_PARSE_ORDER_BY_FAILED, orderByStr);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else if (orderByStr.endsWith('+')) {
|
|
136
|
+
// 简写格式:+ 表示升序
|
|
137
|
+
fieldName = orderByStr.slice(0, -1);
|
|
138
|
+
orderType = 'asc';
|
|
139
|
+
}
|
|
140
|
+
else if (orderByStr.endsWith('-')) {
|
|
141
|
+
// 简写格式:- 表示降序
|
|
142
|
+
fieldName = orderByStr.slice(0, -1);
|
|
143
|
+
orderType = 'desc';
|
|
144
|
+
}
|
|
145
|
+
fieldName = fieldName.trim();
|
|
146
|
+
this.validateFieldName(fieldName, orderByStr);
|
|
147
|
+
return { fieldName, orderType };
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* SQL 注入防护:校验字段名格式
|
|
151
|
+
*/
|
|
152
|
+
static validateFieldName(fieldName, originalValue) {
|
|
153
|
+
if (MixinUtils_1.MixinUtils.isEmpty(fieldName) || !MixinUtils_1.MixinUtils.isValidFieldName(fieldName)) {
|
|
154
|
+
throw new exceptions_1.CommonException(exceptions_1.Exceptions.REQUEST_MODEL_PARSE_ORDER_BY_FAILED, originalValue);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
exports.OrderByUtils = OrderByUtils;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ShardingCrudPro = void 0;
|
|
4
|
+
const OrderByUtils_1 = require("../../libs/crud-pro/utils/OrderByUtils");
|
|
4
5
|
const keys_1 = require("../../libs/crud-pro/models/keys");
|
|
5
6
|
const ShardingConfig_1 = require("./ShardingConfig");
|
|
6
7
|
const ShardingRouter_1 = require("./ShardingRouter");
|
|
@@ -603,38 +604,26 @@ class ShardingCrudPro {
|
|
|
603
604
|
return;
|
|
604
605
|
}
|
|
605
606
|
const orderBy = reqJson.orderBy;
|
|
606
|
-
const expectedDesc = `${timeColumn} DESC`;
|
|
607
|
-
const expectedAsc = `${timeColumn} ASC`;
|
|
608
607
|
// 1. 必须传 orderBy
|
|
609
608
|
if (!orderBy) {
|
|
610
609
|
throw new Error(`[ShardingCrudPro] 查询操作必须传 orderBy 参数。` +
|
|
611
610
|
`期望值: '${timeColumn} DESC' 或 '${timeColumn} ASC'`);
|
|
612
611
|
}
|
|
613
|
-
// 2.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
`当前值: '${orderBy}'`);
|
|
619
|
-
}
|
|
612
|
+
// 2. 使用工具类解析并校验首个排序字段是否为 timeColumn
|
|
613
|
+
const firstOrderBy = OrderByUtils_1.OrderByUtils.getFirstOrderBy(orderBy);
|
|
614
|
+
if (!firstOrderBy) {
|
|
615
|
+
throw new Error(`[ShardingCrudPro] orderBy 参数格式错误,无法解析。` +
|
|
616
|
+
`期望值: '${timeColumn} DESC' 或 '${timeColumn} ASC'`);
|
|
620
617
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
throw new Error(`[ShardingCrudPro] orderBy 数组不能为空,首个排序字段必须为 '${timeColumn} DESC' 或 '${timeColumn} ASC'`);
|
|
625
|
-
}
|
|
626
|
-
const firstItem = orderBy[0];
|
|
627
|
-
const firstItemStr = typeof firstItem === 'string'
|
|
628
|
-
? firstItem
|
|
629
|
-
: `${firstItem.fieldName} ${firstItem.orderType || 'ASC'}`;
|
|
630
|
-
const upper = firstItemStr.trim().toUpperCase();
|
|
631
|
-
if (upper !== expectedDesc.toUpperCase() && upper !== expectedAsc.toUpperCase()) {
|
|
632
|
-
throw new Error(`[ShardingCrudPro] orderBy 数组首个排序字段必须为 '${timeColumn} DESC' 或 '${timeColumn} ASC',` +
|
|
633
|
-
`当前值: '${firstItemStr}'`);
|
|
634
|
-
}
|
|
618
|
+
if (firstOrderBy.fieldName !== timeColumn) {
|
|
619
|
+
throw new Error(`[ShardingCrudPro] orderBy 首个排序字段必须为 '${timeColumn}',` +
|
|
620
|
+
`当前值: '${firstOrderBy.fieldName}'`);
|
|
635
621
|
}
|
|
636
|
-
|
|
637
|
-
|
|
622
|
+
// 校验排序方向是否为 ASC 或 DESC
|
|
623
|
+
const orderType = firstOrderBy.orderType.toUpperCase();
|
|
624
|
+
if (orderType !== 'ASC' && orderType !== 'DESC') {
|
|
625
|
+
throw new Error(`[ShardingCrudPro] orderBy 排序方向必须是 'ASC' 或 'DESC',` +
|
|
626
|
+
`当前值: '${firstOrderBy.orderType}'`);
|
|
638
627
|
}
|
|
639
628
|
}
|
|
640
629
|
/**
|
|
@@ -646,20 +635,7 @@ class ShardingCrudPro {
|
|
|
646
635
|
* @returns true 表示 ASC,false 表示 DESC
|
|
647
636
|
*/
|
|
648
637
|
isAscOrderBy(reqJson) {
|
|
649
|
-
|
|
650
|
-
if (!orderBy)
|
|
651
|
-
return false;
|
|
652
|
-
if (typeof orderBy === 'string') {
|
|
653
|
-
return orderBy.trim().toUpperCase().endsWith('ASC');
|
|
654
|
-
}
|
|
655
|
-
if (Array.isArray(orderBy) && orderBy.length > 0) {
|
|
656
|
-
const firstItem = orderBy[0];
|
|
657
|
-
const firstItemStr = typeof firstItem === 'string'
|
|
658
|
-
? firstItem
|
|
659
|
-
: `${firstItem.fieldName} ${firstItem.orderType || 'ASC'}`;
|
|
660
|
-
return firstItemStr.trim().toUpperCase().endsWith('ASC');
|
|
661
|
-
}
|
|
662
|
-
return false;
|
|
638
|
+
return OrderByUtils_1.OrderByUtils.isFirstOrderByAsc(reqJson.orderBy);
|
|
663
639
|
}
|
|
664
640
|
/**
|
|
665
641
|
* 根据排序方向对分表列表进行排序
|
|
@@ -55,8 +55,8 @@ export declare class ShardingMerger {
|
|
|
55
55
|
* - orderBy 为 timeColumn DESC 或 timeColumn ASC
|
|
56
56
|
*
|
|
57
57
|
* 执行流程:
|
|
58
|
-
* 1.
|
|
59
|
-
* 2.
|
|
58
|
+
* 1. 串行查询分表,按表顺序拼接
|
|
59
|
+
* 2. 达到 maxRows 上限后立即停止,避免查询无关表
|
|
60
60
|
*
|
|
61
61
|
* @param crudPro CrudPro 实例
|
|
62
62
|
* @param tables 分表列表(DESC: 新→旧,ASC: 旧→新)
|
|
@@ -75,8 +75,8 @@ class ShardingMerger {
|
|
|
75
75
|
* - orderBy 为 timeColumn DESC 或 timeColumn ASC
|
|
76
76
|
*
|
|
77
77
|
* 执行流程:
|
|
78
|
-
* 1.
|
|
79
|
-
* 2.
|
|
78
|
+
* 1. 串行查询分表,按表顺序拼接
|
|
79
|
+
* 2. 达到 maxRows 上限后立即停止,避免查询无关表
|
|
80
80
|
*
|
|
81
81
|
* @param crudPro CrudPro 实例
|
|
82
82
|
* @param tables 分表列表(DESC: 新→旧,ASC: 旧→新)
|
|
@@ -86,14 +86,16 @@ class ShardingMerger {
|
|
|
86
86
|
* @returns 数据列表
|
|
87
87
|
*/
|
|
88
88
|
async mergeQuery(crudPro, tables, reqJson, cfgJson, maxRows = 10000) {
|
|
89
|
-
//
|
|
90
|
-
const dataPromises = tables.map(table => this.queryRowsSafe(crudPro, table, reqJson, cfgJson, maxRows));
|
|
91
|
-
const dataResults = await Promise.all(dataPromises);
|
|
92
|
-
// 按表顺序拼接(表顺序即数据顺序,无需排序)
|
|
89
|
+
// 串行查询分表,按表顺序拼接,达到上限即停
|
|
93
90
|
let allRows = [];
|
|
94
|
-
for (
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
for (const table of tables) {
|
|
92
|
+
const needMore = maxRows - allRows.length;
|
|
93
|
+
if (needMore <= 0) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
const rows = await this.queryRowsSafe(crudPro, table, reqJson, cfgJson, needMore);
|
|
97
|
+
allRows = allRows.concat(rows);
|
|
98
|
+
// 达到上限后停止,不再查询后续表
|
|
97
99
|
if (allRows.length >= maxRows) {
|
|
98
100
|
allRows = allRows.slice(0, maxRows);
|
|
99
101
|
break;
|
|
@@ -824,7 +824,30 @@ await sharding.insert({
|
|
|
824
824
|
|
|
825
825
|
### linkColumnRelationDatas
|
|
826
826
|
|
|
827
|
-
独立于 CRUD
|
|
827
|
+
独立于 CRUD 操作,直接对已有数据行执行关联填充。适用于数据来自缓存、外部接口或其他非 CurdMixService 渠道,但仍需要字典翻译、用户信息等关联数据的场景。
|
|
828
|
+
|
|
829
|
+
> ⚠️ 该方法会**原地修改** `rows` 数组中的对象,不会返回新数组。
|
|
830
|
+
|
|
831
|
+
#### 方法签名
|
|
832
|
+
|
|
833
|
+
```typescript
|
|
834
|
+
linkColumnRelationDatas(rows: any[], param: ILinkColumnRelationParam): Promise<void>
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
#### 参数接口
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
interface ILinkColumnRelationParam {
|
|
841
|
+
columnsRelations: ColumnRelation[]; // 关联配置数组
|
|
842
|
+
}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
| 参数 | 类型 | 说明 |
|
|
846
|
+
|------|------|------|
|
|
847
|
+
| `rows` | `any[]` | 待填充的数据行数组;为空或非数组时直接返回 |
|
|
848
|
+
| `param.columnsRelations` | `ColumnRelation[]` | 关联规则配置,与 CRUD 配置中的 `columnsRelation` 格式一致 |
|
|
849
|
+
|
|
850
|
+
#### 基础用法
|
|
828
851
|
|
|
829
852
|
```typescript
|
|
830
853
|
// 场景:从缓存或其他来源获取了数据行,仍需要关联填充
|
|
@@ -840,9 +863,85 @@ await curdMixService.linkColumnRelationDatas(rows, {
|
|
|
840
863
|
},
|
|
841
864
|
],
|
|
842
865
|
});
|
|
843
|
-
// rows
|
|
866
|
+
// rows 中的每行会原地追加 sex_text 字段
|
|
844
867
|
```
|
|
845
868
|
|
|
869
|
+
#### 多关联类型组合
|
|
870
|
+
|
|
871
|
+
```typescript
|
|
872
|
+
const rows = await redisCache.get('order_list');
|
|
873
|
+
|
|
874
|
+
await curdMixService.linkColumnRelationDatas(rows, {
|
|
875
|
+
columnsRelations: [
|
|
876
|
+
// 字典翻译
|
|
877
|
+
{
|
|
878
|
+
relatedType: 'dict',
|
|
879
|
+
relatedCode: 'OrderStatus',
|
|
880
|
+
sourceColumn: 'status',
|
|
881
|
+
targetColumns: [
|
|
882
|
+
{ from: 'label', to: 'status_text' },
|
|
883
|
+
{ from: 'color', to: 'status_color' },
|
|
884
|
+
],
|
|
885
|
+
},
|
|
886
|
+
// 系统配置枚举
|
|
887
|
+
{
|
|
888
|
+
relatedType: 'sysCfgEnum',
|
|
889
|
+
relatedCode: 'PayMethod',
|
|
890
|
+
sourceColumn: 'pay_method',
|
|
891
|
+
targetColumns: [
|
|
892
|
+
{ from: 'label', to: 'pay_method_info.label' },
|
|
893
|
+
{ from: 'style', to: 'pay_method_info.style' },
|
|
894
|
+
],
|
|
895
|
+
},
|
|
896
|
+
// 用户信息填充
|
|
897
|
+
{
|
|
898
|
+
relatedType: 'accountBasic',
|
|
899
|
+
sourceColumn: 'created_by',
|
|
900
|
+
targetColumns: [], // 空 = 使用默认映射
|
|
901
|
+
},
|
|
902
|
+
// 工作台信息
|
|
903
|
+
{
|
|
904
|
+
relatedType: 'workbenchBasic',
|
|
905
|
+
sourceColumn: 'workbench_code',
|
|
906
|
+
targetColumns: [
|
|
907
|
+
{ from: 'workbench_domain', to: 'workbench_info.workbench_domain' },
|
|
908
|
+
{ from: 'workbench_name', to: 'workbench_info.workbench_name' },
|
|
909
|
+
],
|
|
910
|
+
},
|
|
911
|
+
],
|
|
912
|
+
});
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
#### 与 CrudProQuick 配合使用
|
|
916
|
+
|
|
917
|
+
```typescript
|
|
918
|
+
const quick = curdProService.getQuickCrud('mydb', SqlDbType.mysql, 't_order');
|
|
919
|
+
|
|
920
|
+
// 先用 CrudProQuick 获取原始数据(不带关联)
|
|
921
|
+
const rows = await quick.getList({
|
|
922
|
+
condition: { status: 'paid' },
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// 再用 linkColumnRelationDatas 补充关联数据
|
|
926
|
+
await curdMixService.linkColumnRelationDatas(rows, {
|
|
927
|
+
columnsRelations: [
|
|
928
|
+
{
|
|
929
|
+
relatedType: 'dict',
|
|
930
|
+
relatedCode: 'OrderStatus',
|
|
931
|
+
sourceColumn: 'status',
|
|
932
|
+
targetColumns: [{ from: 'label', to: 'status_text' }],
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
relatedType: 'accountBasic',
|
|
936
|
+
sourceColumn: 'created_by',
|
|
937
|
+
targetColumns: [],
|
|
938
|
+
},
|
|
939
|
+
],
|
|
940
|
+
});
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
> **提示**:如果 CRUD 操作本身就需要关联填充,建议直接使用 `CurdMixService.executeCrudByCfg()` 配置 `columnsRelation`,一步完成查询和填充。`linkColumnRelationDatas` 适用于数据已存在的二次填充场景。
|
|
944
|
+
|
|
846
945
|
## ColumnRelation 配置
|
|
847
946
|
|
|
848
947
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "midway-fatcms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "This is a midway component sample",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"src/**/*.html",
|
|
31
31
|
"src/**/*.ico",
|
|
32
32
|
"src/**/*.md",
|
|
33
|
+
".qoder/**/*.md",
|
|
33
34
|
"index.d.ts",
|
|
34
35
|
"tsconfig.json",
|
|
35
36
|
".prettierrc.js",
|