birc-generator 0.5.5 → 0.9.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 +28 -63
- package/bin/birc.js +48 -22
- 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 +340 -35
- 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`。`--migration` / `--seed` 同 `make:model`;已有 Entity 就跳過,只補 SQL / Seeder。`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
|
@@ -21,17 +21,9 @@ CRUD 用 `birc make`。多模組目錄、`BaseDAO`、`ProjectException` 會先
|
|
|
21
21
|
|
|
22
22
|
## 安裝
|
|
23
23
|
|
|
24
|
-
需要 Node 18
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`birc update` 會自己判斷來源。npm 裝的等同
|
|
29
|
-
`npm install -g birc-generator@latest`;`curl` / PowerShell 裝的都拉
|
|
30
|
-
GitLab 到 `~/.birc-generator`。
|
|
31
|
-
|
|
32
|
-
### npm
|
|
33
|
-
|
|
34
|
-
Win11、WSL、macOS、Linux 都可以。
|
|
24
|
+
需要 Node 18 以上。只走 npm。Win11、WSL、macOS、Linux 都可以。一般只要
|
|
25
|
+
裝全域的 `birc`,不必 clone。改產生器、或要跑還沒發到 npm 的 commit,才
|
|
26
|
+
clone。
|
|
35
27
|
|
|
36
28
|
```bash
|
|
37
29
|
npm install -g birc-generator
|
|
@@ -44,47 +36,12 @@ npm update -g birc-generator
|
|
|
44
36
|
npm uninstall -g birc-generator
|
|
45
37
|
```
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
### curl | bash
|
|
52
|
-
|
|
53
|
-
給 macOS、Linux、WSL、Git Bash。程式放到 `~/.birc-generator`,並把
|
|
54
|
-
`birc` 寫進使用者自己的 bin。不需要 sudo。PowerShell 不要貼這條:內建
|
|
55
|
-
`curl` 是 `Invoke-WebRequest`,會報找不到參數 `-fsSL`。改用下面的
|
|
56
|
-
PowerShell 安裝。
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
curl -fsSL https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/raw/main/install.sh | bash
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
關掉終端再開一次,跑 `birc --help`。之後更新用 `birc update`。還沒有
|
|
63
|
-
這個指令的舊安裝,再跑一次上面的 `install.sh`。
|
|
39
|
+
`birc update` 等同 `npm install -g birc-generator@latest`。使用時若偵測到
|
|
40
|
+
新版本,會問要不要更新。說不要的話,同一版本 24 小時內不會再問。不想檢查
|
|
41
|
+
就設 `BIRC_SKIP_UPDATE_CHECK=1`。CI 或不是互動終端也不會問。
|
|
64
42
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
curl -fsSL https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/raw/main/uninstall.sh | bash
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### PowerShell
|
|
72
|
-
|
|
73
|
-
給 Windows 原生 PowerShell 5.1 或 7。落地目錄跟 `curl | bash` 相同
|
|
74
|
-
(`%USERPROFILE%\.birc-generator`),所以之後的 `birc update` 是同一條路。
|
|
75
|
-
`irm | iex` 在記憶體執行,通常不必改 ExecutionPolicy。
|
|
76
|
-
|
|
77
|
-
```powershell
|
|
78
|
-
irm https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/raw/main/install.ps1 | iex
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
關掉終端再開一次,跑 `birc --help`。之後更新用 `birc update`。
|
|
82
|
-
|
|
83
|
-
解除安裝:
|
|
84
|
-
|
|
85
|
-
```powershell
|
|
86
|
-
irm https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/raw/main/uninstall.ps1 | iex
|
|
87
|
-
```
|
|
43
|
+
以前用 `curl | bash` 或 PowerShell 裝到 `~/.birc-generator` 的,改裝 npm
|
|
44
|
+
全域套件,並自行刪掉那個目錄。
|
|
88
45
|
|
|
89
46
|
## 第一次用
|
|
90
47
|
|
|
@@ -146,8 +103,8 @@ birc make
|
|
|
146
103
|
輸入 `Activity` 或 `activity`,會一次寫出 Entity、DAO、Mapper、DTO、
|
|
147
104
|
Service、Controller。小寫會自動收成 PascalCase。只要其中一層,改跑
|
|
148
105
|
`birc make:entity` 或 `birc make:controller`。預設是初稿,完整範例加
|
|
149
|
-
`--example`。欄位可寫 `--fields title:String,startTime:LocalDateTime`,
|
|
150
|
-
|
|
106
|
+
`--example`,或直接帶 `--fields`。欄位可寫 `--fields title:String,startTime:LocalDateTime`,
|
|
107
|
+
就不會再問一次。
|
|
151
108
|
檔已存在時會停下來;要覆寫加 `--force`。
|
|
152
109
|
|
|
153
110
|
腳本或課堂示範可 `birc create Practice --yes`,其餘用預設(docker、log4j2、
|
|
@@ -162,35 +119,44 @@ ValidGroup、Spotless、注入 agent 文件),不再問。
|
|
|
162
119
|
| 指令 | 做什麼 |
|
|
163
120
|
| --- | --- |
|
|
164
121
|
| `birc create` | 開一個多模組 Spring Boot 4 專案。`--yes` 其餘用預設、不要問 |
|
|
165
|
-
| `birc update` | 更新本機 `birc
|
|
122
|
+
| `birc update` | 更新本機 `birc`(`npm install -g birc-generator@latest`) |
|
|
166
123
|
| `birc add` | 往既有專案加模組。沒帶名稱會互動勾選;`birc add email sentry` 直接加。`--force` 覆寫已選的已安裝 feature 檔 |
|
|
167
124
|
| `birc sync` | 用目前 template 覆寫全部已安裝 feature 的檔(不動 yml / gradle) |
|
|
168
|
-
| `birc make` | 一次生 Entity + DAO + Mapper + DTO + Service + Controller |
|
|
169
|
-
| `birc make:entity` | Entity、`XxxDAO`;加 `--dto`、`--mapper`、`--soft-delete`。已有檔要覆寫加 `--force` |
|
|
125
|
+
| `birc make` | 一次生 Entity + DAO + Mapper + DTO + Service + Controller;加 `--migration` / `--seed` |
|
|
126
|
+
| `birc make:entity` | Entity、`XxxDAO`;加 `--migration` / `--seed` / `--dto`、`--mapper`、`--soft-delete`。已有檔要覆寫加 `--force`;只要表或 Seeder 可 `--migration` / `--seed`,不會覆寫 Entity |
|
|
170
127
|
| `birc make:model` | 同 `make:entity`;加 `--migration` / `--seed` / `--controller` / `--soft-delete` |
|
|
171
128
|
| `birc make:mapper` | Mapper 和 CreateRequest / Response |
|
|
172
129
|
| `birc make:service` | `XxxService extends BaseService`,CRUD 在 `BaseServiceImpl` |
|
|
173
130
|
| `birc make:controller` | Controller |
|
|
174
131
|
| `birc make:exception` | 繼承 `ProjectException` 的例外類 |
|
|
175
|
-
| `birc make:migration` | Flyway SQL(`
|
|
132
|
+
| `birc make:migration` | Flyway SQL(`create_book_table` 會讀 Book 欄位;`add_votes_to_users_table` 仍是檔名) |
|
|
133
|
+
| `birc make:seeder` | Java `XxxSeeder`(讀 Entity 或 `--fields`) |
|
|
176
134
|
| `birc docs` | 補 `AGENTS.md` / `PROJECT.md` / `implement.md` / `test.md`(專案名從 `.bircrc.json` 讀) |
|
|
177
135
|
| `birc migrate` | 在專案裡跑 Gradle wrapper 的 `flywayMigrate`(Unix `./gradlew`,Windows `gradlew.bat`) |
|
|
178
136
|
| `birc migrate:rollback` | Flyway Community 做不到 rollback,指令會說明替代作法 |
|
|
179
137
|
| `birc migrate:reset` | `flywayClean` 後再 `flywayMigrate`(`--force` 略過確認) |
|
|
138
|
+
| `birc seed` | 跑 Java Seeder(`bootRun --birc.seed`,跑完結束) |
|
|
139
|
+
| `birc db:wipe` | 清空資料庫(`flywayClean`)。沒加 `--force` 先問一次 |
|
|
180
140
|
|
|
181
141
|
```bash
|
|
182
142
|
birc create Practice --yes
|
|
183
143
|
birc make:entity User --dto
|
|
184
144
|
birc make:entity User --mapper
|
|
185
145
|
birc make:entity User --example
|
|
186
|
-
birc make:entity User --
|
|
146
|
+
birc make:entity User --fields title:String,startTime:LocalDateTime
|
|
147
|
+
birc make Book --fields title:String,author:String,isbn:String,price:BigDecimal,publishedAt:LocalDate --migration --seed
|
|
148
|
+
birc migrate
|
|
149
|
+
birc seed
|
|
150
|
+
birc db:wipe
|
|
187
151
|
birc make:entity User --force
|
|
188
152
|
birc make:model Flight --migration
|
|
189
153
|
birc make:model Flight --seed
|
|
190
154
|
birc make:model Flight --controller
|
|
191
155
|
birc make:model Flight --soft-delete
|
|
192
156
|
birc make:model Flight --migration --soft-delete
|
|
193
|
-
birc make:
|
|
157
|
+
birc make:entity Book --fields title:String,author:String,isbn:String,price:BigDecimal,publishedAt:LocalDate
|
|
158
|
+
birc make:migration create_book_table
|
|
159
|
+
birc make:seeder BookSeeder
|
|
194
160
|
birc add
|
|
195
161
|
birc add email
|
|
196
162
|
birc add email sentry
|
|
@@ -202,7 +168,7 @@ birc migrate
|
|
|
202
168
|
|
|
203
169
|
指令是 `make:entity`,中間一條冒號,跟 Laravel artisan 一樣。
|
|
204
170
|
`birc add` 沒帶名稱時會問你要加哪些模組;也可以寫 `birc add docker` 或 `birc add file-upload`。
|
|
205
|
-
`make:migration add_votes_to_users_table` 裡的 `add_votes_...`
|
|
171
|
+
`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
172
|
|
|
207
173
|
各指令的細節見 wiki:[make](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/基礎/make)、
|
|
208
174
|
[add](https://gitlab.ntubimdbirc.tw/lucashsu95/birc-generator/-/wikis/基礎/add)、
|
|
@@ -261,7 +227,7 @@ Mapper 用 MapStruct。Controller 只轉 DTO,業務寫在 Service。
|
|
|
261
227
|
SSH 部署骨架,帳密仍放 GitLab CI/CD Variables,不要寫進 repo。
|
|
262
228
|
|
|
263
229
|
Flyway Community 沒有 Laravel 的 rollback。`birc migrate:rollback` 只會
|
|
264
|
-
說明替代作法:再寫一筆往前的遷移。`birc migrate` 會真的跑 Gradle wrapper 的 `flywayMigrate
|
|
230
|
+
說明替代作法:再寫一筆往前的遷移。`birc migrate` 會真的跑 Gradle wrapper 的 `flywayMigrate`,不是只印指令。示範資料用 `birc seed`;清空資料庫用 `birc db:wipe`。
|
|
265
231
|
|
|
266
232
|
## 改這個產生器
|
|
267
233
|
|
|
@@ -286,8 +252,7 @@ npm run test:mutation
|
|
|
286
252
|
|
|
287
253
|
發版順序:先把改動推進 GitLab `main`,再改 `package.json` 的 `version`,
|
|
288
254
|
最後 `npm publish`。只推 GitLab、不 publish 的話,`npm install -g` 的人
|
|
289
|
-
|
|
290
|
-
`irm | iex` / `birc update` 會跟 npm 版本對不上。
|
|
255
|
+
拿不到新版。
|
|
291
256
|
|
|
292
257
|
## 接下來
|
|
293
258
|
|
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'],
|
|
31
|
-
['make:entity', 'Entity、XxxDAO(--dto / --mapper / --soft-delete)'],
|
|
30
|
+
['make', '一次生 Entity + DAO + Mapper + DTO + Service + Controller;--migration / --seed'],
|
|
31
|
+
['make:entity', 'Entity、XxxDAO(--migration / --seed / --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:entity / make:model 順便生 create_{table}_table`,
|
|
178
|
+
` ${'--seed'.padEnd(nameWidth + 2)}make / make:entity / 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,21 @@ 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:entity Book --migration',
|
|
202
|
+
' birc make:entity Book --seed',
|
|
203
|
+
' birc make Book --fields title:String,author:String --migration --seed',
|
|
197
204
|
' birc make:entity User --force',
|
|
198
205
|
' birc make:model Flight --migration',
|
|
199
206
|
' birc make:model Flight --seed',
|
|
200
207
|
' birc make:model Flight --controller',
|
|
201
208
|
' birc make:model Flight --soft-delete',
|
|
202
|
-
' birc make:
|
|
209
|
+
' birc make:seeder BookSeeder',
|
|
210
|
+
' birc make:migration create_book_table',
|
|
203
211
|
' birc make:migration add_votes_to_users_table',
|
|
204
212
|
' birc migrate',
|
|
213
|
+
' birc seed',
|
|
214
|
+
' birc db:wipe',
|
|
205
215
|
'',
|
|
206
216
|
'create / update 以外的指令會從目前目錄往上找 .bircrc.json。',
|
|
207
217
|
''
|
|
@@ -236,7 +246,8 @@ async function runGenerator(name, bypassArr, flags) {
|
|
|
236
246
|
process.exit(1);
|
|
237
247
|
}
|
|
238
248
|
|
|
239
|
-
|
|
249
|
+
const hasFields = Boolean(String(flags.fields || '').trim());
|
|
250
|
+
if (flags.example || hasFields) {
|
|
240
251
|
process.env.BIRC_EXAMPLE = '1';
|
|
241
252
|
}
|
|
242
253
|
if (flags.dto) {
|
|
@@ -293,12 +304,8 @@ async function runGenerator(name, bypassArr, flags) {
|
|
|
293
304
|
}
|
|
294
305
|
if (flags.fields) {
|
|
295
306
|
process.env.BIRC_MAKE_FIELDS = flags.fields;
|
|
296
|
-
const { applyMakeFieldsFlag
|
|
307
|
+
const { applyMakeFieldsFlag } = require('./cli-util');
|
|
297
308
|
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
309
|
}
|
|
303
310
|
const answers = await generator.runPrompts(promptBypass);
|
|
304
311
|
if (createYes) {
|
|
@@ -310,7 +317,7 @@ async function runGenerator(name, bypassArr, flags) {
|
|
|
310
317
|
if (flags.fields) {
|
|
311
318
|
answers.fields = flags.fields;
|
|
312
319
|
}
|
|
313
|
-
answers.example = Boolean(answers.example) || flags.example;
|
|
320
|
+
answers.example = Boolean(answers.example) || flags.example || hasFields;
|
|
314
321
|
answers.dto = Boolean(answers.dto) || flags.dto;
|
|
315
322
|
answers.mapper = Boolean(answers.mapper) || flags.mapper;
|
|
316
323
|
answers.migration = Boolean(answers.migration) || flags.migration;
|
|
@@ -391,11 +398,16 @@ function printCreateNextSteps(answers) {
|
|
|
391
398
|
'',
|
|
392
399
|
'生 CRUD 骨架(會從目前目錄往上找 .bircrc.json):',
|
|
393
400
|
' birc make # Entity + DAO + Mapper + DTO + Service + Controller',
|
|
401
|
+
' birc make Book --fields title:String --migration --seed',
|
|
394
402
|
' birc make:entity # Entity / DAO',
|
|
395
403
|
' birc make:entity User --dto',
|
|
396
404
|
' birc make:model Flight --migration --seed --controller',
|
|
405
|
+
' birc make:seeder BookSeeder',
|
|
397
406
|
' birc make:entity User --example',
|
|
398
|
-
' birc make:migration
|
|
407
|
+
' birc make:migration create_book_table',
|
|
408
|
+
' birc migrate',
|
|
409
|
+
' birc seed',
|
|
410
|
+
' birc db:wipe',
|
|
399
411
|
' birc add # 之後再補 docker / email 等功能',
|
|
400
412
|
'',
|
|
401
413
|
'打包:',
|
|
@@ -503,9 +515,14 @@ async function runFlyway(command, flags) {
|
|
|
503
515
|
process.exit(1);
|
|
504
516
|
}
|
|
505
517
|
|
|
506
|
-
if (command === 'migrate:reset') {
|
|
518
|
+
if (command === 'migrate:reset' || command === 'db:wipe') {
|
|
519
|
+
const wiping = command === 'db:wipe';
|
|
507
520
|
if (!flags.force) {
|
|
508
|
-
const ok = await confirmYes(
|
|
521
|
+
const ok = await confirmYes(
|
|
522
|
+
wiping
|
|
523
|
+
? '這會清掉 schema 與資料。確定? [y/N] '
|
|
524
|
+
: '這會清掉 schema 再重跑全部遷移。確定? [y/N] '
|
|
525
|
+
);
|
|
509
526
|
if (!ok) {
|
|
510
527
|
process.stdout.write('已取消。\n');
|
|
511
528
|
return;
|
|
@@ -515,6 +532,9 @@ async function runFlyway(command, flags) {
|
|
|
515
532
|
if (cleanStatus !== 0) {
|
|
516
533
|
process.exit(cleanStatus);
|
|
517
534
|
}
|
|
535
|
+
if (wiping) {
|
|
536
|
+
process.exit(0);
|
|
537
|
+
}
|
|
518
538
|
process.exit(runGradle(['flywayMigrate']));
|
|
519
539
|
}
|
|
520
540
|
|
|
@@ -635,6 +655,12 @@ async function main() {
|
|
|
635
655
|
await runFlyway(command, flags);
|
|
636
656
|
return;
|
|
637
657
|
}
|
|
658
|
+
if (SEED_COMMANDS.has(command)) {
|
|
659
|
+
process.exit(runGradle([
|
|
660
|
+
'bootRun',
|
|
661
|
+
'--args=--spring.main.web-application-type=none --birc.seed'
|
|
662
|
+
]));
|
|
663
|
+
}
|
|
638
664
|
|
|
639
665
|
await runGenerator(command, flags.positionals.slice(1), flags);
|
|
640
666
|
}
|
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,28 +1197,30 @@ 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
|
});
|
|
938
1213
|
|
|
939
1214
|
plop.setGenerator('make:entity', {
|
|
940
|
-
description: '產生 Entity 與 DAO;--dto / --mapper
|
|
1215
|
+
description: '產生 Entity 與 DAO;--migration / --seed / --dto / --mapper / --soft-delete',
|
|
941
1216
|
prompts: [entityNamePrompt, fieldsPrompt],
|
|
942
1217
|
actions(data) {
|
|
943
|
-
const
|
|
1218
|
+
const extras = includeModelMigration(data) || includeModelSeed(data);
|
|
1219
|
+
const actions = makeEntityActions(data, { skipIfExists: extras });
|
|
944
1220
|
if (includeMapperWithEntity(data)) {
|
|
945
|
-
|
|
1221
|
+
actions.push(...makeMapperActions(data));
|
|
946
1222
|
}
|
|
1223
|
+
appendMakeSqlActions(actions, data);
|
|
947
1224
|
return actions;
|
|
948
1225
|
}
|
|
949
1226
|
});
|
|
@@ -1175,6 +1452,7 @@ module.exports = function (plop) {
|
|
|
1175
1452
|
];
|
|
1176
1453
|
actions = actions.concat(exceptionSkeletonActions(root, templateData));
|
|
1177
1454
|
actions = actions.concat(serviceSkeletonActions(root, templateData));
|
|
1455
|
+
actions = actions.concat(seederSkeletonActions(root, templateData));
|
|
1178
1456
|
|
|
1179
1457
|
const configRoot = `${projectDir}/modules/${projectDir}-config/src/main/java/${basePackagePath}/config`;
|
|
1180
1458
|
actions.push(
|
|
@@ -1263,6 +1541,9 @@ module.exports = function (plop) {
|
|
|
1263
1541
|
const parsed = parseMigrationName(data.description);
|
|
1264
1542
|
const version = data.migrationVersion || nextMigrationVersion(path.join(projectRoot(), migrationPath));
|
|
1265
1543
|
const example = isExample(data);
|
|
1544
|
+
const migrationColumns = parsed.kind === 'create'
|
|
1545
|
+
? resolveCreateTableColumns(data, parsed.tableName, config)
|
|
1546
|
+
: [];
|
|
1266
1547
|
return [
|
|
1267
1548
|
{
|
|
1268
1549
|
type: 'add',
|
|
@@ -1273,40 +1554,53 @@ module.exports = function (plop) {
|
|
|
1273
1554
|
tableName: parsed.tableName,
|
|
1274
1555
|
columnName: parsed.columnName,
|
|
1275
1556
|
withExample: example,
|
|
1276
|
-
withSoftDelete: includeSoftDelete(data)
|
|
1557
|
+
withSoftDelete: includeSoftDelete(data),
|
|
1558
|
+
migrationColumns
|
|
1277
1559
|
}
|
|
1278
1560
|
}
|
|
1279
1561
|
];
|
|
1280
1562
|
}
|
|
1281
1563
|
|
|
1282
|
-
function
|
|
1283
|
-
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1564
|
+
function makeSeederActions(data) {
|
|
1565
|
+
data.entityName = entityNameForSeeder(data);
|
|
1566
|
+
const { root, templateData } = makeContext(data);
|
|
1567
|
+
const entityName = data.entityName;
|
|
1568
|
+
const seederClass = `${entityName}Seeder`;
|
|
1569
|
+
const tableName = tableNameFromEntity(entityName);
|
|
1570
|
+
const seedFields = resolveSeedFields(data, tableName, loadProjectConfig());
|
|
1288
1571
|
return [
|
|
1572
|
+
...seederSkeletonActions(root, templateData, { skipIfExists: true }),
|
|
1289
1573
|
{
|
|
1290
1574
|
type: 'add',
|
|
1291
|
-
path: `${
|
|
1292
|
-
templateFile: 'templates/
|
|
1575
|
+
path: `${root}/seeder/${seederClass}.java`,
|
|
1576
|
+
templateFile: 'templates/seeder.hbs',
|
|
1293
1577
|
data: {
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1578
|
+
...templateData,
|
|
1579
|
+
extraImports: fieldImports(seedFields),
|
|
1580
|
+
seedFields,
|
|
1581
|
+
seederClass,
|
|
1582
|
+
entityName
|
|
1297
1583
|
}
|
|
1298
1584
|
}
|
|
1299
1585
|
];
|
|
1300
1586
|
}
|
|
1301
1587
|
|
|
1302
1588
|
function makeModelActions(data) {
|
|
1303
|
-
const actions = makeEntityActions(data);
|
|
1589
|
+
const actions = makeEntityActions(data, { skipIfExists: true });
|
|
1304
1590
|
if (includeMapperWithEntity(data)) {
|
|
1305
1591
|
actions.push(...makeMapperActions(data));
|
|
1306
1592
|
}
|
|
1593
|
+
appendMakeSqlActions(actions, data);
|
|
1594
|
+
if (includeModelController(data)) {
|
|
1595
|
+
actions.push(...makeControllerActions(data));
|
|
1596
|
+
}
|
|
1597
|
+
return actions;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function appendMakeSqlActions(actions, data) {
|
|
1307
1601
|
const config = loadProjectConfig();
|
|
1308
1602
|
const migrationPath = migrationDir(config);
|
|
1309
|
-
|
|
1603
|
+
const version = nextMigrationVersion(path.join(projectRoot(), migrationPath));
|
|
1310
1604
|
if (includeModelMigration(data)) {
|
|
1311
1605
|
const tableName = tableNameFromEntity(data.entityName);
|
|
1312
1606
|
actions.push(
|
|
@@ -1316,13 +1610,9 @@ module.exports = function (plop) {
|
|
|
1316
1610
|
migrationVersion: version
|
|
1317
1611
|
})
|
|
1318
1612
|
);
|
|
1319
|
-
version += 1;
|
|
1320
1613
|
}
|
|
1321
1614
|
if (includeModelSeed(data)) {
|
|
1322
|
-
actions.push(...
|
|
1323
|
-
}
|
|
1324
|
-
if (includeModelController(data)) {
|
|
1325
|
-
actions.push(...makeControllerActions(data));
|
|
1615
|
+
actions.push(...makeSeederActions(data));
|
|
1326
1616
|
}
|
|
1327
1617
|
return actions;
|
|
1328
1618
|
}
|
|
@@ -1342,6 +1632,21 @@ module.exports = function (plop) {
|
|
|
1342
1632
|
actions: makeMigrationActions
|
|
1343
1633
|
});
|
|
1344
1634
|
|
|
1635
|
+
plop.setGenerator('make:seeder', {
|
|
1636
|
+
description: '產生 Java Seeder;讀既有 Entity 或 --fields',
|
|
1637
|
+
prompts: [
|
|
1638
|
+
{
|
|
1639
|
+
type: 'input',
|
|
1640
|
+
name: 'entityName',
|
|
1641
|
+
message: '名稱(例如 BookSeeder):',
|
|
1642
|
+
filter: toPascalCaseName,
|
|
1643
|
+
validate: validatePascalCase
|
|
1644
|
+
},
|
|
1645
|
+
fieldsPrompt
|
|
1646
|
+
],
|
|
1647
|
+
actions: makeSeederActions
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1345
1650
|
plop.setGenerator('migration', {
|
|
1346
1651
|
description: 'make:migration 的別名',
|
|
1347
1652
|
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;`--migration` / `--seed` 同 `make:model`(已有 Entity 就跳過,只補 SQL / Seeder)。`--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` 一致。已有 Entity 時 `make:entity --migration` / `--seed` 跳過 Java,只補 CREATE TABLE / `XxxSeeder`。
|
|
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
|
|