@nakedev/go-scaffold 0.1.4 → 0.3.1
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/LICENSE +21 -0
- package/README.md +133 -44
- package/dist/commands/auth.js +116 -11
- package/dist/commands/create.js +13 -1
- package/dist/commands/generate.js +21 -11
- package/dist/commands/method.js +32 -3
- package/dist/commands/observability.js +114 -0
- package/dist/commands/rbac.js +19 -2
- package/dist/commands/undo.js +331 -0
- package/dist/commands/worker.js +92 -32
- package/dist/index.js +366 -63
- package/dist/prompts/auth-wizard.js +29 -0
- package/dist/prompts/create-wizard.js +5 -2
- package/dist/prompts/generate-wizard.js +57 -0
- package/dist/prompts/worker-wizard.js +25 -0
- package/dist/templates/auth-manifest.js +20 -3
- package/dist/templates/create-manifest.js +13 -20
- package/dist/templates/observability-manifest.js +24 -0
- package/dist/templates/rbac-manifest.js +1 -0
- package/dist/templates/worker-manifest.js +23 -6
- package/dist/utils/auth-patcher.js +96 -21
- package/dist/utils/config.js +58 -10
- package/dist/utils/gocheck.js +57 -5
- package/dist/utils/golangci-patcher.js +73 -0
- package/dist/utils/gomod-patcher.js +53 -0
- package/dist/utils/main-patcher.js +58 -4
- package/dist/utils/marker-patch.js +125 -3
- package/dist/utils/method-patcher.js +17 -2
- package/dist/utils/module-location.js +37 -1
- package/dist/utils/naming.js +50 -2
- package/dist/utils/observability-patcher.js +107 -0
- package/dist/utils/platform-patcher.js +98 -12
- package/dist/utils/rbac-patcher.js +60 -10
- package/package.json +3 -5
- package/templates/add/auth/internal/app/user/errors.go.hbs +7 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +64 -23
- package/templates/add/auth/internal/app/user/jwt.go.hbs +31 -7
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +39 -0
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +3 -0
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +26 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +9 -1
- package/templates/add/auth/internal/app/user/repository.go.hbs +64 -11
- package/templates/add/auth/internal/app/user/repository_test.go.hbs +192 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +105 -21
- package/templates/add/auth/internal/app/user/service_test.go.hbs +81 -2
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +9 -138
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +144 -0
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +147 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +21 -19
- package/templates/add/auth/internal/shared/middleware/ratelimit_memory.go.hbs +63 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit_redis.go.hbs +32 -0
- package/templates/add/auth/migrations/create_auth_tokens.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +16 -0
- package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +9 -5
- package/templates/add/auth/migrations/create_login_throttle.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
- package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_users.up.sql.hbs +13 -2
- package/templates/add/rbac/internal/app/role/dto.go.hbs +8 -3
- package/templates/add/rbac/internal/app/role/handler.go.hbs +4 -1
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +4 -0
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +3 -0
- package/templates/add/rbac/internal/app/role/repository.go.hbs +12 -11
- package/templates/add/rbac/internal/app/role/repository_test.go.hbs +176 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +7 -0
- package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +19 -0
- package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +1 -1
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +5 -5
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -11
- package/templates/add/worker/cmd/worker/main.go.hbs +30 -24
- package/templates/add/worker/internal/platform/mail/mail.go.hbs +21 -0
- package/templates/add/worker/internal/platform/mail/task.go.hbs +31 -34
- package/templates/add/worker/internal/platform/queue/asynq.go.hbs +140 -0
- package/templates/add/worker/internal/platform/queue/queue.go.hbs +87 -0
- package/templates/add/worker/internal/platform/queue/river.go.hbs +148 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +71 -12
- package/templates/create/base/.dockerignore.hbs +13 -0
- package/templates/create/base/.env.example.hbs +18 -9
- package/templates/create/base/.github/dependabot.yml.hbs +20 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +12 -2
- package/templates/create/base/.golangci.yml.hbs +27 -0
- package/templates/create/base/AGENTS.md.hbs +21 -7
- package/templates/create/base/Dockerfile.hbs +42 -0
- package/templates/create/base/Makefile.hbs +43 -13
- package/templates/create/base/README.md.hbs +45 -8
- package/templates/create/base/cmd/api/main.go.hbs +17 -130
- package/templates/create/base/cmd/api/wiring.go.hbs +161 -0
- package/templates/create/base/go.mod.hbs +4 -4
- package/templates/create/base/internal/platform/database/database.go.hbs +30 -11
- package/templates/create/base/internal/shared/config/config.go.hbs +13 -7
- package/templates/create/base/internal/shared/pagination/pagination.go.hbs +11 -0
- package/templates/create/base/internal/shared/tx/tx.go.hbs +47 -0
- package/templates/create/base/redocly.yaml.hbs +21 -0
- package/templates/create/features/docs/architecture.md.hbs +32 -11
- package/templates/create/features/docs/openapi.yaml.hbs +0 -4
- package/templates/create/features/docs/patterns.md.hbs +82 -8
- package/templates/create/features/docs/techstack.md.hbs +8 -3
- package/templates/generate/module/dto.go.hbs +8 -1
- package/templates/generate/module/errors.go.hbs +5 -0
- package/templates/generate/module/field-column.down.sql.hbs +2 -0
- package/templates/generate/module/field-column.up.sql.hbs +15 -0
- package/templates/generate/module/handler_test.go.hbs +8 -1
- package/templates/generate/module/migration.down.sql.hbs +3 -1
- package/templates/generate/module/migration.up.sql.hbs +7 -2
- package/templates/generate/module/minimal/dto.go.hbs +3 -1
- package/templates/generate/module/model/model.go.hbs +10 -1
- package/templates/generate/module/permission.up.sql.hbs +3 -1
- package/templates/generate/module/repository.go.hbs +60 -6
- package/templates/generate/module/repository_test.go.hbs +30 -0
- package/templates/generate/module/service.go.hbs +10 -1
- package/templates/generate/module/service_test.go.hbs +45 -0
- package/dist/commands/remove.js +0 -88
- package/scripts/smoke-test.mjs +0 -2058
- package/templates/add/worker/internal/platform/queue/client.go.hbs +0 -31
- package/templates/add/worker/internal/platform/queue/server.go.hbs +0 -68
- package/tests/integration/default-module.test.mjs +0 -46
- package/tests/integration/generator-naming.test.mjs +0 -81
- package/tests/integration/generator-unit-test-seams.test.mjs +0 -91
- package/tests/integration/legacy-method-compat.test.mjs +0 -222
- package/tests/integration/remove-module.test.mjs +0 -58
- package/tests/unit/naming.test.mjs +0 -94
- package/tests/unit/smoke-isolation.test.mjs +0 -35
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pranob Onphaeng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
A CLI that scaffolds a Gin + GORM + PostgreSQL Go backend, then keeps
|
|
4
4
|
generating consistent domain modules into that project as it grows — the Go
|
|
5
|
-
counterpart to
|
|
5
|
+
counterpart to nest-scaffold.
|
|
6
6
|
|
|
7
|
-
You don't hand-wire a new domain into `cmd/api/
|
|
7
|
+
You don't hand-wire a new domain into `cmd/api/wiring.go`, write the
|
|
8
8
|
handler/service/repository boilerplate, or decide error-handling conventions
|
|
9
9
|
each time — the CLI does that, and every module it generates follows the
|
|
10
10
|
same shape as the last one.
|
|
@@ -52,6 +52,11 @@ go-scaffold generate method orders approve --type patch
|
|
|
52
52
|
|
|
53
53
|
## Commands
|
|
54
54
|
|
|
55
|
+
Every `add` command shows what it's about to do and asks before writing;
|
|
56
|
+
`-y/--yes` skips that (and `--defaults` implies it) for CI and scripts.
|
|
57
|
+
Running `go-scaffold` with no arguments picks the command from a menu.
|
|
58
|
+
|
|
59
|
+
|
|
55
60
|
### `create <name>` — scaffold a new project
|
|
56
61
|
|
|
57
62
|
```bash
|
|
@@ -61,19 +66,20 @@ go-scaffold create my-api --defaults --no-docker --api-prefix beta
|
|
|
61
66
|
```
|
|
62
67
|
|
|
63
68
|
Produces a **bare skeleton only** — `cmd/api`, the shared platform packages
|
|
64
|
-
(config/apperror/dberr/httpx/id/middleware/pagination), Docker+Postgres,
|
|
69
|
+
(config/apperror/dberr/httpx/id/middleware/pagination/tx), Docker+Postgres,
|
|
65
70
|
migrations folder, and the standards docs (`docs/architect/`, `AGENTS.md`,
|
|
66
71
|
`CLAUDE.md`, `.claude/skills/go-scaffold/`). No domain modules — add those
|
|
67
72
|
with `generate module`.
|
|
68
73
|
|
|
69
74
|
| Option | Effect |
|
|
70
75
|
|---|---|
|
|
71
|
-
| `--defaults` | Skip the wizard, use defaults (Docker on, OpenAPI docs on, prefix
|
|
76
|
+
| `--defaults` | Skip the wizard, use defaults (Docker on, OpenAPI docs on, no route prefix) |
|
|
72
77
|
| `--no-docker` | Skip `docker-compose.yml` (with `--defaults`) |
|
|
73
78
|
| `--no-openapi-docs` | Skip `docs/openapi.yaml` (with `--defaults`) |
|
|
74
|
-
| `--
|
|
79
|
+
| `--observability` | Prometheus `/metrics` + OpenTelemetry tracing (with `--defaults`; off by default — `add observability` does the same later) |
|
|
80
|
+
| `--api-prefix <prefix>` | URL prefix every route is grouped under — opt in with e.g. `v1` or `api/v1`; omit it for none |
|
|
75
81
|
|
|
76
|
-
Without `--defaults`, an interactive wizard asks the same
|
|
82
|
+
Without `--defaults`, an interactive wizard asks the same four questions.
|
|
77
83
|
The prefix is a single project-wide choice made once at `create` time —
|
|
78
84
|
there's no per-domain versioning (a domain that needs a real breaking change
|
|
79
85
|
gets a new domain package or a new DTO field, not a duplicated model pointed
|
|
@@ -87,17 +93,21 @@ directory layout if missing).
|
|
|
87
93
|
### `generate module <name>` (alias `m`) — add a domain module
|
|
88
94
|
|
|
89
95
|
```bash
|
|
90
|
-
go-scaffold generate module orders #
|
|
91
|
-
go-scaffold generate module orders --full # opt-in CRUD skeleton
|
|
96
|
+
go-scaffold generate module orders # asks for the shape (and auth, if installed)
|
|
97
|
+
go-scaffold generate module orders --full # opt-in CRUD skeleton, no prompt
|
|
98
|
+
go-scaffold generate module orders --defaults # safe minimal module, no prompt (CI/scripting)
|
|
92
99
|
```
|
|
93
100
|
|
|
101
|
+
Anything you don't pass as a flag is asked for; `--defaults` takes the
|
|
102
|
+
documented defaults (minimal, no auth) and asks nothing.
|
|
103
|
+
|
|
94
104
|
`--full` scaffolds:
|
|
95
105
|
|
|
96
106
|
```text
|
|
97
107
|
internal/app/order/
|
|
98
108
|
├── model/model.go # domain model + GORM table (id/created_at/updated_at — add real fields yourself; a folder so multi-table domains can add more files)
|
|
99
109
|
├── dto.go # request/response structs (empty stubs — add real fields yourself)
|
|
100
|
-
├── errors.go # ORDER_NOT_FOUND / ORDER_CONFLICT / ORDER_HAS_REFERENCES
|
|
110
|
+
├── errors.go # ORDER_NOT_FOUND / ORDER_CONFLICT / ORDER_HAS_REFERENCES / ORDER_STALE
|
|
101
111
|
├── repository.go # GORM data access
|
|
102
112
|
├── service.go # business logic + repository interface (mockable)
|
|
103
113
|
├── handler.go # Gin routes, registered under the project's API prefix
|
|
@@ -115,11 +125,13 @@ surface, or you'd rather add endpoints one at a time.
|
|
|
115
125
|
|
|
116
126
|
Both modes also:
|
|
117
127
|
|
|
118
|
-
- Register the module in `cmd/api/
|
|
128
|
+
- Register the module in `cmd/api/wiring.go` (via marker comments — see
|
|
119
129
|
`// go-scaffold:*` in that file) — full wires an actual route, minimal
|
|
120
130
|
wires an empty route group
|
|
121
|
-
-
|
|
122
|
-
|
|
131
|
+
- Create the module's own Postgres schema (`<module>_svc`, e.g. `order_svc`)
|
|
132
|
+
and add the model to the `AutoMigrate(...)` call
|
|
133
|
+
- Append `migrations/<timestamp>_create_<plural>.{up,down}.sql`, which creates that
|
|
134
|
+
same schema for `AUTO_MIGRATE=false`/production
|
|
123
135
|
|
|
124
136
|
What it does **not** do: invent your fields or wire foreign keys between
|
|
125
137
|
domains — see `docs/architect/patterns.md` in the generated project for the
|
|
@@ -192,26 +204,75 @@ Creates timestamped `migrations/<version>_<name>.up.sql` and `.down.sql` TODO
|
|
|
192
204
|
stubs. The CLI reserves the names; you own the SQL and should apply it with
|
|
193
205
|
`make migrate-up` (or `migrate -path migrations -database "$DB_DSN" up`).
|
|
194
206
|
|
|
195
|
-
### `add worker` — add
|
|
207
|
+
### `add worker` — add background job processing
|
|
196
208
|
|
|
197
209
|
```bash
|
|
198
|
-
go-scaffold add worker
|
|
210
|
+
go-scaffold add worker # asks where jobs should live
|
|
211
|
+
go-scaffold add worker --queue postgres # River (default)
|
|
212
|
+
go-scaffold add worker --queue redis # Asynq
|
|
213
|
+
go-scaffold add worker --defaults # no prompt, Postgres
|
|
199
214
|
```
|
|
200
215
|
|
|
201
|
-
Adds
|
|
202
|
-
|
|
203
|
-
|
|
216
|
+
Adds `internal/platform/queue` (a backend-neutral contract plus one adapter),
|
|
217
|
+
async email delivery, and `cmd/worker`.
|
|
218
|
+
|
|
219
|
+
| | `--queue postgres` (River) | `--queue redis` (Asynq) |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| Extra service to run | none | Redis |
|
|
222
|
+
| Needed by `add auth` | no | no — `add auth --store` decides that separately |
|
|
223
|
+
| Enqueue joins your DB transaction | yes | **no** — needs an outbox |
|
|
224
|
+
| Throughput | thousands/sec | tens of thousands/sec |
|
|
225
|
+
| Inspect pending jobs | plain SQL | asynqmon |
|
|
226
|
+
|
|
227
|
+
The default is Postgres because a job enqueued inside `tx.Do` is then only
|
|
228
|
+
delivered if that transaction commits — no more welcome emails for signups
|
|
229
|
+
that rolled back. Run `make river-migrate` once per database to create
|
|
230
|
+
River's tables, then `make worker`.
|
|
231
|
+
|
|
232
|
+
Application code only ever sees `queue.Job`, `queue.Enqueuer` and
|
|
233
|
+
`queue.Handler` — no backend package appears outside its own adapter file, so
|
|
234
|
+
switching later means writing one adapter, not touching every module that
|
|
235
|
+
enqueues something.
|
|
236
|
+
|
|
237
|
+
```go
|
|
238
|
+
type WelcomeEmail struct{ To string `json:"to"` }
|
|
239
|
+
func (WelcomeEmail) Kind() string { return "email:welcome" }
|
|
240
|
+
|
|
241
|
+
// cmd/api — the job is discarded with the transaction if this fails
|
|
242
|
+
tx.Do(ctx, db, func(ctx context.Context) error {
|
|
243
|
+
if err := repo.Create(ctx, u); err != nil { return err }
|
|
244
|
+
return jobs.Enqueue(ctx, WelcomeEmail{To: u.Email}, nil)
|
|
245
|
+
})
|
|
246
|
+
```
|
|
204
247
|
|
|
205
248
|
### `add auth` — add email/password authentication
|
|
206
249
|
|
|
207
250
|
```bash
|
|
208
|
-
go-scaffold add auth
|
|
251
|
+
go-scaffold add auth # asks where tokens should live, then confirms
|
|
252
|
+
go-scaffold add auth --store postgres # tokens in Postgres, no extra service
|
|
253
|
+
go-scaffold add auth --store redis # tokens in Redis, exact across replicas
|
|
254
|
+
go-scaffold add auth --defaults # Postgres, no prompt at all (CI/scripting)
|
|
209
255
|
```
|
|
210
256
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
Google OAuth routes. Apply the generated migrations;
|
|
214
|
-
convenient in development, while production should use
|
|
257
|
+
Adds JWT access tokens, refresh-token rotation with reuse detection,
|
|
258
|
+
registration/login/logout, password reset, email verification, failed-login
|
|
259
|
+
lockout, and Google OAuth routes. Apply the generated migrations;
|
|
260
|
+
`AUTO_MIGRATE=true` is convenient in development, while production should use
|
|
261
|
+
`migrate up`.
|
|
262
|
+
|
|
263
|
+
No prerequisites. On a project with no worker the verification and reset mail
|
|
264
|
+
is sent inline, and `add worker` later moves it onto the queue for you — the
|
|
265
|
+
two endpoints that send mail block on SMTP until you do.
|
|
266
|
+
|
|
267
|
+
| `--store` | Tokens and rate-limit counters | Extra service |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `postgres` (default) | `user_svc.auth_tokens`, counters in-process | none |
|
|
270
|
+
| `redis` | Redis | Redis |
|
|
271
|
+
|
|
272
|
+
The rate limiter follows the store rather than being chosen separately, because
|
|
273
|
+
"I want this exact across replicas" is one decision. With `postgres` the per-IP
|
|
274
|
+
budget is per-replica; the failed-login lockout is in Postgres either way, since
|
|
275
|
+
that one can't be approximate.
|
|
215
276
|
|
|
216
277
|
### `add rbac` — add roles and permissions
|
|
217
278
|
|
|
@@ -225,31 +286,51 @@ middleware, and role assignment. Its migration seeds the default roles and
|
|
|
225
286
|
permissions, so apply it with `migrate up`: AutoMigrate creates tables but does
|
|
226
287
|
not run SQL seed statements.
|
|
227
288
|
|
|
228
|
-
###
|
|
289
|
+
### `add observability` — add metrics + tracing
|
|
229
290
|
|
|
230
291
|
```bash
|
|
231
|
-
go-scaffold
|
|
292
|
+
go-scaffold add observability # on an existing project
|
|
293
|
+
go-scaffold create my-api --defaults --observability # or at creation time
|
|
232
294
|
```
|
|
233
295
|
|
|
234
|
-
|
|
235
|
-
|
|
296
|
+
Adds Prometheus metrics at `/metrics` and OpenTelemetry tracing for Gin +
|
|
297
|
+
GORM, patched into `cmd/api/wiring.go` and `internal/platform/database` the same
|
|
298
|
+
way `add worker`/`add auth`/`add rbac` patch an existing project. Tracing is
|
|
299
|
+
disabled until `OTEL_EXPORTER_OTLP_ENDPOINT` is configured; `/metrics` works
|
|
300
|
+
either way. `create --observability` is exactly this command run right after
|
|
301
|
+
scaffolding — the two produce the same project.
|
|
236
302
|
|
|
237
|
-
### `
|
|
303
|
+
### `undo module <name>` (alias `undo m`) — take back a `generate module`
|
|
238
304
|
|
|
239
305
|
```bash
|
|
240
|
-
go-scaffold
|
|
241
|
-
go-scaffold
|
|
306
|
+
go-scaffold undo module orders # confirms first
|
|
307
|
+
go-scaffold undo m orders --yes # skip the confirm
|
|
242
308
|
```
|
|
243
309
|
|
|
244
|
-
The inverse of `generate module
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
the
|
|
252
|
-
|
|
310
|
+
The inverse of `generate module`, for the case it's actually the inverse of:
|
|
311
|
+
a module you didn't mean to generate — a typo'd name, a domain you decided
|
|
312
|
+
against. It deletes `internal/app/<name>/`, the per-module docs folder, **and
|
|
313
|
+
the module's migration files**, and reverses the import/AutoMigrate/route in
|
|
314
|
+
`main.go` plus the paths/schemas in `docs/openapi.yaml`. Restores the
|
|
315
|
+
`_ = api` placeholder if it was the last module, so the project still builds.
|
|
316
|
+
|
|
317
|
+
Deleting the migrations is the point. `migrations/embed.go` is a `//go:embed
|
|
318
|
+
*`, so a typo'd `create_oders` left behind once ran on every database created
|
|
319
|
+
from then on. That's only safe while those files exist nowhere but your
|
|
320
|
+
working tree, so `undo` proves it first and refuses loudly otherwise:
|
|
321
|
+
|
|
322
|
+
- **any of them is tracked by git** — it may already have been pulled or
|
|
323
|
+
deployed somewhere, so nothing is deleted. Retire that domain the explicit
|
|
324
|
+
way instead: `go-scaffold generate migration drop_<name>`.
|
|
325
|
+
- **your database is already at or past that version** (read via the `migrate`
|
|
326
|
+
CLI when it's on `PATH` and a DSN is configured) — deleting the files would
|
|
327
|
+
strand `schema_migrations` at a version with no migration behind it. Run
|
|
328
|
+
`migrate ... down` first, then try again.
|
|
329
|
+
|
|
330
|
+
The table itself is never dropped either way — `undo` only reverses what the
|
|
331
|
+
CLI wrote. Prefer it to hand-deleting the folder: it also un-wires `main.go`,
|
|
332
|
+
`.golangci.yml` and the OpenAPI index, and it refuses when another domain
|
|
333
|
+
still imports this one rather than leaving you an un-compilable project.
|
|
253
334
|
|
|
254
335
|
## Why no per-domain versioning
|
|
255
336
|
|
|
@@ -265,7 +346,7 @@ and `float64` in the other for the *same* column, converging it to
|
|
|
265
346
|
data with different, silently incompatible interpretations.
|
|
266
347
|
|
|
267
348
|
Instead, every route in a project is grouped under a single project-wide
|
|
268
|
-
`--api-prefix`
|
|
349
|
+
`--api-prefix` chosen once at `create` time — opt-in, no prefix unless you ask. A domain that
|
|
269
350
|
needs a real breaking change gets a new domain package, or a new field on
|
|
270
351
|
the existing DTO — not a duplicated model pointed at a table it can drift
|
|
271
352
|
out of sync with.
|
|
@@ -273,10 +354,10 @@ out of sync with.
|
|
|
273
354
|
## Project structure produced by `create`
|
|
274
355
|
|
|
275
356
|
```text
|
|
276
|
-
cmd/api/
|
|
357
|
+
cmd/api/wiring.go
|
|
277
358
|
internal/
|
|
278
359
|
├── platform/database/
|
|
279
|
-
├── shared/{config,apperror,dberr,httpx,id,middleware,pagination}/
|
|
360
|
+
├── shared/{config,apperror,dberr,httpx,id,middleware,pagination,tx}/
|
|
280
361
|
└── app/ # empty until you `generate module`
|
|
281
362
|
docs/
|
|
282
363
|
├── architect/{architecture,patterns,techstack}.md
|
|
@@ -284,8 +365,13 @@ docs/
|
|
|
284
365
|
migrations/
|
|
285
366
|
.github/workflows/ci.yml # build, vet, gofmt check, golangci-lint, go test (with a Postgres service)
|
|
286
367
|
Makefile
|
|
368
|
+
.env.example
|
|
369
|
+
.gitignore
|
|
287
370
|
.golangci.yml
|
|
371
|
+
redocly.yaml # if openapi docs enabled
|
|
372
|
+
docker-compose.yml # if Docker enabled
|
|
288
373
|
.vscode/settings.json
|
|
374
|
+
README.md
|
|
289
375
|
AGENTS.md
|
|
290
376
|
CLAUDE.md
|
|
291
377
|
.claude/skills/go-scaffold/SKILL.md
|
|
@@ -294,11 +380,14 @@ go-scaffold.config.json
|
|
|
294
380
|
|
|
295
381
|
## Supported stack
|
|
296
382
|
|
|
383
|
+
Pinned in the generated `go.mod` — this table mirrors
|
|
384
|
+
`templates/create/base/go.mod.hbs`, which is the source of truth.
|
|
385
|
+
|
|
297
386
|
| Package | Version |
|
|
298
387
|
|---|---|
|
|
299
|
-
| Gin | v1.10.
|
|
300
|
-
| GORM + postgres driver | v1.
|
|
301
|
-
| validator/v10 | v10.
|
|
388
|
+
| Gin | v1.10.1 |
|
|
389
|
+
| GORM + postgres driver | v1.31.2 / v1.6.2 |
|
|
390
|
+
| validator/v10 | v10.30.3 |
|
|
302
391
|
| google/uuid | v1.6.0 |
|
|
303
392
|
|
|
304
393
|
## License
|
package/dist/commands/auth.js
CHANGED
|
@@ -11,8 +11,13 @@ const config_1 = require("../utils/config");
|
|
|
11
11
|
const template_renderer_1 = require("../utils/template-renderer");
|
|
12
12
|
const auth_manifest_1 = require("../templates/auth-manifest");
|
|
13
13
|
const auth_patcher_1 = require("../utils/auth-patcher");
|
|
14
|
+
const platform_patcher_1 = require("../utils/platform-patcher");
|
|
15
|
+
const worker_manifest_1 = require("../templates/worker-manifest");
|
|
16
|
+
const golangci_patcher_1 = require("../utils/golangci-patcher");
|
|
14
17
|
const migrations_1 = require("../utils/migrations");
|
|
15
18
|
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
19
|
+
const gocheck_1 = require("../utils/gocheck");
|
|
20
|
+
const gomod_patcher_1 = require("../utils/gomod-patcher");
|
|
16
21
|
// URL (relative to the api prefix) -> docs file (relative to docs/) for every
|
|
17
22
|
// route `add auth` registers — kept next to AUTH_FILES's route list so the
|
|
18
23
|
// two are easy to eyeball together when a route changes.
|
|
@@ -36,16 +41,29 @@ const AUTH_OPENAPI_PATHS = [
|
|
|
36
41
|
// (no roles/permissions) — that's a separate opt-in on top of this, since
|
|
37
42
|
// most projects need "is this caller logged in" long before they need "can
|
|
38
43
|
// this caller do X".
|
|
39
|
-
async function addAuth(projectDir = process.cwd()) {
|
|
44
|
+
async function addAuth(store = "postgres", projectDir = process.cwd()) {
|
|
40
45
|
const config = (0, config_1.readConfig)(projectDir);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
// No longer a prerequisite. Without a worker the verification and reset mail
|
|
47
|
+
// goes out inline instead of through a queue — a real trade (those two
|
|
48
|
+
// endpoints then block on SMTP), but not one worth forcing a second binary
|
|
49
|
+
// and a queue-backend decision on someone who only wanted login.
|
|
50
|
+
const worker = config.features.worker ?? false;
|
|
44
51
|
const userDir = path_1.default.join(projectDir, "internal", "app", "user");
|
|
45
52
|
if (fs_extra_1.default.existsSync(userDir)) {
|
|
46
53
|
throw new Error(`${userDir} already exists — auth looks like it's already been added`);
|
|
47
54
|
}
|
|
48
|
-
|
|
55
|
+
const parsedBefore = (0, gocheck_1.parseChecks)(projectDir);
|
|
56
|
+
// Only `--store redis` needs Redis. With `--store postgres` (the default)
|
|
57
|
+
// auth adds no service at all — which is the whole point of the option.
|
|
58
|
+
if (store === "redis")
|
|
59
|
+
await ensureRedis(projectDir, config.goModule);
|
|
60
|
+
// the SMTP client, and the config it reads, normally arrive with `add worker`
|
|
61
|
+
if (!worker) {
|
|
62
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, worker_manifest_1.MAIL_CLIENT_ONLY, { goModule: config.goModule });
|
|
63
|
+
(0, platform_patcher_1.patchConfigForSMTP)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
|
|
64
|
+
patchEnvExampleForSMTP(path_1.default.join(projectDir, ".env.example"));
|
|
65
|
+
}
|
|
66
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, (0, auth_manifest_1.authFiles)(store), { goModule: config.goModule });
|
|
49
67
|
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
50
68
|
fs_extra_1.default.ensureDirSync(migrationsDir);
|
|
51
69
|
const usersVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
@@ -62,8 +80,35 @@ async function addAuth(projectDir = process.cwd()) {
|
|
|
62
80
|
{ template: "add/auth/migrations/create_identities.up.sql.hbs", output: path_1.default.join("migrations", `${identitiesVersion}_create_identities.up.sql`) },
|
|
63
81
|
{ template: "add/auth/migrations/create_identities.down.sql.hbs", output: path_1.default.join("migrations", `${identitiesVersion}_create_identities.down.sql`) },
|
|
64
82
|
], {});
|
|
83
|
+
// the failed-attempt counter is not a store choice — lockout has to survive a
|
|
84
|
+
// deploy and mean the same thing on every replica whichever store holds tokens
|
|
85
|
+
const throttleVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
86
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
|
|
87
|
+
{ template: "add/auth/migrations/create_login_throttle.up.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.up.sql`) },
|
|
88
|
+
{ template: "add/auth/migrations/create_login_throttle.down.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.down.sql`) },
|
|
89
|
+
], {});
|
|
90
|
+
if (store === "postgres") {
|
|
91
|
+
const authTokensVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
92
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
|
|
93
|
+
{ template: "add/auth/migrations/create_auth_tokens.up.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.up.sql`) },
|
|
94
|
+
{ template: "add/auth/migrations/create_auth_tokens.down.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.down.sql`) },
|
|
95
|
+
], {});
|
|
96
|
+
}
|
|
97
|
+
(0, golangci_patcher_1.patchGolangciForModule)(path_1.default.join(projectDir, ".golangci.yml"), config.goModule, "user");
|
|
65
98
|
(0, auth_patcher_1.patchConfigForAuth)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
|
|
66
|
-
(0, auth_patcher_1.patchMainGoForAuth)(path_1.default.join(projectDir, "cmd", "api", "
|
|
99
|
+
(0, auth_patcher_1.patchMainGoForAuth)(path_1.default.join(projectDir, "cmd", "api", "wiring.go"), {
|
|
100
|
+
goModule: config.goModule,
|
|
101
|
+
queueBackend: config.features.queue ?? "asynq",
|
|
102
|
+
store,
|
|
103
|
+
worker,
|
|
104
|
+
});
|
|
105
|
+
(0, gomod_patcher_1.patchGoModRequires)(path_1.default.join(projectDir, "go.mod"), [
|
|
106
|
+
"github.com/golang-jwt/jwt/v5 v5.3.1",
|
|
107
|
+
"golang.org/x/crypto v0.52.0",
|
|
108
|
+
"golang.org/x/oauth2 v0.36.0",
|
|
109
|
+
// go-redis only when something in this project actually constructs a client
|
|
110
|
+
...(store === "redis" ? ["github.com/redis/go-redis/v9 v9.22.0"] : []),
|
|
111
|
+
]);
|
|
67
112
|
patchEnvExample(path_1.default.join(projectDir, ".env.example"));
|
|
68
113
|
patchMakefile(path_1.default.join(projectDir, "Makefile"));
|
|
69
114
|
let docsMessage = "";
|
|
@@ -81,10 +126,20 @@ async function addAuth(projectDir = process.cwd()) {
|
|
|
81
126
|
docsMessage = "\ndocs: docs/auth/*.yaml, wired into docs/openapi.yaml";
|
|
82
127
|
}
|
|
83
128
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
84
|
-
|
|
129
|
+
// parse-only: jwt/oauth2/bcrypt aren't in go.mod until the `go mod tidy`
|
|
130
|
+
// printed below, so `go vet` can't be the gate here.
|
|
131
|
+
(0, gocheck_1.assertStillParses)(projectDir, parsedBefore, "added auth");
|
|
132
|
+
(0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, auth: true, authStore: store } });
|
|
85
133
|
console.log(picocolors_1.default.green("\nadded internal/app/user/, internal/shared/middleware/auth.go, and cmd/seed"));
|
|
86
|
-
console.log(
|
|
87
|
-
"
|
|
134
|
+
console.log(worker
|
|
135
|
+
? "verification + password-reset mail goes through the queue"
|
|
136
|
+
: "verification + password-reset mail is sent inline (no worker) — run `add worker` later to move it onto the queue");
|
|
137
|
+
console.log(store === "postgres"
|
|
138
|
+
? "tokens + rate-limit counters: Postgres (user_svc.auth_tokens) and in-process — no Redis"
|
|
139
|
+
: "tokens + rate-limit counters: Redis");
|
|
140
|
+
console.log("registered POST /auth/{register,login,refresh,logout,forgot-password,reset-password,verify-email}, " +
|
|
141
|
+
"GET /auth/google/{login,callback}, GET /users/me, and " +
|
|
142
|
+
"POST /users/me/{resend-verification,logout-all} in cmd/api/wiring.go" +
|
|
88
143
|
docsMessage);
|
|
89
144
|
console.log(picocolors_1.default.dim("\nnext: go mod tidy, then apply the new migrations (AUTO_MIGRATE=true picks them up automatically in dev)\n" +
|
|
90
145
|
"seed an admin: SEED_ADMIN_EMAIL=... SEED_ADMIN_PASSWORD=... make seed"));
|
|
@@ -100,8 +155,14 @@ function patchMakefile(makefilePath) {
|
|
|
100
155
|
"# environment, not .env, so a real secret never sits in a checked-in file.\n" +
|
|
101
156
|
"# --fixtures adds throwaway dev sample users, never use it outside dev.\n" +
|
|
102
157
|
"seed:\n" +
|
|
103
|
-
|
|
104
|
-
|
|
158
|
+
// `$$` throughout: make expands a single `$` as a variable reference, so
|
|
159
|
+
// `$//` became `//` and left sed an unterminated s/// expression — the
|
|
160
|
+
// recipe then failed, `.env` never loaded, and a bare `export` dumped the
|
|
161
|
+
// whole environment. Matches every target in Makefile.hbs verbatim.
|
|
162
|
+
"\t@set -a; [ -f $(ENV_FILE) ] && . ./$(ENV_FILE); set +a; go run ./cmd/seed $(ARGS)\n";
|
|
163
|
+
// Function replacer — target contains a literal "$$", see worker.ts's
|
|
164
|
+
// patchMakefile for why a string replacement would silently mangle it.
|
|
165
|
+
content = content.replace(/\nbuild:/, () => `${target}\nbuild:`);
|
|
105
166
|
fs_extra_1.default.writeFileSync(makefilePath, content);
|
|
106
167
|
}
|
|
107
168
|
function patchEnvExample(envExamplePath) {
|
|
@@ -117,6 +178,12 @@ function patchEnvExample(envExamplePath) {
|
|
|
117
178
|
"JWT_ACCESS_TTL_MIN=15\n" +
|
|
118
179
|
"JWT_REFRESH_TTL_MIN=43200\n" +
|
|
119
180
|
"COOKIE_SECURE=false\n" +
|
|
181
|
+
"# strict | lax | none — the refresh cookie's SameSite. Keep strict while the\n" +
|
|
182
|
+
"# frontend is the same site as this API (localhost:3000 -> localhost:8080 is,\n" +
|
|
183
|
+
"# and so is app.example.com -> api.example.com). A frontend on a different\n" +
|
|
184
|
+
"# site entirely needs none, together with COOKIE_SECURE=true, or the browser\n" +
|
|
185
|
+
"# never sends the cookie to /auth/refresh and sessions die at every expiry.\n" +
|
|
186
|
+
"COOKIE_SAMESITE=strict\n" +
|
|
120
187
|
"\nPASSWORD_RESET_TTL_MIN=30\n" +
|
|
121
188
|
"PASSWORD_RESET_URL=http://localhost:3000/reset-password\n" +
|
|
122
189
|
"\nEMAIL_VERIFY_TTL_MIN=1440\n" +
|
|
@@ -127,3 +194,41 @@ function patchEnvExample(envExamplePath) {
|
|
|
127
194
|
"GOOGLE_REDIRECT_URL=\n";
|
|
128
195
|
fs_extra_1.default.writeFileSync(envExamplePath, content);
|
|
129
196
|
}
|
|
197
|
+
// ensureRedis adds internal/platform/cache + its config/env/main.go wiring
|
|
198
|
+
// when the project doesn't have Redis yet — which is the normal case once
|
|
199
|
+
// the queue lives in Postgres. Idempotent: a project whose queue is Redis
|
|
200
|
+
// already has all of this and nothing happens.
|
|
201
|
+
async function ensureRedis(projectDir, goModule) {
|
|
202
|
+
const cacheGo = path_1.default.join(projectDir, "internal", "platform", "cache", "redis.go");
|
|
203
|
+
if (fs_extra_1.default.existsSync(cacheGo))
|
|
204
|
+
return;
|
|
205
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, [{ template: "add/worker/internal/platform/cache/redis.go.hbs", output: "internal/platform/cache/redis.go" }], { goModule });
|
|
206
|
+
(0, platform_patcher_1.patchConfigForRedis)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
|
|
207
|
+
(0, platform_patcher_1.patchMainGoForWorker)(path_1.default.join(projectDir, "cmd", "api", "wiring.go"), goModule);
|
|
208
|
+
(0, platform_patcher_1.patchComposeForRedis)(path_1.default.join(projectDir, "docker-compose.yml"));
|
|
209
|
+
(0, platform_patcher_1.patchCiForRedis)(path_1.default.join(projectDir, ".github", "workflows", "ci.yml"));
|
|
210
|
+
const envExamplePath = path_1.default.join(projectDir, ".env.example");
|
|
211
|
+
if (fs_extra_1.default.existsSync(envExamplePath)) {
|
|
212
|
+
const content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
|
|
213
|
+
if (!content.includes("REDIS_URL")) {
|
|
214
|
+
fs_extra_1.default.writeFileSync(envExamplePath, content.replace(/\n?$/, "\n") + "\n# refresh-token store\nREDIS_URL=redis://localhost:6379/0\n");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// patchEnvExampleForSMTP mirrors `add worker`'s own SMTP block, for the case
|
|
219
|
+
// where auth installed the mail client without a worker. Same append-once
|
|
220
|
+
// shape as every other env patcher.
|
|
221
|
+
function patchEnvExampleForSMTP(envExamplePath) {
|
|
222
|
+
if (!fs_extra_1.default.existsSync(envExamplePath))
|
|
223
|
+
return;
|
|
224
|
+
const content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
|
|
225
|
+
if (content.includes("SMTP_HOST"))
|
|
226
|
+
return;
|
|
227
|
+
fs_extra_1.default.writeFileSync(envExamplePath, content.replace(/\n?$/, "\n") +
|
|
228
|
+
"\n# leave SMTP_HOST unset to log emails instead of sending them (dev default)\n" +
|
|
229
|
+
"SMTP_HOST=\n" +
|
|
230
|
+
"SMTP_PORT=587\n" +
|
|
231
|
+
"SMTP_USERNAME=\n" +
|
|
232
|
+
"SMTP_PASSWORD=\n" +
|
|
233
|
+
"SMTP_FROM=no-reply@example.local\n");
|
|
234
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -13,6 +13,7 @@ const config_1 = require("../utils/config");
|
|
|
13
13
|
const naming_1 = require("../utils/naming");
|
|
14
14
|
const create_wizard_1 = require("../prompts/create-wizard");
|
|
15
15
|
const version_1 = require("../utils/version");
|
|
16
|
+
const observability_1 = require("./observability");
|
|
16
17
|
async function createProject(rawName, opts) {
|
|
17
18
|
const trimmed = (rawName ?? (await (0, create_wizard_1.promptProjectName)())).trim();
|
|
18
19
|
if (!trimmed)
|
|
@@ -28,7 +29,11 @@ async function createProject(rawName, opts) {
|
|
|
28
29
|
let apiPrefix;
|
|
29
30
|
if (opts.defaults) {
|
|
30
31
|
features = { docker: opts.docker ?? true, openapiDocs: opts.openapiDocs ?? true, observability: opts.observability ?? false };
|
|
31
|
-
|
|
32
|
+
// "" to match what the wizard's Enter now gives. --defaults means "don't
|
|
33
|
+
// ask me", not "give me something I didn't ask for", and a prefix is a
|
|
34
|
+
// project-wide decision you cannot change later without rewriting routes
|
|
35
|
+
// and every OpenAPI path.
|
|
36
|
+
apiPrefix = (0, naming_1.normalizeApiPrefix)(opts.apiPrefix ?? "");
|
|
32
37
|
const check = (0, naming_1.validateApiPrefix)(apiPrefix);
|
|
33
38
|
if (check !== true)
|
|
34
39
|
throw new Error(check);
|
|
@@ -47,6 +52,13 @@ async function createProject(rawName, opts) {
|
|
|
47
52
|
await (0, template_renderer_1.applyTemplateEntries)(projectDir, create_manifest_1.CREATE_MANIFEST, context);
|
|
48
53
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
49
54
|
(0, config_1.writeConfig)(projectDir, { projectName, goModule, apiPrefix, features, scaffoldVersion: (0, version_1.cliVersion)() });
|
|
55
|
+
// Composed the same way a user would do it by hand — `create` always
|
|
56
|
+
// renders the plain base, then this layers the same patches `add
|
|
57
|
+
// observability` would apply on an existing project, so the two paths
|
|
58
|
+
// produce identical output.
|
|
59
|
+
if (features.observability) {
|
|
60
|
+
await (0, observability_1.addObservability)(projectDir, { silent: true });
|
|
61
|
+
}
|
|
50
62
|
console.log(picocolors_1.default.green(`\ncreated ${projectName}/`));
|
|
51
63
|
console.log(`\ncd ${projectName}`);
|
|
52
64
|
if (features.docker)
|
|
@@ -14,6 +14,7 @@ const template_renderer_1 = require("../utils/template-renderer");
|
|
|
14
14
|
const module_manifest_1 = require("../templates/module-manifest");
|
|
15
15
|
const main_patcher_1 = require("../utils/main-patcher");
|
|
16
16
|
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
17
|
+
const golangci_patcher_1 = require("../utils/golangci-patcher");
|
|
17
18
|
const migrations_1 = require("../utils/migrations");
|
|
18
19
|
const gocheck_1 = require("../utils/gocheck");
|
|
19
20
|
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
@@ -43,6 +44,11 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
43
44
|
if (fs_extra_1.default.existsSync(moduleDir) && fs_extra_1.default.readdirSync(moduleDir).length > 0) {
|
|
44
45
|
throw new Error(`${moduleDir} already exists — pick a different name or delete it first`);
|
|
45
46
|
}
|
|
47
|
+
// Every marker this command patches has to be there before the first file
|
|
48
|
+
// is written — a failure after the module folder and its migration exist is
|
|
49
|
+
// a dead end, since the retry trips the "already exists" guard above.
|
|
50
|
+
const mainGoPath = path_1.default.join(projectDir, "cmd", "api", "wiring.go");
|
|
51
|
+
(0, main_patcher_1.assertMainGoPatchable)(mainGoPath);
|
|
46
52
|
// snapshot before writing anything, so assertNoDrift below can tell "we broke
|
|
47
53
|
// it" from "it was already broken"
|
|
48
54
|
const checkBefore = (0, gocheck_1.typeChecks)(projectDir);
|
|
@@ -63,19 +69,22 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
63
69
|
// skip if a create_<plural> migration already exists — re-running after
|
|
64
70
|
// only the module folder was deleted shouldn't leave a duplicate migration.
|
|
65
71
|
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
72
|
+
const slug = (0, naming_1.migrationSlug)(naming);
|
|
66
73
|
const migrationExists = fs_extra_1.default.existsSync(migrationsDir) &&
|
|
67
|
-
fs_extra_1.default
|
|
74
|
+
fs_extra_1.default
|
|
75
|
+
.readdirSync(migrationsDir)
|
|
76
|
+
.some((f) => (0, naming_1.migrationSlugAliases)(naming).some((alias) => f.endsWith(`_create_${alias}.up.sql`)));
|
|
68
77
|
let seq = "";
|
|
69
78
|
if (!migrationExists) {
|
|
70
79
|
seq = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
71
80
|
const migrationEntries = [
|
|
72
81
|
{
|
|
73
82
|
template: "generate/module/migration.up.sql.hbs",
|
|
74
|
-
output: path_1.default.join("migrations", `${seq}_create_${
|
|
83
|
+
output: path_1.default.join("migrations", `${seq}_create_${slug}.up.sql`),
|
|
75
84
|
},
|
|
76
85
|
{
|
|
77
86
|
template: "generate/module/migration.down.sql.hbs",
|
|
78
|
-
output: path_1.default.join("migrations", `${seq}_create_${
|
|
87
|
+
output: path_1.default.join("migrations", `${seq}_create_${slug}.down.sql`),
|
|
79
88
|
},
|
|
80
89
|
];
|
|
81
90
|
await (0, template_renderer_1.applyTemplateEntries)(projectDir, migrationEntries, context);
|
|
@@ -90,21 +99,22 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
90
99
|
const permissionEntries = [
|
|
91
100
|
{
|
|
92
101
|
template: "generate/module/permission.up.sql.hbs",
|
|
93
|
-
output: path_1.default.join("migrations", `${permissionSeq}_add_${
|
|
102
|
+
output: path_1.default.join("migrations", `${permissionSeq}_add_${slug}_permission.up.sql`),
|
|
94
103
|
},
|
|
95
104
|
{
|
|
96
105
|
template: "generate/module/permission.down.sql.hbs",
|
|
97
|
-
output: path_1.default.join("migrations", `${permissionSeq}_add_${
|
|
106
|
+
output: path_1.default.join("migrations", `${permissionSeq}_add_${slug}_permission.down.sql`),
|
|
98
107
|
},
|
|
99
108
|
];
|
|
100
109
|
await (0, template_renderer_1.applyTemplateEntries)(projectDir, permissionEntries, context);
|
|
101
110
|
}
|
|
102
|
-
|
|
111
|
+
(0, golangci_patcher_1.patchGolangciForModule)(path_1.default.join(projectDir, ".golangci.yml"), config.goModule, naming.pkg);
|
|
103
112
|
(0, main_patcher_1.patchMainGo)(mainGoPath, {
|
|
104
113
|
goModule: config.goModule,
|
|
105
114
|
modulePath,
|
|
106
115
|
pkg: naming.pkg,
|
|
107
116
|
pascalName: naming.pascalName,
|
|
117
|
+
schemaName: naming.schemaName,
|
|
108
118
|
auth: opts.auth,
|
|
109
119
|
permission: opts.permission,
|
|
110
120
|
});
|
|
@@ -125,10 +135,10 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
125
135
|
const routePath = config.apiPrefix ? `/${config.apiPrefix}/${naming.plural}` : `/${naming.plural}`;
|
|
126
136
|
console.log(picocolors_1.default.green(`\ngenerated internal/app/${modulePath}/`));
|
|
127
137
|
if (opts.full) {
|
|
128
|
-
console.log(`registered route ${routePath} in cmd/api/
|
|
138
|
+
console.log(`registered route ${routePath} in cmd/api/wiring.go`);
|
|
129
139
|
}
|
|
130
140
|
else {
|
|
131
|
-
console.log(`registered empty route group ${routePath} in cmd/api/
|
|
141
|
+
console.log(`registered empty route group ${routePath} in cmd/api/wiring.go — ` +
|
|
132
142
|
`add endpoints with \`go-scaffold generate method ${naming.pkg} <name> --type ...\``);
|
|
133
143
|
}
|
|
134
144
|
if (opts.permission) {
|
|
@@ -141,13 +151,13 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
141
151
|
console.log(picocolors_1.default.yellow(`note: this project has auth installed, but ${routePath} is PUBLIC — re-run with --auth (and --permission <code> if you also have rbac) to require login`));
|
|
142
152
|
}
|
|
143
153
|
if (seq) {
|
|
144
|
-
console.log(`migration: migrations/${seq}_create_${
|
|
154
|
+
console.log(`migration: migrations/${seq}_create_${slug}.{up,down}.sql`);
|
|
145
155
|
}
|
|
146
156
|
else {
|
|
147
|
-
console.log(`migration: reused existing migrations/*_create_${
|
|
157
|
+
console.log(`migration: reused existing migrations/*_create_${slug}.{up,down}.sql`);
|
|
148
158
|
}
|
|
149
159
|
if (permissionSeq) {
|
|
150
|
-
console.log(`migration: migrations/${permissionSeq}_add_${
|
|
160
|
+
console.log(`migration: migrations/${permissionSeq}_add_${slug}_permission.{up,down}.sql (seeds the "${opts.permission}" permission — grant it to a role via PATCH /roles/:code/permissions)`);
|
|
151
161
|
}
|
|
152
162
|
if (docsMessage)
|
|
153
163
|
console.log(docsMessage);
|