birc-generator 0.9.1 → 1.0.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/PROJECT.md +23 -4
- package/README.md +13 -11
- package/bin/birc.js +10 -4
- package/bin/cli-util.js +30 -2
- package/lib/create.js +150 -0
- package/lib/features.js +558 -0
- package/lib/make.js +658 -0
- package/lib/project.js +234 -0
- package/package.json +5 -3
- package/plopfile.js +60 -1340
- package/project-docs/PROJECT.md.hbs +13 -0
- package/templates/aop/OperationLogAspect.hbs +10 -3
- package/templates/auth/AuthController.hbs +73 -0
- package/templates/auth/AuthFlowTest.hbs +112 -0
- package/templates/auth/AuthSecurityCustomizer.hbs +38 -0
- package/templates/auth/AuthUser.hbs +54 -0
- package/templates/auth/AuthUserDAO.hbs +9 -0
- package/templates/auth/JpaUserDetailsService.hbs +47 -0
- package/templates/auth/JwtAuthenticationFilter.hbs +50 -0
- package/templates/auth/JwtProperties.hbs +35 -0
- package/templates/auth/JwtSecretEnvTest.hbs +68 -0
- package/templates/auth/JwtSecretEnvironmentPostProcessor.hbs +102 -0
- package/templates/auth/JwtService.hbs +60 -0
- package/templates/auth/LoginFailedException.hbs +27 -0
- package/templates/auth/LoginRequest.hbs +16 -0
- package/templates/auth/V1__create_auth_users_table.sql.hbs +13 -0
- package/templates/auth/application-yml-block.hbs +7 -0
- package/templates/auth/build-gradle-dep.hbs +4 -0
- package/templates/auth/spring.factories.hbs +1 -0
- package/templates/base/CorsTest.java.hbs +93 -0
- package/templates/base/README.md.hbs +6 -0
- package/templates/base/application-test.yml.hbs +3 -0
- package/templates/base/application.yml.hbs +12 -0
- package/templates/clockin/ClockInApiException.hbs +37 -0
- package/templates/clockin/ClockInApiResponse.hbs +14 -0
- package/templates/clockin/ClockInClient.hbs +251 -0
- package/templates/clockin/ClockInClientConfig.hbs +41 -0
- package/templates/clockin/ClockInController.hbs +77 -0
- package/templates/clockin/ClockInPage.hbs +11 -0
- package/templates/clockin/ClockInProperties.hbs +34 -0
- package/templates/clockin/ClockInRecord.hbs +20 -0
- package/templates/clockin/MemberImage.hbs +7 -0
- package/templates/clockin/OnDutyWeek.hbs +26 -0
- package/templates/clockin/UnclockedMember.hbs +7 -0
- package/templates/clockin/UserPermission.hbs +11 -0
- package/templates/clockin/application-yml-block.hbs +10 -0
- package/templates/docker/docker-compose.prod.yml.hbs +5 -0
- package/templates/docker/docker-compose.yml.hbs +7 -1
- package/templates/docker/env.example.hbs +15 -0
- package/templates/file-upload/FileExtensionUtils.hbs +45 -0
- package/templates/file-upload/FileExtensionUtilsTest.hbs +48 -0
- package/templates/file-upload/FileStorageServiceImpl.hbs +6 -0
- package/templates/multi-module/config/SecurityConfig.java.hbs +123 -8
- package/templates/multi-module/config/SecurityCustomizer.java.hbs +24 -0
- package/templates/openapi/ApiDocsAccessTest.hbs +60 -0
- package/templates/openapi/application-yml-block.hbs +8 -0
- package/test.md +6 -3
- package/versions.js +1 -0
package/lib/make.js
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const VERSIONS = require('../versions');
|
|
4
|
+
const { toPascalCaseName } = require('../bin/cli-util');
|
|
5
|
+
const {
|
|
6
|
+
loadProjectConfig,
|
|
7
|
+
resolvePersistence,
|
|
8
|
+
projectRoot
|
|
9
|
+
} = require('./project');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_STRING_COLUMN_LENGTH = 50;
|
|
12
|
+
|
|
13
|
+
function parseFields(fields) {
|
|
14
|
+
if (!fields || !String(fields).trim()) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
return String(fields)
|
|
18
|
+
.split(',')
|
|
19
|
+
.map((f) => {
|
|
20
|
+
const [name, type] = f.split(':');
|
|
21
|
+
const fieldName = (name || '').trim();
|
|
22
|
+
const fieldType = (type || 'String').trim();
|
|
23
|
+
return {
|
|
24
|
+
name: fieldName,
|
|
25
|
+
type: fieldType,
|
|
26
|
+
snakeName: fieldName
|
|
27
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
28
|
+
.replace(/[\s-]+/g, '_')
|
|
29
|
+
.toLowerCase(),
|
|
30
|
+
columnLength: fieldType === 'String' ? DEFAULT_STRING_COLUMN_LENGTH : null
|
|
31
|
+
};
|
|
32
|
+
})
|
|
33
|
+
.filter((f) => f.name);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isExample(data) {
|
|
37
|
+
return Boolean(data && data.example)
|
|
38
|
+
|| process.env.BIRC_EXAMPLE === '1'
|
|
39
|
+
|| parseFields((data && data.fields) || process.env.BIRC_MAKE_FIELDS).length > 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function includeMapperWithEntity(data) {
|
|
43
|
+
return Boolean(data && (data.dto || data.mapper))
|
|
44
|
+
|| process.env.BIRC_MAKE_DTO === '1'
|
|
45
|
+
|| process.env.BIRC_MAKE_MAPPER === '1';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function includeModelMigration(data) {
|
|
49
|
+
return Boolean(data && data.migration) || process.env.BIRC_MAKE_MIGRATION === '1';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function includeModelSeed(data) {
|
|
53
|
+
return Boolean(data && data.seed) || process.env.BIRC_MAKE_SEED === '1';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function skipExistingMakeFiles(data) {
|
|
57
|
+
return includeModelMigration(data) || includeModelSeed(data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function includeModelController(data) {
|
|
61
|
+
return Boolean(data && data.controller) || process.env.BIRC_MAKE_CONTROLLER === '1';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function includeSoftDelete(data) {
|
|
65
|
+
return Boolean(data && data.softDelete) || process.env.BIRC_MAKE_SOFT_DELETE === '1';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toSnakeCase(text) {
|
|
69
|
+
return String(text || '')
|
|
70
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
71
|
+
.replace(/[\s-]+/g, '_')
|
|
72
|
+
.toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function tableNameFromEntity(entityName) {
|
|
76
|
+
return toSnakeCase(entityName);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const JAVA_SQL_TYPES = Object.freeze({
|
|
80
|
+
String: `VARCHAR(${DEFAULT_STRING_COLUMN_LENGTH}) NULL`,
|
|
81
|
+
Integer: 'INT NULL',
|
|
82
|
+
int: 'INT NULL',
|
|
83
|
+
Long: 'BIGINT NULL',
|
|
84
|
+
long: 'BIGINT NULL',
|
|
85
|
+
Boolean: 'TINYINT(1) NULL',
|
|
86
|
+
boolean: 'TINYINT(1) NULL',
|
|
87
|
+
BigDecimal: 'DECIMAL(19, 2) NULL',
|
|
88
|
+
LocalDate: 'DATE NULL',
|
|
89
|
+
LocalDateTime: 'TIMESTAMP NULL',
|
|
90
|
+
Instant: 'TIMESTAMP NULL',
|
|
91
|
+
Double: 'DOUBLE NULL',
|
|
92
|
+
double: 'DOUBLE NULL',
|
|
93
|
+
Float: 'FLOAT NULL',
|
|
94
|
+
float: 'FLOAT NULL',
|
|
95
|
+
Short: 'SMALLINT NULL',
|
|
96
|
+
short: 'SMALLINT NULL',
|
|
97
|
+
Byte: 'TINYINT NULL',
|
|
98
|
+
byte: 'TINYINT NULL'
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function sqlTypeForColumn(javaType, columnName) {
|
|
102
|
+
if (columnName === 'created_at') {
|
|
103
|
+
return 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP';
|
|
104
|
+
}
|
|
105
|
+
if (columnName === 'updated_at') {
|
|
106
|
+
return 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP';
|
|
107
|
+
}
|
|
108
|
+
if (columnName === 'deleted_at') {
|
|
109
|
+
return 'TIMESTAMP NULL';
|
|
110
|
+
}
|
|
111
|
+
return JAVA_SQL_TYPES[javaType] || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function fieldsToMigrationColumns(fields, options) {
|
|
115
|
+
const withTimestamps = Boolean(options && options.withTimestamps);
|
|
116
|
+
const withSoftDelete = Boolean(options && options.withSoftDelete);
|
|
117
|
+
const columns = [];
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
|
|
120
|
+
function addColumn(name, javaType) {
|
|
121
|
+
const columnName = sanitizeMigrationToken(name);
|
|
122
|
+
if (!columnName || columnName === 'id' || seen.has(columnName)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const sqlType = sqlTypeForColumn(javaType, columnName);
|
|
126
|
+
if (!sqlType) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
seen.add(columnName);
|
|
130
|
+
columns.push({ name: columnName, sqlType });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const field of fields || []) {
|
|
134
|
+
addColumn(field.snakeName || toSnakeCase(field.name), field.type);
|
|
135
|
+
}
|
|
136
|
+
if (withTimestamps) {
|
|
137
|
+
addColumn('created_at', 'LocalDateTime');
|
|
138
|
+
addColumn('updated_at', 'LocalDateTime');
|
|
139
|
+
}
|
|
140
|
+
if (withSoftDelete) {
|
|
141
|
+
addColumn('deleted_at', 'LocalDateTime');
|
|
142
|
+
}
|
|
143
|
+
return columns;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function listJavaFiles(dir, acc = []) {
|
|
147
|
+
if (!fs.existsSync(dir)) {
|
|
148
|
+
return acc;
|
|
149
|
+
}
|
|
150
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
151
|
+
const full = path.join(dir, entry.name);
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
listJavaFiles(full, acc);
|
|
154
|
+
} else if (entry.isFile() && entry.name.endsWith('.java')) {
|
|
155
|
+
acc.push(full);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return acc;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseEntityColumns(source) {
|
|
162
|
+
const fields = [];
|
|
163
|
+
const fieldRe =
|
|
164
|
+
/((?:@\w+(?:\s*\([^)]*\))?\s*)*)private\s+([A-Za-z][A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*;/g;
|
|
165
|
+
let match = fieldRe.exec(source);
|
|
166
|
+
while (match) {
|
|
167
|
+
const annotations = match[1] || '';
|
|
168
|
+
const type = match[2];
|
|
169
|
+
const name = match[3];
|
|
170
|
+
const columnAttr = annotations.match(/@Column\s*\(([^)]*)\)/);
|
|
171
|
+
let snakeName = toSnakeCase(name);
|
|
172
|
+
if (columnAttr) {
|
|
173
|
+
const named = columnAttr[1].match(/name\s*=\s*"([^"]+)"/);
|
|
174
|
+
if (named) {
|
|
175
|
+
snakeName = named[1];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
fields.push({ name, type, snakeName });
|
|
179
|
+
match = fieldRe.exec(source);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
fields,
|
|
183
|
+
withSoftDelete: /@SoftDelete\b/.test(source)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function findEntitySourceForTable(tableName, config) {
|
|
188
|
+
if (!tableName) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const persistence = resolvePersistence(config);
|
|
192
|
+
const entityDir = path.join(projectRoot(), persistence.entityRoot);
|
|
193
|
+
const files = listJavaFiles(entityDir);
|
|
194
|
+
const escaped = String(tableName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
195
|
+
const tableRe = new RegExp(`@Table\\s*\\([^)]*name\\s*=\\s*"${escaped}"`, 'i');
|
|
196
|
+
for (const file of files) {
|
|
197
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
198
|
+
if (tableRe.test(source)) {
|
|
199
|
+
return source;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const className = toPascalCaseName(String(tableName).toLowerCase());
|
|
203
|
+
if (!className) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const match = files.find((file) => path.basename(file, '.java') === className);
|
|
207
|
+
if (!match) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return fs.readFileSync(match, 'utf8');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function javaLiteralForSeed(javaType) {
|
|
214
|
+
if (javaType === 'String') {
|
|
215
|
+
return '"example"';
|
|
216
|
+
}
|
|
217
|
+
if (javaType === 'LocalDate') {
|
|
218
|
+
return 'LocalDate.parse("2020-01-01")';
|
|
219
|
+
}
|
|
220
|
+
if (javaType === 'LocalDateTime') {
|
|
221
|
+
return 'LocalDateTime.parse("2020-01-01T00:00:00")';
|
|
222
|
+
}
|
|
223
|
+
if (javaType === 'Instant') {
|
|
224
|
+
return 'Instant.parse("2020-01-01T00:00:00Z")';
|
|
225
|
+
}
|
|
226
|
+
if (javaType === 'BigDecimal') {
|
|
227
|
+
return 'new BigDecimal("0")';
|
|
228
|
+
}
|
|
229
|
+
if (javaType === 'Boolean' || javaType === 'boolean') {
|
|
230
|
+
return 'false';
|
|
231
|
+
}
|
|
232
|
+
if (javaType === 'Long' || javaType === 'long') {
|
|
233
|
+
return '0L';
|
|
234
|
+
}
|
|
235
|
+
if (javaType === 'Integer' || javaType === 'int') {
|
|
236
|
+
return '0';
|
|
237
|
+
}
|
|
238
|
+
if (javaType === 'Short' || javaType === 'short') {
|
|
239
|
+
return '(short) 0';
|
|
240
|
+
}
|
|
241
|
+
if (javaType === 'Byte' || javaType === 'byte') {
|
|
242
|
+
return '(byte) 0';
|
|
243
|
+
}
|
|
244
|
+
if (javaType === 'Double' || javaType === 'double') {
|
|
245
|
+
return '0D';
|
|
246
|
+
}
|
|
247
|
+
if (javaType === 'Float' || javaType === 'float') {
|
|
248
|
+
return '0F';
|
|
249
|
+
}
|
|
250
|
+
if (JAVA_SQL_TYPES[javaType]) {
|
|
251
|
+
return '0';
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const SKIP_SEED_FIELDS = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt', 'created_at', 'updated_at', 'deleted_at']);
|
|
257
|
+
|
|
258
|
+
function fieldsToSeedFields(fields) {
|
|
259
|
+
const result = [];
|
|
260
|
+
const seen = new Set();
|
|
261
|
+
|
|
262
|
+
for (const field of fields || []) {
|
|
263
|
+
const name = String(field.name || '').trim();
|
|
264
|
+
const snakeName = field.snakeName || toSnakeCase(name);
|
|
265
|
+
if (!name || SKIP_SEED_FIELDS.has(name) || SKIP_SEED_FIELDS.has(snakeName) || seen.has(name)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const literal = javaLiteralForSeed(field.type);
|
|
269
|
+
if (!literal) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
seen.add(name);
|
|
273
|
+
result.push({
|
|
274
|
+
name,
|
|
275
|
+
type: field.type,
|
|
276
|
+
setter: `set${name.charAt(0).toUpperCase()}${name.slice(1)}`,
|
|
277
|
+
literal
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function resolveSeedFields(data, tableName, config) {
|
|
284
|
+
const explicitFields = parseFields(data.fields || process.env.BIRC_MAKE_FIELDS);
|
|
285
|
+
if (explicitFields.length > 0) {
|
|
286
|
+
return fieldsToSeedFields(explicitFields);
|
|
287
|
+
}
|
|
288
|
+
const source = findEntitySourceForTable(tableName, config);
|
|
289
|
+
if (source) {
|
|
290
|
+
return fieldsToSeedFields(parseEntityColumns(source).fields);
|
|
291
|
+
}
|
|
292
|
+
if (isExample(data)) {
|
|
293
|
+
return fieldsToSeedFields([{ name: 'name', type: 'String', snakeName: 'name' }]);
|
|
294
|
+
}
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function entityNameForSeeder(data) {
|
|
299
|
+
const raw = toPascalCaseName((data && data.entityName) || '');
|
|
300
|
+
return raw.replace(/Seeder$/, '') || raw;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function resolveCreateTableColumns(data, tableName, config) {
|
|
304
|
+
const example = isExample(data);
|
|
305
|
+
const flaggedSoftDelete = includeSoftDelete(data);
|
|
306
|
+
const explicitFields = parseFields(data.fields || process.env.BIRC_MAKE_FIELDS);
|
|
307
|
+
if (explicitFields.length > 0) {
|
|
308
|
+
return fieldsToMigrationColumns(explicitFields, {
|
|
309
|
+
withTimestamps: example,
|
|
310
|
+
withSoftDelete: flaggedSoftDelete
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const source = findEntitySourceForTable(tableName, config);
|
|
314
|
+
if (source) {
|
|
315
|
+
const entity = parseEntityColumns(source);
|
|
316
|
+
return fieldsToMigrationColumns(entity.fields, {
|
|
317
|
+
withTimestamps: false,
|
|
318
|
+
withSoftDelete: flaggedSoftDelete || entity.withSoftDelete
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (example) {
|
|
322
|
+
return fieldsToMigrationColumns(
|
|
323
|
+
[{ name: 'name', type: 'String', snakeName: 'name' }],
|
|
324
|
+
{ withTimestamps: true, withSoftDelete: flaggedSoftDelete }
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return fieldsToMigrationColumns([], {
|
|
328
|
+
withTimestamps: false,
|
|
329
|
+
withSoftDelete: flaggedSoftDelete
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function fieldImports(fields) {
|
|
334
|
+
const types = new Set((fields || []).map((field) => field.type));
|
|
335
|
+
const imports = [];
|
|
336
|
+
if (types.has('LocalDateTime')) imports.push('java.time.LocalDateTime');
|
|
337
|
+
if (types.has('LocalDate')) imports.push('java.time.LocalDate');
|
|
338
|
+
if (types.has('Instant')) imports.push('java.time.Instant');
|
|
339
|
+
if (types.has('BigDecimal')) imports.push('java.math.BigDecimal');
|
|
340
|
+
return imports;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function resolveMakeFields(data) {
|
|
344
|
+
const fields = parseFields(data.fields || process.env.BIRC_MAKE_FIELDS);
|
|
345
|
+
if (fields.length > 0) {
|
|
346
|
+
return fields;
|
|
347
|
+
}
|
|
348
|
+
if (isExample(data)) {
|
|
349
|
+
return [{
|
|
350
|
+
name: 'name',
|
|
351
|
+
type: 'String',
|
|
352
|
+
snakeName: 'name',
|
|
353
|
+
columnLength: DEFAULT_STRING_COLUMN_LENGTH
|
|
354
|
+
}];
|
|
355
|
+
}
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 最後防線:prompt 驗證被繞過時(CLI 直呼、程式化呼叫),輸出值也不得
|
|
360
|
+
// 帶 SQL 特殊字元或路徑字元——tableName/columnName 進 Flyway SQL,
|
|
361
|
+
// description 同時進檔名。
|
|
362
|
+
function sanitizeMigrationToken(value) {
|
|
363
|
+
return String(value || '').replace(/[^a-zA-Z0-9_]/g, '');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseMigrationName(name) {
|
|
367
|
+
const raw = String(name || '').trim();
|
|
368
|
+
let match = raw.match(/^create_(.+)_table$/i);
|
|
369
|
+
if (match) {
|
|
370
|
+
return {
|
|
371
|
+
kind: 'create',
|
|
372
|
+
tableName: sanitizeMigrationToken(match[1]),
|
|
373
|
+
columnName: null,
|
|
374
|
+
description: sanitizeMigrationToken(raw) || 'migration'
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
match = raw.match(/^add_(.+)_to_(.+)_table$/i);
|
|
378
|
+
if (match) {
|
|
379
|
+
return {
|
|
380
|
+
kind: 'add',
|
|
381
|
+
columnName: sanitizeMigrationToken(match[1]),
|
|
382
|
+
tableName: sanitizeMigrationToken(match[2]),
|
|
383
|
+
description: sanitizeMigrationToken(raw) || 'migration'
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
match = raw.match(/^remove_(.+)_from_(.+)_table$/i);
|
|
387
|
+
if (match) {
|
|
388
|
+
return {
|
|
389
|
+
kind: 'remove',
|
|
390
|
+
columnName: sanitizeMigrationToken(match[1]),
|
|
391
|
+
tableName: sanitizeMigrationToken(match[2]),
|
|
392
|
+
description: sanitizeMigrationToken(raw) || 'migration'
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const customDescription = sanitizeMigrationToken(raw);
|
|
396
|
+
return { kind: 'custom', tableName: null, columnName: null, description: customDescription || 'migration' };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function nextMigrationVersion(migrationDir) {
|
|
400
|
+
if (!fs.existsSync(migrationDir)) {
|
|
401
|
+
return 1;
|
|
402
|
+
}
|
|
403
|
+
const versions = fs
|
|
404
|
+
.readdirSync(migrationDir)
|
|
405
|
+
.filter((file) => file.startsWith('V') && file.endsWith('.sql'))
|
|
406
|
+
.map((file) => {
|
|
407
|
+
const match = file.match(/^V(\d+)__/);
|
|
408
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
409
|
+
});
|
|
410
|
+
return versions.length ? Math.max(...versions) + 1 : 1;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function migrationTemplate(kind) {
|
|
414
|
+
if (kind === 'create') return 'templates/migration/create-table.hbs';
|
|
415
|
+
if (kind === 'add') return 'templates/migration/add-column.hbs';
|
|
416
|
+
if (kind === 'remove') return 'templates/migration/remove-column.hbs';
|
|
417
|
+
return 'templates/migration/custom.hbs';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function makeContext(data) {
|
|
421
|
+
if (data && data.entityName) {
|
|
422
|
+
data.entityName = toPascalCaseName(data.entityName);
|
|
423
|
+
}
|
|
424
|
+
const config = loadProjectConfig();
|
|
425
|
+
const persistence = resolvePersistence(config);
|
|
426
|
+
const basePackage = config.basePackage;
|
|
427
|
+
const srcPath = config.srcPath || 'src/main/java';
|
|
428
|
+
const root = `${srcPath}/${basePackage.replace(/\./g, '/')}`;
|
|
429
|
+
const fields = resolveMakeFields(data);
|
|
430
|
+
const example = isExample(data);
|
|
431
|
+
const extraImports = fieldImports(fields);
|
|
432
|
+
const entityImports = extraImports.includes('java.time.LocalDateTime') || !example
|
|
433
|
+
? extraImports
|
|
434
|
+
: extraImports.concat('java.time.LocalDateTime');
|
|
435
|
+
return {
|
|
436
|
+
config,
|
|
437
|
+
persistence,
|
|
438
|
+
root,
|
|
439
|
+
gradlePath: config.buildGradlePath || 'build.gradle',
|
|
440
|
+
templateData: {
|
|
441
|
+
...VERSIONS,
|
|
442
|
+
basePackage,
|
|
443
|
+
makeFields: fields,
|
|
444
|
+
extraImports,
|
|
445
|
+
entityImports,
|
|
446
|
+
withExample: example,
|
|
447
|
+
withSoftDelete: includeSoftDelete(data),
|
|
448
|
+
entityPackage: persistence.entityPackage,
|
|
449
|
+
daoPackage: persistence.daoPackage
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function exceptionClassName(name) {
|
|
455
|
+
const base = toPascalCaseName(name);
|
|
456
|
+
return /Exception$/i.test(base) ? base.replace(/exception$/i, 'Exception') : `${base}Exception`;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function exceptionSkeletonActions(root, templateData) {
|
|
460
|
+
return [
|
|
461
|
+
{ type: 'add', path: `${root}/exception/ProjectException.java`, templateFile: 'templates/exception/ProjectException.hbs', data: templateData },
|
|
462
|
+
{ type: 'add', path: `${root}/exception/NotFoundException.java`, templateFile: 'templates/exception/NotFoundException.hbs', data: templateData },
|
|
463
|
+
{ type: 'add', path: `${root}/controller/ExceptionHandleController.java`, templateFile: 'templates/exception/ExceptionHandleController.hbs', data: templateData },
|
|
464
|
+
{ type: 'add', path: `${root}/web/Result.java`, templateFile: 'templates/base/Result.hbs', data: templateData }
|
|
465
|
+
];
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function serviceSkeletonActions(root, templateData) {
|
|
469
|
+
return [
|
|
470
|
+
{ type: 'add', path: `${root}/mapper/EntityMapper.java`, templateFile: 'templates/base/EntityMapper.hbs', data: templateData },
|
|
471
|
+
{ type: 'add', path: `${root}/service/BaseService.java`, templateFile: 'templates/base/BaseService.hbs', data: templateData },
|
|
472
|
+
{ type: 'add', path: `${root}/service/impl/BaseServiceImpl.java`, templateFile: 'templates/base/BaseServiceImpl.hbs', data: templateData }
|
|
473
|
+
];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function seederSkeletonActions(root, templateData, options) {
|
|
477
|
+
const skipIfExists = Boolean(options && options.skipIfExists);
|
|
478
|
+
return [
|
|
479
|
+
{ type: 'add', path: `${root}/seeder/Seeder.java`, templateFile: 'templates/seeder/Seeder.hbs', data: templateData, skipIfExists },
|
|
480
|
+
{ type: 'add', path: `${root}/seeder/SeedRunner.java`, templateFile: 'templates/seeder/SeedRunner.hbs', data: templateData, skipIfExists }
|
|
481
|
+
];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function makeEntityActions(data, options) {
|
|
485
|
+
const { persistence, templateData } = makeContext(data);
|
|
486
|
+
const skipIfExists = Boolean(options && options.skipIfExists);
|
|
487
|
+
return [
|
|
488
|
+
{ type: 'add', path: `${persistence.entityRoot}/{{pascalCase entityName}}.java`, templateFile: 'templates/entity.hbs', data: templateData, skipIfExists },
|
|
489
|
+
{ type: 'add', path: `${persistence.daoRoot}/{{pascalCase entityName}}DAO.java`, templateFile: 'templates/dao.hbs', data: templateData, skipIfExists }
|
|
490
|
+
];
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function makeMapperActions(data, options) {
|
|
494
|
+
const { root, gradlePath, templateData } = makeContext(data);
|
|
495
|
+
const skipIfExists = Boolean(options && options.skipIfExists);
|
|
496
|
+
return [
|
|
497
|
+
{ type: 'add', path: `${root}/dto/{{pascalCase entityName}}CreateRequest.java`, templateFile: 'templates/dto-request.hbs', data: templateData, skipIfExists },
|
|
498
|
+
{ type: 'add', path: `${root}/dto/{{pascalCase entityName}}Response.java`, templateFile: 'templates/dto-response.hbs', data: templateData, skipIfExists },
|
|
499
|
+
{ type: 'add', path: `${root}/mapper/{{pascalCase entityName}}Mapper.java`, templateFile: 'templates/mapper.hbs', data: templateData, skipIfExists },
|
|
500
|
+
{
|
|
501
|
+
type: 'appendFragment',
|
|
502
|
+
path: gradlePath,
|
|
503
|
+
pattern: /\/\/ birc-generator:dependency-anchor\n/,
|
|
504
|
+
templateFile: 'templates/mapper-gradle-dep.hbs',
|
|
505
|
+
data: templateData,
|
|
506
|
+
marker: 'mapstruct',
|
|
507
|
+
commentToken: '//',
|
|
508
|
+
abortOnFail: true
|
|
509
|
+
}
|
|
510
|
+
];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function makeServiceActions(data, options) {
|
|
514
|
+
const { root, templateData } = makeContext(data);
|
|
515
|
+
const skipIfExists = Boolean(options && options.skipIfExists);
|
|
516
|
+
return [
|
|
517
|
+
{ type: 'add', path: `${root}/service/{{pascalCase entityName}}Service.java`, templateFile: 'templates/service.hbs', data: templateData, skipIfExists },
|
|
518
|
+
{ type: 'add', path: `${root}/service/impl/{{pascalCase entityName}}ServiceImpl.java`, templateFile: 'templates/serviceImpl.hbs', data: templateData, skipIfExists }
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function makeControllerActions(data, options) {
|
|
523
|
+
const { root, templateData } = makeContext(data);
|
|
524
|
+
const skipIfExists = Boolean(options && options.skipIfExists);
|
|
525
|
+
return [
|
|
526
|
+
{
|
|
527
|
+
type: 'add',
|
|
528
|
+
path: `${root}/web/Result.java`,
|
|
529
|
+
templateFile: 'templates/base/Result.hbs',
|
|
530
|
+
data: templateData,
|
|
531
|
+
skipIfExists: true
|
|
532
|
+
},
|
|
533
|
+
{ type: 'add', path: `${root}/controller/{{pascalCase entityName}}Controller.java`, templateFile: 'templates/controller.hbs', data: templateData, skipIfExists }
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function makeStackActions(data) {
|
|
538
|
+
const skipIfExists = skipExistingMakeFiles(data);
|
|
539
|
+
return appendMakeSqlActions([
|
|
540
|
+
...makeEntityActions(data, { skipIfExists }),
|
|
541
|
+
...makeMapperActions(data, { skipIfExists }),
|
|
542
|
+
...makeServiceActions(data, { skipIfExists }),
|
|
543
|
+
...makeControllerActions(data, { skipIfExists })
|
|
544
|
+
], data);
|
|
545
|
+
}
|
|
546
|
+
function migrationDir(config) {
|
|
547
|
+
const resourcesPath = config.resourcesPath || 'src/main/resources';
|
|
548
|
+
return config.migrationPath || `${resourcesPath}/db/migration`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function makeMigrationActions(data) {
|
|
552
|
+
const config = loadProjectConfig();
|
|
553
|
+
const migrationPath = migrationDir(config);
|
|
554
|
+
const parsed = parseMigrationName(data.description);
|
|
555
|
+
const version = data.migrationVersion || nextMigrationVersion(path.join(projectRoot(), migrationPath));
|
|
556
|
+
const example = isExample(data);
|
|
557
|
+
const migrationColumns = parsed.kind === 'create'
|
|
558
|
+
? resolveCreateTableColumns(data, parsed.tableName, config)
|
|
559
|
+
: [];
|
|
560
|
+
return [
|
|
561
|
+
{
|
|
562
|
+
type: 'add',
|
|
563
|
+
path: `${migrationPath}/V${version}__${parsed.description}.sql`,
|
|
564
|
+
templateFile: migrationTemplate(parsed.kind),
|
|
565
|
+
data: {
|
|
566
|
+
description: parsed.description,
|
|
567
|
+
tableName: parsed.tableName,
|
|
568
|
+
columnName: parsed.columnName,
|
|
569
|
+
withExample: example,
|
|
570
|
+
withSoftDelete: includeSoftDelete(data),
|
|
571
|
+
migrationColumns
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
];
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function makeSeederActions(data) {
|
|
578
|
+
data.entityName = entityNameForSeeder(data);
|
|
579
|
+
const { root, templateData } = makeContext(data);
|
|
580
|
+
const entityName = data.entityName;
|
|
581
|
+
const seederClass = `${entityName}Seeder`;
|
|
582
|
+
const tableName = tableNameFromEntity(entityName);
|
|
583
|
+
const seedFields = resolveSeedFields(data, tableName, loadProjectConfig());
|
|
584
|
+
return [
|
|
585
|
+
...seederSkeletonActions(root, templateData, { skipIfExists: true }),
|
|
586
|
+
{
|
|
587
|
+
type: 'add',
|
|
588
|
+
path: `${root}/seeder/${seederClass}.java`,
|
|
589
|
+
templateFile: 'templates/seeder.hbs',
|
|
590
|
+
data: {
|
|
591
|
+
...templateData,
|
|
592
|
+
extraImports: fieldImports(seedFields),
|
|
593
|
+
seedFields,
|
|
594
|
+
seederClass,
|
|
595
|
+
entityName
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function makeModelActions(data) {
|
|
602
|
+
const actions = makeEntityActions(data, { skipIfExists: true });
|
|
603
|
+
if (includeMapperWithEntity(data)) {
|
|
604
|
+
actions.push(...makeMapperActions(data));
|
|
605
|
+
}
|
|
606
|
+
appendMakeSqlActions(actions, data);
|
|
607
|
+
if (includeModelController(data)) {
|
|
608
|
+
actions.push(...makeControllerActions(data));
|
|
609
|
+
}
|
|
610
|
+
return actions;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function appendMakeSqlActions(actions, data) {
|
|
614
|
+
const config = loadProjectConfig();
|
|
615
|
+
const migrationPath = migrationDir(config);
|
|
616
|
+
const version = nextMigrationVersion(path.join(projectRoot(), migrationPath));
|
|
617
|
+
if (includeModelMigration(data)) {
|
|
618
|
+
const tableName = tableNameFromEntity(data.entityName);
|
|
619
|
+
actions.push(
|
|
620
|
+
...makeMigrationActions({
|
|
621
|
+
...data,
|
|
622
|
+
description: `create_${tableName}_table`,
|
|
623
|
+
migrationVersion: version
|
|
624
|
+
})
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
if (includeModelSeed(data)) {
|
|
628
|
+
actions.push(...makeSeederActions(data));
|
|
629
|
+
}
|
|
630
|
+
return actions;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
module.exports = {
|
|
634
|
+
isExample,
|
|
635
|
+
includeMapperWithEntity,
|
|
636
|
+
includeModelMigration,
|
|
637
|
+
includeModelSeed,
|
|
638
|
+
skipExistingMakeFiles,
|
|
639
|
+
includeModelController,
|
|
640
|
+
includeSoftDelete,
|
|
641
|
+
toSnakeCase,
|
|
642
|
+
parseMigrationName,
|
|
643
|
+
validateMigrationName: require('./project').validateMigrationName,
|
|
644
|
+
makeContext,
|
|
645
|
+
exceptionClassName,
|
|
646
|
+
exceptionSkeletonActions,
|
|
647
|
+
serviceSkeletonActions,
|
|
648
|
+
seederSkeletonActions,
|
|
649
|
+
makeEntityActions,
|
|
650
|
+
makeMapperActions,
|
|
651
|
+
makeServiceActions,
|
|
652
|
+
makeControllerActions,
|
|
653
|
+
makeStackActions,
|
|
654
|
+
makeModelActions,
|
|
655
|
+
makeMigrationActions,
|
|
656
|
+
makeSeederActions,
|
|
657
|
+
appendMakeSqlActions
|
|
658
|
+
};
|