midway-fatcms 0.0.5 → 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.
Files changed (132) hide show
  1. package/.qoder/skills/midway-fatcms-crud/SKILL.md +375 -0
  2. package/.qoder/skills/midway-fatcms-crud/examples.md +990 -0
  3. package/.qoder/skills/midway-fatcms-crud/reference.md +568 -0
  4. package/README.md +377 -134
  5. package/dist/controller/manage/CrudStandardDesignApi.d.ts +0 -2
  6. package/dist/controller/manage/CrudStandardDesignApi.js +11 -85
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +2 -0
  9. package/dist/libs/crud-pro/CrudPro.d.ts +9 -1
  10. package/dist/libs/crud-pro/CrudPro.js +15 -0
  11. package/dist/libs/crud-pro/README.md +809 -0
  12. package/dist/libs/crud-pro/README_FUNC.md +193 -0
  13. package/dist/libs/crud-pro/exceptions.d.ts +2 -0
  14. package/dist/libs/crud-pro/exceptions.js +2 -0
  15. package/dist/libs/crud-pro/interfaces.d.ts +34 -1
  16. package/dist/libs/crud-pro/models/ExecuteContext.d.ts +3 -3
  17. package/dist/libs/crud-pro/models/ExecuteContext.js +2 -0
  18. package/dist/libs/crud-pro/models/RequestModel.d.ts +6 -2
  19. package/dist/libs/crud-pro/models/RequestModel.js +20 -53
  20. package/dist/libs/crud-pro/models/ResModel.d.ts +6 -4
  21. package/dist/libs/crud-pro/models/ServiceHub.d.ts +1 -0
  22. package/dist/libs/crud-pro/models/keys.d.ts +6 -1
  23. package/dist/libs/crud-pro/models/keys.js +5 -0
  24. package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.d.ts +52 -0
  25. package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.js +158 -0
  26. package/dist/libs/crud-pro/services/CrudProExecuteSqlService.js +20 -1
  27. package/dist/libs/crud-pro/services/CrudProFieldValidateService.d.ts +7 -0
  28. package/dist/libs/crud-pro/services/CrudProFieldValidateService.js +32 -0
  29. package/dist/libs/crud-pro/services/CrudProGenSqlService.d.ts +13 -0
  30. package/dist/libs/crud-pro/services/CrudProGenSqlService.js +44 -7
  31. package/dist/libs/crud-pro/services/CrudProOriginToExecuteSql.d.ts +43 -0
  32. package/dist/libs/crud-pro/services/CrudProOriginToExecuteSql.js +132 -1
  33. package/dist/libs/crud-pro/services/CrudProTableMetaService.d.ts +15 -1
  34. package/dist/libs/crud-pro/services/CrudProTableMetaService.js +107 -0
  35. package/dist/libs/crud-pro/services/CurdProServiceHub.d.ts +5 -1
  36. package/dist/libs/crud-pro/services/CurdProServiceHub.js +11 -0
  37. package/dist/libs/crud-pro/utils/DateTimeUtils.d.ts +1 -0
  38. package/dist/libs/crud-pro/utils/DateTimeUtils.js +3 -0
  39. package/dist/libs/crud-pro/utils/MixinUtils.d.ts +32 -0
  40. package/dist/libs/crud-pro/utils/MixinUtils.js +85 -1
  41. package/dist/libs/crud-pro/utils/OrderByUtils.d.ts +70 -0
  42. package/dist/libs/crud-pro/utils/OrderByUtils.js +158 -0
  43. package/dist/libs/crud-pro/utils/ValidateUtils.js +1 -1
  44. package/dist/libs/crud-sharding/ROUTING_LOGIC.md +944 -0
  45. package/dist/libs/crud-sharding/ShardingConfig.d.ts +218 -0
  46. package/dist/libs/crud-sharding/ShardingConfig.js +32 -0
  47. package/dist/libs/crud-sharding/ShardingCountCache.d.ts +69 -0
  48. package/dist/libs/crud-sharding/ShardingCountCache.js +160 -0
  49. package/dist/libs/crud-sharding/ShardingCrudPro.d.ts +363 -0
  50. package/dist/libs/crud-sharding/ShardingCrudPro.js +675 -0
  51. package/dist/libs/crud-sharding/ShardingMerger.d.ts +130 -0
  52. package/dist/libs/crud-sharding/ShardingMerger.js +282 -0
  53. package/dist/libs/crud-sharding/ShardingRouter.d.ts +69 -0
  54. package/dist/libs/crud-sharding/ShardingRouter.js +377 -0
  55. package/dist/libs/crud-sharding/ShardingTableCreator.d.ts +146 -0
  56. package/dist/libs/crud-sharding/ShardingTableCreator.js +805 -0
  57. package/dist/libs/crud-sharding/ShardingUtils.d.ts +38 -0
  58. package/dist/libs/crud-sharding/ShardingUtils.js +77 -0
  59. package/dist/libs/crud-sharding/index.d.ts +45 -0
  60. package/dist/libs/crud-sharding/index.js +55 -0
  61. package/dist/models/StandardColumns.d.ts +71 -0
  62. package/dist/models/StandardColumns.js +28 -0
  63. package/dist/service/SysAppService.js +2 -2
  64. package/dist/service/SysConfigService.js +1 -1
  65. package/dist/service/SysDictDataService.js +2 -2
  66. package/dist/service/SysMenuService.js +1 -1
  67. package/dist/service/UserAccountService.d.ts +1 -1
  68. package/dist/service/crudstd/CrudStdService.d.ts +0 -1
  69. package/dist/service/crudstd/CrudStdService.js +0 -27
  70. package/dist/service/curd/CrudProQuick.d.ts +134 -4
  71. package/dist/service/curd/CrudProQuick.js +155 -3
  72. package/dist/service/curd/CurdMixService.d.ts +2 -1
  73. package/dist/service/curd/CurdMixService.js +5 -1
  74. package/dist/service/curd/CurdProService.d.ts +44 -2
  75. package/dist/service/curd/CurdProService.js +53 -1
  76. package/dist/service/curd/README.md +1100 -0
  77. package/dist/service/curd/fixSoftDelete.d.ts +14 -0
  78. package/dist/service/curd/fixSoftDelete.js +29 -11
  79. package/dist/service/flow/FlowConfigService.js +1 -1
  80. package/dist/service/flow/FlowInstanceCrudService.js +1 -1
  81. package/package.json +4 -1
  82. package/src/controller/gateway/AsyncTaskController.ts +1 -1
  83. package/src/controller/manage/CrudStandardDesignApi.ts +16 -100
  84. package/src/index.ts +3 -0
  85. package/src/libs/crud-pro/CrudPro.ts +19 -1
  86. package/src/libs/crud-pro/README.md +809 -0
  87. package/src/libs/crud-pro/README_FUNC.md +193 -0
  88. package/src/libs/crud-pro/exceptions.ts +2 -0
  89. package/src/libs/crud-pro/interfaces.ts +38 -1
  90. package/src/libs/crud-pro/models/ExecuteContext.ts +6 -3
  91. package/src/libs/crud-pro/models/RequestModel.ts +23 -65
  92. package/src/libs/crud-pro/models/ResModel.ts +10 -4
  93. package/src/libs/crud-pro/models/ServiceHub.ts +2 -0
  94. package/src/libs/crud-pro/models/keys.ts +5 -0
  95. package/src/libs/crud-pro/services/CrudProDataTypeConvertService.ts +171 -0
  96. package/src/libs/crud-pro/services/CrudProExecuteSqlService.ts +24 -1
  97. package/src/libs/crud-pro/services/CrudProFieldValidateService.ts +53 -1
  98. package/src/libs/crud-pro/services/CrudProGenSqlService.ts +51 -7
  99. package/src/libs/crud-pro/services/CrudProOriginToExecuteSql.ts +159 -2
  100. package/src/libs/crud-pro/services/CrudProTableMetaService.ts +139 -1
  101. package/src/libs/crud-pro/services/CurdProServiceHub.ts +16 -1
  102. package/src/libs/crud-pro/utils/DateTimeUtils.ts +3 -0
  103. package/src/libs/crud-pro/utils/MixinUtils.ts +97 -1
  104. package/src/libs/crud-pro/utils/OrderByUtils.ts +169 -0
  105. package/src/libs/crud-pro/utils/ValidateUtils.ts +1 -1
  106. package/src/libs/crud-sharding/ROUTING_LOGIC.md +944 -0
  107. package/src/libs/crud-sharding/ShardingConfig.ts +240 -0
  108. package/src/libs/crud-sharding/ShardingCountCache.ts +200 -0
  109. package/src/libs/crud-sharding/ShardingCrudPro.ts +835 -0
  110. package/src/libs/crud-sharding/ShardingMerger.ts +384 -0
  111. package/src/libs/crud-sharding/ShardingRouter.ts +512 -0
  112. package/src/libs/crud-sharding/ShardingTableCreator.ts +1007 -0
  113. package/src/libs/crud-sharding/ShardingUtils.ts +84 -0
  114. package/src/libs/crud-sharding/index.ts +64 -0
  115. package/src/models/StandardColumns.ts +76 -0
  116. package/src/service/FileCenterService.ts +1 -1
  117. package/src/service/SysAppService.ts +2 -2
  118. package/src/service/SysConfigService.ts +1 -1
  119. package/src/service/SysDictDataService.ts +2 -2
  120. package/src/service/SysMenuService.ts +2 -2
  121. package/src/service/WorkbenchService.ts +1 -1
  122. package/src/service/anyapi/AnyApiService.ts +1 -1
  123. package/src/service/asyncTask/AsyncTaskRunnerService.ts +1 -1
  124. package/src/service/crudstd/CrudStdService.ts +0 -32
  125. package/src/service/curd/CrudProQuick.ts +164 -5
  126. package/src/service/curd/CurdMixService.ts +7 -2
  127. package/src/service/curd/CurdProService.ts +62 -3
  128. package/src/service/curd/README.md +1100 -0
  129. package/src/service/curd/fixCfgModel.ts +1 -2
  130. package/src/service/curd/fixSoftDelete.ts +38 -16
  131. package/src/service/flow/FlowConfigService.ts +1 -1
  132. package/src/service/flow/FlowInstanceCrudService.ts +1 -1
