@shys/crud 1.0.1-beta.0
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/dist/cli.d.ts +2 -0
- package/dist/cli.js +305 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +115 -0
- package/dist/db.d.ts +5 -0
- package/dist/db.js +132 -0
- package/dist/generator.d.ts +27 -0
- package/dist/generator.js +322 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/orm.d.ts +2 -0
- package/dist/orm.js +57 -0
- package/dist/renderer.d.ts +14 -0
- package/dist/renderer.js +62 -0
- package/dist/templates.d.ts +70 -0
- package/dist/templates.js +528 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +33 -0
- package/package.json +51 -0
- package/templates/controller.njk +65 -0
- package/templates/crud-page-vo.njk +59 -0
- package/templates/dto-create.njk +18 -0
- package/templates/dto-index.njk +4 -0
- package/templates/dto-query.njk +71 -0
- package/templates/dto-replace.njk +21 -0
- package/templates/dto-update.njk +22 -0
- package/templates/entity.njk +60 -0
- package/templates/service.njk +20 -0
- package/templates/vo-index.njk +2 -0
- package/templates/vo-list.njk +15 -0
- package/templates/vo-single.njk +24 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
const JSTypeMap = {
|
|
2
|
+
int: 'number', integer: 'number', bigint: 'number', smallint: 'number', tinyint: 'number', mediumint: 'number',
|
|
3
|
+
decimal: 'number', numeric: 'number', float: 'number', double: 'number', real: 'number',
|
|
4
|
+
char: 'string', varchar: 'string', text: 'string', tinytext: 'string', mediumtext: 'string', longtext: 'string',
|
|
5
|
+
enum: 'string', set: 'string', json: 'any',
|
|
6
|
+
date: 'Date', datetime: 'Date', timestamp: 'Date', time: 'Date', year: 'number',
|
|
7
|
+
binary: 'Buffer', varbinary: 'Buffer', blob: 'Buffer',
|
|
8
|
+
bool: 'boolean', boolean: 'boolean', bit: 'boolean',
|
|
9
|
+
};
|
|
10
|
+
const NO_LENGTH_ORM_TYPES = /^(text|tinytext|mediumtext|longtext|blob|tinyblob|mediumblob|longblob|json)$/i;
|
|
11
|
+
function normalizeOrmType(type) {
|
|
12
|
+
return (String(type)
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/\(\d+.*?\)/, '')
|
|
15
|
+
.trim() || 'varchar');
|
|
16
|
+
}
|
|
17
|
+
function fieldSupportsLength(f) {
|
|
18
|
+
return !NO_LENGTH_ORM_TYPES.test(normalizeOrmType(f.type));
|
|
19
|
+
}
|
|
20
|
+
function jsTypeOf(field) {
|
|
21
|
+
const base = JSTypeMap[field.type] || 'string';
|
|
22
|
+
if (field.nullable)
|
|
23
|
+
return `${base} | null`;
|
|
24
|
+
return base;
|
|
25
|
+
}
|
|
26
|
+
function createDtoTypeOf(field) {
|
|
27
|
+
const base = JSTypeMap[field.type] || 'string';
|
|
28
|
+
return `${base}${field.nullable ? ' | null' : ''}`;
|
|
29
|
+
}
|
|
30
|
+
function joiSwagType(f) {
|
|
31
|
+
const t = f.type;
|
|
32
|
+
if (t === 'int' || t === 'bigint' || t === 'smallint' || t === 'tinyint' || t === 'mediumint' || t === 'integer')
|
|
33
|
+
return `'integer'`;
|
|
34
|
+
if (t === 'decimal' || t === 'numeric' || t === 'float' || t === 'double' || t === 'real' || t === 'number')
|
|
35
|
+
return `'number'`;
|
|
36
|
+
if (t === 'datetime' || t === 'timestamp' || t === 'date')
|
|
37
|
+
return `'string'`;
|
|
38
|
+
if (t === 'bool' || t === 'boolean')
|
|
39
|
+
return `'boolean'`;
|
|
40
|
+
return `'string'`;
|
|
41
|
+
}
|
|
42
|
+
function joiRuleOf(field, opts = {}) {
|
|
43
|
+
const { required = false, allowNull = false } = opts;
|
|
44
|
+
const t = field.type;
|
|
45
|
+
let j;
|
|
46
|
+
switch (t) {
|
|
47
|
+
case 'int':
|
|
48
|
+
case 'integer':
|
|
49
|
+
case 'bigint':
|
|
50
|
+
case 'smallint':
|
|
51
|
+
case 'tinyint':
|
|
52
|
+
case 'mediumint':
|
|
53
|
+
j = 'Joi.number().integer()';
|
|
54
|
+
break;
|
|
55
|
+
case 'decimal':
|
|
56
|
+
case 'numeric':
|
|
57
|
+
case 'float':
|
|
58
|
+
case 'double':
|
|
59
|
+
case 'real':
|
|
60
|
+
j = 'Joi.number()';
|
|
61
|
+
break;
|
|
62
|
+
case 'date':
|
|
63
|
+
case 'datetime':
|
|
64
|
+
case 'timestamp':
|
|
65
|
+
j = 'Joi.date()';
|
|
66
|
+
break;
|
|
67
|
+
case 'bool':
|
|
68
|
+
case 'boolean':
|
|
69
|
+
j = 'Joi.boolean()';
|
|
70
|
+
break;
|
|
71
|
+
default:
|
|
72
|
+
j = 'Joi.string()' + (field.len ? `.max(${field.len})` : '');
|
|
73
|
+
}
|
|
74
|
+
if (allowNull)
|
|
75
|
+
j += '.allow(null)';
|
|
76
|
+
if (required)
|
|
77
|
+
j += '.required()';
|
|
78
|
+
return j;
|
|
79
|
+
}
|
|
80
|
+
export function entityTemplate(params) {
|
|
81
|
+
const { orm, fields, tableName, Pascal, originalPkInfo, hasPk } = params;
|
|
82
|
+
if (orm !== 'typeorm') {
|
|
83
|
+
return `// ${orm} Entity 模板占位(TypeORM 是当前唯一完整生成的 ORM)\nexport class ${Pascal} {}\n`;
|
|
84
|
+
}
|
|
85
|
+
const idx = fields.filter((f) => !f.pk && (String(f.name).toUpperCase() === 'COUNTRYCODE' || /_id$/.test(f.name)));
|
|
86
|
+
const hasDateField = fields.some((f) => /datetime|date|timestamp/i.test(f.type));
|
|
87
|
+
const columns = fields.map((f) => renderColumnDecorators(f, idx)).join('\n');
|
|
88
|
+
return `import { Entity, Column${hasPk ? ', PrimaryGeneratedColumn' : ''}${idx.length ? ', Index' : ''} } from 'typeorm';
|
|
89
|
+
import { Expose${hasDateField ? ', Transform' : ''} } from 'class-transformer';
|
|
90
|
+
import { ApiProperty, ApiPropertyOptional } from '@midwayjs/swagger';
|
|
91
|
+
import { Rule } from '@midwayjs/validation';
|
|
92
|
+
import { z } from 'zod';
|
|
93
|
+
import { BaseEntity } from '../../../framework/entity/base.entity';
|
|
94
|
+
|
|
95
|
+
@Entity(${JSON.stringify(tableName)}, {
|
|
96
|
+
comment: ${JSON.stringify(tableName + ' 表')},
|
|
97
|
+
})
|
|
98
|
+
export class ${Pascal} extends BaseEntity {
|
|
99
|
+
${hasPk && originalPkInfo ? renderPkOverride(originalPkInfo) : ''}${columns}}
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
function renderPkOverride(pkInfo) {
|
|
103
|
+
const swagType = joiSwagType(pkInfo);
|
|
104
|
+
const zodRule = `z.number().int().describe(${JSON.stringify(pkInfo.comment || '主键ID')})`;
|
|
105
|
+
const jsType = JSTypeMap[pkInfo.type] || 'number';
|
|
106
|
+
return ` @PrimaryGeneratedColumn({
|
|
107
|
+
type: ${JSON.stringify(pkInfo.type)},
|
|
108
|
+
name: ${JSON.stringify(pkInfo.name)},
|
|
109
|
+
comment: ${JSON.stringify(pkInfo.comment || '主键')},
|
|
110
|
+
})
|
|
111
|
+
@ApiProperty({
|
|
112
|
+
type: ${swagType},
|
|
113
|
+
description: ${JSON.stringify(pkInfo.comment || '主键ID')},
|
|
114
|
+
})
|
|
115
|
+
@Expose()
|
|
116
|
+
@Rule(${zodRule})
|
|
117
|
+
${pkInfo.camel}: ${jsType};
|
|
118
|
+
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
function renderColumnDecorators(f, idx) {
|
|
122
|
+
const swagType = joiSwagType(f);
|
|
123
|
+
const isDate = /datetime|date|timestamp/i.test(f.type);
|
|
124
|
+
const supportsLength = fieldSupportsLength(f);
|
|
125
|
+
const ormType = normalizeOrmType(f.type);
|
|
126
|
+
const zodBase = /int|bigint|smallint|tinyint|mediumint|integer/.test(f.type)
|
|
127
|
+
? 'z.number().int()'
|
|
128
|
+
: /decimal|numeric|float|double|real/.test(f.type)
|
|
129
|
+
? 'z.number()'
|
|
130
|
+
: /date|datetime|timestamp/.test(f.type)
|
|
131
|
+
? 'z.date()'
|
|
132
|
+
: /bool|boolean/.test(f.type)
|
|
133
|
+
? 'z.boolean()'
|
|
134
|
+
: `z.string()${f.len ? `.max(${f.len})` : ''}`;
|
|
135
|
+
const zodRule = `${zodBase}.describe(${JSON.stringify(f.comment || f.camel)})`;
|
|
136
|
+
const opts = [
|
|
137
|
+
`type: ${JSON.stringify(ormType)}`,
|
|
138
|
+
supportsLength && f.len ? `length: ${f.len}` : null,
|
|
139
|
+
`name: ${JSON.stringify(f.name)}`,
|
|
140
|
+
f.nullable ? `nullable: true` : `nullable: false`,
|
|
141
|
+
f.defaultVal != null ? `default: ${JSON.stringify(f.defaultVal)}` : null,
|
|
142
|
+
f.comment ? `comment: ${JSON.stringify(f.comment)}` : null,
|
|
143
|
+
]
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join(',\n ');
|
|
146
|
+
const idxDeco = idx.some((i) => i.name === f.name) ? ` @Index()\n` : '';
|
|
147
|
+
const transformDeco = isDate
|
|
148
|
+
? ` @Transform(({ value }) => {
|
|
149
|
+
if (value == null) return null;
|
|
150
|
+
const d = value instanceof Date ? value : new Date(value);
|
|
151
|
+
if (isNaN(d.getTime())) return value == null ? null : String(value);
|
|
152
|
+
return d.toISOString();
|
|
153
|
+
})\n`
|
|
154
|
+
: '';
|
|
155
|
+
return `${idxDeco} @Column({
|
|
156
|
+
${opts}
|
|
157
|
+
})
|
|
158
|
+
@ApiPropertyOptional({
|
|
159
|
+
type: ${swagType},
|
|
160
|
+
${supportsLength && f.len ? ` maxLength: ${f.len},\n` : ''}${f.nullable ? ` nullable: true,\n` : ''} description: ${JSON.stringify(f.comment || f.camel)},\n${f.defaultVal != null ? ` example: ${JSON.stringify(f.defaultVal)},\n` : ''} })
|
|
161
|
+
@Expose()
|
|
162
|
+
@Rule(${zodRule}.nullable().optional())
|
|
163
|
+
${transformDeco} ${f.camel}?: ${jsTypeOf(f)};
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
export function createDtoTemplate(params) {
|
|
167
|
+
const { CreateName, Pascal, createFields } = params;
|
|
168
|
+
const props = createFields
|
|
169
|
+
.map((f) => {
|
|
170
|
+
const swag = ` @ApiPropertyOptional({\n type: ${joiSwagType(f)},\n${f.len ? ` maxLength: ${f.len},\n` : ''}${f.nullable ? ` nullable: true,\n` : ''} description: ${JSON.stringify(f.comment || f.camel)},\n${f.defaultVal != null ? ` example: ${JSON.stringify(f.defaultVal)},\n` : ''} })`;
|
|
171
|
+
const rule = ` @Rule(${joiRuleOf(f, { required: false, allowNull: f.nullable })}.description(${JSON.stringify(f.comment || f.camel)}))`;
|
|
172
|
+
return `${swag}\n${rule}\n ${f.camel}?: ${createDtoTypeOf(f)};\n`;
|
|
173
|
+
})
|
|
174
|
+
.join('\n');
|
|
175
|
+
return `import { Rule } from '@midwayjs/validation';
|
|
176
|
+
import { ApiPropertyOptional } from '@midwayjs/swagger';
|
|
177
|
+
import * as Joi from 'joi';
|
|
178
|
+
|
|
179
|
+
export class ${CreateName} {
|
|
180
|
+
${props}}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
export function updateDtoTemplate(params) {
|
|
184
|
+
const { UpdateName, CreateName, writable } = params;
|
|
185
|
+
const picks = writable.map((f) => JSON.stringify(f.camel));
|
|
186
|
+
const typeArgs = writable.map((f) => `'${f.camel}'`).join(' | ');
|
|
187
|
+
const overrides = writable
|
|
188
|
+
.map((f) => {
|
|
189
|
+
return ` @ApiPropertyOptional({\n type: ${joiSwagType(f)},\n${f.len ? ` maxLength: ${f.len},\n` : ''}${f.nullable ? ` nullable: true,\n` : ''} description: ${JSON.stringify((f.comment || f.camel) + '(PATCH:不传则不修改)')},\n })\n override ${f.camel}?: ${createDtoTypeOf(f)};\n`;
|
|
190
|
+
})
|
|
191
|
+
.join('\n');
|
|
192
|
+
return `import { ApiPropertyOptional } from '@midwayjs/swagger';
|
|
193
|
+
import { PickDto } from '@midwayjs/validation';
|
|
194
|
+
import { ${CreateName} } from './create.dto';
|
|
195
|
+
|
|
196
|
+
type _UpdateFields = Pick<${CreateName}, ${typeArgs}>;
|
|
197
|
+
|
|
198
|
+
const _UpdatePicked = PickDto(${CreateName}, [
|
|
199
|
+
${picks.join(',\n ')}
|
|
200
|
+
]) as unknown as new () => Partial<_UpdateFields>;
|
|
201
|
+
|
|
202
|
+
export class ${UpdateName} extends _UpdatePicked {
|
|
203
|
+
${overrides}}
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
export function replaceDtoTemplate(params) {
|
|
207
|
+
const { ReplaceName, CreateName, writable } = params;
|
|
208
|
+
const picks = writable.map((f) => JSON.stringify(f.camel));
|
|
209
|
+
const overrides = writable
|
|
210
|
+
.map((f) => {
|
|
211
|
+
const swagType = joiSwagType(f);
|
|
212
|
+
return ` @ApiProperty({\n type: ${swagType},\n${f.len ? ` maxLength: ${f.len},\n` : ''}${f.nullable ? ` nullable: true,\n` : ''} required: true,\n description: ${JSON.stringify((f.comment || f.camel) + '(PUT:必须完整传入)')},\n })\n @Rule(${joiRuleOf(f, { required: true, allowNull: f.nullable })}.description(${JSON.stringify(f.comment || f.camel)}))\n override ${f.camel}: ${createDtoTypeOf(f)};\n`;
|
|
213
|
+
})
|
|
214
|
+
.join('\n');
|
|
215
|
+
return `import { Rule, PickDto } from '@midwayjs/validation';
|
|
216
|
+
import { ApiProperty } from '@midwayjs/swagger';
|
|
217
|
+
import * as Joi from 'joi';
|
|
218
|
+
import { ${CreateName} } from './create.dto';
|
|
219
|
+
|
|
220
|
+
export class ${ReplaceName} extends PickDto(${CreateName}, [
|
|
221
|
+
${picks.join(',\n ')}
|
|
222
|
+
]) {
|
|
223
|
+
${overrides}}
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
export function queryDtoTemplate(params) {
|
|
227
|
+
const { QueryName, CreateName, filterable } = params;
|
|
228
|
+
const picks = filterable.map((f) => JSON.stringify(f.camel));
|
|
229
|
+
const typeArgs = filterable.map((f) => `'${f.camel}'`).join(' | ');
|
|
230
|
+
const pickOverrides = filterable
|
|
231
|
+
.map((f) => {
|
|
232
|
+
const sType = joiSwagType(f);
|
|
233
|
+
return ` @ApiPropertyOptional({\n type: ${sType},\n${f.len ? ` maxLength: ${f.len},\n` : ''} description: ${JSON.stringify((f.comment || f.camel) + '(列表筛选条件,可选)')},\n })\n override ${f.camel}?: ${JSTypeMap[f.type] || 'string'}${f.nullable ? ' | null' : ''};\n`;
|
|
234
|
+
})
|
|
235
|
+
.join('\n');
|
|
236
|
+
const sortEx = filterable[0] ? `${filterable[0].camel}:ASC,id:DESC` : 'id:DESC';
|
|
237
|
+
const filterEx = filterable
|
|
238
|
+
.slice(0, 2)
|
|
239
|
+
.map((f) => {
|
|
240
|
+
const isStr = /char|varchar|text/i.test(f.type);
|
|
241
|
+
return isStr ? `${f.camel}:like:a` : `${f.camel}:gte:10`;
|
|
242
|
+
})
|
|
243
|
+
.join(',');
|
|
244
|
+
const searchFields = filterable.filter((f) => /char|varchar|text/i.test(f.type)).map((f) => f.camel).join('/') || '无';
|
|
245
|
+
return `import { Rule, PickDto } from '@midwayjs/validation';
|
|
246
|
+
import { ApiPropertyOptional } from '@midwayjs/swagger';
|
|
247
|
+
import * as Joi from 'joi';
|
|
248
|
+
import { ${CreateName} } from './create.dto';
|
|
249
|
+
|
|
250
|
+
type _QueryFilterFields = Pick<${CreateName}, ${typeArgs || 'never'}>;
|
|
251
|
+
|
|
252
|
+
const _QueryFilterPicked = PickDto(${CreateName}, [
|
|
253
|
+
${picks.join(',\n ')}
|
|
254
|
+
]) as unknown as new () => Partial<_QueryFilterFields>;
|
|
255
|
+
|
|
256
|
+
export class ${QueryName} extends _QueryFilterPicked {
|
|
257
|
+
${pickOverrides}
|
|
258
|
+
${pickOverrides ? '\n' : ''} @ApiPropertyOptional({
|
|
259
|
+
type: 'integer',
|
|
260
|
+
minimum: 1,
|
|
261
|
+
default: 1,
|
|
262
|
+
description: '当前页码,从1开始',
|
|
263
|
+
example: 1,
|
|
264
|
+
})
|
|
265
|
+
@Rule(Joi.number().integer().min(1).default(1).description('当前页码,从1开始,默认1'))
|
|
266
|
+
page?: number;
|
|
267
|
+
|
|
268
|
+
@ApiPropertyOptional({
|
|
269
|
+
type: 'integer',
|
|
270
|
+
minimum: 1,
|
|
271
|
+
maximum: 100,
|
|
272
|
+
default: 10,
|
|
273
|
+
description: '每页条数,范围 1-100',
|
|
274
|
+
example: 10,
|
|
275
|
+
})
|
|
276
|
+
@Rule(Joi.number().integer().min(1).max(100).default(10).description('每页条数,1-100,默认10'))
|
|
277
|
+
limit?: number;
|
|
278
|
+
|
|
279
|
+
@ApiPropertyOptional({
|
|
280
|
+
type: 'string',
|
|
281
|
+
description: ${JSON.stringify('排序字段。标准格式:field:ASC 或 field:DESC;多字段用英文逗号分隔,或多次传 sort。示例:id:DESC、' + sortEx)},
|
|
282
|
+
example: 'id:DESC',
|
|
283
|
+
})
|
|
284
|
+
@Rule(Joi.string().description('排序字段,例如 id:ASC 或 name:DESC,id:DESC'))
|
|
285
|
+
sort?: string;
|
|
286
|
+
|
|
287
|
+
@ApiPropertyOptional({
|
|
288
|
+
type: 'string',
|
|
289
|
+
description:
|
|
290
|
+
'过滤条件(兼容两种写法)。官方标准:field||op||value,多条件多次传或逗号分隔。简写:field:op:value 逗号分隔。' +
|
|
291
|
+
'操作符 eq/ne/gt/gte/lt/lte/in/like,别名 ==/!=/>/>=/</<=/contains。' +
|
|
292
|
+
'示例:age||gte||18,name||like||张 或 age:gte:18,name:like:张',
|
|
293
|
+
example: ${JSON.stringify(filterEx || 'id:gte:1')},
|
|
294
|
+
})
|
|
295
|
+
@Rule(Joi.string().description('过滤条件,支持 field||op||value 或 field:op:value,逗号分隔多条件'))
|
|
296
|
+
filter?: string;
|
|
297
|
+
|
|
298
|
+
@ApiPropertyOptional({
|
|
299
|
+
type: 'string',
|
|
300
|
+
description: ${JSON.stringify('搜索关键字,对可搜索字段(' + searchFields + ')模糊匹配')},
|
|
301
|
+
example: 'zhang',
|
|
302
|
+
})
|
|
303
|
+
@Rule(Joi.string().description('搜索关键字,用于模糊搜索可搜索字段'))
|
|
304
|
+
search?: string;
|
|
305
|
+
}
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
export function singleVOTemplate(params) {
|
|
309
|
+
const { VOName, fields } = params;
|
|
310
|
+
const props = fields
|
|
311
|
+
.map((f) => {
|
|
312
|
+
const t = f.type;
|
|
313
|
+
const isDate = /datetime|date|timestamp/i.test(t);
|
|
314
|
+
const swagType = isDate ? `'string'` : joiSwagType(f);
|
|
315
|
+
const lines = [];
|
|
316
|
+
lines.push(` @ApiProperty({`);
|
|
317
|
+
lines.push(` type: ${swagType},`);
|
|
318
|
+
if (f.len)
|
|
319
|
+
lines.push(` maxLength: ${f.len},`);
|
|
320
|
+
if (f.nullable)
|
|
321
|
+
lines.push(` nullable: true,`);
|
|
322
|
+
lines.push(` description: ${JSON.stringify(f.comment || f.camel)},`);
|
|
323
|
+
lines.push(` })`);
|
|
324
|
+
lines.push(` @Expose()`);
|
|
325
|
+
if (isDate) {
|
|
326
|
+
lines.push(` @Transform(({ value }) => {`);
|
|
327
|
+
lines.push(` if (value == null) return null;`);
|
|
328
|
+
lines.push(` const d = value instanceof Date ? value : new Date(value);`);
|
|
329
|
+
lines.push(` if (isNaN(d.getTime())) return value == null ? null : String(value);`);
|
|
330
|
+
lines.push(` return d.toISOString();`);
|
|
331
|
+
lines.push(` })`);
|
|
332
|
+
}
|
|
333
|
+
if (/password/i.test(f.name) || /password/i.test(f.camel)) {
|
|
334
|
+
lines.push(` @Transform(() => undefined, { toPlainOnly: true })`);
|
|
335
|
+
}
|
|
336
|
+
const jst = JSTypeMap[f.type] || 'string';
|
|
337
|
+
lines.push(` ${f.camel}: ${isDate ? `string` : jst}${f.nullable ? ' | null' : ''};`);
|
|
338
|
+
return lines.join('\n');
|
|
339
|
+
})
|
|
340
|
+
.join('\n\n');
|
|
341
|
+
return `import { Exclude, Expose${fields.some((f) => /datetime|date|timestamp|password/i.test(f.type + f.name + f.camel)) ? ', Transform' : ''} } from 'class-transformer';
|
|
342
|
+
import { ApiProperty } from '@midwayjs/swagger';
|
|
343
|
+
|
|
344
|
+
@Exclude()
|
|
345
|
+
export class ${VOName} {
|
|
346
|
+
${props}
|
|
347
|
+
}
|
|
348
|
+
`;
|
|
349
|
+
}
|
|
350
|
+
export function listVOTemplate(params) {
|
|
351
|
+
const { ListVOName, VOName, Camel } = params;
|
|
352
|
+
return `import {
|
|
353
|
+
CrudPageMetaVO,
|
|
354
|
+
createCrudPageListVO,
|
|
355
|
+
type CrudPageResult,
|
|
356
|
+
} from '../../../../framework/vo/crudPage.vo';
|
|
357
|
+
import { ${VOName} } from './${Camel}.vo';
|
|
358
|
+
|
|
359
|
+
export { CrudPageMetaVO };
|
|
360
|
+
export type { CrudPageResult };
|
|
361
|
+
|
|
362
|
+
export const ${ListVOName} = createCrudPageListVO(${VOName}, {
|
|
363
|
+
className: ${JSON.stringify(ListVOName)},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
export type ${ListVOName}Type = CrudPageResult<${VOName}>;
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
export function crudPageCommonTemplate() {
|
|
370
|
+
return `import { ApiProperty } from '@midwayjs/swagger';
|
|
371
|
+
import { Exclude, Expose, Type } from 'class-transformer';
|
|
372
|
+
|
|
373
|
+
@Exclude()
|
|
374
|
+
export class CrudPageMetaVO {
|
|
375
|
+
@ApiProperty({ type: 'integer', description: '当前页码,从 1 开始', example: 1 })
|
|
376
|
+
@Expose()
|
|
377
|
+
page: number;
|
|
378
|
+
|
|
379
|
+
@ApiProperty({ type: 'integer', description: '每页条数', example: 10 })
|
|
380
|
+
@Expose()
|
|
381
|
+
limit: number;
|
|
382
|
+
|
|
383
|
+
@ApiProperty({ type: 'integer', description: '总记录数', example: 1024 })
|
|
384
|
+
@Expose()
|
|
385
|
+
total: number;
|
|
386
|
+
|
|
387
|
+
@ApiProperty({ type: 'integer', description: '总页数', example: 103 })
|
|
388
|
+
@Expose()
|
|
389
|
+
pageCount: number;
|
|
390
|
+
|
|
391
|
+
@ApiProperty({ type: 'boolean', description: '是否存在下一页', example: true })
|
|
392
|
+
@Expose()
|
|
393
|
+
hasNext: boolean;
|
|
394
|
+
|
|
395
|
+
@ApiProperty({ type: 'boolean', description: '是否存在上一页', example: false })
|
|
396
|
+
@Expose()
|
|
397
|
+
hasPrev: boolean;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export interface CrudPageResult<T> {
|
|
401
|
+
data: T[];
|
|
402
|
+
meta: CrudPageMetaVO;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
type ClassLike<T = any> = new (...args: any[]) => T;
|
|
406
|
+
|
|
407
|
+
export function createCrudPageListVO<T extends ClassLike>(
|
|
408
|
+
ItemVOClass: T,
|
|
409
|
+
options?: { className?: string }
|
|
410
|
+
): ClassLike<CrudPageResult<InstanceType<T>>> {
|
|
411
|
+
const baseName = options?.className || \`\${ItemVOClass.name}ListVO\`;
|
|
412
|
+
|
|
413
|
+
@Exclude()
|
|
414
|
+
class ListVO implements CrudPageResult<InstanceType<T>> {
|
|
415
|
+
@ApiProperty({ type: () => [ItemVOClass], description: '当前页数据列表' })
|
|
416
|
+
@Expose()
|
|
417
|
+
@Type(() => ItemVOClass)
|
|
418
|
+
data: InstanceType<T>[];
|
|
419
|
+
|
|
420
|
+
@ApiProperty({ type: () => CrudPageMetaVO, description: '分页元信息' })
|
|
421
|
+
@Expose()
|
|
422
|
+
@Type(() => CrudPageMetaVO)
|
|
423
|
+
meta: CrudPageMetaVO;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
Object.defineProperty(ListVO, 'name', { value: baseName, writable: false });
|
|
427
|
+
return ListVO as ClassLike<CrudPageResult<InstanceType<T>>>;
|
|
428
|
+
}
|
|
429
|
+
`;
|
|
430
|
+
}
|
|
431
|
+
export function serviceTemplate(params) {
|
|
432
|
+
const { ServiceName, Pascal, Camel, ormInfo } = params;
|
|
433
|
+
if (ormInfo.orm === 'typeorm') {
|
|
434
|
+
return `import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
|
|
435
|
+
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
436
|
+
import { Repository } from 'typeorm';
|
|
437
|
+
import { ${ormInfo.serviceParent} } ${ormInfo.importCrudOrm.replace(/^from /, '')};
|
|
438
|
+
import { ${Pascal} } from '../entity/${Camel}.entity';
|
|
439
|
+
|
|
440
|
+
@Provide()
|
|
441
|
+
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
|
442
|
+
export class ${ServiceName} extends ${ormInfo.serviceParent}<${Pascal}> {
|
|
443
|
+
@InjectEntityModel(${Pascal})
|
|
444
|
+
repo: Repository<${Pascal}>;
|
|
445
|
+
}
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
return `// ${ormInfo.orm} Service 模板(请按项目注入方式调整)
|
|
449
|
+
import { Provide } from '@midwayjs/core';
|
|
450
|
+
|
|
451
|
+
@Provide()
|
|
452
|
+
export class ${ServiceName} {}
|
|
453
|
+
`;
|
|
454
|
+
}
|
|
455
|
+
export function controllerTemplate(params) {
|
|
456
|
+
const { CtrlName, Pascal, ServiceName, CreateName, UpdateName, ReplaceName, QueryName, VOName, ListVOName, Camel, pluralKebab, tagName, desc, sortable, filterable, searchable, pkCamel, } = params;
|
|
457
|
+
const sArr = (arr) => arr.map((x) => JSON.stringify(x)).join(', ');
|
|
458
|
+
const idField = pkCamel || 'id';
|
|
459
|
+
const defaultSort = searchable && searchable.length
|
|
460
|
+
? `[{ field: ${JSON.stringify(sortable[0] || idField)}, order: 'DESC' }]`
|
|
461
|
+
: `[{ field: 'id', order: 'DESC' }]`;
|
|
462
|
+
return `import { Controller, Inject } from '@midwayjs/core';
|
|
463
|
+
import { CrudPiped } from '@shys/crud-piped';
|
|
464
|
+
import { ${Pascal} } from '../entity/${Camel}.entity';
|
|
465
|
+
import { ${ServiceName} } from '../service/${Camel}.service';
|
|
466
|
+
import {
|
|
467
|
+
${CreateName},
|
|
468
|
+
${UpdateName},
|
|
469
|
+
${ReplaceName},
|
|
470
|
+
${QueryName},
|
|
471
|
+
} from '../dto/${Camel}';
|
|
472
|
+
import { ${VOName}, ${ListVOName} } from '../vo/${Camel}';
|
|
473
|
+
|
|
474
|
+
@Controller(${JSON.stringify('/api/' + pluralKebab)}, {
|
|
475
|
+
tagName: ${JSON.stringify(tagName)},
|
|
476
|
+
description: ${JSON.stringify(desc)},
|
|
477
|
+
})
|
|
478
|
+
@CrudPiped({
|
|
479
|
+
model: ${Pascal},
|
|
480
|
+
service: ${ServiceName},
|
|
481
|
+
id: ${JSON.stringify(idField)},
|
|
482
|
+
dto: {
|
|
483
|
+
create: ${CreateName},
|
|
484
|
+
update: ${UpdateName},
|
|
485
|
+
replace: ${ReplaceName},
|
|
486
|
+
query: ${QueryName},
|
|
487
|
+
},
|
|
488
|
+
serialize: {
|
|
489
|
+
get: ${VOName},
|
|
490
|
+
list: ${ListVOName},
|
|
491
|
+
create: ${VOName},
|
|
492
|
+
update: ${VOName},
|
|
493
|
+
},
|
|
494
|
+
routes: {
|
|
495
|
+
only: ['list', 'detail', 'create', 'update', 'replace', 'delete'],
|
|
496
|
+
},
|
|
497
|
+
query: {
|
|
498
|
+
maxLimit: 100,
|
|
499
|
+
defaultLimit: 10,
|
|
500
|
+
sortable: [${sArr(sortable)}],
|
|
501
|
+
filterable: [${sArr(filterable)}],
|
|
502
|
+
searchable: [${sArr(searchable)}],
|
|
503
|
+
defaultSort: ${defaultSort},
|
|
504
|
+
},
|
|
505
|
+
piped: {
|
|
506
|
+
create: {
|
|
507
|
+
enabled: true,
|
|
508
|
+
stages: ['deserialize', 'sanitize', 'mapFields', 'transform', 'validate'],
|
|
509
|
+
validate: { enabled: true, errorStatus: 422 },
|
|
510
|
+
},
|
|
511
|
+
update: {
|
|
512
|
+
enabled: true,
|
|
513
|
+
stages: ['deserialize', 'sanitize', 'mapFields', 'transform', 'validate'],
|
|
514
|
+
validate: { enabled: true, errorStatus: 422 },
|
|
515
|
+
},
|
|
516
|
+
replace: {
|
|
517
|
+
enabled: true,
|
|
518
|
+
stages: ['deserialize', 'sanitize', 'mapFields', 'transform', 'validate'],
|
|
519
|
+
validate: { enabled: true, errorStatus: 422 },
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
})
|
|
523
|
+
export class ${CtrlName} {
|
|
524
|
+
@Inject()
|
|
525
|
+
crudService: ${ServiceName};
|
|
526
|
+
}
|
|
527
|
+
`;
|
|
528
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface CrudDbConfig {
|
|
2
|
+
host?: string;
|
|
3
|
+
port?: number;
|
|
4
|
+
user?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
database?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CrudConfig {
|
|
10
|
+
baseSrcPath?: string;
|
|
11
|
+
moduleName?: string;
|
|
12
|
+
tableName?: string;
|
|
13
|
+
db?: CrudDbConfig;
|
|
14
|
+
noDb?: boolean;
|
|
15
|
+
force?: boolean;
|
|
16
|
+
dryRun?: boolean;
|
|
17
|
+
compile?: boolean;
|
|
18
|
+
outputPath?: string;
|
|
19
|
+
templatesPath?: string;
|
|
20
|
+
onlyEntity?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface CliOptions extends CrudConfig {
|
|
23
|
+
help?: boolean;
|
|
24
|
+
_?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface FieldInfo {
|
|
27
|
+
name: string;
|
|
28
|
+
camel: string;
|
|
29
|
+
type: string;
|
|
30
|
+
len: number | null;
|
|
31
|
+
nullable: boolean;
|
|
32
|
+
pk: boolean;
|
|
33
|
+
auto: boolean;
|
|
34
|
+
defaultVal: any;
|
|
35
|
+
comment: string;
|
|
36
|
+
}
|
|
37
|
+
export interface OrmInfo {
|
|
38
|
+
orm: string;
|
|
39
|
+
ormSuffix: string;
|
|
40
|
+
serviceParent: string;
|
|
41
|
+
importCrudOrm: string;
|
|
42
|
+
injectEntity: string;
|
|
43
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function toCamelCase(name: string): string;
|
|
2
|
+
export declare function toPascalCase(name: string): string;
|
|
3
|
+
export declare function toKebabCase(name: string): string;
|
|
4
|
+
export declare function pluralEn(word: string): string;
|
|
5
|
+
export declare function toPluralKebab(name: string): string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function toCamelCase(name) {
|
|
2
|
+
return String(name)
|
|
3
|
+
.replace(/^[A-Z]+(?![a-z])/, (m) => m.toLowerCase())
|
|
4
|
+
.replace(/[_-]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
|
|
5
|
+
}
|
|
6
|
+
export function toPascalCase(name) {
|
|
7
|
+
const c = toCamelCase(name);
|
|
8
|
+
return c ? c.charAt(0).toUpperCase() + c.slice(1) : '';
|
|
9
|
+
}
|
|
10
|
+
export function toKebabCase(name) {
|
|
11
|
+
return toCamelCase(name)
|
|
12
|
+
.replace(/([A-Z])/g, '-$1')
|
|
13
|
+
.replace(/^-/, '')
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
export function pluralEn(word) {
|
|
17
|
+
const w = String(word);
|
|
18
|
+
if (/s$|sh$|ch$|x$|z$/i.test(w))
|
|
19
|
+
return w + 'es';
|
|
20
|
+
if (/[^aeiou]y$/i.test(w))
|
|
21
|
+
return w.replace(/y$/i, 'ies');
|
|
22
|
+
return w + 's';
|
|
23
|
+
}
|
|
24
|
+
export function toPluralKebab(name) {
|
|
25
|
+
const kebab = toKebabCase(name);
|
|
26
|
+
const last = kebab.includes('-')
|
|
27
|
+
? kebab.slice(kebab.lastIndexOf('-') + 1)
|
|
28
|
+
: kebab;
|
|
29
|
+
const prefix = kebab.includes('-')
|
|
30
|
+
? kebab.slice(0, kebab.lastIndexOf('-') + 1)
|
|
31
|
+
: '';
|
|
32
|
+
return prefix + pluralEn(last);
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shys/crud",
|
|
3
|
+
"version": "1.0.1-beta.0",
|
|
4
|
+
"description": "MidwayJS v4 模块化 CRUD CLI 生成器",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "李静强",
|
|
8
|
+
"email": "376503128@qq.com"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"bin": {
|
|
13
|
+
"shys-crud": "dist/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"templates"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"midway",
|
|
25
|
+
"midwayjs",
|
|
26
|
+
"crud",
|
|
27
|
+
"cli",
|
|
28
|
+
"code-generator",
|
|
29
|
+
"typeorm"
|
|
30
|
+
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"chalk": "^5.3.0",
|
|
33
|
+
"cosmiconfig": "^9.0.0",
|
|
34
|
+
"fs-extra": "^11.2.0",
|
|
35
|
+
"mysql2": "^3.11.0",
|
|
36
|
+
"nunjucks": "^3.2.4"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/fs-extra": "^11.0.4",
|
|
40
|
+
"@types/node": "^20.0.0",
|
|
41
|
+
"@types/nunjucks": "^3.2.6",
|
|
42
|
+
"typescript": "^5.3.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">= 18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public",
|
|
49
|
+
"registry": "https://registry.npmjs.org/"
|
|
50
|
+
}
|
|
51
|
+
}
|