midway-fatcms 0.0.8 → 0.0.9

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 (75) hide show
  1. package/.qoder/skills/midway-fatcms/02-crud-quick.md +38 -0
  2. package/.qoder/skills/midway-fatcms/03-crud-sharding.md +37 -36
  3. package/.qoder/skills/midway-fatcms/07-examples.md +4 -0
  4. package/dist/configuration.d.ts +10 -0
  5. package/dist/configuration.js +26 -0
  6. package/dist/controller/helpers.controller.d.ts +6 -0
  7. package/dist/controller/helpers.controller.js +19 -0
  8. package/dist/libs/crud-pro/CrudPro.d.ts +29 -2
  9. package/dist/libs/crud-pro/CrudPro.js +58 -2
  10. package/dist/libs/crud-pro/exceptions.d.ts +7 -0
  11. package/dist/libs/crud-pro/exceptions.js +7 -0
  12. package/dist/libs/crud-pro/interfaces.d.ts +1 -0
  13. package/dist/libs/crud-pro/models/CrudResult.d.ts +3 -2
  14. package/dist/libs/crud-pro/models/CrudResult.js +1 -1
  15. package/dist/libs/crud-pro/models/ServiceHub.d.ts +2 -0
  16. package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.d.ts +70 -2
  17. package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.js +205 -13
  18. package/dist/libs/crud-pro/services/CrudProTableMetaService.d.ts +36 -0
  19. package/dist/libs/crud-pro/services/CrudProTableMetaService.js +97 -3
  20. package/dist/libs/crud-pro/services/CurdProServiceHub.d.ts +2 -0
  21. package/dist/libs/crud-pro/services/CurdProServiceHub.js +6 -0
  22. package/dist/libs/crud-pro-quick/CrudProQuick.d.ts +93 -6
  23. package/dist/libs/crud-pro-quick/CrudProQuick.js +192 -32
  24. package/dist/libs/crud-sharding/ShardingBase.d.ts +78 -0
  25. package/dist/libs/crud-sharding/ShardingBase.js +179 -0
  26. package/dist/libs/crud-sharding/ShardingByCustomCrud.d.ts +35 -0
  27. package/dist/libs/crud-sharding/ShardingByCustomCrud.js +297 -0
  28. package/dist/libs/crud-sharding/ShardingByHashCrud.d.ts +38 -0
  29. package/dist/libs/crud-sharding/ShardingByHashCrud.js +86 -0
  30. package/dist/libs/crud-sharding/ShardingByKeyCrud.d.ts +39 -0
  31. package/dist/libs/crud-sharding/ShardingByKeyCrud.js +74 -0
  32. package/dist/libs/crud-sharding/ShardingByTimeCrud.d.ts +66 -0
  33. package/dist/libs/crud-sharding/ShardingByTimeCrud.js +524 -0
  34. package/dist/libs/crud-sharding/ShardingConfig.d.ts +10 -8
  35. package/dist/libs/crud-sharding/ShardingConfig.js +3 -3
  36. package/dist/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +1 -1
  37. package/dist/libs/crud-sharding/index.d.ts +10 -13
  38. package/dist/libs/crud-sharding/index.js +21 -17
  39. package/dist/models/RedisKeys.d.ts +1 -0
  40. package/dist/models/RedisKeys.js +1 -0
  41. package/dist/service/TableMetaCacheRedisSubscriber.d.ts +31 -0
  42. package/dist/service/TableMetaCacheRedisSubscriber.js +98 -0
  43. package/dist/service/curd/CurdMixService.d.ts +2 -2
  44. package/dist/service/curd/CurdProService.d.ts +109 -5
  45. package/dist/service/curd/CurdProService.js +127 -7
  46. package/package.json +1 -1
  47. package/src/configuration.ts +27 -0
  48. package/src/controller/helpers.controller.ts +15 -0
  49. package/src/libs/crud-pro/CrudPro.ts +73 -4
  50. package/src/libs/crud-pro/exceptions.ts +8 -0
  51. package/src/libs/crud-pro/interfaces.ts +1 -0
  52. package/src/libs/crud-pro/models/CrudResult.ts +5 -5
  53. package/src/libs/crud-pro/models/ServiceHub.ts +4 -0
  54. package/src/libs/crud-pro/services/CrudProDataTypeConvertService.ts +238 -15
  55. package/src/libs/crud-pro/services/CrudProTableMetaService.ts +110 -2
  56. package/src/libs/crud-pro/services/CurdProServiceHub.ts +8 -0
  57. package/src/libs/crud-pro-quick/CrudProQuick.ts +234 -46
  58. package/src/libs/crud-sharding/ShardingBase.ts +256 -0
  59. package/src/libs/crud-sharding/ShardingByCustomCrud.ts +329 -0
  60. package/src/libs/crud-sharding/ShardingByHashCrud.ts +111 -0
  61. package/src/libs/crud-sharding/ShardingByKeyCrud.ts +97 -0
  62. package/src/libs/crud-sharding/ShardingByTimeCrud.ts +628 -0
  63. package/src/libs/crud-sharding/ShardingConfig.ts +10 -8
  64. package/src/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +1 -1
  65. package/src/libs/crud-sharding/index.ts +17 -14
  66. package/src/models/RedisKeys.ts +1 -0
  67. package/src/service/TableMetaCacheRedisSubscriber.ts +105 -0
  68. package/src/service/curd/CurdMixService.ts +2 -2
  69. package/src/service/curd/CurdProService.ts +131 -9
  70. package/dist/libs/crud-sharding/ShardingCrudPro.d.ts +0 -208
  71. package/dist/libs/crud-sharding/ShardingCrudPro.js +0 -879
  72. package/dist/libs/crud-sharding/ShardingRouter.d.ts +0 -70
  73. package/dist/libs/crud-sharding/ShardingRouter.js +0 -396
  74. package/src/libs/crud-sharding/ShardingCrudPro.ts +0 -1105
  75. package/src/libs/crud-sharding/ShardingRouter.ts +0 -533