@@ -201,7 +201,15 @@ class CrudProExecuteSqlService extends CrudProServiceBase {
201
201
  }
202
202
 
203
203
  private toQueryResByResPicker(rows: any[], originRes: any, sqlCfgModel: SqlCfgModel) {
204
- const resPicker = sqlCfgModel.resPicker;
204
+ let resPicker = sqlCfgModel.resPicker;
205
+
206
+ // resPicker 未设置时,根据 SQL 类型自动推断
207
+ if (isEmpty(resPicker)) {
208
+ const crudType = sqlCfgModel.getCrudType();
209
+ if (crudType === KeyOfCrudTypes.INSERT || crudType === KeyOfCrudTypes.UPDATE || crudType === KeyOfCrudTypes.DELETE) {
210
+ resPicker = KeysOfSqlResPicker.UPDATE_RESULT;
211
+ }
212
+ }
205
213
 
206
214
  // 返回第一行
207
215
  if (KeysOfSqlResPicker.RESULT_FIRST_ROW === resPicker) {
@@ -220,6 +228,19 @@ class CrudProExecuteSqlService extends CrudProServiceBase {
220
228
  return Number(map0['total_count'] || 0);
221
229
  }
222
230
 
231
+ // 判断元素是否存在,返回 boolean
232
+ if (KeysOfSqlResPicker.RESULT_IS_EXIST === resPicker) {
233
+ if (isEmpty(rows) || rows.length === 0) {
234
+ return false;
235
+ }
236
+ const val = rows[0]['is_exist'];
237
+ // MySQL/SQLServer 返回 0/1,PostgreSQL 返回 false/true
238
+ if (typeof val === 'boolean') {
239
+ return val;
240
+ }
241
+ return Number(val) > 0;
242
+ }
243
+
223
244
  // 增删改res的内容是修改结果:包括: insert\delete\update
224
245
  if (KeysOfSqlResPicker.UPDATE_RESULT === resPicker) {
225
246
  if (sqlCfgModel.sqlDbType === SqlDbType.postgres) {
@@ -248,6 +269,8 @@ class CrudProExecuteSqlService extends CrudProServiceBase {
248
269
  return rows;
249
270
  }
250
271
 
272
+
273
+
251
274
  //其他配置:形如: sqlRes[0].total_count
252
275
  if (typeof resPicker === 'string') {
253
276
  if (resPicker.startsWith('sqlRes')) {
@@ -2,7 +2,7 @@ import { RequestModel } from '../models/RequestModel';
2
2
  import { CrudProServiceBase } from './CrudProServiceBase';
3
3
  import { RequestCfgModel } from '../models/RequestCfgModel';
4
4
  import { MixinUtils } from '../utils/MixinUtils';
5
- import { KeysOfConditions, KeysOfValidators } from '../models/keys';
5
+ import { KeysOfConditions, KeysOfSimpleSQL, KeysOfValidators } from '../models/keys';
6
6
  import { CommonException, Exceptions } from '../exceptions';
7
7
  import { ModelUtils } from '../utils/ModelUtils';
8
8
  import { ICustomValidateFunc, IFuncCfgModel, IValidatorCfgItem } from '../interfaces';
@@ -175,6 +175,58 @@ class CrudProFieldValidateService extends CrudProServiceBase {
175
175
  }
176
176
  }
177
177
  }
178
+
179
+ /**
180
+ * 校验 data 字段类型是否符合 sqlSimpleName 的要求
181
+ *
182
+ * - SIMPLE_BATCH_INSERT: data 必须是数组
183
+ * - 其他 INSERT/UPDATE 类型: data 必须是普通对象
184
+ */
185
+ validateDataType(cfgModel: RequestCfgModel, reqModel: RequestModel): void {
186
+ const sqlSimpleName = cfgModel.sqlSimpleName;
187
+ if (!sqlSimpleName) {
188
+ return;
189
+ }
190
+
191
+ const data = reqModel.data;
192
+
193
+ const isArrayData = Array.isArray(data);
194
+
195
+ if (sqlSimpleName === KeysOfSimpleSQL.SIMPLE_BATCH_INSERT) {
196
+ if (!isArrayData || data.length === 0) {
197
+ throw new CommonException(
198
+ Exceptions.CFG_INVALID_DATA_TYPE,
199
+ `${sqlSimpleName} 要求 data 必须是数组。并且不能为空数组。`
200
+ );
201
+ }
202
+ return;
203
+ }
204
+
205
+ const needsObjectData = [
206
+ KeysOfSimpleSQL.SIMPLE_INSERT,
207
+ KeysOfSimpleSQL.SIMPLE_UPDATE,
208
+ KeysOfSimpleSQL.SIMPLE_INSERT_ON_DUPLICATE_UPDATE,
209
+ KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE,
210
+ ].includes(sqlSimpleName as KeysOfSimpleSQL);
211
+
212
+
213
+ if(needsObjectData && (!data || Object.keys(data).length === 0)) {
214
+ throw new CommonException(
215
+ Exceptions.CFG_INVALID_DATA_TYPE,
216
+ `${sqlSimpleName} 要求 data 必须是对象,不能为空`
217
+ );
218
+ }
219
+
220
+ if (needsObjectData && isArrayData) {
221
+ throw new CommonException(
222
+ Exceptions.CFG_INVALID_DATA_TYPE,
223
+ `${sqlSimpleName} 要求 data 必须是普通对象,不能是数组`
224
+ );
225
+ }
226
+
227
+
228
+
229
+ }
178
230
  }
179
231
 
180
232
  export { CrudProFieldValidateService };
@@ -39,7 +39,11 @@ class CrudProGenSqlService extends CrudProServiceBase {
39
39
  let sql1 = null;
40
40
  let sql2 = null;
41
41
  let sql3 = null;
42
- if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_QUERY, simpleSqlName)) {
42
+
43
+ if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_QUERY_EXIST, simpleSqlName)) {
44
+ sql1 = this.generateOriginSqlForIsExist(cfgModel);
45
+ cfgModel.addSqlCfgModel('is_exist', sql1, KeysOfSqlResPicker.RESULT_IS_EXIST);
46
+ } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_QUERY, simpleSqlName)) {
43
47
  sql1 = 'select @@columns from @@table where @@asWhere:condition @@orderBys @@offsetLimit';
44
48
  cfgModel.addSqlCfgModel('rows', sql1);
45
49
  } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_QUERY_ONE, simpleSqlName)) {
@@ -61,6 +65,9 @@ class CrudProGenSqlService extends CrudProServiceBase {
61
65
  } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_INSERT, simpleSqlName)) {
62
66
  sql1 = this.generateOriginSqlForInsert(cfgModel);
63
67
  cfgModel.addSqlCfgModel('affected', sql1, KeysOfSqlResPicker.UPDATE_RESULT);
68
+ } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_BATCH_INSERT, simpleSqlName)) {
69
+ sql1 = this.generateOriginSqlForBatchInsert(cfgModel);
70
+ cfgModel.addSqlCfgModel('affected', sql1, KeysOfSqlResPicker.UPDATE_RESULT);
64
71
  } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_UPDATE, simpleSqlName)) {
65
72
  sql1 = 'update @@table set @@asUpdate:data where @@asWhere:condition ';
66
73
  cfgModel.addSqlCfgModel('affected', sql1, KeysOfSqlResPicker.UPDATE_RESULT);
@@ -68,20 +75,21 @@ class CrudProGenSqlService extends CrudProServiceBase {
68
75
  sql1 = this.generateOriginSqlForDuplicateInsert(cfgModel);
69
76
  cfgModel.addSqlCfgModel('affected', sql1, KeysOfSqlResPicker.UPDATE_RESULT);
70
77
  } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE, simpleSqlName)) {
71
- sql1 = 'select count(0) as total_count from @@table where @@asWhere:condition ';
72
- sql2 = 'insert into @@table ( @@asInsertKeys:data ) values( @@asInsertValues:data )';
78
+
79
+ sql1 = this.generateOriginSqlForIsExist(cfgModel); // 判断元素是否存在
80
+ sql2 = this.generateOriginSqlForInsert(cfgModel); // 插入语句
73
81
  sql3 = 'update @@table set @@asUpdate:data where @@asWhere:condition ';
74
82
 
75
83
  const insertWhen2 = {
76
84
  functionName: 'eq',
77
- functionParams: [{ contextAsNumber: 'res.total_count' }, { constNumber: 0 }],
85
+ functionParams: [{ contextAsBool: 'res.is_exist' }, { constBool: false }],
78
86
  };
79
87
  const updateWhen3 = {
80
- functionName: 'gt',
81
- functionParams: [{ contextAsNumber: 'res.total_count' }, { constNumber: 0 }],
88
+ functionName: 'eq',
89
+ functionParams: [{ contextAsBool: 'res.is_exist' }, { constBool: true }],
82
90
  };
83
91
 
84
- cfgModel.addSqlCfgModel('total_count', sql1, KeysOfSqlResPicker.RESULT_TOTAL_COUNT);
92
+ cfgModel.addSqlCfgModel('is_exist', sql1, KeysOfSqlResPicker.RESULT_IS_EXIST);
85
93
  cfgModel.addSqlCfgModel('insert_affected', sql2, KeysOfSqlResPicker.UPDATE_RESULT, insertWhen2);
86
94
  cfgModel.addSqlCfgModel('update_affected', sql3, KeysOfSqlResPicker.UPDATE_RESULT, updateWhen3);
87
95
  }
