@shys/crud 1.0.1-beta.4 → 1.0.1-beta.6

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/README.md CHANGED
@@ -143,6 +143,8 @@ export default {
143
143
  onlyEntity: false,
144
144
  noDb: false,
145
145
  templatesPath: './my-templates',
146
+ baseFieldNames: ['createTime', 'updateTime', 'deleteTime'],
147
+ excludeWritableFields: ['ctime', 'mtime', 'created_at', 'updated_at', 'create_time', 'update_time'],
146
148
  db: {
147
149
  host: '127.0.0.1',
148
150
  port: 3306,
@@ -165,6 +167,8 @@ export default {
165
167
  | `dryRun` | `boolean` | `false` | 模拟运行模式。只打印将要生成的文件列表,不实际写入磁盘。适合预览效果。 |
166
168
  | `compile` | `boolean` | `false` | 生成完成后是否自动执行 `mwtsc --cleanOutDir` 进行 TypeScript 编译。 |
167
169
  | `onlyEntity` | `boolean` | `false` | 仅生成 entity 实体文件,不生成 DTO、VO、Service、Controller 等其他文件。适合只需要实体类的场景。 |
170
+ | `baseFieldNames` | `string[]` | `["createTime","updateTime","deleteTime"]` | BaseEntity 基础字段列表(驼峰命名)。这些字段会从 Entity 模板中排除(不重复声明)。如果你的基类使用了不同的字段名,可以在此配置。 |
171
+ | `excludeWritableFields` | `string[]` | `["ctime","mtime","created_at","updated_at","create_time","update_time"]` | 自动写入的时间戳字段列表(数据库原始字段名)。这些字段不会出现在 Create/Update/Replace DTO 中,因为它们通常由数据库或框架自动维护。 |
168
172
  | `noDb` | `boolean` | `false` | 不连接数据库读取表结构。启用后使用内置模板字段生成(字段为示例占位,需手动修改)。 |
169
173
  | `templatesPath` | `string` | 无 | 自定义模板目录路径。在此目录下放置与默认模板同名的 `.njk` 文件即可覆盖对应模板。未覆盖的模板继续使用默认模板。 |
170
174
  | `db.host` | `string` | `127.0.0.1` | 数据库主机地址。 |
@@ -207,76 +211,162 @@ export default {
207
211
 
208
212
  ### 模板变量参考
209
213
 
214
+ 所有模板共享以下通用命名变量:
215
+
216
+ | 变量 | 类型 | 说明 | 示例 |
217
+ |------|------|------|------|
218
+ | `tableName` | `string` | 数据库表名 | `"sys_user"` |
219
+ | `Camel` | `string` | 表名的 camelCase 形式 | `"sysUser"` |
220
+ | `Pascal` | `string` | 表名的 PascalCase 形式 | `"SysUser"` |
221
+ | `CreateName` | `string` | Create DTO 类名 | `"CreateSysUserDto"` |
222
+ | `UpdateName` | `string` | Update DTO 类名 | `"UpdateSysUserDto"` |
223
+ | `ReplaceName` | `string` | Replace DTO 类名 | `"ReplaceSysUserDto"` |
224
+ | `QueryName` | `string` | Query DTO 类名 | `"QuerySysUserDto"` |
225
+ | `VOName` | `string` | 单个 VO 类名 | `"SysUserVO"` |
226
+ | `ListVOName` | `string` | 列表 VO 类名 | `"SysUserListVO"` |
227
+ | `ServiceName` | `string` | Service 类名 | `"SysUserService"` |
228
+ | `CtrlName` | `string` | Controller 类名 | `"SysUserController"` |
229
+ | `pluralKebab` | `string` | 复数 kebab-case 路由名 | `"sys-users"` |
230
+ | `tagName` | `string` | Swagger tag 名 | `"SysUser管理"` |
231
+ | `desc` | `string` | 接口描述 | `"sys_user 表增删改查接口"` |
232
+
233
+ #### 字段对象(Field)属性
234
+
235
+ 所有字段列表(如 `fields`、`createFields`、`writable`、`filterable`)中的每个字段对象都包含以下属性:
236
+
237
+ | 属性 | 类型 | 说明 | 示例 |
238
+ |------|------|------|------|
239
+ | `name` | `string` | 数据库字段名 | `"user_name"` |
240
+ | `camel` | `string` | 驼峰命名 | `"userName"` |
241
+ | `type` | `string` | 数据库原始类型 | `"varchar"` |
242
+ | `ormType` | `string` | ORM 类型(标准化后) | `"varchar"` |
243
+ | `jsType` | `string` | JavaScript 类型(含 null) | `"string \| null"` |
244
+ | `createDtoType` | `string` | Create DTO 中的类型 | `"string \| null"` |
245
+ | `voType` | `string` | VO 中的类型(日期转 string) | `"string"` |
246
+ | `swagType` | `string` | Swagger 类型字符串 | `"'string'"` |
247
+ | `len` | `number \| null` | 字段长度 | `64` |
248
+ | `nullable` | `boolean` | 是否可空 | `true` |
249
+ | `comment` | `string` | 字段注释 | `"用户名"` |
250
+ | `defaultVal` | `any` | 默认值 | `""` |
251
+ | `pk` | `boolean` | 是否主键 | `false` |
252
+ | `auto` | `boolean` | 是否自增 | `false` |
253
+ | `joiRule` | `string` | Joi 验证规则(非必填) | `"Joi.string().max(64).label('用户名').allow(null)"` |
254
+ | `joiRuleRequired` | `string` | Joi 验证规则(必填) | `"Joi.string().max(64).label('用户名').required()"` |
255
+ | `supportsLength` | `boolean` | 该类型是否支持长度 | `true` |
256
+ | `isDate` | `boolean` | 是否日期类型 | `false` |
257
+ | `isSensitive` | `boolean` | 是否敏感字段(密码等) | `false` |
258
+ | `idx` | `boolean` | 是否索引字段(仅 entity 模板) | `false` |
259
+
260
+ ---
261
+
210
262
  #### entity.njk
211
263
 
212
- ```
213
- orm: string // ORM 类型:typeorm/sequelize/mikro-orm
214
- ormSuffix: string // 实体文件后缀:entity/model
215
- fields: Array<{ // 字段列表(排除主键和基础字段)
216
- name: string // 数据库字段名
217
- camel: string // 驼峰命名
218
- type: string // 数据库类型
219
- jsType: string // JavaScript 类型
220
- ormType: string // ORM 类型
221
- len: number | null // 字段长度
222
- nullable: boolean // 是否可空
223
- comment: string // 字段注释
224
- supportsLength: boolean
225
- isDate: boolean
226
- isSensitive: boolean
227
- idx: boolean // 是否索引字段
228
- }>
229
- tableName: string // 表名
230
- Pascal: string // PascalCase 命名
231
- Camel: string // camelCase 命名
232
- pkInfo: Object | null // 主键信息
233
- hasPk: boolean // 是否有主键
234
- hasDateField: boolean // 是否有日期字段
235
- idxCount: number // 索引字段数量
236
- ```
264
+ | 变量 | 类型 | 说明 |
265
+ |------|------|------|
266
+ | `orm` | `string` | ORM 类型:`typeorm` / `sequelize` / `mikro` / `mongoose` |
267
+ | `ormSuffix` | `string` | 实体文件后缀:`entity` / `model` |
268
+ | `fields` | `Field[]` | 字段列表(排除主键和 BaseEntity 基础字段) |
269
+ | `tableName` | `string` | 表名 |
270
+ | `Pascal` | `string` | PascalCase 命名 |
271
+ | `Camel` | `string` | camelCase 命名 |
272
+ | `pkInfo` | `Field \| null` | 主键字段信息(已增强) |
273
+ | `hasPk` | `boolean` | 是否有主键 |
274
+ | `hasDateField` | `boolean` | 是否包含日期字段 |
275
+ | `idxCount` | `number` | 索引字段数量 |
276
+
277
+ #### dto-create.njk
278
+
279
+ | 变量 | 类型 | 说明 |
280
+ |------|------|------|
281
+ | `CreateName` | `string` | Create DTO 类名 |
282
+ | `Pascal` | `string` | PascalCase 命名 |
283
+ | `Camel` | `string` | camelCase 命名 |
284
+ | `createFields` | `Field[]` | 可创建字段列表(排除自增主键) |
285
+ | `picks` | `string` | Pick 字段列表字符串,如 `"name",\n "email"` |
237
286
 
238
- #### dto-create.njk / dto-update.njk / dto-replace.njk
287
+ #### dto-update.njk
239
288
 
240
- ```
241
- CreateName: string // Create DTO 类名
242
- UpdateName: string // Update DTO 类名
243
- ReplaceName: string // Replace DTO 类名
244
- Pascal: string // PascalCase 命名
245
- createFields: Array // 可创建字段
246
- writable: Array // 可写字段
247
- picks: string // Pick 类型的字段列表字符串
248
- typeArgs: string // 类型联合字符串
249
- ```
289
+ | 变量 | 类型 | 说明 |
290
+ |------|------|------|
291
+ | `UpdateName` | `string` | Update DTO 类名 |
292
+ | `CreateName` | `string` | Create DTO 类名(用于继承) |
293
+ | `writable` | `Field[]` | 可写字段列表(排除时间戳字段) |
294
+ | `picks` | `string` | Pick 字段列表字符串 |
295
+ | `typeArgs` | `string` | 类型联合字符串,如 `'name' \| 'email'` |
296
+
297
+ #### dto-replace.njk
298
+
299
+ | 变量 | 类型 | 说明 |
300
+ |------|------|------|
301
+ | `ReplaceName` | `string` | Replace DTO 类名 |
302
+ | `CreateName` | `string` | Create DTO 类名(用于继承) |
303
+ | `writable` | `Field[]` | 可写字段列表 |
304
+ | `picks` | `string` | Pick 字段列表字符串 |
305
+
306
+ #### dto-query.njk
307
+
308
+ | 变量 | 类型 | 说明 |
309
+ |------|------|------|
310
+ | `QueryName` | `string` | Query DTO 类名 |
311
+ | `filterable` | `Field[]` | 可筛选字段列表(含主键) |
312
+ | `picks` | `string` | Pick 字段列表字符串 |
313
+ | `typeArgs` | `string` | 类型联合字符串 |
314
+ | `sortDescription` | `string` | sort 字段的描述文本 |
315
+ | `filterExample` | `string` | filter 字段的示例值 |
316
+ | `searchDescription` | `string` | search 字段的描述文本 |
317
+
318
+ #### vo-single.njk
319
+
320
+ | 变量 | 类型 | 说明 |
321
+ |------|------|------|
322
+ | `VOName` | `string` | 单个 VO 类名 |
323
+ | `Pascal` | `string` | PascalCase 命名(Entity 类名,用于继承) |
324
+ | `Camel` | `string` | camelCase 命名 |
325
+ | `fields` | `Field[]` | 所有字段(含主键,排除 BaseEntity 基础字段) |
326
+ | `hasTransform` | `boolean` | 是否有需要转换的字段(日期/敏感) |
327
+
328
+ #### vo-list.njk
329
+
330
+ | 变量 | 类型 | 说明 |
331
+ |------|------|------|
332
+ | `ListVOName` | `string` | 列表 VO 类名 |
333
+ | `VOName` | `string` | 单个 VO 类名 |
334
+ | `Camel` | `string` | camelCase 命名 |
335
+
336
+ #### vo-index.njk
337
+
338
+ | 变量 | 类型 | 说明 |
339
+ |------|------|------|
340
+ | `Camel` | `string` | camelCase 命名 |
250
341
 
251
342
  #### service.njk
252
343
 
253
- ```
254
- ServiceName: string // Service 类名
255
- Pascal: string // PascalCase 命名
256
- Camel: string // camelCase 命名
257
- ormInfo: {
258
- orm: string
259
- ormSuffix: string
260
- serviceParent: string // 父类名
261
- importCrudOrm: string // 导入语句
262
- injectEntity: string // 装饰器名
263
- }
264
- ```
344
+ | 变量 | 类型 | 说明 |
345
+ |------|------|------|
346
+ | `ServiceName` | `string` | Service 类名 |
347
+ | `Pascal` | `string` | PascalCase 命名 |
348
+ | `Camel` | `string` | camelCase 命名 |
349
+ | `ormInfo` | `object` | ORM 信息对象 |
265
350
 
266
351
  #### controller.njk
267
352
 
268
- ```
269
- CtrlName: string // Controller 类名
270
- ServiceName: string // Service 类名
271
- Pascal: string
272
- Camel: string
273
- pluralKebab: string // 复数 kebab-case 路由名
274
- tagName: string // Swagger tag
275
- desc: string // 接口描述
276
- sortable: string[] // 可排序字段
277
- filterable: string[] // 可过滤字段
278
- searchable: string[] // 可搜索字段
279
- ```
353
+ | 变量 | 类型 | 说明 |
354
+ |------|------|------|
355
+ | `CtrlName` | `string` | Controller 类名 |
356
+ | `Pascal` | `string` | PascalCase 命名 |
357
+ | `ServiceName` | `string` | Service 类名 |
358
+ | `CreateName` / `UpdateName` / `ReplaceName` / `QueryName` | `string` | DTO 类名 |
359
+ | `VOName` / `ListVOName` | `string` | VO 类名 |
360
+ | `Camel` | `string` | camelCase 命名 |
361
+ | `pluralKebab` | `string` | 复数 kebab-case 路由名 |
362
+ | `tagName` | `string` | Swagger tag 名 |
363
+ | `desc` | `string` | 接口描述 |
364
+ | `sortable` | `string` | 可排序字段 JSON 字符串 |
365
+ | `filterable` | `string` | 可过滤字段 JSON 字符串 |
366
+ | `searchable` | `string` | 可搜索字段 JSON 字符串 |
367
+ | `pkCamel` | `string` | 主键驼峰名 |
368
+ | `idField` | `string` | ID 字段名 |
369
+ | `defaultSort` | `string` | 默认排序配置 JSON 字符串 |
280
370
 
281
371
  ### 自定义模板示例
282
372
 
package/dist/cli.js CHANGED
@@ -277,6 +277,8 @@ export async function runCli(argv = process.argv.slice(2)) {
277
277
  outputPath,
278
278
  templatesPath,
279
279
  onlyEntity: cli.onlyEntity,
280
+ baseFieldNames: cli.baseFieldNames,
281
+ excludeWritableFields: cli.excludeWritableFields,
280
282
  });
281
283
  console.log(`\n${chalk.cyan(`生成清单(${built.files.length})`)}`);
282
284
  const stats = writeAll(built.files, { force: !!cli.force, dryRun: !!cli.dryRun });
package/dist/config.js CHANGED
@@ -107,6 +107,10 @@ export function mergeConfigs(...configs) {
107
107
  result.templatesPath = config.templatesPath;
108
108
  if (config.onlyEntity !== undefined)
109
109
  result.onlyEntity = config.onlyEntity;
110
+ if (config.baseFieldNames !== undefined)
111
+ result.baseFieldNames = config.baseFieldNames;
112
+ if (config.excludeWritableFields !== undefined)
113
+ result.excludeWritableFields = config.excludeWritableFields;
110
114
  if (config.db) {
111
115
  result.db = { ...(result.db || {}), ...config.db };
112
116
  }
package/dist/db.js CHANGED
@@ -52,7 +52,7 @@ export async function getTableFieldsFromDb(dbConfig, tableName) {
52
52
  database: database || '',
53
53
  });
54
54
  try {
55
- const [rows] = await connection.execute(`SELECT column_name, data_type, is_nullable, column_type, column_comment, column_default, character_maximum_length, extra
55
+ const [rows] = await connection.execute(`SELECT column_name, data_type, is_nullable, column_type, column_comment, column_default, character_maximum_length, extra, column_key
56
56
  FROM information_schema.COLUMNS
57
57
  WHERE table_schema = ? AND table_name = ?
58
58
  ORDER BY ordinal_position`, [database, tableName]);
@@ -68,7 +68,8 @@ export async function getTableFieldsFromDb(dbConfig, tableName) {
68
68
  const defaultVal = row.COLUMN_DEFAULT || row.column_default;
69
69
  const length = row.CHARACTER_MAXIMUM_LENGTH || row.character_maximum_length;
70
70
  const extra = row.EXTRA || row.extra || '';
71
- const isPk = extra.includes('PRI') || columnName.toLowerCase().includes('id');
71
+ const columnKey = row.COLUMN_KEY || row.column_key || '';
72
+ const isPk = columnKey === 'PRI';
72
73
  const isAuto = extra.includes('auto_increment');
73
74
  return {
74
75
  name: columnName,
@@ -23,5 +23,7 @@ export interface BuildFilesParams {
23
23
  templatesPath?: string;
24
24
  renderer?: TemplateRenderer;
25
25
  onlyEntity?: boolean;
26
+ baseFieldNames?: string[];
27
+ excludeWritableFields?: string[];
26
28
  }
27
29
  export declare function buildFiles(params: BuildFilesParams): BuildResult;
package/dist/generator.js CHANGED
@@ -1,19 +1,54 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { toCamelCase, toPascalCase, toPluralKebab } from './utils.js';
3
3
  import { createRenderer } from './renderer.js';
4
- const BASE_FIELD_NAMES = ['createTime', 'updateTime', 'deleteTime'];
4
+ const DEFAULT_BASE_FIELD_NAMES = ['createTime', 'updateTime', 'deleteTime'];
5
+ const DEFAULT_EXCLUDE_WRITABLE_FIELDS = [
6
+ 'ctime',
7
+ 'mtime',
8
+ 'created_at',
9
+ 'updated_at',
10
+ 'create_time',
11
+ 'update_time',
12
+ ];
5
13
  const JSTypeMap = {
6
- int: 'number', integer: 'number', bigint: 'number', smallint: 'number', tinyint: 'number', mediumint: 'number',
7
- decimal: 'number', numeric: 'number', float: 'number', double: 'number', real: 'number',
8
- char: 'string', varchar: 'string', text: 'string', tinytext: 'string', mediumtext: 'string', longtext: 'string',
9
- enum: 'string', set: 'string', json: 'any',
10
- date: 'Date', datetime: 'Date', timestamp: 'Date', time: 'Date', year: 'number',
11
- binary: 'Buffer', varbinary: 'Buffer', blob: 'Buffer',
12
- bool: 'boolean', boolean: 'boolean', bit: 'boolean',
14
+ int: 'number',
15
+ integer: 'number',
16
+ bigint: 'number',
17
+ smallint: 'number',
18
+ tinyint: 'number',
19
+ mediumint: 'number',
20
+ decimal: 'number',
21
+ numeric: 'number',
22
+ float: 'number',
23
+ double: 'number',
24
+ real: 'number',
25
+ char: 'string',
26
+ varchar: 'string',
27
+ text: 'string',
28
+ tinytext: 'string',
29
+ mediumtext: 'string',
30
+ longtext: 'string',
31
+ enum: 'string',
32
+ set: 'string',
33
+ json: 'any',
34
+ date: 'Date',
35
+ datetime: 'Date',
36
+ timestamp: 'Date',
37
+ time: 'Date',
38
+ year: 'number',
39
+ binary: 'Buffer',
40
+ varbinary: 'Buffer',
41
+ blob: 'Buffer',
42
+ bool: 'boolean',
43
+ boolean: 'boolean',
44
+ bit: 'boolean',
13
45
  };
14
46
  const NO_LENGTH_ORM_TYPES = /^(text|tinytext|mediumtext|longtext|blob|tinyblob|mediumblob|longblob|json)$/i;
15
47
  function normalizeOrmType(type) {
16
- return String(type).toLowerCase().replace(/\(\d+.*?\)/, '').trim() || 'varchar';
48
+ return (String(type)
49
+ .toLowerCase()
50
+ .replace(/\(\d+.*?\)/, '')
51
+ .trim() || 'varchar');
17
52
  }
18
53
  function fieldSupportsLength(f) {
19
54
  return !NO_LENGTH_ORM_TYPES.test(normalizeOrmType(f.type));
@@ -30,9 +65,19 @@ function createDtoTypeOf(field) {
30
65
  }
31
66
  function joiSwagType(f) {
32
67
  const t = f.type;
33
- if (t === 'int' || t === 'bigint' || t === 'smallint' || t === 'tinyint' || t === 'mediumint' || t === 'integer')
68
+ if (t === 'int' ||
69
+ t === 'bigint' ||
70
+ t === 'smallint' ||
71
+ t === 'tinyint' ||
72
+ t === 'mediumint' ||
73
+ t === 'integer')
34
74
  return `'integer'`;
35
- if (t === 'decimal' || t === 'numeric' || t === 'float' || t === 'double' || t === 'real' || t === 'number')
75
+ if (t === 'decimal' ||
76
+ t === 'numeric' ||
77
+ t === 'float' ||
78
+ t === 'double' ||
79
+ t === 'real' ||
80
+ t === 'number')
36
81
  return `'number'`;
37
82
  if (t === 'datetime' || t === 'timestamp' || t === 'date')
38
83
  return `'string'`;
@@ -114,13 +159,18 @@ function enhanceField(f, extra = {}) {
114
159
  supportsLength: fieldSupportsLength(f),
115
160
  isDate: /datetime|date|timestamp/i.test(f.type),
116
161
  isSensitive: /password|passwd|salt|token|secret|private_key/i.test(f.name + f.camel),
117
- voType: /datetime|date|timestamp/i.test(f.type) ? 'string' : (JSTypeMap[f.type] || 'string'),
162
+ voType: /datetime|date|timestamp/i.test(f.type)
163
+ ? 'string'
164
+ : JSTypeMap[f.type] || 'string',
118
165
  idx: !!extra.idx,
119
166
  };
120
167
  }
121
168
  export function buildFiles(params) {
122
- const { moduleName, tableName, baseSrcPath, ormInfo, fields, outputPath, templatesPath, renderer: customRenderer, onlyEntity } = params;
169
+ const { moduleName, tableName, baseSrcPath, ormInfo, fields, outputPath, templatesPath, renderer: customRenderer, onlyEntity, baseFieldNames, excludeWritableFields, } = params;
123
170
  const renderer = customRenderer || createRenderer({ customTemplatesPath: templatesPath });
171
+ const finalBaseFieldNames = baseFieldNames ?? DEFAULT_BASE_FIELD_NAMES;
172
+ const finalExcludeWritable = excludeWritableFields ?? DEFAULT_EXCLUDE_WRITABLE_FIELDS;
173
+ const excludeWritableRegex = new RegExp(finalExcludeWritable.map(s => `^${s}$`).join('|'), 'i');
124
174
  const Camel = toCamelCase(tableName);
125
175
  const Pascal = toPascalCase(tableName);
126
176
  const CreateName = `Create${Pascal}Dto`;
@@ -136,18 +186,19 @@ export function buildFiles(params) {
136
186
  const desc = `${tableName} 表增删改查接口`;
137
187
  const modRoot = resolve(outputPath || baseSrcPath, 'modules', moduleName);
138
188
  const resultFiles = [];
139
- const originalPkInfo = fields.find((f) => f.pk);
189
+ const originalPkInfo = fields.find(f => f.pk);
140
190
  const hasPk = !!originalPkInfo;
141
- const entityFields = fields.filter((f) => {
142
- if (BASE_FIELD_NAMES.includes(f.camel))
191
+ const entityFields = fields.filter(f => {
192
+ if (finalBaseFieldNames.includes(f.camel))
143
193
  return false;
144
194
  if (f.pk)
145
195
  return false;
146
196
  return true;
147
197
  });
148
- const idxFields = entityFields.filter((f) => !f.pk && (String(f.name).toUpperCase() === 'COUNTRYCODE' || /_id$/.test(f.name)));
149
- const hasDateField = entityFields.some((f) => /datetime|date|timestamp/i.test(f.type));
150
- const enhancedEntityFields = entityFields.map((f) => enhanceField(f, { idx: idxFields.some((i) => i.name === f.name) }));
198
+ const idxFields = entityFields.filter(f => !f.pk &&
199
+ (String(f.name).toUpperCase() === 'COUNTRYCODE' || /_id$/.test(f.name)));
200
+ const hasDateField = entityFields.some(f => /datetime|date|timestamp/i.test(f.type));
201
+ const enhancedEntityFields = entityFields.map(f => enhanceField(f, { idx: idxFields.some(i => i.name === f.name) }));
151
202
  const enhancedPkInfo = originalPkInfo ? enhanceField(originalPkInfo) : null;
152
203
  // Entity
153
204
  resultFiles.push({
@@ -176,37 +227,46 @@ export function buildFiles(params) {
176
227
  searchable: [],
177
228
  };
178
229
  }
179
- const createFields = entityFields.filter((f) => !(f.pk && f.auto));
180
- const writable = createFields.filter((f) => !/ctime|mtime|created_at|updated_at|create_time|update_time/i.test(f.name));
230
+ const createFields = entityFields.filter(f => !(f.pk && f.auto));
231
+ const writable = createFields.filter(f => !excludeWritableRegex.test(f.name));
181
232
  const isSensitive = (f) => /password|passwd|salt|token|secret|private_key/i.test(f.name + f.camel);
182
233
  const pkCamel = originalPkInfo?.camel || '';
183
- const sortableBase = entityFields.filter((f) => !isSensitive(f)).map((f) => f.camel);
234
+ const sortableBase = entityFields
235
+ .filter(f => !isSensitive(f))
236
+ .map(f => f.camel);
184
237
  const sortable = pkCamel && !sortableBase.includes(pkCamel)
185
238
  ? [pkCamel, ...sortableBase]
186
239
  : sortableBase;
187
- const filterableBase = entityFields.filter((f) => !isSensitive(f));
188
- const filterableArr = pkCamel && !filterableBase.some((f) => f.camel === pkCamel)
240
+ const filterableBase = entityFields.filter(f => !isSensitive(f));
241
+ const filterableArr = pkCamel && !filterableBase.some(f => f.camel === pkCamel)
189
242
  ? [originalPkInfo, ...filterableBase]
190
243
  : filterableBase;
191
- const filterable = filterableArr.map((f) => f.camel);
244
+ const filterable = filterableArr.map(f => f.camel);
192
245
  const searchable = filterableArr
193
- .filter((f) => /char|varchar|text/i.test(f.type))
194
- .map((f) => f.camel);
195
- const enhancedCreateFields = createFields.map((f) => enhanceField(f));
196
- const enhancedWritable = writable.map((f) => enhanceField(f));
197
- const enhancedFilterable = filterableArr.map((f) => enhanceField(f));
246
+ .filter(f => /char|varchar|text/i.test(f.type))
247
+ .map(f => f.camel);
248
+ const enhancedCreateFields = createFields.map(f => enhanceField(f));
249
+ const enhancedWritable = writable.map(f => enhanceField(f));
250
+ const enhancedFilterable = filterableArr.map(f => enhanceField(f));
198
251
  // Create DTO
252
+ const createPicks = enhancedCreateFields
253
+ .map(f => JSON.stringify(f.camel))
254
+ .join(',\n ');
199
255
  resultFiles.push({
200
256
  path: resolve(modRoot, `dto/${Camel}/create.dto.ts`),
201
257
  content: renderer.render('dto-create', {
202
258
  CreateName,
203
259
  Pascal,
260
+ Camel,
204
261
  createFields: enhancedCreateFields,
262
+ picks: createPicks,
205
263
  }),
206
264
  });
207
265
  // Update DTO
208
- const updatePicks = enhancedWritable.map((f) => JSON.stringify(f.camel)).join(',\n ');
209
- const updateTypeArgs = enhancedWritable.map((f) => `'${f.camel}'`).join(' | ');
266
+ const updatePicks = enhancedWritable
267
+ .map(f => JSON.stringify(f.camel))
268
+ .join(',\n ');
269
+ const updateTypeArgs = enhancedWritable.map(f => `'${f.camel}'`).join(' | ');
210
270
  resultFiles.push({
211
271
  path: resolve(modRoot, `dto/${Camel}/update.dto.ts`),
212
272
  content: renderer.render('dto-update', {
@@ -218,7 +278,9 @@ export function buildFiles(params) {
218
278
  }),
219
279
  });
220
280
  // Replace DTO
221
- const replacePicks = enhancedWritable.map((f) => JSON.stringify(f.camel)).join(',\n ');
281
+ const replacePicks = enhancedWritable
282
+ .map(f => JSON.stringify(f.camel))
283
+ .join(',\n ');
222
284
  resultFiles.push({
223
285
  path: resolve(modRoot, `dto/${Camel}/replace.dto.ts`),
224
286
  content: renderer.render('dto-replace', {
@@ -229,12 +291,14 @@ export function buildFiles(params) {
229
291
  }),
230
292
  });
231
293
  // Query DTO
232
- const queryPicks = enhancedFilterable.map((f) => JSON.stringify(f.camel)).join(',\n ');
233
- const queryTypeArgs = enhancedFilterable.map((f) => `'${f.camel}'`).join(' | ');
294
+ const queryPicks = enhancedFilterable
295
+ .map(f => JSON.stringify(f.camel))
296
+ .join(',\n ');
297
+ const queryTypeArgs = enhancedFilterable.map(f => `'${f.camel}'`).join(' | ');
234
298
  const sortEx = filterable[0] ? `${filterable[0]}:ASC,id:DESC` : 'id:DESC';
235
299
  const filterEx = filterableArr
236
300
  .slice(0, 2)
237
- .map((f) => {
301
+ .map(f => {
238
302
  const isStr = /char|varchar|text/i.test(f.type);
239
303
  return isStr ? `${f.camel}:like:a` : `${f.camel}:gte:10`;
240
304
  })
@@ -248,7 +312,8 @@ export function buildFiles(params) {
248
312
  filterable: enhancedFilterable,
249
313
  picks: queryPicks,
250
314
  typeArgs: queryTypeArgs || 'never',
251
- sortDescription: '排序字段。标准格式:field:ASC 或 field:DESC;多字段用英文逗号分隔,或多次传 sort。示例:id:DESC、' + sortEx,
315
+ sortDescription: '排序字段。标准格式:field:ASC 或 field:DESC;多字段用英文逗号分隔,或多次传 sort。示例:id:DESC、' +
316
+ sortEx,
252
317
  filterExample: filterEx || 'id:gte:1',
253
318
  searchDescription: '搜索关键字,对可搜索字段(' + searchFields + ')模糊匹配',
254
319
  }),
@@ -259,12 +324,17 @@ export function buildFiles(params) {
259
324
  content: renderer.render('dto-index', {}),
260
325
  });
261
326
  // Single VO
262
- const allVoFields = [...(originalPkInfo ? [originalPkInfo] : []), ...entityFields].map((f) => enhanceField(f));
263
- const voHasTransform = allVoFields.some((f) => f.isDate || f.isSensitive);
327
+ const allVoFields = [
328
+ ...(originalPkInfo ? [originalPkInfo] : []),
329
+ ...entityFields,
330
+ ].map(f => enhanceField(f));
331
+ const voHasTransform = allVoFields.some(f => f.isDate || f.isSensitive);
264
332
  resultFiles.push({
265
333
  path: resolve(modRoot, `vo/${Camel}/${Camel}.vo.ts`),
266
334
  content: renderer.render('vo-single', {
267
335
  VOName,
336
+ Pascal,
337
+ Camel,
268
338
  fields: allVoFields,
269
339
  hasTransform: voHasTransform,
270
340
  }),
@@ -294,7 +364,7 @@ export function buildFiles(params) {
294
364
  }),
295
365
  });
296
366
  // Controller
297
- const sArr = (arr) => arr.map((x) => JSON.stringify(x)).join(', ');
367
+ const sArr = (arr) => arr.map(x => JSON.stringify(x)).join(', ');
298
368
  const idField = pkCamel || 'id';
299
369
  const defaultSort = searchable && searchable.length
300
370
  ? `[{ field: ${JSON.stringify(sortable[0] || idField)}, order: 'DESC' }]`
@@ -302,10 +372,19 @@ export function buildFiles(params) {
302
372
  resultFiles.push({
303
373
  path: resolve(modRoot, `controller/${Camel}.controller.ts`),
304
374
  content: renderer.render('controller', {
305
- CtrlName, Pascal, ServiceName,
306
- CreateName, UpdateName, ReplaceName, QueryName,
307
- VOName, ListVOName,
308
- Camel, pluralKebab, tagName, desc,
375
+ CtrlName,
376
+ Pascal,
377
+ ServiceName,
378
+ CreateName,
379
+ UpdateName,
380
+ ReplaceName,
381
+ QueryName,
382
+ VOName,
383
+ ListVOName,
384
+ Camel,
385
+ pluralKebab,
386
+ tagName,
387
+ desc,
309
388
  sortable: sArr(sortable),
310
389
  filterable: sArr(filterable),
311
390
  searchable: sArr(searchable),
@@ -321,5 +400,13 @@ export function buildFiles(params) {
321
400
  content: renderer.render('crud-page-vo', {}),
322
401
  createOnly: true,
323
402
  });
324
- return { files: resultFiles, Pascal, pluralKebab, Camel, sortable, filterable, searchable };
403
+ return {
404
+ files: resultFiles,
405
+ Pascal,
406
+ pluralKebab,
407
+ Camel,
408
+ sortable,
409
+ filterable,
410
+ searchable,
411
+ };
325
412
  }
package/dist/types.d.ts CHANGED
@@ -18,6 +18,8 @@ export interface CrudConfig {
18
18
  outputPath?: string;
19
19
  templatesPath?: string;
20
20
  onlyEntity?: boolean;
21
+ baseFieldNames?: string[];
22
+ excludeWritableFields?: string[];
21
23
  }
22
24
  export interface CliOptions extends CrudConfig {
23
25
  help?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shys/crud",
3
- "version": "1.0.1-beta.4",
3
+ "version": "1.0.1-beta.6",
4
4
  "description": "MidwayJS v4 模块化 CRUD CLI 生成器",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -1,18 +1,6 @@
1
- import { Rule } from '@midwayjs/validation';
2
- import { ApiPropertyOptional } from '@midwayjs/swagger';
3
- import * as Joi from 'joi';
1
+ import { PickDto } from '@midwayjs/validation';
2
+ import { {{ Pascal }} } from '../../entity/{{ Camel }}.entity';
4
3
 
5
- export class {{ CreateName }} {
6
- {% for f in createFields -%}
7
- @ApiPropertyOptional({
8
- type: {{ f.swagType | json }},
9
- {% if f.len %} maxLength: {{ f.len }},
10
- {% endif %}{% if f.nullable %} nullable: true,
11
- {% endif %} description: {{ (f.comment or f.camel) | json }},
12
- {% if f.defaultVal != null %} example: {{ f.defaultVal | json }},
13
- {% endif %} })
14
- @Rule({{ f.joiRule }}.description({{ (f.comment or f.camel) | json }}))
15
- {{ f.camel }}?: {{ f.createDtoType }};
16
-
17
- {% endfor -%}
18
- }
4
+ export class {{ CreateName }} extends PickDto({{ Pascal }}, [
5
+ {{ picks }}
6
+ ]) {}
@@ -1,71 +1,17 @@
1
- import { Rule, PickDto } from '@midwayjs/validation';
2
1
  import { ApiPropertyOptional } from '@midwayjs/swagger';
2
+ import { Rule } from '@midwayjs/validation';
3
3
  import * as Joi from 'joi';
4
- import { {{ CreateName }} } from './create.dto';
4
+ import { CrudBaseQueryDto } from '../../../../framework/dto/base.query.dto';
5
5
 
6
- type _QueryFilterFields = Pick<{{ CreateName }}, {{ typeArgs }}>;
7
-
8
- const _QueryFilterPicked = PickDto({{ CreateName }}, [
9
- {{ picks }}
10
- ]) as unknown as new () => Partial<_QueryFilterFields>;
11
-
12
- export class {{ QueryName }} extends _QueryFilterPicked {
6
+ export class {{ QueryName }} extends CrudBaseQueryDto {
13
7
  {% for f in filterable -%}
14
8
  @ApiPropertyOptional({
15
9
  type: {{ f.swagType | json }},
16
10
  {% if f.len %} maxLength: {{ f.len }},
17
- {% endif %} description: {{ ((f.comment or f.camel) + '(列表筛选条件,可选)') | json }},
11
+ {% endif %} description: {{ (f.comment or f.camel) | json }},
18
12
  })
19
- override {{ f.camel }}?: {{ f.jsType }};
13
+ @Rule({{ f.joiRule }}.optional())
14
+ {{ f.camel }}?: {{ f.jsType }};
20
15
 
21
16
  {% endfor -%}
22
- {% if filterable.length %}
23
-
24
- {% endif %} @ApiPropertyOptional({
25
- type: 'integer',
26
- minimum: 1,
27
- default: 1,
28
- description: '当前页码,从1开始',
29
- example: 1,
30
- })
31
- @Rule(Joi.number().integer().min(1).default(1).description('当前页码,从1开始,默认1'))
32
- page?: number;
33
-
34
- @ApiPropertyOptional({
35
- type: 'integer',
36
- minimum: 1,
37
- maximum: 100,
38
- default: 10,
39
- description: '每页条数,范围 1-100',
40
- example: 10,
41
- })
42
- @Rule(Joi.number().integer().min(1).max(100).default(10).description('每页条数,1-100,默认10'))
43
- limit?: number;
44
-
45
- @ApiPropertyOptional({
46
- type: 'string',
47
- description: {{ sortDescription | json }},
48
- example: 'id:DESC',
49
- })
50
- @Rule(Joi.string().description('排序字段,例如 id:ASC 或 name:DESC,id:DESC'))
51
- sort?: string;
52
-
53
- @ApiPropertyOptional({
54
- type: 'string',
55
- description:
56
- '过滤条件(兼容两种写法)。官方标准:field||op||value,多条件多次传或逗号分隔。简写:field:op:value 逗号分隔。' +
57
- '操作符 eq/ne/gt/gte/lt/lte/in/like,别名 ==/!=/>/>=/</<=/contains。' +
58
- '示例:age||gte||18,name||like||张 或 age:gte:18,name:like:张',
59
- example: {{ filterExample | json }},
60
- })
61
- @Rule(Joi.string().description('过滤条件,支持 field||op||value 或 field:op:value,逗号分隔多条件'))
62
- filter?: string;
63
-
64
- @ApiPropertyOptional({
65
- type: 'string',
66
- description: {{ searchDescription | json }},
67
- example: 'zhang',
68
- })
69
- @Rule(Joi.string().description('搜索关键字,用于模糊搜索可搜索字段'))
70
- search?: string;
71
17
  }
@@ -14,7 +14,7 @@ export class {{ ReplaceName }} extends PickDto({{ CreateName }}, [
14
14
  {% endif %} required: true,
15
15
  description: {{ ((f.comment or f.camel) + '(PUT:必须完整传入)') | json }},
16
16
  })
17
- @Rule({{ f.joiRuleRequired }}.description({{ (f.comment or f.camel) | json }}))
17
+ @Rule({{ f.joiRuleRequired }})
18
18
  override {{ f.camel }}: {{ f.createDtoType }};
19
19
 
20
20
  {% endfor -%}
@@ -2,21 +2,8 @@ import { ApiPropertyOptional } from '@midwayjs/swagger';
2
2
  import { PickDto } from '@midwayjs/validation';
3
3
  import { {{ CreateName }} } from './create.dto';
4
4
 
5
- type _UpdateFields = Pick<{{ CreateName }}, {{ typeArgs }}>;
6
-
7
- const _UpdatePicked = PickDto({{ CreateName }}, [
5
+ export class {{ UpdateName }} extends PickDto({{ CreateName }}, [
8
6
  {{ picks }}
9
- ]) as unknown as new () => Partial<_UpdateFields>;
10
-
11
- export class {{ UpdateName }} extends _UpdatePicked {
12
- {% for f in writable -%}
13
- @ApiPropertyOptional({
14
- type: {{ f.swagType | json }},
15
- {% if f.len %} maxLength: {{ f.len }},
16
- {% endif %}{% if f.nullable %} nullable: true,
17
- {% endif %} description: {{ ((f.comment or f.camel) + '(PATCH:不传则不修改)') | json }},
18
- })
19
- override {{ f.camel }}?: {{ f.createDtoType }};
7
+ ]) {
20
8
 
21
- {% endfor -%}
22
9
  }
@@ -1,24 +1,3 @@
1
- import { Exclude, Expose{% if hasTransform %}, Transform{% endif %} } from 'class-transformer';
2
- import { ApiProperty } from '@midwayjs/swagger';
1
+ import { {{ Pascal }} } from '../../entity/{{ Camel }}.entity';
3
2
 
4
- @Exclude()
5
- export class {{ VOName }} {
6
- {% for f in fields -%}
7
- @ApiProperty({
8
- type: {{ f.swagType | json }},
9
- {% if f.len %} maxLength: {{ f.len }},
10
- {% endif %}{% if f.nullable %} nullable: true,
11
- {% endif %} description: {{ (f.comment or f.camel) | json }},
12
- })
13
- @Expose()
14
- {% if f.isDate %} @Transform(({ value }) => {
15
- if (value == null) return null;
16
- const d = value instanceof Date ? value : new Date(value);
17
- if (isNaN(d.getTime())) return value == null ? null : String(value);
18
- return d.toISOString();
19
- })
20
- {% endif %}{% if f.isSensitive %} @Transform(() => undefined, { toPlainOnly: true })
21
- {% endif %} {{ f.camel }}: {{ f.voType }}{% if f.nullable %} | null{% endif %};
22
-
23
- {% endfor -%}
24
- }
3
+ export class {{ VOName }} extends {{ Pascal }} {}