birc-generator 0.5.5 → 0.8.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 +5 -5
- package/README.md +17 -8
- package/bin/birc.js +45 -21
- package/bin/cli-util.js +16 -14
- package/bin/home-install.js +5 -3
- package/bin/update-check.js +1 -1
- package/install.ps1 +2 -2
- package/install.sh +10 -3
- package/package.json +1 -1
- package/plopfile.js +335 -32
- package/project-docs/PROJECT.md.hbs +4 -1
- package/scripts/install-bin.sh +2 -2
- package/templates/base/README.md.hbs +15 -1
- package/templates/entity.hbs +1 -1
- package/templates/migration/add-column.hbs +1 -1
- package/templates/migration/create-table.hbs +2 -5
- package/templates/seeder/SeedRunner.hbs +31 -0
- package/templates/seeder/Seeder.hbs +6 -0
- package/templates/seeder.hbs +38 -0
- package/test.md +4 -1
- package/uninstall.ps1 +1 -1
- package/templates/migration/seed.hbs +0 -7
package/PROJECT.md
CHANGED
|
@@ -12,7 +12,7 @@ anchor 就不要重寫整份設定;推測以後會用到的模組先不要做
|
|
|
12
12
|
| 要做的事 | Seam | 誰會吃到 |
|
|
13
13
|
| --- | --- | --- |
|
|
14
14
|
| 新的可選模組 | `FEATURES` + `templates/<key>/` | `create`、`add`(共用 `featureActions`) |
|
|
15
|
-
| `make` / `make:*` 骨架 | `templates/*.hbs` + `makeStackActions` / 單層 actions | `make`(整組)、`make:entity`、`make:model`、`make:mapper`、`make:service`、`make:controller`、`make:exception`、`make:migration` |
|
|
15
|
+
| `make` / `make:*` 骨架 | `templates/*.hbs` + `makeStackActions` / 單層 actions | `make`(整組)、`make:entity`、`make:model`、`make:mapper`、`make:service`、`make:controller`、`make:exception`、`make:migration`、`make:seeder` |
|
|
16
16
|
| 注入給產出專案的 agent 文件 | `project-docs/` | `create`(`injectDocs`)、`docs` |
|
|
17
17
|
| create 的基礎骨架 | `templates/base/`、`templates/multi-module/`、`templates/gradle-wrapper/` | `create`(含給人看的 `README.md`) |
|
|
18
18
|
| CLI 進入點 | `bin/birc.js`、`bin/home-install.js`、`install.sh` / `install.ps1` | `birc`(banner、版本、`update`、直接呼叫 node-plop) |
|
|
@@ -48,7 +48,7 @@ anchor 就不要重寫整份設定;推測以後會用到的模組先不要做
|
|
|
48
48
|
|
|
49
49
|
## 慣例
|
|
50
50
|
|
|
51
|
-
**make 一次整組。** `make` 串 `makeStackActions`:Entity + DAO + Mapper + DTO + Service + Controller
|
|
51
|
+
**make 一次整組。** `make` 串 `makeStackActions`:Entity + DAO + Mapper + DTO + Service + Controller。`--migration` 生 `create_{table}_table`(Flyway SQL)。`--seed` 生 `XxxSeeder`(Java,走 DAO;`birc seed` 才執行)。單層指令共用同一組 actions,不另寫一份。`make:entity` 產出 Entity、`XxxDAO extends BaseDAO`。`make:model` 同 Entity + DAO;`--migration` / `--seed` 同上,`--controller` 生 Controller。`make:seeder BookSeeder` 只生 Seeder(寫 `Book` 也可以,結尾 `Seeder` 會剝掉再對 Entity)。`--soft-delete` 在 Entity 加 Hibernate `@SoftDelete(columnName = "deleted_at", strategy = TIMESTAMP)`,migration 加 `deleted_at TIMESTAMP NULL`;`deleteById` 不用改。不做 restore / withTrashed。`make:entity --dto` 與 `make:entity --mapper` 一併生 Mapper 與 Request / Response DTO(同一組檔)。`make:mapper` 連 Request / Response DTO,並把 MapStruct 依賴接到 `build.gradle` 的 dependency-anchor 後面(已有則跳過)。`make:service` 的 `XxxService extends BaseService`,CRUD 在 `BaseServiceImpl`;多出來的業務方法寫在 `XxxService` / `XxxServiceImpl`。指令是 `make:entity`,不是 `make::entity`。預設只生引入與初稿;加 `--example` 或 `--fields` 才生完整範例(CRUD、欄位、`@Column`)。`--example` 的 Entity 與 `make:migration` 一樣帶 `created_at` / `updated_at`(DB 填,JPA `insertable = false`),不進 CreateRequest。多模組時 Entity / DAO 落在 `databaseconfig`。路徑以 `.bircrc.json` 的 `entityPath` / `daoPath` 為準。
|
|
52
52
|
|
|
53
53
|
**例外走 ProjectException。** 基底是專案內的 `ProjectException`(`getErrorCode` / `getHttpStatus`),不是 starter 的 `BusinessException`。`create` 會生 `ProjectException`、`NotFoundException`、`ExceptionHandleController`、`web/Result`。`make:exception` 生的類別一律 `extends ProjectException`。Controller 回傳專案內 `Result<T>`,不引用 `tw.edu.ntub.birc.starter`。
|
|
54
54
|
|
|
@@ -67,7 +67,7 @@ anchor 就不要重寫整份設定;推測以後會用到的模組先不要做
|
|
|
67
67
|
**版本只改一處。** Spring Boot、Flyway、Sentry、MapStruct、Spotless、dependency-management
|
|
68
68
|
plugin 與 starter 版本都在 `versions.js`;`create` / `add` / `make:mapper` 將它們注入 template。
|
|
69
69
|
|
|
70
|
-
**`birc` 的 dest。** `bin/birc.js` 把 plop 的 cwd 釘在這個 repo(找 `plopfile.js` 與 `templates/`),dest 與 `.bircrc.json` 走呼叫者目錄(`BIRC_PROJECT_ROOT` + `--dest`)。`create` / `update` 用目前目錄;目前目錄已有 `.bircrc.json`、或目標資料夾已在,`create` 會先失敗。`birc create --yes` 其餘用預設、不要問。`add` / `make:*` / `migrate` / `docs` 從目前目錄往上找 `.bircrc.json`,檔案落在找到的專案根。`birc add email sentry` 可帶 feature 名(也接受 `file-upload` 這種 kebab);沒帶參數仍互動勾選。打錯功能名會給近似建議。未知旗標會失敗並提示正確名稱。`docs` 用 `.bircrc.json` 的專案名,不再問。`make` 撞到已有檔會提示 `--force`;`--force` 覆寫那組 Java。`--fields`
|
|
70
|
+
**`birc` 的 dest。** `bin/birc.js` 把 plop 的 cwd 釘在這個 repo(找 `plopfile.js` 與 `templates/`),dest 與 `.bircrc.json` 走呼叫者目錄(`BIRC_PROJECT_ROOT` + `--dest`)。`create` / `update` 用目前目錄;目前目錄已有 `.bircrc.json`、或目標資料夾已在,`create` 會先失敗。`birc create --yes` 其餘用預設、不要問。`add` / `make:*` / `migrate` / `seed` / `db:wipe` / `docs` 從目前目錄往上找 `.bircrc.json`,檔案落在找到的專案根。`birc add email sentry` 可帶 feature 名(也接受 `file-upload` 這種 kebab);沒帶參數仍互動勾選。打錯功能名會給近似建議。未知旗標會失敗並提示正確名稱。`docs` 用 `.bircrc.json` 的專案名,不再問。`make` 撞到已有檔會提示 `--force`;`--force` 覆寫那組 Java。`--fields` 有帶就當完整產出,不必再加 `--example`。
|
|
71
71
|
|
|
72
72
|
**`birc update`。** 先 `detectInstallKind`:`npm` 跑 `npm install -g birc-generator@latest`。curl / PowerShell / clone 走 `bin/home-install.js`(不要再把 Windows 暫存路徑丟給 bash)。`~/.birc-generator` 的 kind 是 `home`,跟用 bash 還是 `irm | iex` 裝無關。
|
|
73
73
|
|
|
@@ -76,8 +76,8 @@ anchor:`dependencies {}` 的 `// birc-generator:dependency-anchor`、`plugins
|
|
|
76
76
|
`// birc-generator:plugin-anchor`、`allprojects {}` 的
|
|
77
77
|
`// birc-generator:allprojects-anchor`。`add` 的 gradle append 只有 dest 檔裡有對應那一行才插得進去。
|
|
78
78
|
|
|
79
|
-
**`make:migration`。** Laravel 風格名稱:`create_users_table`、`add_votes_to_users_table`、`remove_votes_from_users_table`。檔案落在 `src/main/resources/db/migration/V{n}__{name}.sql`。
|
|
79
|
+
**`make:migration`。** Laravel 風格名稱:`create_users_table`、`add_votes_to_users_table`、`remove_votes_from_users_table`。檔案落在 `src/main/resources/db/migration/V{n}__{name}.sql`。`create_{table}_table` 讀既有 Entity(先對 `@Table(name)`,沒對上再用檔名)把欄位寫進 CREATE TABLE;`@SoftDelete` 加 `deleted_at`。String 預設長度 50(Entity `length = 50`、SQL `VARCHAR(50)`)。找不到 Entity 時只有 `id`,或 `--example` 時的 `name` + timestamps 初稿。`make` / `make:model --migration --fields` 以旗標欄位為準。`--seed` / `make:seeder BookSeeder` 生 Java `XxxSeeder`(讀 `--fields` 或既有 Entity,略過 `id` / timestamps);沒有欄位時 `run()` 是空的。示範資料不進 Flyway。
|
|
80
80
|
|
|
81
|
-
**`migrate`。** `birc migrate` 在目標專案跑 Gradle wrapper 的 `flywayMigrate`(Unix `./gradlew`,Windows `gradlew.bat`)。`migrate:reset` 是 `flywayClean` + `flywayMigrate`(`--force`
|
|
81
|
+
**`migrate` / `seed` / `db:wipe`。** `birc migrate` 在目標專案跑 Gradle wrapper 的 `flywayMigrate`(Unix `./gradlew`,Windows `gradlew.bat`)。`migrate:reset` 是 `flywayClean` + `flywayMigrate`(`--force` 略過確認)。`birc db:wipe` 只跑 `flywayClean`(`--force` 略過確認)。`birc seed` 跑 `bootRun --args=--spring.main.web-application-type=none --birc.seed`,執行 `Seeder` 後結束。找不到 wrapper 或 spawn 失敗要印錯誤,不要只帶 banner 就結束。Flyway Community **沒有** Laravel 的 rollback;`migrate:rollback` 只說明這件事。undo 要 Flyway Teams 的 `flywayUndo` 加 `U{version}__*.sql`。
|
|
82
82
|
|
|
83
83
|
產出的註解與注入文件用繁中,跟現有 template 一致。目標專案設定欄位以 `.bircrc.example.json` 與 `templates/base/bircrc.hbs` 為準。
|
package/README.md
CHANGED
|
@@ -146,8 +146,8 @@ birc make
|
|
|
146
146
|
輸入 `Activity` 或 `activity`,會一次寫出 Entity、DAO、Mapper、DTO、
|
|
147
147
|
Service、Controller。小寫會自動收成 PascalCase。只要其中一層,改跑
|
|
148
148
|
`birc make:entity` 或 `birc make:controller`。預設是初稿,完整範例加
|
|
149
|
-
`--example`。欄位可寫 `--fields title:String,startTime:LocalDateTime`,
|
|
150
|
-
|
|
149
|
+
`--example`,或直接帶 `--fields`。欄位可寫 `--fields title:String,startTime:LocalDateTime`,
|
|
150
|
+
就不會再問一次。
|
|
151
151
|
檔已存在時會停下來;要覆寫加 `--force`。
|
|
152
152
|
|
|
153
153
|
腳本或課堂示範可 `birc create Practice --yes`,其餘用預設(docker、log4j2、
|
|
@@ -165,32 +165,41 @@ ValidGroup、Spotless、注入 agent 文件),不再問。
|
|
|
165
165
|
| `birc update` | 更新本機 `birc`。npm 裝的跑 `npm install -g`;curl / PowerShell / clone 拉 GitLab `main` |
|
|
166
166
|
| `birc add` | 往既有專案加模組。沒帶名稱會互動勾選;`birc add email sentry` 直接加。`--force` 覆寫已選的已安裝 feature 檔 |
|
|
167
167
|
| `birc sync` | 用目前 template 覆寫全部已安裝 feature 的檔(不動 yml / gradle) |
|
|
168
|
-
| `birc make` | 一次生 Entity + DAO + Mapper + DTO + Service + Controller |
|
|
168
|
+
| `birc make` | 一次生 Entity + DAO + Mapper + DTO + Service + Controller;加 `--migration` / `--seed` |
|
|
169
169
|
| `birc make:entity` | Entity、`XxxDAO`;加 `--dto`、`--mapper`、`--soft-delete`。已有檔要覆寫加 `--force` |
|
|
170
170
|
| `birc make:model` | 同 `make:entity`;加 `--migration` / `--seed` / `--controller` / `--soft-delete` |
|
|
171
171
|
| `birc make:mapper` | Mapper 和 CreateRequest / Response |
|
|
172
172
|
| `birc make:service` | `XxxService extends BaseService`,CRUD 在 `BaseServiceImpl` |
|
|
173
173
|
| `birc make:controller` | Controller |
|
|
174
174
|
| `birc make:exception` | 繼承 `ProjectException` 的例外類 |
|
|
175
|
-
| `birc make:migration` | Flyway SQL(`
|
|
175
|
+
| `birc make:migration` | Flyway SQL(`create_book_table` 會讀 Book 欄位;`add_votes_to_users_table` 仍是檔名) |
|
|
176
|
+
| `birc make:seeder` | Java `XxxSeeder`(讀 Entity 或 `--fields`) |
|
|
176
177
|
| `birc docs` | 補 `AGENTS.md` / `PROJECT.md` / `implement.md` / `test.md`(專案名從 `.bircrc.json` 讀) |
|
|
177
178
|
| `birc migrate` | 在專案裡跑 Gradle wrapper 的 `flywayMigrate`(Unix `./gradlew`,Windows `gradlew.bat`) |
|
|
178
179
|
| `birc migrate:rollback` | Flyway Community 做不到 rollback,指令會說明替代作法 |
|
|
179
180
|
| `birc migrate:reset` | `flywayClean` 後再 `flywayMigrate`(`--force` 略過確認) |
|
|
181
|
+
| `birc seed` | 跑 Java Seeder(`bootRun --birc.seed`,跑完結束) |
|
|
182
|
+
| `birc db:wipe` | 清空資料庫(`flywayClean`)。沒加 `--force` 先問一次 |
|
|
180
183
|
|
|
181
184
|
```bash
|
|
182
185
|
birc create Practice --yes
|
|
183
186
|
birc make:entity User --dto
|
|
184
187
|
birc make:entity User --mapper
|
|
185
188
|
birc make:entity User --example
|
|
186
|
-
birc make:entity User --
|
|
189
|
+
birc make:entity User --fields title:String,startTime:LocalDateTime
|
|
190
|
+
birc make Book --fields title:String,author:String,isbn:String,price:BigDecimal,publishedAt:LocalDate --migration --seed
|
|
191
|
+
birc migrate
|
|
192
|
+
birc seed
|
|
193
|
+
birc db:wipe
|
|
187
194
|
birc make:entity User --force
|
|
188
195
|
birc make:model Flight --migration
|
|
189
196
|
birc make:model Flight --seed
|
|
190
197
|
birc make:model Flight --controller
|
|
191
198
|
birc make:model Flight --soft-delete
|
|
192
199
|
birc make:model Flight --migration --soft-delete
|
|
193
|
-
birc make:
|
|
200
|
+
birc make:entity Book --fields title:String,author:String,isbn:String,price:BigDecimal,publishedAt:LocalDate
|
|
201
|
+
birc make:migration create_book_table
|
|
202
|
+
birc make:seeder BookSeeder
|
|
194
203
|
birc add
|
|
195
204
|
birc add email
|
|
196
205
|
birc add email sentry
|
|
@@ -202,7 +211,7 @@ birc migrate
|
|
|
202
211
|
|
|
203
212
|
指令是 `make:entity`,中間一條冒號,跟 Laravel artisan 一樣。
|
|
204
213
|
`birc add` 沒帶名稱時會問你要加哪些模組;也可以寫 `birc add docker` 或 `birc add file-upload`。
|
|
205
|
-
`make:migration add_votes_to_users_table` 裡的 `add_votes_...`
|
|
214
|
+
`make:migration create_book_table` 會讀已有的 Book Entity 把欄位寫進 SQL。`birc make Book --fields title:String --migration --seed` 同一指令生骨架、CREATE TABLE 與 `BookSeeder`,再 `birc migrate`、`birc seed`。只補 Seeder 用 `birc make:seeder BookSeeder`。清空資料庫用 `birc db:wipe`。`add_votes_to_users_table` 裡的 `add_votes_...` 仍只是檔名。
|
|
206
215
|
|
|
207
216
|
各指令的細節見 wiki:[make](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/基礎/make)、
|
|
208
217
|
[add](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/基礎/add)、
|
|
@@ -261,7 +270,7 @@ Mapper 用 MapStruct。Controller 只轉 DTO,業務寫在 Service。
|
|
|
261
270
|
SSH 部署骨架,帳密仍放 GitLab CI/CD Variables,不要寫進 repo。
|
|
262
271
|
|
|
263
272
|
Flyway Community 沒有 Laravel 的 rollback。`birc migrate:rollback` 只會
|
|
264
|
-
說明替代作法:再寫一筆往前的遷移。`birc migrate` 會真的跑 Gradle wrapper 的 `flywayMigrate
|
|
273
|
+
說明替代作法:再寫一筆往前的遷移。`birc migrate` 會真的跑 Gradle wrapper 的 `flywayMigrate`,不是只印指令。示範資料用 `birc seed`;清空資料庫用 `birc db:wipe`。
|
|
265
274
|
|
|
266
275
|
## 改這個產生器
|
|
267
276
|
|
package/bin/birc.js
CHANGED
|
@@ -25,16 +25,17 @@ const HELP_SECTIONS = [
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
title: '骨架',
|
|
28
|
-
note: '寫法是 make:種類 [名稱]。預設只生引入與初稿;加 --example 才有完整範例',
|
|
28
|
+
note: '寫法是 make:種類 [名稱]。預設只生引入與初稿;加 --example 或 --fields 才有完整範例',
|
|
29
29
|
commands: [
|
|
30
|
-
['make', '一次生 Entity + DAO + Mapper + DTO + Service + Controller'],
|
|
30
|
+
['make', '一次生 Entity + DAO + Mapper + DTO + Service + Controller;--migration / --seed'],
|
|
31
31
|
['make:entity', 'Entity、XxxDAO(--dto / --mapper / --soft-delete)'],
|
|
32
32
|
['make:model', '同 make:entity;--migration / --seed / --controller / --soft-delete'],
|
|
33
33
|
['make:mapper', 'Mapper、CreateRequest、Response'],
|
|
34
34
|
['make:service', 'XxxService extends BaseService'],
|
|
35
35
|
['make:controller', 'Controller'],
|
|
36
36
|
['make:exception', '繼承 ProjectException 的例外'],
|
|
37
|
-
['make:migration', 'Flyway SQL
|
|
37
|
+
['make:migration', 'Flyway SQL(create_book_table 會讀 Entity 欄位)'],
|
|
38
|
+
['make:seeder', 'Java Seeder(讀 Entity 欄位;birc seed 才執行)'],
|
|
38
39
|
]
|
|
39
40
|
},
|
|
40
41
|
{
|
|
@@ -42,7 +43,9 @@ const HELP_SECTIONS = [
|
|
|
42
43
|
commands: [
|
|
43
44
|
['migrate', '套用待執行的遷移(flywayMigrate)'],
|
|
44
45
|
['migrate:reset', '清 schema 再重跑(flywayClean + migrate)'],
|
|
45
|
-
['migrate:rollback', 'Community 做不到 rollback,會印替代作法']
|
|
46
|
+
['migrate:rollback', 'Community 做不到 rollback,會印替代作法'],
|
|
47
|
+
['seed', '跑 Java Seeder(bootRun --birc.seed,跑完就結束)'],
|
|
48
|
+
['db:wipe', '清空資料庫(flywayClean)。沒加 --force 先問一次']
|
|
46
49
|
]
|
|
47
50
|
},
|
|
48
51
|
{
|
|
@@ -53,7 +56,8 @@ const HELP_SECTIONS = [
|
|
|
53
56
|
}
|
|
54
57
|
];
|
|
55
58
|
|
|
56
|
-
const FLYWAY_COMMANDS = new Set(['migrate', 'migrate:rollback', 'migrate:reset']);
|
|
59
|
+
const FLYWAY_COMMANDS = new Set(['migrate', 'migrate:rollback', 'migrate:reset', 'db:wipe']);
|
|
60
|
+
const SEED_COMMANDS = new Set(['seed']);
|
|
57
61
|
|
|
58
62
|
function allCommandNames() {
|
|
59
63
|
return HELP_SECTIONS.flatMap((section) => section.commands.map(([name]) => name));
|
|
@@ -170,12 +174,12 @@ function printHelp() {
|
|
|
170
174
|
` ${'--example'.padEnd(nameWidth + 2)}make:* 產出完整範例`,
|
|
171
175
|
` ${'--dto'.padEnd(nameWidth + 2)}make:entity 順便生 Mapper 與 DTO`,
|
|
172
176
|
` ${'--mapper'.padEnd(nameWidth + 2)}同 --dto`,
|
|
173
|
-
` ${'--migration'.padEnd(nameWidth + 2)}make:model 順便生 create_{table}_table`,
|
|
174
|
-
` ${'--seed'.padEnd(nameWidth + 2)}make:model 順便生
|
|
177
|
+
` ${'--migration'.padEnd(nameWidth + 2)}make / make:model 順便生 create_{table}_table`,
|
|
178
|
+
` ${'--seed'.padEnd(nameWidth + 2)}make / make:model 順便生 Java Seeder`,
|
|
175
179
|
` ${'--controller'.padEnd(nameWidth + 2)}make:model 順便生 Controller`,
|
|
176
180
|
` ${'--soft-delete'.padEnd(nameWidth + 2)}make:entity / make:model 加 deleted_at 軟刪`,
|
|
177
|
-
` ${'--fields'.padEnd(nameWidth + 2)}make
|
|
178
|
-
` ${'--force'.padEnd(nameWidth + 2)}migrate:reset 略過確認;add 覆寫已選的已安裝 feature;make 覆寫已有 Java`,
|
|
181
|
+
` ${'--fields'.padEnd(nameWidth + 2)}make 的欄位,例如 title:String,startTime:LocalDateTime(有帶就當完整產出)`,
|
|
182
|
+
` ${'--force'.padEnd(nameWidth + 2)}migrate:reset / db:wipe 略過確認;add 覆寫已選的已安裝 feature;make 覆寫已有 Java`,
|
|
179
183
|
` ${'--yes'.padEnd(nameWidth + 2)}create 其餘用預設、不要問`,
|
|
180
184
|
'',
|
|
181
185
|
'例子',
|
|
@@ -193,15 +197,19 @@ function printHelp() {
|
|
|
193
197
|
' birc make:entity User --dto',
|
|
194
198
|
' birc make:entity User --mapper',
|
|
195
199
|
' birc make:entity User --example',
|
|
196
|
-
' birc make:entity User --
|
|
200
|
+
' birc make:entity User --fields title:String,startTime:LocalDateTime',
|
|
201
|
+
' birc make Book --fields title:String,author:String --migration --seed',
|
|
197
202
|
' birc make:entity User --force',
|
|
198
203
|
' birc make:model Flight --migration',
|
|
199
204
|
' birc make:model Flight --seed',
|
|
200
205
|
' birc make:model Flight --controller',
|
|
201
206
|
' birc make:model Flight --soft-delete',
|
|
202
|
-
' birc make:
|
|
207
|
+
' birc make:seeder BookSeeder',
|
|
208
|
+
' birc make:migration create_book_table',
|
|
203
209
|
' birc make:migration add_votes_to_users_table',
|
|
204
210
|
' birc migrate',
|
|
211
|
+
' birc seed',
|
|
212
|
+
' birc db:wipe',
|
|
205
213
|
'',
|
|
206
214
|
'create / update 以外的指令會從目前目錄往上找 .bircrc.json。',
|
|
207
215
|
''
|
|
@@ -236,7 +244,8 @@ async function runGenerator(name, bypassArr, flags) {
|
|
|
236
244
|
process.exit(1);
|
|
237
245
|
}
|
|
238
246
|
|
|
239
|
-
|
|
247
|
+
const hasFields = Boolean(String(flags.fields || '').trim());
|
|
248
|
+
if (flags.example || hasFields) {
|
|
240
249
|
process.env.BIRC_EXAMPLE = '1';
|
|
241
250
|
}
|
|
242
251
|
if (flags.dto) {
|
|
@@ -293,12 +302,8 @@ async function runGenerator(name, bypassArr, flags) {
|
|
|
293
302
|
}
|
|
294
303
|
if (flags.fields) {
|
|
295
304
|
process.env.BIRC_MAKE_FIELDS = flags.fields;
|
|
296
|
-
const { applyMakeFieldsFlag
|
|
305
|
+
const { applyMakeFieldsFlag } = require('./cli-util');
|
|
297
306
|
applyMakeFieldsFlag(generator.prompts, flags.fields);
|
|
298
|
-
if (name === 'make' || String(name).startsWith('make:')) {
|
|
299
|
-
const warn = fieldsWithoutExampleMessage(flags);
|
|
300
|
-
if (warn) process.stderr.write(`${warn}\n`);
|
|
301
|
-
}
|
|
302
307
|
}
|
|
303
308
|
const answers = await generator.runPrompts(promptBypass);
|
|
304
309
|
if (createYes) {
|
|
@@ -310,7 +315,7 @@ async function runGenerator(name, bypassArr, flags) {
|
|
|
310
315
|
if (flags.fields) {
|
|
311
316
|
answers.fields = flags.fields;
|
|
312
317
|
}
|
|
313
|
-
answers.example = Boolean(answers.example) || flags.example;
|
|
318
|
+
answers.example = Boolean(answers.example) || flags.example || hasFields;
|
|
314
319
|
answers.dto = Boolean(answers.dto) || flags.dto;
|
|
315
320
|
answers.mapper = Boolean(answers.mapper) || flags.mapper;
|
|
316
321
|
answers.migration = Boolean(answers.migration) || flags.migration;
|
|
@@ -391,11 +396,16 @@ function printCreateNextSteps(answers) {
|
|
|
391
396
|
'',
|
|
392
397
|
'生 CRUD 骨架(會從目前目錄往上找 .bircrc.json):',
|
|
393
398
|
' birc make # Entity + DAO + Mapper + DTO + Service + Controller',
|
|
399
|
+
' birc make Book --fields title:String --migration --seed',
|
|
394
400
|
' birc make:entity # Entity / DAO',
|
|
395
401
|
' birc make:entity User --dto',
|
|
396
402
|
' birc make:model Flight --migration --seed --controller',
|
|
403
|
+
' birc make:seeder BookSeeder',
|
|
397
404
|
' birc make:entity User --example',
|
|
398
|
-
' birc make:migration
|
|
405
|
+
' birc make:migration create_book_table',
|
|
406
|
+
' birc migrate',
|
|
407
|
+
' birc seed',
|
|
408
|
+
' birc db:wipe',
|
|
399
409
|
' birc add # 之後再補 docker / email 等功能',
|
|
400
410
|
'',
|
|
401
411
|
'打包:',
|
|
@@ -503,9 +513,14 @@ async function runFlyway(command, flags) {
|
|
|
503
513
|
process.exit(1);
|
|
504
514
|
}
|
|
505
515
|
|
|
506
|
-
if (command === 'migrate:reset') {
|
|
516
|
+
if (command === 'migrate:reset' || command === 'db:wipe') {
|
|
517
|
+
const wiping = command === 'db:wipe';
|
|
507
518
|
if (!flags.force) {
|
|
508
|
-
const ok = await confirmYes(
|
|
519
|
+
const ok = await confirmYes(
|
|
520
|
+
wiping
|
|
521
|
+
? '這會清掉 schema 與資料。確定? [y/N] '
|
|
522
|
+
: '這會清掉 schema 再重跑全部遷移。確定? [y/N] '
|
|
523
|
+
);
|
|
509
524
|
if (!ok) {
|
|
510
525
|
process.stdout.write('已取消。\n');
|
|
511
526
|
return;
|
|
@@ -515,6 +530,9 @@ async function runFlyway(command, flags) {
|
|
|
515
530
|
if (cleanStatus !== 0) {
|
|
516
531
|
process.exit(cleanStatus);
|
|
517
532
|
}
|
|
533
|
+
if (wiping) {
|
|
534
|
+
process.exit(0);
|
|
535
|
+
}
|
|
518
536
|
process.exit(runGradle(['flywayMigrate']));
|
|
519
537
|
}
|
|
520
538
|
|
|
@@ -635,6 +653,12 @@ async function main() {
|
|
|
635
653
|
await runFlyway(command, flags);
|
|
636
654
|
return;
|
|
637
655
|
}
|
|
656
|
+
if (SEED_COMMANDS.has(command)) {
|
|
657
|
+
process.exit(runGradle([
|
|
658
|
+
'bootRun',
|
|
659
|
+
'--args=--spring.main.web-application-type=none --birc.seed'
|
|
660
|
+
]));
|
|
661
|
+
}
|
|
638
662
|
|
|
639
663
|
await runGenerator(command, flags.positionals.slice(1), flags);
|
|
640
664
|
}
|
package/bin/cli-util.js
CHANGED
|
@@ -56,10 +56,11 @@ function projectConfigMissingMessage() {
|
|
|
56
56
|
return '找不到 .bircrc.json,先在專案根目錄建立設定檔(可複製 .bircrc.example.json 改名,或用 birc create 建新專案)';
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function findProjectRoot(startDir, exists = fs.existsSync) {
|
|
59
|
+
function findProjectRoot(startDir, exists = fs.existsSync, maxDepth = 10) {
|
|
60
60
|
let current = path.resolve(startDir || '');
|
|
61
61
|
const { root } = path.parse(current);
|
|
62
|
-
|
|
62
|
+
let depth = 0;
|
|
63
|
+
while (depth < maxDepth) {
|
|
63
64
|
if (exists(path.join(current, '.bircrc.json'))) {
|
|
64
65
|
return current;
|
|
65
66
|
}
|
|
@@ -71,7 +72,9 @@ function findProjectRoot(startDir, exists = fs.existsSync) {
|
|
|
71
72
|
return null;
|
|
72
73
|
}
|
|
73
74
|
current = parent;
|
|
75
|
+
depth += 1;
|
|
74
76
|
}
|
|
77
|
+
return null;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
function toKebabName(value) {
|
|
@@ -208,14 +211,6 @@ function alreadyExistsMessage(fail = {}) {
|
|
|
208
211
|
return `${fileName} 已在,要覆寫用 --force`;
|
|
209
212
|
}
|
|
210
213
|
|
|
211
|
-
function fieldsWithoutExampleMessage(flags = {}) {
|
|
212
|
-
const hasFields = Boolean(String(flags.fields || '').trim());
|
|
213
|
-
if (!hasFields || flags.example) {
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
return '--fields 要配 --example 才會寫進 Entity';
|
|
217
|
-
}
|
|
218
|
-
|
|
219
214
|
function levenshtein(left, right) {
|
|
220
215
|
const a = String(left || '');
|
|
221
216
|
const b = String(right || '');
|
|
@@ -291,10 +286,18 @@ function formatMakeNextSteps(name, answers = {}) {
|
|
|
291
286
|
const command = String(name || '');
|
|
292
287
|
const wantsMigrate = command === 'make:migration'
|
|
293
288
|
|| command === 'migration'
|
|
294
|
-
|| Boolean(answers.migration)
|
|
295
|
-
|
|
289
|
+
|| Boolean(answers.migration);
|
|
290
|
+
const wantsSeed = command === 'make:seeder' || Boolean(answers.seed);
|
|
291
|
+
const lines = [''];
|
|
296
292
|
if (wantsMigrate) {
|
|
297
|
-
|
|
293
|
+
lines.push('套用遷移:', ' birc migrate');
|
|
294
|
+
}
|
|
295
|
+
if (wantsSeed) {
|
|
296
|
+
lines.push('寫入示範資料:', ' birc seed');
|
|
297
|
+
}
|
|
298
|
+
if (wantsMigrate || wantsSeed) {
|
|
299
|
+
lines.push('');
|
|
300
|
+
return lines;
|
|
298
301
|
}
|
|
299
302
|
if (command === 'make' || command.startsWith('make:')) {
|
|
300
303
|
return ['', '骨架已寫好。建表可加 --migration,再 birc migrate。', ''];
|
|
@@ -416,7 +419,6 @@ module.exports = {
|
|
|
416
419
|
applyCreatePromptDefaults,
|
|
417
420
|
applyCreateYes,
|
|
418
421
|
alreadyExistsMessage,
|
|
419
|
-
fieldsWithoutExampleMessage,
|
|
420
422
|
suggestCommand,
|
|
421
423
|
shouldPrintBanner,
|
|
422
424
|
formatAddNextSteps,
|
package/bin/home-install.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const https = require('https');
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const { spawnSync } = require('child_process');
|
|
6
7
|
|
|
7
|
-
const DEFAULT_BASE_URL = 'https://gitlab.ntubimdbirc.tw/
|
|
8
|
+
const DEFAULT_BASE_URL = 'https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator';
|
|
8
9
|
|
|
9
10
|
function npmCmd(platform = process.platform) {
|
|
10
11
|
return platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
@@ -84,7 +85,8 @@ function installCommand(jsPath, {
|
|
|
84
85
|
fs.mkdirSync(dir, { recursive: true });
|
|
85
86
|
|
|
86
87
|
if (platform === 'win32') {
|
|
87
|
-
|
|
88
|
+
const cmdPath = resolved.replace(/\//g, '\\');
|
|
89
|
+
fs.writeFileSync(path.join(dir, 'birc.cmd'), `@echo off\r\nnode "${cmdPath}" %*\r\n`);
|
|
88
90
|
fs.writeFileSync(
|
|
89
91
|
path.join(dir, 'birc.ps1'),
|
|
90
92
|
`& node "${resolved.replace(/\\/g, '/')}" @args\n`
|
|
@@ -241,7 +243,7 @@ async function confirmCustomBase(baseUrl) {
|
|
|
241
243
|
}
|
|
242
244
|
|
|
243
245
|
function npmInstall(cwd, platform = process.platform) {
|
|
244
|
-
spawnOrThrow(npmCmd(platform), ['install'], {
|
|
246
|
+
spawnOrThrow(npmCmd(platform), ['install', '--ignore-scripts'], {
|
|
245
247
|
cwd,
|
|
246
248
|
stdio: 'inherit',
|
|
247
249
|
shell: platform === 'win32',
|
package/bin/update-check.js
CHANGED
|
@@ -7,7 +7,7 @@ const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
|
7
7
|
|
|
8
8
|
const NPM_LATEST = `https://registry.npmjs.org/${pkg.name}/latest`;
|
|
9
9
|
const GITLAB_PACKAGE =
|
|
10
|
-
'https://gitlab.ntubimdbirc.tw/
|
|
10
|
+
'https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/raw/main/package.json';
|
|
11
11
|
const SNOOZE_FILE = path.join(os.homedir(), '.birc-update-check.json');
|
|
12
12
|
const FETCH_TIMEOUT_MS = 2500;
|
|
13
13
|
const MAX_REDIRECTS = 5;
|
package/install.ps1
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# 用法:
|
|
2
|
-
# irm https://gitlab.ntubimdbirc.tw/
|
|
2
|
+
# irm https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/raw/main/install.ps1 | iex
|
|
3
3
|
# 或下載後執行: powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1
|
|
4
4
|
# 需要 Node.js 18+ 與 npm。程式會放到 $HOME\.birc-generator,跟 curl | bash 同一處。
|
|
5
5
|
|
|
@@ -19,7 +19,7 @@ function Write-Err([string]$Message) {
|
|
|
19
19
|
exit 1
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
$BaseUrl = if ($env:BIRC_BASE_URL) { $env:BIRC_BASE_URL.TrimEnd('/') } else { 'https://gitlab.ntubimdbirc.tw/
|
|
22
|
+
$BaseUrl = if ($env:BIRC_BASE_URL) { $env:BIRC_BASE_URL.TrimEnd('/') } else { 'https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator' }
|
|
23
23
|
$InstallDir = Join-Path $HOME '.birc-generator'
|
|
24
24
|
|
|
25
25
|
function Test-NodeAndNpm {
|
package/install.sh
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
set -e
|
|
3
3
|
|
|
4
|
-
# 用法: curl -fsSL https://gitlab.ntubimdbirc.tw/
|
|
4
|
+
# 用法: curl -fsSL https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/raw/main/install.sh | bash
|
|
5
5
|
# 或下載後執行: bash install.sh
|
|
6
6
|
|
|
7
7
|
RED='\033[0;31m'
|
|
@@ -14,7 +14,7 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
|
14
14
|
error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
|
|
15
15
|
|
|
16
16
|
INSTALL_DIR="$HOME/.birc-generator"
|
|
17
|
-
DEFAULT_BASE_URL="https://gitlab.ntubimdbirc.tw/
|
|
17
|
+
DEFAULT_BASE_URL="https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator"
|
|
18
18
|
BASE_URL="${BIRC_BASE_URL:-$DEFAULT_BASE_URL}"
|
|
19
19
|
BASE_URL="${BASE_URL%/}"
|
|
20
20
|
|
|
@@ -161,7 +161,14 @@ download_project() {
|
|
|
161
161
|
tmp_tar="$(mktemp)"
|
|
162
162
|
mkdir -p "$INSTALL_DIR"
|
|
163
163
|
curl -fsSL "$TAR_URL" -o "$tmp_tar"
|
|
164
|
+
|
|
165
|
+
info "驗證下載檔完整性..."
|
|
166
|
+
if ! file "$tmp_tar" | grep -qi gzip; then
|
|
167
|
+
rm -f "$tmp_tar"
|
|
168
|
+
error "下載的檔案不是有效的 gzip 壓縮檔。可能來源不正確或已被篡改。已取消安裝。"
|
|
169
|
+
fi
|
|
164
170
|
validate_tar_safety "$tmp_tar"
|
|
171
|
+
|
|
165
172
|
tar xzf "$tmp_tar" -C "$INSTALL_DIR" --strip-components=1
|
|
166
173
|
rm -f "$tmp_tar"
|
|
167
174
|
cd "$INSTALL_DIR"
|
|
@@ -170,7 +177,7 @@ download_project() {
|
|
|
170
177
|
|
|
171
178
|
install_dependencies() {
|
|
172
179
|
info "安裝 Node.js 依賴..."
|
|
173
|
-
npm install
|
|
180
|
+
npm install --ignore-scripts
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
create_symlink() {
|
package/package.json
CHANGED
package/plopfile.js
CHANGED
|
@@ -4,10 +4,10 @@ const { randomInt } = require('node:crypto');
|
|
|
4
4
|
const VERSIONS = require('./versions');
|
|
5
5
|
const { deriveBasePackage, projectConfigMissingMessage, toPascalCaseName, createDestConflict } = require('./bin/cli-util');
|
|
6
6
|
|
|
7
|
-
// .env 的 DB 密碼用:
|
|
7
|
+
// .env 的 DB 密碼用:16 碼混合大小寫英文 + 數字,隨機生成,避免 change_me 這種預設值。
|
|
8
8
|
function randomDbPassword() {
|
|
9
|
-
const
|
|
10
|
-
return Array.from({ length:
|
|
9
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
10
|
+
return Array.from({ length: 16 }, () => chars[randomInt(chars.length)]).join('');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function projectRoot() {
|
|
@@ -156,6 +156,8 @@ function validateMigrationName(value) {
|
|
|
156
156
|
: '遷移名稱只能包含英文字母、數字和底線,例如 create_users_table。';
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
const DEFAULT_STRING_COLUMN_LENGTH = 50;
|
|
160
|
+
|
|
159
161
|
function parseFields(fields) {
|
|
160
162
|
if (!fields || !String(fields).trim()) {
|
|
161
163
|
return [];
|
|
@@ -165,20 +167,24 @@ function parseFields(fields) {
|
|
|
165
167
|
.map((f) => {
|
|
166
168
|
const [name, type] = f.split(':');
|
|
167
169
|
const fieldName = (name || '').trim();
|
|
170
|
+
const fieldType = (type || 'String').trim();
|
|
168
171
|
return {
|
|
169
172
|
name: fieldName,
|
|
170
|
-
type:
|
|
173
|
+
type: fieldType,
|
|
171
174
|
snakeName: fieldName
|
|
172
175
|
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
173
176
|
.replace(/[\s-]+/g, '_')
|
|
174
|
-
.toLowerCase()
|
|
177
|
+
.toLowerCase(),
|
|
178
|
+
columnLength: fieldType === 'String' ? DEFAULT_STRING_COLUMN_LENGTH : null
|
|
175
179
|
};
|
|
176
180
|
})
|
|
177
181
|
.filter((f) => f.name);
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
function isExample(data) {
|
|
181
|
-
return Boolean(data && data.example)
|
|
185
|
+
return Boolean(data && data.example)
|
|
186
|
+
|| process.env.BIRC_EXAMPLE === '1'
|
|
187
|
+
|| parseFields((data && data.fields) || process.env.BIRC_MAKE_FIELDS).length > 0;
|
|
182
188
|
}
|
|
183
189
|
|
|
184
190
|
function includeMapperWithEntity(data) {
|
|
@@ -214,11 +220,266 @@ function tableNameFromEntity(entityName) {
|
|
|
214
220
|
return toSnakeCase(entityName);
|
|
215
221
|
}
|
|
216
222
|
|
|
223
|
+
const JAVA_SQL_TYPES = Object.freeze({
|
|
224
|
+
String: `VARCHAR(${DEFAULT_STRING_COLUMN_LENGTH}) NULL`,
|
|
225
|
+
Integer: 'INT NULL',
|
|
226
|
+
int: 'INT NULL',
|
|
227
|
+
Long: 'BIGINT NULL',
|
|
228
|
+
long: 'BIGINT NULL',
|
|
229
|
+
Boolean: 'TINYINT(1) NULL',
|
|
230
|
+
boolean: 'TINYINT(1) NULL',
|
|
231
|
+
BigDecimal: 'DECIMAL(19, 2) NULL',
|
|
232
|
+
LocalDate: 'DATE NULL',
|
|
233
|
+
LocalDateTime: 'TIMESTAMP NULL',
|
|
234
|
+
Instant: 'TIMESTAMP NULL',
|
|
235
|
+
Double: 'DOUBLE NULL',
|
|
236
|
+
double: 'DOUBLE NULL',
|
|
237
|
+
Float: 'FLOAT NULL',
|
|
238
|
+
float: 'FLOAT NULL',
|
|
239
|
+
Short: 'SMALLINT NULL',
|
|
240
|
+
short: 'SMALLINT NULL',
|
|
241
|
+
Byte: 'TINYINT NULL',
|
|
242
|
+
byte: 'TINYINT NULL'
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
function sqlTypeForColumn(javaType, columnName) {
|
|
246
|
+
if (columnName === 'created_at') {
|
|
247
|
+
return 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP';
|
|
248
|
+
}
|
|
249
|
+
if (columnName === 'updated_at') {
|
|
250
|
+
return 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP';
|
|
251
|
+
}
|
|
252
|
+
if (columnName === 'deleted_at') {
|
|
253
|
+
return 'TIMESTAMP NULL';
|
|
254
|
+
}
|
|
255
|
+
return JAVA_SQL_TYPES[javaType] || null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function fieldsToMigrationColumns(fields, options) {
|
|
259
|
+
const withTimestamps = Boolean(options && options.withTimestamps);
|
|
260
|
+
const withSoftDelete = Boolean(options && options.withSoftDelete);
|
|
261
|
+
const columns = [];
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
|
|
264
|
+
function addColumn(name, javaType) {
|
|
265
|
+
const columnName = sanitizeMigrationToken(name);
|
|
266
|
+
if (!columnName || columnName === 'id' || seen.has(columnName)) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const sqlType = sqlTypeForColumn(javaType, columnName);
|
|
270
|
+
if (!sqlType) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
seen.add(columnName);
|
|
274
|
+
columns.push({ name: columnName, sqlType });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const field of fields || []) {
|
|
278
|
+
addColumn(field.snakeName || toSnakeCase(field.name), field.type);
|
|
279
|
+
}
|
|
280
|
+
if (withTimestamps) {
|
|
281
|
+
addColumn('created_at', 'LocalDateTime');
|
|
282
|
+
addColumn('updated_at', 'LocalDateTime');
|
|
283
|
+
}
|
|
284
|
+
if (withSoftDelete) {
|
|
285
|
+
addColumn('deleted_at', 'LocalDateTime');
|
|
286
|
+
}
|
|
287
|
+
return columns;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function listJavaFiles(dir, acc = []) {
|
|
291
|
+
if (!fs.existsSync(dir)) {
|
|
292
|
+
return acc;
|
|
293
|
+
}
|
|
294
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
295
|
+
const full = path.join(dir, entry.name);
|
|
296
|
+
if (entry.isDirectory()) {
|
|
297
|
+
listJavaFiles(full, acc);
|
|
298
|
+
} else if (entry.isFile() && entry.name.endsWith('.java')) {
|
|
299
|
+
acc.push(full);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return acc;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function parseEntityColumns(source) {
|
|
306
|
+
const fields = [];
|
|
307
|
+
const fieldRe =
|
|
308
|
+
/((?:@\w+(?:\s*\([^)]*\))?\s*)*)private\s+([A-Za-z][A-Za-z0-9_]*)\s+([A-Za-z_][A-Za-z0-9_]*)\s*;/g;
|
|
309
|
+
let match = fieldRe.exec(source);
|
|
310
|
+
while (match) {
|
|
311
|
+
const annotations = match[1] || '';
|
|
312
|
+
const type = match[2];
|
|
313
|
+
const name = match[3];
|
|
314
|
+
const columnAttr = annotations.match(/@Column\s*\(([^)]*)\)/);
|
|
315
|
+
let snakeName = toSnakeCase(name);
|
|
316
|
+
if (columnAttr) {
|
|
317
|
+
const named = columnAttr[1].match(/name\s*=\s*"([^"]+)"/);
|
|
318
|
+
if (named) {
|
|
319
|
+
snakeName = named[1];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
fields.push({ name, type, snakeName });
|
|
323
|
+
match = fieldRe.exec(source);
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
fields,
|
|
327
|
+
withSoftDelete: /@SoftDelete\b/.test(source)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function findEntitySourceForTable(tableName, config) {
|
|
332
|
+
if (!tableName) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
const persistence = resolvePersistence(config);
|
|
336
|
+
const entityDir = path.join(projectRoot(), persistence.entityRoot);
|
|
337
|
+
const files = listJavaFiles(entityDir);
|
|
338
|
+
const escaped = String(tableName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
339
|
+
const tableRe = new RegExp(`@Table\\s*\\([^)]*name\\s*=\\s*"${escaped}"`, 'i');
|
|
340
|
+
for (const file of files) {
|
|
341
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
342
|
+
if (tableRe.test(source)) {
|
|
343
|
+
return source;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const className = toPascalCaseName(String(tableName).toLowerCase());
|
|
347
|
+
if (!className) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const match = files.find((file) => path.basename(file, '.java') === className);
|
|
351
|
+
if (!match) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
return fs.readFileSync(match, 'utf8');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function javaLiteralForSeed(javaType) {
|
|
358
|
+
if (javaType === 'String') {
|
|
359
|
+
return '"example"';
|
|
360
|
+
}
|
|
361
|
+
if (javaType === 'LocalDate') {
|
|
362
|
+
return 'LocalDate.parse("2020-01-01")';
|
|
363
|
+
}
|
|
364
|
+
if (javaType === 'LocalDateTime') {
|
|
365
|
+
return 'LocalDateTime.parse("2020-01-01T00:00:00")';
|
|
366
|
+
}
|
|
367
|
+
if (javaType === 'Instant') {
|
|
368
|
+
return 'Instant.parse("2020-01-01T00:00:00Z")';
|
|
369
|
+
}
|
|
370
|
+
if (javaType === 'BigDecimal') {
|
|
371
|
+
return 'new BigDecimal("0")';
|
|
372
|
+
}
|
|
373
|
+
if (javaType === 'Boolean' || javaType === 'boolean') {
|
|
374
|
+
return 'false';
|
|
375
|
+
}
|
|
376
|
+
if (javaType === 'Long' || javaType === 'long') {
|
|
377
|
+
return '0L';
|
|
378
|
+
}
|
|
379
|
+
if (javaType === 'Integer' || javaType === 'int') {
|
|
380
|
+
return '0';
|
|
381
|
+
}
|
|
382
|
+
if (javaType === 'Short' || javaType === 'short') {
|
|
383
|
+
return '(short) 0';
|
|
384
|
+
}
|
|
385
|
+
if (javaType === 'Byte' || javaType === 'byte') {
|
|
386
|
+
return '(byte) 0';
|
|
387
|
+
}
|
|
388
|
+
if (javaType === 'Double' || javaType === 'double') {
|
|
389
|
+
return '0D';
|
|
390
|
+
}
|
|
391
|
+
if (javaType === 'Float' || javaType === 'float') {
|
|
392
|
+
return '0F';
|
|
393
|
+
}
|
|
394
|
+
if (JAVA_SQL_TYPES[javaType]) {
|
|
395
|
+
return '0';
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const SKIP_SEED_FIELDS = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt', 'created_at', 'updated_at', 'deleted_at']);
|
|
401
|
+
|
|
402
|
+
function fieldsToSeedFields(fields) {
|
|
403
|
+
const result = [];
|
|
404
|
+
const seen = new Set();
|
|
405
|
+
|
|
406
|
+
for (const field of fields || []) {
|
|
407
|
+
const name = String(field.name || '').trim();
|
|
408
|
+
const snakeName = field.snakeName || toSnakeCase(name);
|
|
409
|
+
if (!name || SKIP_SEED_FIELDS.has(name) || SKIP_SEED_FIELDS.has(snakeName) || seen.has(name)) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const literal = javaLiteralForSeed(field.type);
|
|
413
|
+
if (!literal) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
seen.add(name);
|
|
417
|
+
result.push({
|
|
418
|
+
name,
|
|
419
|
+
type: field.type,
|
|
420
|
+
setter: `set${name.charAt(0).toUpperCase()}${name.slice(1)}`,
|
|
421
|
+
literal
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function resolveSeedFields(data, tableName, config) {
|
|
428
|
+
const explicitFields = parseFields(data.fields || process.env.BIRC_MAKE_FIELDS);
|
|
429
|
+
if (explicitFields.length > 0) {
|
|
430
|
+
return fieldsToSeedFields(explicitFields);
|
|
431
|
+
}
|
|
432
|
+
const source = findEntitySourceForTable(tableName, config);
|
|
433
|
+
if (source) {
|
|
434
|
+
return fieldsToSeedFields(parseEntityColumns(source).fields);
|
|
435
|
+
}
|
|
436
|
+
if (isExample(data)) {
|
|
437
|
+
return fieldsToSeedFields([{ name: 'name', type: 'String', snakeName: 'name' }]);
|
|
438
|
+
}
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function entityNameForSeeder(data) {
|
|
443
|
+
const raw = toPascalCaseName((data && data.entityName) || '');
|
|
444
|
+
return raw.replace(/Seeder$/, '') || raw;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function resolveCreateTableColumns(data, tableName, config) {
|
|
448
|
+
const example = isExample(data);
|
|
449
|
+
const flaggedSoftDelete = includeSoftDelete(data);
|
|
450
|
+
const explicitFields = parseFields(data.fields || process.env.BIRC_MAKE_FIELDS);
|
|
451
|
+
if (explicitFields.length > 0) {
|
|
452
|
+
return fieldsToMigrationColumns(explicitFields, {
|
|
453
|
+
withTimestamps: example,
|
|
454
|
+
withSoftDelete: flaggedSoftDelete
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const source = findEntitySourceForTable(tableName, config);
|
|
458
|
+
if (source) {
|
|
459
|
+
const entity = parseEntityColumns(source);
|
|
460
|
+
return fieldsToMigrationColumns(entity.fields, {
|
|
461
|
+
withTimestamps: false,
|
|
462
|
+
withSoftDelete: flaggedSoftDelete || entity.withSoftDelete
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (example) {
|
|
466
|
+
return fieldsToMigrationColumns(
|
|
467
|
+
[{ name: 'name', type: 'String', snakeName: 'name' }],
|
|
468
|
+
{ withTimestamps: true, withSoftDelete: flaggedSoftDelete }
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return fieldsToMigrationColumns([], {
|
|
472
|
+
withTimestamps: false,
|
|
473
|
+
withSoftDelete: flaggedSoftDelete
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
217
477
|
function fieldImports(fields) {
|
|
218
478
|
const types = new Set((fields || []).map((field) => field.type));
|
|
219
479
|
const imports = [];
|
|
220
480
|
if (types.has('LocalDateTime')) imports.push('java.time.LocalDateTime');
|
|
221
481
|
if (types.has('LocalDate')) imports.push('java.time.LocalDate');
|
|
482
|
+
if (types.has('Instant')) imports.push('java.time.Instant');
|
|
222
483
|
if (types.has('BigDecimal')) imports.push('java.math.BigDecimal');
|
|
223
484
|
return imports;
|
|
224
485
|
}
|
|
@@ -229,7 +490,12 @@ function resolveMakeFields(data) {
|
|
|
229
490
|
return fields;
|
|
230
491
|
}
|
|
231
492
|
if (isExample(data)) {
|
|
232
|
-
return [{
|
|
493
|
+
return [{
|
|
494
|
+
name: 'name',
|
|
495
|
+
type: 'String',
|
|
496
|
+
snakeName: 'name',
|
|
497
|
+
columnLength: DEFAULT_STRING_COLUMN_LENGTH
|
|
498
|
+
}];
|
|
233
499
|
}
|
|
234
500
|
return [];
|
|
235
501
|
}
|
|
@@ -351,6 +617,14 @@ function serviceSkeletonActions(root, templateData) {
|
|
|
351
617
|
];
|
|
352
618
|
}
|
|
353
619
|
|
|
620
|
+
function seederSkeletonActions(root, templateData, options) {
|
|
621
|
+
const skipIfExists = Boolean(options && options.skipIfExists);
|
|
622
|
+
return [
|
|
623
|
+
{ type: 'add', path: `${root}/seeder/Seeder.java`, templateFile: 'templates/seeder/Seeder.hbs', data: templateData, skipIfExists },
|
|
624
|
+
{ type: 'add', path: `${root}/seeder/SeedRunner.java`, templateFile: 'templates/seeder/SeedRunner.hbs', data: templateData, skipIfExists }
|
|
625
|
+
];
|
|
626
|
+
}
|
|
627
|
+
|
|
354
628
|
function uniqueFeatureKeys(features) {
|
|
355
629
|
return [...new Set((features || []).filter((featureKey) => FEATURES[featureKey]))];
|
|
356
630
|
}
|
|
@@ -874,11 +1148,12 @@ module.exports = function (plop) {
|
|
|
874
1148
|
&& !String((answers && answers.fields) || process.env.BIRC_MAKE_FIELDS || '').trim()
|
|
875
1149
|
};
|
|
876
1150
|
|
|
877
|
-
function makeEntityActions(data) {
|
|
1151
|
+
function makeEntityActions(data, options) {
|
|
878
1152
|
const { persistence, templateData } = makeContext(data);
|
|
1153
|
+
const skipIfExists = Boolean(options && options.skipIfExists);
|
|
879
1154
|
return [
|
|
880
|
-
{ type: 'add', path: `${persistence.entityRoot}/{{pascalCase entityName}}.java`, templateFile: 'templates/entity.hbs', data: templateData },
|
|
881
|
-
{ type: 'add', path: `${persistence.daoRoot}/{{pascalCase entityName}}DAO.java`, templateFile: 'templates/dao.hbs', data: templateData }
|
|
1155
|
+
{ type: 'add', path: `${persistence.entityRoot}/{{pascalCase entityName}}.java`, templateFile: 'templates/entity.hbs', data: templateData, skipIfExists },
|
|
1156
|
+
{ type: 'add', path: `${persistence.daoRoot}/{{pascalCase entityName}}DAO.java`, templateFile: 'templates/dao.hbs', data: templateData, skipIfExists }
|
|
882
1157
|
];
|
|
883
1158
|
}
|
|
884
1159
|
|
|
@@ -922,16 +1197,16 @@ module.exports = function (plop) {
|
|
|
922
1197
|
}
|
|
923
1198
|
|
|
924
1199
|
function makeStackActions(data) {
|
|
925
|
-
return [
|
|
1200
|
+
return appendMakeSqlActions([
|
|
926
1201
|
...makeEntityActions(data),
|
|
927
1202
|
...makeMapperActions(data),
|
|
928
1203
|
...makeServiceActions(data),
|
|
929
1204
|
...makeControllerActions(data)
|
|
930
|
-
];
|
|
1205
|
+
], data);
|
|
931
1206
|
}
|
|
932
1207
|
|
|
933
1208
|
plop.setGenerator('make', {
|
|
934
|
-
description: '一次產生 Entity + DAO + Mapper + DTO + Service + Controller',
|
|
1209
|
+
description: '一次產生 Entity + DAO + Mapper + DTO + Service + Controller;--migration / --seed',
|
|
935
1210
|
prompts: [entityNamePrompt, fieldsPrompt],
|
|
936
1211
|
actions: makeStackActions
|
|
937
1212
|
});
|
|
@@ -1175,6 +1450,7 @@ module.exports = function (plop) {
|
|
|
1175
1450
|
];
|
|
1176
1451
|
actions = actions.concat(exceptionSkeletonActions(root, templateData));
|
|
1177
1452
|
actions = actions.concat(serviceSkeletonActions(root, templateData));
|
|
1453
|
+
actions = actions.concat(seederSkeletonActions(root, templateData));
|
|
1178
1454
|
|
|
1179
1455
|
const configRoot = `${projectDir}/modules/${projectDir}-config/src/main/java/${basePackagePath}/config`;
|
|
1180
1456
|
actions.push(
|
|
@@ -1263,6 +1539,9 @@ module.exports = function (plop) {
|
|
|
1263
1539
|
const parsed = parseMigrationName(data.description);
|
|
1264
1540
|
const version = data.migrationVersion || nextMigrationVersion(path.join(projectRoot(), migrationPath));
|
|
1265
1541
|
const example = isExample(data);
|
|
1542
|
+
const migrationColumns = parsed.kind === 'create'
|
|
1543
|
+
? resolveCreateTableColumns(data, parsed.tableName, config)
|
|
1544
|
+
: [];
|
|
1266
1545
|
return [
|
|
1267
1546
|
{
|
|
1268
1547
|
type: 'add',
|
|
@@ -1273,40 +1552,53 @@ module.exports = function (plop) {
|
|
|
1273
1552
|
tableName: parsed.tableName,
|
|
1274
1553
|
columnName: parsed.columnName,
|
|
1275
1554
|
withExample: example,
|
|
1276
|
-
withSoftDelete: includeSoftDelete(data)
|
|
1555
|
+
withSoftDelete: includeSoftDelete(data),
|
|
1556
|
+
migrationColumns
|
|
1277
1557
|
}
|
|
1278
1558
|
}
|
|
1279
1559
|
];
|
|
1280
1560
|
}
|
|
1281
1561
|
|
|
1282
|
-
function
|
|
1283
|
-
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1562
|
+
function makeSeederActions(data) {
|
|
1563
|
+
data.entityName = entityNameForSeeder(data);
|
|
1564
|
+
const { root, templateData } = makeContext(data);
|
|
1565
|
+
const entityName = data.entityName;
|
|
1566
|
+
const seederClass = `${entityName}Seeder`;
|
|
1567
|
+
const tableName = tableNameFromEntity(entityName);
|
|
1568
|
+
const seedFields = resolveSeedFields(data, tableName, loadProjectConfig());
|
|
1288
1569
|
return [
|
|
1570
|
+
...seederSkeletonActions(root, templateData, { skipIfExists: true }),
|
|
1289
1571
|
{
|
|
1290
1572
|
type: 'add',
|
|
1291
|
-
path: `${
|
|
1292
|
-
templateFile: 'templates/
|
|
1573
|
+
path: `${root}/seeder/${seederClass}.java`,
|
|
1574
|
+
templateFile: 'templates/seeder.hbs',
|
|
1293
1575
|
data: {
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1576
|
+
...templateData,
|
|
1577
|
+
extraImports: fieldImports(seedFields),
|
|
1578
|
+
seedFields,
|
|
1579
|
+
seederClass,
|
|
1580
|
+
entityName
|
|
1297
1581
|
}
|
|
1298
1582
|
}
|
|
1299
1583
|
];
|
|
1300
1584
|
}
|
|
1301
1585
|
|
|
1302
1586
|
function makeModelActions(data) {
|
|
1303
|
-
const actions = makeEntityActions(data);
|
|
1587
|
+
const actions = makeEntityActions(data, { skipIfExists: true });
|
|
1304
1588
|
if (includeMapperWithEntity(data)) {
|
|
1305
1589
|
actions.push(...makeMapperActions(data));
|
|
1306
1590
|
}
|
|
1591
|
+
appendMakeSqlActions(actions, data);
|
|
1592
|
+
if (includeModelController(data)) {
|
|
1593
|
+
actions.push(...makeControllerActions(data));
|
|
1594
|
+
}
|
|
1595
|
+
return actions;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function appendMakeSqlActions(actions, data) {
|
|
1307
1599
|
const config = loadProjectConfig();
|
|
1308
1600
|
const migrationPath = migrationDir(config);
|
|
1309
|
-
|
|
1601
|
+
const version = nextMigrationVersion(path.join(projectRoot(), migrationPath));
|
|
1310
1602
|
if (includeModelMigration(data)) {
|
|
1311
1603
|
const tableName = tableNameFromEntity(data.entityName);
|
|
1312
1604
|
actions.push(
|
|
@@ -1316,13 +1608,9 @@ module.exports = function (plop) {
|
|
|
1316
1608
|
migrationVersion: version
|
|
1317
1609
|
})
|
|
1318
1610
|
);
|
|
1319
|
-
version += 1;
|
|
1320
1611
|
}
|
|
1321
1612
|
if (includeModelSeed(data)) {
|
|
1322
|
-
actions.push(...
|
|
1323
|
-
}
|
|
1324
|
-
if (includeModelController(data)) {
|
|
1325
|
-
actions.push(...makeControllerActions(data));
|
|
1613
|
+
actions.push(...makeSeederActions(data));
|
|
1326
1614
|
}
|
|
1327
1615
|
return actions;
|
|
1328
1616
|
}
|
|
@@ -1342,6 +1630,21 @@ module.exports = function (plop) {
|
|
|
1342
1630
|
actions: makeMigrationActions
|
|
1343
1631
|
});
|
|
1344
1632
|
|
|
1633
|
+
plop.setGenerator('make:seeder', {
|
|
1634
|
+
description: '產生 Java Seeder;讀既有 Entity 或 --fields',
|
|
1635
|
+
prompts: [
|
|
1636
|
+
{
|
|
1637
|
+
type: 'input',
|
|
1638
|
+
name: 'entityName',
|
|
1639
|
+
message: '名稱(例如 BookSeeder):',
|
|
1640
|
+
filter: toPascalCaseName,
|
|
1641
|
+
validate: validatePascalCase
|
|
1642
|
+
},
|
|
1643
|
+
fieldsPrompt
|
|
1644
|
+
],
|
|
1645
|
+
actions: makeSeederActions
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1345
1648
|
plop.setGenerator('migration', {
|
|
1346
1649
|
description: 'make:migration 的別名',
|
|
1347
1650
|
prompts: migrationPrompts,
|
|
@@ -42,6 +42,7 @@ DB → Entity → Mapper → XxxResponse → Result<T>
|
|
|
42
42
|
- `mapper/`(`EntityMapper`;`XxxMapper extends EntityMapper`)
|
|
43
43
|
- `service/`、`service/impl/`(`BaseService`;`XxxService extends BaseService`)
|
|
44
44
|
- `controller/`
|
|
45
|
+
- `seeder/`(`Seeder`、`SeedRunner`、`XxxSeeder`)
|
|
45
46
|
- `web/Result`
|
|
46
47
|
|
|
47
48
|
## 分層(baseline)
|
|
@@ -52,14 +53,16 @@ DB → Entity → Mapper → XxxResponse → Result<T>
|
|
|
52
53
|
- `dao`:`XxxDAO extends BaseDAO<Entity, ID>`。自訂查詢才在介面加方法。資料庫 VIEW 不能寫入時,那個 DAO `extends BaseViewDAO`(沒有 save / delete)。不要為了跟 DAO 對稱再做一個 `BaseViewService`。
|
|
53
54
|
- `service`:Controller 注入 `XxxService` 介面。一對一 `XxxServiceImpl` 是正常的;介面的價值是額外方法的掛點與 Spring 注入,不是預先準備第二套實作。查詢 API 大到該獨立時,另開一個查詢介面(例如只含 search / count),不要把八十個方法塞進同一個 Service。
|
|
54
55
|
- `controller`:參數驗證與呼叫 Service。回傳專案內 `web.Result<T>`。不回 Entity、不注入 DAO、不引用 `birc-spring-boot-starter`。
|
|
56
|
+
- `seeder`:示範資料。`XxxSeeder implements Seeder`,用 DAO `save`。`birc seed` 才跑,不要把 INSERT 寫進 Flyway。
|
|
55
57
|
|
|
56
58
|
## 統一規範
|
|
57
59
|
|
|
58
60
|
- 例外丟 `ProjectException` 子類(例如 `NotFoundException`),由 `ExceptionHandleController` 轉成回應。Controller 不手動 try-catch 包錯誤訊息。
|
|
59
|
-
- 整組骨架用 `birc make`;單層用 `birc make:entity` / `make:model` / `make:mapper` / `make:service` / `make:controller` / `make:exception`。`make:entity` 只生 Entity 與 DAO;`--dto` 與 `--mapper` 一併生 Mapper 與 DTO。`make:model` 可加 `--migration` / `--seed` / `--controller
|
|
61
|
+
- 整組骨架用 `birc make`;單層用 `birc make:entity` / `make:model` / `make:mapper` / `make:service` / `make:controller` / `make:exception`。`make:entity` 只生 Entity 與 DAO;`--dto` 與 `--mapper` 一併生 Mapper 與 DTO。`make:model` 可加 `--migration` / `--seed` / `--controller`。`make` 也可加 `--migration` / `--seed`。`make:seeder BookSeeder` 生 Java Seeder。`--soft-delete` 用 Hibernate `@SoftDelete` 的 `deleted_at`,查詢自動排除已刪;不要自寫 `@SQLDelete`。預設是初稿,完整範例加 `--example` 或 `--fields`。`make:migration create_{table}_table` 讀對應 Entity 欄位寫進 SQL。`--seed` 生 Java Seeder,用 DAO 塞資料;`birc seed` 才執行。清空資料庫用 `birc db:wipe`。不要複製其他 Entity 的程式碼來改名。
|
|
60
62
|
- 欄位驗證用專案內 `validation/ValidGroup`。CRUD 用 `Create` / `Update`。`Delete` 給「刪除還要帶原因、確認碼、樂觀鎖 version」的 body。`Submit` 給草稿可少填、送審要全填(活動送審就是這組)。Controller 的 `@Validated` 與欄位 `groups` 對同一組。
|
|
61
63
|
- email / sso / oauth / validGroup 用 `birc add`,不要從別的專案抄設定。
|
|
62
64
|
- 格式化走 Spotless(`./gradlew spotlessApply`),不要另裝 Checkstyle。
|
|
65
|
+
- CORS 只改 `CORS_ALLOWED_ORIGIN_PATTERNS`(逗號分隔 Origin,不要尾斜線、不要 `*`)。`SecurityConfig` 已經用 `http.cors()` 接 `CorsConfigurationSource`。不要在 `WebMvcConfigurer` 加 `addCorsMappings`,Spring Security 會擋 preflight。空白 = 不開放跨來源。
|
|
63
66
|
|
|
64
67
|
## 已啟用的功能模組
|
|
65
68
|
|
package/scripts/install-bin.sh
CHANGED
|
@@ -53,9 +53,9 @@ EOF
|
|
|
53
53
|
|
|
54
54
|
if birc_is_windows; then
|
|
55
55
|
if command -v cygpath >/dev/null 2>&1; then
|
|
56
|
-
win_path="$(cygpath -w "$script_path")"
|
|
56
|
+
win_path="$(cygpath -w "$script_path" | sed 's|/|\\\\|g')"
|
|
57
57
|
else
|
|
58
|
-
win_path="$script_path"
|
|
58
|
+
win_path="$(echo "$script_path" | sed 's|/|\\\\|g')"
|
|
59
59
|
fi
|
|
60
60
|
printf '@echo off\nnode "%s" %%*\n' "$win_path" > "$bin_dir/birc.cmd"
|
|
61
61
|
fi
|
|
@@ -37,13 +37,27 @@ Get-Content .env | ForEach-Object {
|
|
|
37
37
|
Windows 用 `.\gradlew.bat bootRun`。
|
|
38
38
|
{{/if}}
|
|
39
39
|
|
|
40
|
+
## 前端跨來源(CORS)
|
|
41
|
+
|
|
42
|
+
瀏覽器從別的 Origin 打 API 時,設環境變數後重啟:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
CORS_ALLOWED_ORIGIN_PATTERNS=http://localhost:5173
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
逗號分隔、不要尾斜線、不要 `*`。只改這變數;不要在 `WebMvcConfigurer` 加 `addCorsMappings`,Spring Security 不會吃。
|
|
49
|
+
|
|
40
50
|
## 生骨架
|
|
41
51
|
|
|
42
52
|
在有 `.bircrc.json` 的目錄跑即可;子目錄也可以,`birc` 會往上找。
|
|
43
53
|
|
|
44
54
|
```bash
|
|
45
55
|
birc make
|
|
46
|
-
birc make
|
|
56
|
+
birc make Book --fields title:String --migration --seed
|
|
57
|
+
birc make:seeder BookSeeder
|
|
58
|
+
birc migrate
|
|
59
|
+
birc seed
|
|
60
|
+
birc db:wipe
|
|
47
61
|
birc add email
|
|
48
62
|
```
|
|
49
63
|
|
package/templates/entity.hbs
CHANGED
|
@@ -33,7 +33,7 @@ public class {{pascalCase entityName}} {
|
|
|
33
33
|
{{#if withExample}}
|
|
34
34
|
{{#each makeFields}}
|
|
35
35
|
|
|
36
|
-
@Column(name = "{{this.snakeName}}")
|
|
36
|
+
@Column(name = "{{this.snakeName}}"{{#if this.columnLength}}, length = {{this.columnLength}}{{/if}})
|
|
37
37
|
private {{this.type}} {{this.name}};
|
|
38
38
|
{{/each}}
|
|
39
39
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
-- {{description}}
|
|
2
2
|
|
|
3
3
|
CREATE TABLE {{tableName}} (
|
|
4
|
-
id BIGINT AUTO_INCREMENT PRIMARY KEY{{#
|
|
5
|
-
name
|
|
6
|
-
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
7
|
-
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP{{/if}}{{#if withSoftDelete}},
|
|
8
|
-
deleted_at TIMESTAMP NULL{{/if}}
|
|
4
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY{{#each migrationColumns}},
|
|
5
|
+
{{this.name}} {{this.sqlType}}{{/each}}
|
|
9
6
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package {{basePackage}}.seeder;
|
|
2
|
+
|
|
3
|
+
import java.util.List;
|
|
4
|
+
import org.springframework.boot.ApplicationArguments;
|
|
5
|
+
import org.springframework.boot.ApplicationRunner;
|
|
6
|
+
import org.springframework.boot.SpringApplication;
|
|
7
|
+
import org.springframework.context.ConfigurableApplicationContext;
|
|
8
|
+
import org.springframework.stereotype.Component;
|
|
9
|
+
|
|
10
|
+
@Component
|
|
11
|
+
public class SeedRunner implements ApplicationRunner {
|
|
12
|
+
|
|
13
|
+
private final List<Seeder> seeders;
|
|
14
|
+
private final ConfigurableApplicationContext context;
|
|
15
|
+
|
|
16
|
+
public SeedRunner(List<Seeder> seeders, ConfigurableApplicationContext context) {
|
|
17
|
+
this.seeders = seeders;
|
|
18
|
+
this.context = context;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Override
|
|
22
|
+
public void run(ApplicationArguments args) {
|
|
23
|
+
if (!args.containsOption("birc.seed")) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
for (Seeder seeder : seeders) {
|
|
27
|
+
seeder.run();
|
|
28
|
+
}
|
|
29
|
+
System.exit(SpringApplication.exit(context, () -> 0));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package {{basePackage}}.seeder;
|
|
2
|
+
|
|
3
|
+
{{#each extraImports}}
|
|
4
|
+
import {{this}};
|
|
5
|
+
{{/each}}
|
|
6
|
+
{{#if seedFields.length}}
|
|
7
|
+
import lombok.RequiredArgsConstructor;
|
|
8
|
+
import org.springframework.stereotype.Component;
|
|
9
|
+
import {{daoPackage}}.{{pascalCase entityName}}DAO;
|
|
10
|
+
import {{entityPackage}}.{{pascalCase entityName}};
|
|
11
|
+
{{else}}
|
|
12
|
+
import org.springframework.stereotype.Component;
|
|
13
|
+
{{/if}}
|
|
14
|
+
|
|
15
|
+
@Component
|
|
16
|
+
{{#if seedFields.length}}
|
|
17
|
+
@RequiredArgsConstructor
|
|
18
|
+
{{/if}}
|
|
19
|
+
public class {{seederClass}} implements Seeder {
|
|
20
|
+
{{#if seedFields.length}}
|
|
21
|
+
|
|
22
|
+
private final {{pascalCase entityName}}DAO {{camelCase entityName}}DAO;
|
|
23
|
+
|
|
24
|
+
{{/if}}
|
|
25
|
+
@Override
|
|
26
|
+
public void run() {
|
|
27
|
+
{{#if seedFields.length}}
|
|
28
|
+
if ({{camelCase entityName}}DAO.count() > 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
{{pascalCase entityName}} {{camelCase entityName}} = new {{pascalCase entityName}}();
|
|
32
|
+
{{#each seedFields}}
|
|
33
|
+
{{camelCase ../entityName}}.{{this.setter}}({{{this.literal}}});
|
|
34
|
+
{{/each}}
|
|
35
|
+
{{camelCase entityName}}DAO.save({{camelCase entityName}});
|
|
36
|
+
{{/if}}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/test.md
CHANGED
|
@@ -35,8 +35,11 @@
|
|
|
35
35
|
- CLI dest:檔落在呼叫者目錄,不是 generator repo。
|
|
36
36
|
- 產出 Java 沒有 `tw.edu.ntub.birc.starter` / `birc-spring-boot-starter`。
|
|
37
37
|
- `make:entity --dto` / `--mapper` 與 `make:mapper` 同一組 Mapper + DTO;預設 `make:entity` 不生它們。
|
|
38
|
-
- `make
|
|
38
|
+
- `make --fields --migration --seed` 寫 CREATE TABLE 與 Java `XxxSeeder`,不生 seed SQL;`make:model --migration` 的表名跟 Entity `@Table` 一致。
|
|
39
|
+
- `make:seeder` / `--seed` 讀既有 Entity 欄位寫進 `setXxx`;沒有欄位時 `run()` 是空的。`make:seeder BookSeeder` 生 `BookSeeder.java`,不是 `BookSeederSeeder`。
|
|
40
|
+
- `make:migration create_{table}_table` 讀既有 Entity 欄位寫進 SQL;找不到 Entity 時仍只有 `id`。
|
|
39
41
|
- `--soft-delete` 的 Entity 用 `deleted_at` + `SoftDeleteType.TIMESTAMP`;沒加 flag 的 Entity / migration 沒有這欄。
|
|
42
|
+
- `--fields` 有帶就當完整產出,不必再加 `--example`。
|
|
40
43
|
- `--example` 的驗證群組是專案 `ValidGroup.Create` / `Update`,不是 starter `OnCreate`。`Delete` / `Submit` 是空的 marker,給「刪除要帶 body」與送審用,預設 CRUD 不標;不要刪掉。
|
|
41
44
|
- Flyway plugin 版本跟得上 Gradle wrapper;`flywayMigrate` 不能再碰到已刪的 Gradle convention API。
|
|
42
45
|
- 安裝/更新路徑:`detectInstallKind`、自訂 base URL 預設拒絕、tarball 路徑不可逃出目標目錄。
|
package/uninstall.ps1
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# 用法:
|
|
2
|
-
# irm https://gitlab.ntubimdbirc.tw/
|
|
2
|
+
# irm https://gitlab.ntubimdbirc.tw/birc-backend/birc-generator/-/raw/main/uninstall.ps1 | iex
|
|
3
3
|
# 或下載後執行: powershell -NoProfile -ExecutionPolicy Bypass -File uninstall.ps1
|
|
4
4
|
# 只拆 curl / PowerShell 裝到 $HOME\.birc-generator 的那份,不會 npm uninstall -g。
|
|
5
5
|
|