@@ -91,6 +99,27 @@ class CrudProGenSqlService extends CrudProServiceBase {
91
99
  }
92
100
  }
93
101
 
102
+
103
+
104
+
105
+
106
+ /**
107
+ * 判断元素是否存在
108
+ * 使用 SELECT EXISTS 子查询,数据库引擎找到第一条匹配记录即停止扫描,比 COUNT(*) 更高效
109
+ * @param cfgModel
110
+ * @private
111
+ */
112
+ private generateOriginSqlForIsExist(cfgModel: RequestCfgModel): string {
113
+ if (cfgModel.sqlDbType === SqlDbType.postgres) {
114
+ return 'SELECT EXISTS(SELECT 1 FROM @@table WHERE @@asWhere:condition) AS is_exist';
115
+ }
116
+ if (cfgModel.sqlDbType === SqlDbType.sqlserver) {
117
+ return 'SELECT CASE WHEN EXISTS(SELECT 1 FROM @@table WHERE @@asWhere:condition) THEN 1 ELSE 0 END AS is_exist';
118
+ }
119
+ return 'SELECT EXISTS(SELECT 1 FROM @@table WHERE @@asWhere:condition) AS is_exist';
120
+ }
121
+
122
+
94
123
  /**
95
124
  * 删除语句
96
125
  * @param cfgModel
@@ -157,6 +186,21 @@ class CrudProGenSqlService extends CrudProServiceBase {
157
186
  return 'insert into @@table ( @@asInsertKeys:data ) values( @@asInsertValues:data )'; // 关键字的前后,必须有空格
158
187
  }
159
188
 
189
+ /**
190
+ * 创建批量插入语句
191
+ * @param cfgModel
192
+ * @private
193
+ */
194
+ private generateOriginSqlForBatchInsert(cfgModel: RequestCfgModel): string {
195
+ if (cfgModel.sqlDbType === SqlDbType.postgres) {
196
+ return 'insert into @@table ( @@asBatchInsertKeys:data ) values @@asBatchInsertValues:data RETURNING * ';
197
+ }
198
+ if (cfgModel.sqlDbType === SqlDbType.sqlserver) {
199
+ return 'insert into @@table ( @@asBatchInsertKeys:data ) OUTPUT INSERTED.* values @@asBatchInsertValues:data ';
200
+ }
201
+ return 'insert into @@table ( @@asBatchInsertKeys:data ) values @@asBatchInsertValues:data '; // MySQL 默认语法
202
+ }
203
+
160
204
  private async generateExecuteSql() {
161
205
  const exeCtx = this.getExecuteContext();
162
206
 
@@ -3,7 +3,7 @@ import { SqlCfgModel } from '../models/SqlCfgModel';
3
3
  import { ITableMetaQuery } from '../interfaces';
4
4
  import { MixinUtils } from '../utils/MixinUtils';
5
5
  import { SqlSegArg } from '../models/SqlSegArg';
6
- import { KeysOfCustomSQL, SqlDbType } from '../models/keys';
6
+ import { KeysOfCustomSQL, KeyOfCrudTypes, SqlDbType } from '../models/keys';
7
7
  import { RequestModel } from '../models/RequestModel';
8
8
  import { CommonException, Exceptions } from '../exceptions';
9
9
  import { RequestCfgModel } from '../models/RequestCfgModel';
@@ -73,7 +73,43 @@ class CrudProOriginToExecuteSql extends CrudProServiceBase {
73
73
  }
74
74
 
75
75
  sqlCfgModel.executeSqlArgs = argList;
76
- sqlCfgModel.executeSql = executeSql;
76
+ sqlCfgModel.executeSql = this.appendDialectClause(executeSql, sqlCfgModel);
77
+ }
78
+
79
+ /**
80
+ * 对于自定义 INSERT SQL,自动追加数据库方言子句:
81
+ * - PostgreSQL: RETURNING *
82
+ * - SQL Server: OUTPUT INSERTED.*
83
+ * - MySQL: 无需追加,驱动自动返回 insertId
84
+ *
85
+ * 仅当 originSql 中用户未手动添加这些子句时才追加
86
+ */
87
+ private appendDialectClause(executeSql: string, sqlCfgModel: SqlCfgModel): string {
88
+ const sqlDbType = sqlCfgModel.sqlDbType;
89
+ const sqlUpper = executeSql.trim().toUpperCase();
90
+
91
+ if (!sqlUpper.startsWith(KeyOfCrudTypes.INSERT)) {
92
+ return executeSql;
93
+ }
94
+
95
+ if (sqlDbType === SqlDbType.postgres) {
96
+ if (sqlUpper.indexOf('RETURNING') < 0) {
97
+ return executeSql + ' RETURNING *';
98
+ }
99
+ } else if (sqlDbType === SqlDbType.sqlserver) {
100
+ if (sqlUpper.indexOf('OUTPUT INSERTED') < 0) {
101
+ // SQL Server: OUTPUT INSERTED.* 需放在 VALUES 之前
102
+ // INSERT INTO table (...) OUTPUT INSERTED.* VALUES (...)
103
+ const valuesIndex = sqlUpper.lastIndexOf('VALUES');
104
+ if (valuesIndex > 0) {
105
+ const beforeValues = executeSql.substring(0, valuesIndex);
106
+ const fromValues = executeSql.substring(valuesIndex);
107
+ return beforeValues + ' OUTPUT INSERTED.* ' + fromValues;
108
+ }
109
+ }
110
+ }
111
+
112
+ return executeSql;
77
113
  }