@@ -123,9 +123,39 @@ let CurdProService = class CurdProService extends BaseService_1.BaseService {
123
123
  super(...arguments);
124
124
  this.responseCfgHandlers = {};
125
125
  }
126
+ /**
127
+ * 注册“响应配置处理器”(response cfg handler)。
128
+ *
129
+ * 这些处理器会在一次 CRUD/SQL 执行完成后被调用,用于对 `ExecuteContext` 做统一的二次加工,
130
+ * 例如:追加扩展数据、做字段脱敏、统一包装返回结构等。
131
+ *
132
+ * @param key 处理器类型标识(业务自定义枚举/常量)
133
+ * @param handler 处理器实现
134
+ *
135
+ * @example
136
+ * curdProService.setResponseCfgHandlers(RelatedType.USER, {
137
+ * async handleExecuteContextPrepare(ctx) {
138
+ * // 可在执行结果处理前做准备
139
+ * },
140
+ * async handleExecuteContext(ctx) {
141
+ * // 对 ctx.result / ctx.data 等进行二次加工
142
+ * },
143
+ * });
144
+ */
126
145
  setResponseCfgHandlers(key, handler) {
127
146
  this.responseCfgHandlers[key] = handler;
128
147
  }
148
+ /**
149
+ * 获取当前已注册的“响应配置处理器”集合。
150
+ *
151
+ * 一般用于调试或在外层框架中统一检查/复用已有 handler。
152
+ *
153
+ * @returns 以 `RelatedType` 为 key 的处理器映射表
154
+ *
155
+ * @example
156
+ * const handlers = curdProService.getResponseCfgHandlers();
157
+ * const userHandler = handlers[RelatedType.USER];
158
+ */
129
159
  getResponseCfgHandlers() {
130
160
  return this.responseCfgHandlers;
131
161
  }
@@ -153,8 +183,21 @@ let CurdProService = class CurdProService extends BaseService_1.BaseService {
153
183
  return crudPro;
154
184
  }