78
114
 
79
115
  private async generateWordMap(originSql: string, sqlCfgModel: SqlCfgModel): Promise<Record<string, SqlSegArg>> {
@@ -123,6 +159,10 @@ class CrudProOriginToExecuteSql extends CrudProServiceBase {
123
159
  return this.generateDataAsInsertKeys(reqModel, sqlCfg, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_AS_INSERT_KEYS));
124
160
  } else if (word.startsWith(KeysOfCustomSQL.SQL_AS_INSERT_VALUES)) {
125
161
  return this.generateDataAsInsertValues(reqModel, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_AS_INSERT_VALUES));
162
+ } else if (word.startsWith(KeysOfCustomSQL.SQL_AS_BATCH_INSERT_KEYS)) {
163
+ return this.generateDataAsBatchInsertKeys(reqModel, sqlCfg, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_AS_BATCH_INSERT_KEYS));
164
+ } else if (word.startsWith(KeysOfCustomSQL.SQL_AS_BATCH_INSERT_VALUES)) {
165
+ return this.generateDataAsBatchInsertValues(reqModel, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_AS_BATCH_INSERT_VALUES));
126
166
  } else if (word.startsWith(KeysOfCustomSQL.SQL_FUNCTION)) {
127
167
  return this.generateSqlJavaFunction(reqCfg, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_FUNCTION));
128
168
  } else {
@@ -334,6 +374,123 @@ class CrudProOriginToExecuteSql extends CrudProServiceBase {
334
374
  return new SqlSegArg(sql, argList);
335
375
  }
336
376
 
377
+ /**
378
+ * 获取批量插入的字段名列表
379
+ *
380
+ * 策略:
381
+ * - 如果指定了 columns,直接使用 columns(允许数据行包含额外字段,仅插入指定列)
382
+ * - 否则取第一条数据的 keys,并校验后续行是否一致(严格模式)
383
+ *
384
+ * 严格模式限制:
385
+ * - 所有数据行必须包含完全相同的字段
386
+ * - 字段数量必须一致
387
+ * - 字段名必须一致(顺序可不同)
388
+ * - 若不一致,抛出 BATCH_INSERT_KEYS_MISMATCH 异常
389
+ *
390
+ * @param req RequestModel 请求模型
391
+ * @param dataArray 数据数组
392
+ * @returns 排序后的字段名数组
393
+ */
394
+ private getBatchInsertKeys(req: RequestModel, dataArray: Record<string, any>[]): string[] {
395
+ // 如果指定了 columns,直接使用
396
+ if (MixinUtils.isNotEmpty(req.columns) && req.columns.length > 0) {
397
+ return [...req.columns].sort();
398
+ }
399
+
400
+ // 严格模式:取第一条数据的 keys,并校验后续行
401
+ const firstRow = dataArray[0];
402
+ const keys = Object.keys(firstRow).sort();
403
+
404
+ for (let i = 1; i < dataArray.length; i++) {
405
+ const row = dataArray[i];
406
+ const rowKeys = Object.keys(row).sort();
407
+
408
+ if (rowKeys.length !== keys.length) {
409
+ throw new CommonException(
410
+ Exceptions.BATCH_INSERT_KEYS_MISMATCH,
411
+ `批量插入第 ${i + 1} 行数据字段数量不一致,期望 ${keys.length} 个,实际 ${rowKeys.length} 个`
412
+ );
413
+ }
414
+
415
+ for (let j = 0; j < keys.length; j++) {
416
+ if (rowKeys[j] !== keys[j]) {
417
+ throw new CommonException(
418
+ Exceptions.BATCH_INSERT_KEYS_MISMATCH,
419
+ `批量插入第 ${i + 1} 行数据字段不一致,缺少字段 "${keys[j]}" 或包含额外字段`
420
+ );
421
+ }
422
+ }
423
+ }
424
+
425
+ return keys;
426
+ }
427
+
428
+ /**
429
+ * 批量插入:生成列名部分
430
+ *
431
+ * 列名获取策略:
432
+ * - 优先使用 req.columns(若指定)
433
+ * - 否则使用严格模式:取第一条数据的 keys,校验后续行字段一致性
434
+ */
435
+ private generateDataAsBatchInsertKeys(req: RequestModel, sqlCfgModel: SqlCfgModel, dataName: string): SqlSegArg {
436
+ const dataArray = req.getCondOrDataAsArray(dataName);
437
+
438
+ if (MixinUtils.isEmpty(dataArray) || dataArray.length === 0) {
439
+ throw new CommonException(Exceptions.DATA_GET_DATA_EMPTY_ON_INSERT_KEYS, dataName);
440
+ }
441
+
442
+ const keys = this.getBatchInsertKeys(req, dataArray);
443
+
444
+ const columnList = keys.map(columnName => {
445
+ return toSqlColumnName(columnName, sqlCfgModel);
446
+ });
447
+
448
+ const sql = columnList.join(','); // `column1`,`column2`,`column3`
449
+ return new SqlSegArg(sql);
450
+ }
451
+
452
+ /**
453
+ * 批量插入:生成值部分,格式为 (?,?,?),(?,?,?),...
454
+ *
455
+ * 列名获取策略:
456
+ * - 优先使用 req.columns(若指定)
457
+ * - 否则使用严格模式:取第一条数据的 keys,校验后续行字段一致性
458
+ */
459
+ private generateDataAsBatchInsertValues(req: RequestModel, dataName: string): SqlSegArg {
460
+ const dataArray = req.getCondOrDataAsArray(dataName);
461
+
462
+ if (MixinUtils.isEmpty(dataArray) || dataArray.length === 0) {
463
+ throw new CommonException(Exceptions.DATA_GET_DATA_EMPTY_ON_INSERT_VALUES, dataName);
464
+ }
465
+
466
+ const keys = this.getBatchInsertKeys(req, dataArray);
467
+
468
+ const valueGroups: string[] = [];
469
+ const argList: any[] = [];
470
+
471
+ for (let i = 0; i < dataArray.length; i++) {
472
+ const row = dataArray[i];
473
+ const rowValues: string[] = [];
474
+
475
+ for (let j = 0; j < keys.length; j++) {
476
+ const key = keys[j];
477
+ const arg = row[key];
478
+
479
+ if (sqlFuncUtils.isSqlFuncArg(arg)) {
480
+ rowValues.push(sqlFuncUtils.toFuncSQL(arg));
481
+ } else {
482
+ rowValues.push('?');
483
+ argList.push(arg);
484
+ }
485
+ }
486
+
487
+ valueGroups.push('(' + rowValues.join(',') + ')');
488
+ }
489
+
490
+ const sql = valueGroups.join(','); // "(?,?,?),(?,?,?),..."
491
+ return new SqlSegArg(sql, argList);
492
+ }
493
+
337
494
  private generateSqlJavaFunction(requestCfg: RequestCfgModel, tmpFuncName: string): SqlSegArg {
338
495
  const exeCtx = this.getExecuteContext();
339
496
 
@@ -1,13 +1,16 @@
1
1
  import { CrudProServiceBase } from './CrudProServiceBase';
2
- import { IExecuteUnsafeQueryCtx, ITableColumn, ITableMeta, ITableMetaQuery } from '../interfaces';
2
+ import { IExecuteUnsafeQueryCtx, ITableColumn, ITableInfo, ITableListResult, ITableMeta, ITableMetaQuery, ITableNamesQuery, ITableNamesOptions } from '../interfaces';
3
3
  import { SqlDbType } from '../models/keys';
4
4
  // import { pickAndConvertRowsByMix } from '../utils/sqlConvert/convertMix';
5
5
 
6
6
  class CrudProTableMetaCache {
7
7
  private cacheMap: Record<string, ITableMeta> = {};
8
+ private tableInfoCacheMap: Record<string, { tableInfos: ITableListResult; expiredTime: number }> = {};
9
+
8
10
  private getCacheKey(query: ITableMetaQuery) {
9
11
  return `${query.sqlDatabase}:::${query.sqlSchema}:::${query.sqlDbType}:::${query.sqlTable}`;
10
12
  }
13
+
11
14
  public getMeta(query: ITableMetaQuery) {
12
15
  const cacheKey = this.getCacheKey(query);
13
16
  const obj = this.cacheMap[cacheKey];
@@ -21,6 +24,21 @@ class CrudProTableMetaCache {
21
24
  const cacheKey = this.getCacheKey(query);
22
25
  this.cacheMap[cacheKey] = metaObj;
23
26
  }
27
+
28
+ public getTableInfos(cacheKey: string): ITableListResult | null {
29
+ const cached = this.tableInfoCacheMap[cacheKey];
30
+ if (cached && cached.expiredTime > Date.now()) {
31
+ return cached.tableInfos;
32
+ }
33
+ return null;
34
+ }
35
+
36
+ public setTableInfos(cacheKey: string, tableInfos: ITableListResult, cacheTime: number): void {
37
+ this.tableInfoCacheMap[cacheKey] = {
38
+ tableInfos,
39
+ expiredTime: Date.now() + cacheTime,
40
+ };
41
+ }
24
42
  }
25
43
 
26
44
  const metaCache = new CrudProTableMetaCache();
@@ -35,6 +53,126 @@ class CrudProTableMetaService extends CrudProServiceBase {
35
53
  return obj;
36
54
  }
37
55
 
56
+ // ============ 获取表和视图信息(带类型) ============
57
+
58
+
59
+ /**
60
+ * 获取数据库所有表和视图信息(包含类型)
61
+ *
62
+ * @param query 数据库查询参数
63
+ * @param options 缓存控制选项
64
+ * - skipCache: 跳过缓存,直接查询数据库
65
+ * - refreshCache: 查询后更新缓存
66
+ */
67
+ public async getAllTableInfos(
68
+ query: ITableNamesQuery,
69
+ options: ITableNamesOptions = {}
70
+ ): Promise<ITableListResult> {
71
+ const { skipCache = false, refreshCache = false } = options;
72
+
73
+ // 1. 尝试从缓存获取(使用独立的缓存key)
74
+ const cacheKey = this.getTableInfoCacheKey(query);
75
+ if (!skipCache) {
76
+ const cached = metaCache.getTableInfos(cacheKey);
77
+ if (cached) {
78
+ return cached;
79
+ }
80
+ }
81
+
82
+ // 2. 查询数据库
83
+ const result = await this.loadAllTableInfos(query);
84
+
85
+ // 3. 更新缓存
86
+ if (!skipCache || refreshCache) {
87
+ const { tableMetaCacheTime } = this.getContextCfg();
88
+ metaCache.setTableInfos(cacheKey, result, tableMetaCacheTime || 3600000);
89
+ }
90
+
91
+ return result;
92
+ }
93
+
94
+ private getTableInfoCacheKey(query: ITableNamesQuery): string {
95
+ return `tableInfo:::${query.sqlDatabase}:::${query.sqlSchema || 'public'}:::${query.sqlDbType}`;
96
+ }
97
+
98
+ private async loadAllTableInfos(query: ITableNamesQuery): Promise<ITableListResult> {
99
+ const { sqlDbType } = query;
100
+
101
+ if (sqlDbType === SqlDbType.mysql) {
102
+ return this.loadMySQLAllTableInfos(query);
103
+ } else if (sqlDbType === SqlDbType.postgres) {
104
+ return this.loadPostgreSQLAllTableInfos(query);
105
+ } else if (sqlDbType === SqlDbType.sqlserver) {
106
+ return this.loadSQLServerAllTableInfos(query);
107
+ }
108
+
109
+ throw new Error('暂不支持的数据库类型:' + sqlDbType);
110
+ }
111
+
112
+ private async loadMySQLAllTableInfos(query: ITableNamesQuery): Promise<ITableListResult> {
113
+ const dbUtil = { sqlDatabase: query.sqlDatabase, sqlDbType: query.sqlDbType } as IExecuteUnsafeQueryCtx;
114
+
115
+ // 获取物理表
116
+ const tableSql = `SHOW TABLES FROM ${query.sqlDatabase}`;
117
+ const tableRes = await this.executeUnsafeQuery(dbUtil, tableSql);
118
+ const tableNames: string[] = (tableRes.rows || []).map((row: any) => Object.values(row)[0] as string);
119
+
120
+ // 获取视图
121
+ const viewSql = `SHOW FULL TABLES FROM ${query.sqlDatabase} WHERE Table_type = 'VIEW'`;
122
+ const viewRes = await this.executeUnsafeQuery(dbUtil, viewSql);
123
+ const viewNames: string[] = (viewRes.rows || []).map((row: any) => Object.values(row)[0] as string);
124
+
125
+ const tables: ITableInfo[] = [
126
+ ...tableNames.map(name => ({ name, tableType: 'table' })),
127
+ ...viewNames.map(name => ({ name, tableType: 'view' })),
128
+ ];
129
+
130
+ return { tables, tableNames, viewNames };
131
+ }
132
+
133
+ private async loadPostgreSQLAllTableInfos(query: ITableNamesQuery): Promise<ITableListResult> {
134
+ const schema = query.sqlSchema || 'public';
135
+ const dbUtil = { sqlDatabase: query.sqlDatabase, sqlDbType: query.sqlDbType } as IExecuteUnsafeQueryCtx;
136
+
137
+ // 获取物理表
138
+ const tableSql = `SELECT tablename FROM pg_tables WHERE schemaname = '${schema}' ORDER BY tablename`;
139
+ const tableRes = await this.executeUnsafeQuery(dbUtil, tableSql);
140
+ const tableNames: string[] = (tableRes.rows || []).map((row: any) => row.tablename);
141
+
142
+ // 获取视图
143
+ const viewSql = `SELECT viewname FROM pg_views WHERE schemaname = '${schema}' ORDER BY viewname`;
144
+ const viewRes = await this.executeUnsafeQuery(dbUtil, viewSql);
145
+ const viewNames: string[] = (viewRes.rows || []).map((row: any) => row.viewname);
146
+
147
+ const tables: ITableInfo[] = [
148
+ ...tableNames.map(name => ({ name, tableType: 'table' })),
149
+ ...viewNames.map(name => ({ name, tableType: 'view' })),
150
+ ];
151
+
152
+ return { tables, tableNames, viewNames };
153
+ }
154
+
155
+ private async loadSQLServerAllTableInfos(query: ITableNamesQuery): Promise<ITableListResult> {
156
+ const dbUtil = { sqlDatabase: query.sqlDatabase, sqlDbType: query.sqlDbType } as IExecuteUnsafeQueryCtx;
157
+
158
+ // 获取物理表
159
+ const tableSql = `SELECT name FROM sys.tables`;
160
+ const tableRes = await this.executeUnsafeQuery(dbUtil, tableSql);
161
+ const tableNames: string[] = (tableRes.rows || []).map((row: any) => row.name);
162
+
163
+ // 获取视图
164
+ const viewSql = `SELECT name FROM sys.views`;
165
+ const viewRes = await this.executeUnsafeQuery(dbUtil, viewSql);
166
+ const viewNames: string[] = (viewRes.rows || []).map((row: any) => row.name);
167
+
168
+ const tables: ITableInfo[] = [
169
+ ...tableNames.map(name => ({ name, tableType: 'table' })),
170
+ ...viewNames.map(name => ({ name, tableType: 'view' })),
171
+ ];
172
+
173
+ return { tables, tableNames, viewNames };
174
+ }
175
+
38
176
  private async loadTableMeta(query: ITableMetaQuery): Promise<ITableMeta> {
39
177
  const { tableMetaCacheTime } = this.getContextCfg();
40
178
 
@@ -1,4 +1,4 @@
1
- import { IFuncCfgModel, IRequestCfgModel, ISqlCfgModel, ITableMeta, ITableMetaQuery } from '../interfaces';
1
+ import { IFuncCfgModel, IRequestCfgModel, ISqlCfgModel, ITableListResult, ITableMeta, ITableMetaQuery, ITableNamesOptions, ITableNamesQuery } from '../interfaces';
2
2
  import { RequestModel } from '../models/RequestModel';
3
3
  import { CrudProFieldValidateService } from './CrudProFieldValidateService';
4
4
  import { CrudProFieldUpdateService } from './CrudProFieldUpdateService';
@@ -14,6 +14,7 @@ import { FuncContext } from '../models/FuncContext';
14
14
  import { CrudProExecuteFuncService } from './CrudProExecuteFuncService';
15
15
  import { CrudProTableMetaService } from './CrudProTableMetaService';
16
16
  import { CrudProDataFilterService } from './CrudProDataFilterService';
17
+ import { CrudProDataTypeConvertService } from './CrudProDataTypeConvertService';
17
18
 
18
19
  class CurdProServiceHub implements ICurdProServiceHub {
19
20
  private readonly executeContext: ExecuteContext;
@@ -27,6 +28,7 @@ class CurdProServiceHub implements ICurdProServiceHub {
27
28
  private readonly executeFuncService: CrudProExecuteFuncService;
28
29
  private readonly tableMetaService: CrudProTableMetaService;
29
30
  private readonly dataFilterService: CrudProDataFilterService;
31
+ private readonly dataTypeConvertService: CrudProDataTypeConvertService;
30
32
 
31
33
  constructor(executeContext: ExecuteContext) {
32
34
  this.executeContext = executeContext;
@@ -39,6 +41,7 @@ class CurdProServiceHub implements ICurdProServiceHub {
39
41
  this.executeFuncService = new CrudProExecuteFuncService(this);
40
42
  this.tableMetaService = new CrudProTableMetaService(this);
41
43
  this.dataFilterService = new CrudProDataFilterService(this);
44
+ this.dataTypeConvertService = new CrudProDataTypeConvertService(this);
42
45
  }
43
46
 
44
47
  getExecuteContext(): ExecuteContext {
@@ -57,6 +60,10 @@ class CurdProServiceHub implements ICurdProServiceHub {
57
60
  return this.fieldValidateService.validateByCfg(cfgModel, reqModel);
58
61
  }
59
62
 
63
+ validateDataType(cfgModel: RequestCfgModel, reqModel: RequestModel) {
64
+ return this.fieldValidateService.validateDataType(cfgModel, reqModel);
65
+ }
66
+
60
67
  async validateByAuthCfg(cfgModel: RequestCfgModel, reqModel: RequestModel): Promise<any> {
61
68
  const contextFunc = this.executeContext.contextFunc;
62
69
  if (contextFunc && contextFunc.validateByAuthCfg) {
@@ -91,9 +98,17 @@ class CurdProServiceHub implements ICurdProServiceHub {
91
98
  return await this.tableMetaService.getTableMeta(query);
92
99
  }
93
100
 
101
+ async getAllTableInfos(query: ITableNamesQuery, options?: ITableNamesOptions): Promise<ITableListResult> {
102
+ return await this.tableMetaService.getAllTableInfos(query, options);
103
+ }
104
+
94
105
  async filterDataByTableMeta(data: Record<string, any>, sqlCfgModel: ISqlCfgModel | SqlCfgModel): Promise<Record<string, any>> {
95
106
  return await this.dataFilterService.filterDataByTableMeta(data, sqlCfgModel);
96
107
  }
108
+
109
+ async convertDataTypeByTableMeta(reqModel: RequestModel, cfgModel: RequestCfgModel): Promise<void> {
110
+ return await this.dataTypeConvertService.convertDataTypeByTableMeta(reqModel, cfgModel);
111
+ }
97
112
  }
98
113
 
99
114
  export { CurdProServiceHub };
@@ -5,6 +5,9 @@ const DateTimeUtils = {
5
5
  getCurrentTimeStampMs(): number {
6
6
  return Date.now();
7
7
  },
8
+ getCurrentTimeString(): string {
9
+ return moment().format('YYYY-MM-DD HH:mm:ss');
10
+ },
8
11
  // 以秒为单位的时间戳
9
12
  getCurrentTimeStampSecond(): number {
10
13
  return Math.floor(Date.now() / 1000);