155
185
  /**
156
- * 依赖缓存中与加载的配置
157
- * @param reqJson
186
+ * 通过 `method` 走配置化 CRUD 执行(从缓存/加载的配置中解析)。
187
+ *
188
+ * 适用于:前端/调用方只传 `method` + 入参,由服务端根据 `method` 找到对应的 CRUD 配置并执行。
189
+ * 在非调试模式下会启用配置缓存(调试模式关闭缓存,便于开发即时生效)。
190
+ *
191
+ * @param reqJson 请求模型,必须包含 `method`
192
+ * @returns `ExecuteContext`(包含执行过程与结果数据)
193
+ *
194
+ * @example
195
+ * const ctx = await curdProService.executeCrud({
196
+ * method: 'user.list',
197
+ * pageNo: 1,
198
+ * pageSize: 20,
199
+ * });
200
+ * console.log(ctx.result);
158
201
  */
159
202
  async executeCrud(reqJson) {
160
203
  if (!reqJson.method) {
@@ -165,12 +208,45 @@ let CurdProService = class CurdProService extends BaseService_1.BaseService {
165
208
  const isEnableCache = !this.isEnableDebug();
166
209
  return curdPro.executeCrud(reqJson, isEnableCache);
167
210
  }
168
- // 直接执行一个SQL
211
+ /**
212
+ * 直接执行一段 SQL(不依赖 `method` 配置)。
213
+ *
214
+ * 适用于:后台管理/运维脚本/内部工具需要直接跑 SQL 的场景(仍会复用本服务的连接池、日志、visitor、事务等上下文)。
215
+ *
216
+ * @param sqlCfgModel SQL 执行配置(数据库、类型、SQL、参数等)
217
+ * @returns SQL 执行结果
218
+ *
219
+ * @example
220
+ * const res = await curdProService.executeSQL({
221
+ * sqlDbType: SqlDbType.mysql,
222
+ * sqlDatabase: 'mydb',
223
+ * executeSql: 'select * from t_user where id = ?',
224
+ * executeSqlArgs: [1],
225
+ * });
226
+ */
169
227
  async executeSQL(sqlCfgModel) {
170
228
  const curdPro = this.getCrudPro();
171
229
  return curdPro.executeSQL(sqlCfgModel);
172
230
  }
173
- // 直接执行一个请求
231
+ /**
232
+ * 直接按指定配置执行一次 CRUD(不走缓存查找)。
233
+ *
234
+ * 适用于:调用方已经拿到了/动态生成了本次要执行的 CRUD 配置,想直接执行并得到结果。
235
+ * 该方法会对 `cfgModel` 做两类标准化处理:
236
+ * - 统一修正/补全配置(`fixCfgModel`)
237
+ * - 应用软删除规则(`fixSoftDelete`)
238
+ *
239
+ * @param reqJson 本次请求入参
240
+ * @param cfgModel 本次请求对应的 CRUD 配置
241
+ * @returns `ExecuteContext`
242
+ *
243
+ * @example
244
+ * const cfg = await curdProService.getCachedCfgByMethod('user.list');
245
+ * const ctx = await curdProService.executeCrudByCfg(
246
+ * { method: 'user.list', pageNo: 1, pageSize: 20 },
247
+ * cfg as any
248
+ * );
249
+ */
174
250
  async executeCrudByCfg(reqJson, cfgModel) {
175
251
  this.logInfo('executeCrudByCfg', cfgModel);
176
252
  const curdPro = this.getCrudPro();
@@ -210,7 +286,7 @@ let CurdProService = class CurdProService extends BaseService_1.BaseService {
210
286
  * 获取分表 CRUD 操作器
211
287
  *
212
288
  * @param param 参数对象
213
- * @returns ShardingCrudPro 实例
289
+ * @returns ShardingBase 实例(实际类型根据 shardingConfig.type 决定)
214
290
  *
215
291
  * @example
216
292
  * const sharding = curdProService.getShardingCrud({
@@ -225,16 +301,60 @@ let CurdProService = class CurdProService extends BaseService_1.BaseService {
225
301
  */
226
302
  getShardingCrud(param) {
227
303
  const { sqlDatabase, sqlDbType, shardingConfig } = param;
228
- // 传入工厂函数,每次需要时创建新的 CrudPro 实例
229
- const sharding = new crud_sharding_1.ShardingCrudPro(() => this.getCrudPro(), shardingConfig);
304
+ const factory = () => this.getCrudPro();
305
+ let sharding;
306
+ switch (shardingConfig.type) {
307
+ case crud_sharding_1.ShardingType.YEAR:
308
+ case crud_sharding_1.ShardingType.MONTH:
309
+ case crud_sharding_1.ShardingType.DAY:
310
+ sharding = new crud_sharding_1.ShardingByTimeCrud(factory, shardingConfig);
311
+ break;
312
+ case crud_sharding_1.ShardingType.HASH:
313
+ sharding = new crud_sharding_1.ShardingByHashCrud(factory, shardingConfig);
314
+ break;
315
+ case crud_sharding_1.ShardingType.KEY:
316
+ sharding = new crud_sharding_1.ShardingByKeyCrud(factory, shardingConfig);
317
+ break;
318
+ case crud_sharding_1.ShardingType.CUSTOM:
319
+ sharding = new crud_sharding_1.ShardingByCustomCrud(factory, shardingConfig);
320
+ break;
321
+ default:
322
+ throw new Error(`[CurdProService] 不支持的分表类型: ${shardingConfig.type}`);
323
+ }
230
324
  sharding.setBaseCfg({ sqlDatabase, sqlDbType });
231
325
  return sharding;
232
326
  }
327
+ /**
328
+ * 根据 `method` 获取(可能来自缓存的)CRUD 配置。
329
+ *
330
+ * 适用于:你只想先拿到配置做检查/二次加工/审计,再决定是否执行。
331
+ * 非调试模式会启用缓存(调试模式关闭缓存)。
332
+ *
333
+ * @param method 配置方法名,例如:`user.list`
334
+ * @returns CRUD 配置模型
335
+ *
336
+ * @example
337
+ * const cfg = await curdProService.getCachedCfgByMethod('user.list');
338
+ * console.log(cfg.sqlSimpleName);
339
+ */
233
340
  async getCachedCfgByMethod(method) {
234
341
  const curdPro = this.getCrudPro();
235
342
  const isEnableCache = !this.isEnableDebug(); // 开发环境不使用缓存
236
343
  return curdPro.getCachedCfgByMethod(method, isEnableCache);
237
344
  }
345
+ /**
346
+ * 获取数据库的表/视图等结构信息列表(用于数据源探测、表选择器、代码生成等场景)。
347
+ *
348
+ * @param query 查询条件(例如:指定数据库、关键字过滤、分页等)
349
+ * @param options 可选项(例如:是否包含视图/系统表、大小写处理等,具体以接口定义为准)
350
+ * @returns 表信息列表结果
351
+ *
352
+ * @example
353
+ * const tables = await curdProService.getAllTableInfos(
354
+ * { sqlDbType: SqlDbType.mysql, sqlDatabase: 'mydb' },
355
+ * { skipCache: true }
356
+ * );
357
+ */
238
358
  async getAllTableInfos(query, options) {
239
359
  const curdPro = this.getCrudPro();
240
360
  return curdPro.getAllTableInfos(query, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "midway-fatcms",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "This is a midway component sample",
5
5
  "main": "dist/index.js",
6
6
  "typings": "index.d.ts",
@@ -15,6 +15,8 @@ import { GlobalMiddleware } from './middleware/global.middleware';
15
15
  import { ForbiddenMiddleware } from './middleware/forbidden.middleware';
16
16
  import { SCHEDULE_QUEUE, ANONYMOUS_CONTEXT, INNER_SCHEDULE_INTERVAL } from './schedule';
17
17
  import { privateAES } from './libs/utils/crypto-utils';
18
+ import { initTableMetaCachePublishClient, initTableMetaCacheSubscriber } from './service/TableMetaCacheRedisSubscriber';
19
+ import { RedisService } from '@midwayjs/redis';
18
20
 
19
21
  @Configuration({
20
22
  // namespace: 'fatcms',
@@ -65,6 +67,11 @@ export class ContainerLifeCycle {
65
67
  */
66
68
  ANONYMOUS_CONTEXT.setApp(this.app);
67
69
 
70
+ /**
71
+ * 初始化表元数据缓存 Redis 广播
72
+ */
73
+ await this.initTableMetaCacheRedis();
74
+
68
75
  /**
69
76
  * 启动定时任务
70
77
  */
@@ -78,6 +85,26 @@ export class ContainerLifeCycle {
78
85
  // this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
79
86
  }
80
87
 
88
+ /**
89
+ * 初始化表元数据缓存的 Redis Pub/Sub 广播
90
+ *
91
+ * - 发布客户端:复用已有的 RedisService 实例
92
+ * - 订阅客户端:通过 duplicate() 创建独立连接
93
+ *
94
+ * 集群部署时,任意节点调用 clearTableMetaCacheWithBroadcast()
95
+ * 会通过 Redis 广播通知所有节点清空本地缓存。
96
+ */
97
+ private async initTableMetaCacheRedis() {
98
+ try {
99
+ const redisService = await this.app.getApplicationContext().getAsync(RedisService);
100
+ initTableMetaCachePublishClient(redisService);
101
+ initTableMetaCacheSubscriber(redisService);
102
+ } catch (e) {
103
+ // Redis 未配置或不可用时,降级为单节点模式(仅清空本地缓存)
104
+ this.app.getLogger().warn('ContainerLifeCycle ==> initTableMetaCacheRedis Redis不可用,降级为单节点模式:', e.message);
105
+ }
106
+ }
107
+
81
108
  private async startScheduleOnReady() {
82
109
  const logger = this.app.getLogger();
83
110
  const config = this.app.getConfig();
@@ -7,6 +7,7 @@ import { createUniqueId } from '@/libs/utils/functions';
7
7
  import { privateAES } from '@/libs/utils/crypto-utils';
8
8
  import { CommonResult } from '@/libs/utils/common-dto';
9
9
  import {SystemRoleCode} from "@/models/SystemPerm";
10
+ import { clearTableMetaCacheWithBroadcast } from '@/service/TableMetaCacheRedisSubscriber';
10
11
 
11
12
  // http://127.0.0.1:7002/ns/api/helpers/getApiScript?prefix=/ns/api/manage
12
13
  // http://127.0.0.1:7002/ns/api/helpers/getApiScript
@@ -145,6 +146,20 @@ export class HelpersApi {
145
146
  });
146
147
  }
147
148
 
149
+ /**
150
+ * 工具函数: 清空表元数据缓存(集群广播)
151
+ * 清空本节点缓存,并通过 Redis Pub/Sub 广播通知集群中其他节点。
152
+ * 清空后,下次请求会自动从数据库重新加载(懒加载刷新)。
153
+ */
154
+ @Get('/clearTableMetaCache')
155
+ async clearTableMetaCacheApi() {
156
+ const stats = clearTableMetaCacheWithBroadcast();
157
+ return CommonResult.successRes({
158
+ message: '表元数据缓存已清空(已广播集群)',
159
+ beforeClear: stats,
160
+ });
161
+ }
162
+
148
163
  private checkLocalPermissionEnv() {
149
164
  //是否是开发者
150
165
  const isDevelopUser = (): boolean=> {
@@ -1,4 +1,4 @@
1
- import { ICrudProCfg, ILogger, IRequestCfgModel, IRequestModel, ISqlCfgModel, ITableListResult, ITableNamesOptions, ITableNamesQuery, IVisitor, ExecuteSQLResult } from './interfaces';
1
+ import { ICrudProCfg, ILogger, IRequestCfgModel, IRequestModel, ISqlCfgModel, ITableListResult, ITableMetaQuery, ITableNamesOptions, ITableNamesQuery, IVisitor, ExecuteSQLResult } from './interfaces';
2
2
  import { ExecuteContext } from './models/ExecuteContext';
3
3
  import { RequestModel } from './models/RequestModel';
4
4
  import { CurdProServiceHub } from './services/CurdProServiceHub';
@@ -165,12 +165,16 @@ class CrudPro {
165
165
  exeCtx.setReqModel(reqModel);
166
166
  exeCtx.setCfgModel(cfgModel);
167
167
 
168
+ // insertOrUpdate 参数校验
169
+ this.validateParamsIfNeeded(reqModel, cfgModel);
170
+
168
171
  // 如果是 sqlSimpleName模式的 update/insert,则需要将 reqJson.data 部分根据真实的表结构进行过滤
169
172
  await this.filterDataByTableMetaIfNeeded(reqModel, cfgModel);
170
173
 
171
- // 根据表结构字段类型,自动转换数据格式(如 PostgreSQL ARRAY 字段)
174
+ //插入和更新时: 根据表结构字段类型,自动转换 data 数据格式(如 PostgreSQL ARRAY 字段)
172
175
  await this.convertDataFieldTypeIfNeeded(reqModel, cfgModel);
173
-
176
+ //查询时:根据表结构字段类型,自动转换 condition 数据格式(防止数据库隐式类型转换导致索引失效)
177
+ await this.convertConditionTypeIfNeeded(reqModel, cfgModel);
174
178
  // 参数校验
175
179
  this.serviceHub.validateDataType(cfgModel, reqModel);
176
180
  this.serviceHub.validateByAllow(cfgModel, reqModel);
@@ -214,6 +218,19 @@ class CrudPro {
214
218
  return this.serviceHub.getAllTableInfos(query, options);
215
219
  }
216
220
 
221
+ /**
222
+ * 获取表的主键列名列表
223
+ *
224
+ * 复用 CrudProTableMetaService 的 getTableMeta 缓存,
225
+ * 从 columnDetails 中筛选 isPrimaryKey 的列。
226
+ *
227
+ * @param query 表元数据查询参数
228
+ * @returns 主键列名数组
229
+ */
230
+ public async getPrimaryKeyColumns(query: ITableMetaQuery): Promise<string[]> {
231
+ return this.serviceHub.getPrimaryKeyColumns(query);
232
+ }
233
+
217
234
  /**
218
235
  * 如果是 INSERT/UPDATE 操作(sqlSimpleName 模式),根据真实表结构过滤 reqModel.data 中的字段
219
236
  * 避免传入不存在的字段导致 SQL 执行报错
@@ -266,6 +283,48 @@ class CrudPro {
266
283
  reqModel.data = filteredData;
267
284
  }
268
285
 
286
+ /**
287
+ * 校验特定操作的必填参数
288
+ *
289
+ * - insertOrUpdate 和 update:condition 和 data 均为必填
290
+ * - insertOnDuplicateUpdate 和 insert:只有data 为必填
291
+ * @param reqModel 请求模型
292
+ * @param cfgModel 配置模型
293
+ * @throws CommonException 如果缺少必填参数
294
+ */
295
+ private validateParamsIfNeeded(reqModel: RequestModel, cfgModel: RequestCfgModel): void {
296
+ // insertOrUpdate 和 update:condition 和 data 均为必填
297
+ if (cfgModel.sqlSimpleName === KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE ||
298
+ cfgModel.sqlSimpleName === KeysOfSimpleSQL.SIMPLE_UPDATE) {
299
+ if (!reqModel.condition || Object.keys(reqModel.condition).length === 0) {
300
+ throw new CommonException(
301
+ Exceptions.INSERT_OR_UPDATE_CONDITION_EMPTY,
302
+ '[CrudPro] insertOrUpdate/update 操作的 condition 不能为空。' +
303
+ 'condition 用于判断记录是否存在或定位更新目标。'
304
+ );
305
+ }
306
+ if (!reqModel.data || Object.keys(reqModel.data as Record<string, any>).length === 0) {
307
+ throw new CommonException(
308
+ Exceptions.INSERT_OR_UPDATE_DATA_EMPTY,
309
+ '[CrudPro] insertOrUpdate/update 操作的 data 不能为空。' +
310
+ 'data 用于指定插入或更新的内容。'
311
+ );
312
+ }
313
+ }
314
+
315
+ // insertOnDuplicateUpdate 和 insert:只有 data 为必填
316
+ if (cfgModel.sqlSimpleName === KeysOfSimpleSQL.SIMPLE_INSERT_ON_DUPLICATE_UPDATE ||
317
+ cfgModel.sqlSimpleName === KeysOfSimpleSQL.SIMPLE_INSERT) {
318
+ if (!reqModel.data || Object.keys(reqModel.data as Record<string, any>).length === 0) {
319
+ throw new CommonException(
320
+ Exceptions.INSERT_OR_DUPLICATE_UPDATE_DATA_EMPTY,
321
+ '[CrudPro] insert/insertOnDuplicateUpdate 操作的 data 不能为空。' +
322
+ 'data 用于指定插入或更新的内容。'
323
+ );
324
+ }
325
+ }
326
+ }
327
+
269
328
  /**
270
329
  * 判断 sqlSimpleName 是否为 INSERT/UPDATE 类型
271
330
  * @param sqlSimpleName 简单 SQL 名称
@@ -282,7 +341,7 @@ class CrudPro {
282
341
  }
283
342
 
284
343
  /**
285
- * 根据表结构字段类型,自动转换数据格式
344
+ * 根据表结构字段类型,自动转换 data 数据格式
286
345
  * 例如:PostgreSQL 的 ARRAY 类型字段,需要将 JSON 数组转为 PG 数组字面量格式
287
346
  * @param reqModel 请求模型
288
347
  * @param cfgModel 配置模型
@@ -291,6 +350,16 @@ class CrudPro {
291
350
  await this.serviceHub.convertDataTypeByTableMeta(reqModel, cfgModel);
292
351
  }
293
352
 
353
+ /**
354
+ * 查询时:根据表结构字段类型,自动转换 condition 数据格式
355
+ * 防止数据库隐式类型转换导致索引失效(如 WHERE int_col = '123' 导致索引无法命中)
356
+ * @param reqModel 请求模型
357
+ * @param cfgModel 配置模型
358
+ */
359
+ private async convertConditionTypeIfNeeded(reqModel: RequestModel, cfgModel: RequestCfgModel): Promise<void> {
360
+ await this.serviceHub.convertConditionTypeByTableMeta(reqModel, cfgModel);
361
+ }
362
+
294
363
  private async executeSQLList() {
295
364
  try {
296
365
  await this.serviceHub.executeSqlCfgModels();
@@ -61,6 +61,13 @@ export enum Exceptions {
61
61
  DATA_GET_DATA_EMPTY_ON_INSERT_VALUES = 'DATA_GET_DATA_EMPTY_ON_INSERT_VALUES',
62
62
  BATCH_INSERT_KEYS_MISMATCH = 'BATCH_INSERT_KEYS_MISMATCH', // 批量插入时字段不一致
63
63
 
64
+ /**
65
+ * insertOrUpdate 参数缺失
66
+ */
67
+ INSERT_OR_UPDATE_CONDITION_EMPTY = 'INSERT_OR_UPDATE_CONDITION_EMPTY', // insertOrUpdate/update 的 condition 不能为空
68
+ INSERT_OR_UPDATE_DATA_EMPTY = 'INSERT_OR_UPDATE_DATA_EMPTY', // insertOrUpdate/update 的 data 不能为空
69
+ INSERT_OR_DUPLICATE_UPDATE_DATA_EMPTY = 'INSERT_ON_DUPLICATE_UPDATE_DATA_EMPTY', // insertOnDuplicateUpdate/insert 的 data 不能为空
70
+
64
71
  /**
65
72
  * 请求的Method字段为空
66
73
  */
@@ -85,6 +92,7 @@ export enum Exceptions {
85
92
  RUN_PICK_ERR_VISITOR_FIELD_NULL = 'RUN_PICK_ERR_VISITOR_FIELD_NULL',
86
93
  RUN_EXECUTE_VALIDATE = 'RUN_EXECUTE_VALIDATE',
87
94
  RUN_FUNCTION_NOT_FOUND = 'RUN_FUNCTION_NOT_FOUND',
95
+ MORE_THAN_ONE_RECORDS_FOUND = 'MORE_THAN_ONE_RECORDS_FOUND',
88
96
 
89
97
  /**
90
98
  * 参数校验错误
@@ -17,6 +17,7 @@ export interface ITableColumn {
17
17
  type: string; // 数据类型
18
18
  isNullable: boolean; // 是否可空
19
19
  isPrimaryKey?: boolean; // 是否主键
20
+ isIdentity?: boolean; // 是否自增列(SQL Server)
20
21
  defaultValue?: any; // 默认值
21
22
  maxLength?: number; // 最大长度
22
23
  comment?: string; // 字段注释
@@ -89,11 +89,14 @@ class CrudWriteResult extends CrudResultBase {
89
89
  class CrudQueryOneResult<T = Record<string, any>> extends CrudResultBase {
90
90
  readonly row: T | null;
91
91
  readonly found: boolean;
92
+ /** 数据来源的物理表名(分表场景下为实际分表名,如 t_order_202403) */
93
+ readonly fromTable: string;
92
94
 
93
- constructor(data: { row: T | null; rawContext: ExecuteContext; debugInfo?: CrudResultDebugInfo }) {
95
+ constructor(data: { row: T | null; rawContext: ExecuteContext; debugInfo?: CrudResultDebugInfo; fromTable: string }) {
94
96
  super(data.rawContext, data.debugInfo);
95
97
  this.row = data.row;
96
- this.found = (this.row !== null && this.row !== undefined );
98
+ this.found = (this.row !== null && this.row !== undefined);
99
+ this.fromTable = data.fromTable;
97
100
  }
98
101
  }
99
102
 
@@ -145,13 +148,11 @@ class CrudCountResult extends CrudResultBase {
145
148
 
146
149
  /** InsertOrUpdate 结果 */
147
150
  class CrudUpsertResult extends CrudResultBase {
148
- readonly affected?: ResModelAffected;
149
151
  readonly insertAffected?: ResModelAffected;
150
152
  readonly updateAffected?: ResModelAffected;
151
153
  readonly isExist: boolean;
152
154
 
153
155
  constructor(data: {
154
- affected?: ResModelAffected;
155
156
  insertAffected?: ResModelAffected;
156
157
  updateAffected?: ResModelAffected;
157
158
  isExist: boolean;
@@ -159,7 +160,6 @@ class CrudUpsertResult extends CrudResultBase {
159
160
  debugInfo?: CrudResultDebugInfo;
160
161
  }) {
161
162
  super(data.rawContext, data.debugInfo);
162
- this.affected = data.affected;
163
163
  this.insertAffected = data.insertAffected;
164
164
  this.updateAffected = data.updateAffected;
165
165
  this.isExist = data.isExist;
@@ -30,5 +30,9 @@ export interface ICurdProServiceHub {
30
30
 
31
31
  getTableMeta(query: ITableMetaQuery): Promise<ITableMeta>;
32
32
 
33
+ getPrimaryKeyColumns(query: ITableMetaQuery): Promise<string[]>;
34
+
33
35
  convertDataTypeByTableMeta(reqModel: RequestModel, cfgModel: RequestCfgModel): Promise<void>;
36
+
37
+ convertConditionTypeByTableMeta(reqModel: RequestModel, cfgModel: RequestCfgModel): Promise<void>;
34
38
  }