@nakedev/go-scaffold 0.1.2 → 0.1.4
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/README.md +93 -14
- package/dist/commands/auth.js +129 -0
- package/dist/commands/create.js +3 -2
- package/dist/commands/generate.js +61 -2
- package/dist/commands/method.js +66 -15
- package/dist/commands/migration.js +34 -0
- package/dist/commands/rbac.js +103 -0
- package/dist/commands/remove.js +36 -20
- package/dist/commands/worker.js +75 -0
- package/dist/index.js +71 -7
- package/dist/prompts/create-wizard.js +6 -1
- package/dist/prompts/generate-wizard.js +8 -0
- package/dist/templates/auth-manifest.js +19 -0
- package/dist/templates/create-manifest.js +25 -0
- package/dist/templates/module-manifest.js +2 -0
- package/dist/templates/rbac-manifest.js +17 -0
- package/dist/templates/worker-manifest.js +12 -0
- package/dist/utils/auth-patcher.js +96 -0
- package/dist/utils/gocheck.js +65 -0
- package/dist/utils/main-patcher.js +8 -1
- package/dist/utils/method-patcher.js +80 -16
- package/dist/utils/migrations.js +30 -8
- package/dist/utils/module-location.js +22 -0
- package/dist/utils/naming.js +75 -12
- package/dist/utils/openapi-patcher.js +35 -1
- package/dist/utils/platform-patcher.js +59 -0
- package/dist/utils/rbac-patcher.js +277 -0
- package/dist/utils/smoke-run.js +31 -0
- package/dist/utils/version.js +24 -0
- package/package.json +15 -6
- package/scripts/smoke-test.mjs +2058 -0
- package/templates/add/auth/cmd/seed/main.go.hbs +76 -0
- package/templates/add/auth/docs/forgot-password.yaml.hbs +19 -0
- package/templates/add/auth/docs/google-callback.yaml.hbs +22 -0
- package/templates/add/auth/docs/google-login.yaml.hbs +7 -0
- package/templates/add/auth/docs/login.yaml.hbs +19 -0
- package/templates/add/auth/docs/logout.yaml.hbs +8 -0
- package/templates/add/auth/docs/refresh.yaml.hbs +15 -0
- package/templates/add/auth/docs/register.yaml.hbs +19 -0
- package/templates/add/auth/docs/reset-password.yaml.hbs +16 -0
- package/templates/add/auth/docs/schemas.yaml.hbs +58 -0
- package/templates/add/auth/docs/users-me-logout-all.yaml.hbs +9 -0
- package/templates/add/auth/docs/users-me-resend-verification.yaml.hbs +10 -0
- package/templates/add/auth/docs/users-me.yaml.hbs +12 -0
- package/templates/add/auth/docs/verify-email.yaml.hbs +16 -0
- package/templates/add/auth/internal/app/user/dto.go.hbs +77 -0
- package/templates/add/auth/internal/app/user/errors.go.hbs +36 -0
- package/templates/add/auth/internal/app/user/handler.go.hbs +235 -0
- package/templates/add/auth/internal/app/user/jwt.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +28 -0
- package/templates/add/auth/internal/app/user/model/user.go.hbs +22 -0
- package/templates/add/auth/internal/app/user/repository.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/service.go.hbs +447 -0
- package/templates/add/auth/internal/app/user/service_test.go.hbs +237 -0
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +159 -0
- package/templates/add/auth/internal/shared/middleware/auth.go.hbs +64 -0
- package/templates/add/auth/internal/shared/middleware/ratelimit.go.hbs +44 -0
- package/templates/add/auth/migrations/create_identities.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +11 -0
- package/templates/add/auth/migrations/create_users.down.sql.hbs +1 -0
- package/templates/add/auth/migrations/create_users.up.sql.hbs +9 -0
- package/templates/add/rbac/docs/permissions.yaml.hbs +24 -0
- package/templates/add/rbac/docs/role-permissions.yaml.hbs +31 -0
- package/templates/add/rbac/docs/role.yaml.hbs +17 -0
- package/templates/add/rbac/docs/roles.yaml.hbs +43 -0
- package/templates/add/rbac/docs/schemas.yaml.hbs +37 -0
- package/templates/add/rbac/docs/user-set-role.yaml.hbs +23 -0
- package/templates/add/rbac/docs/user.yaml.hbs +16 -0
- package/templates/add/rbac/docs/users.yaml.hbs +23 -0
- package/templates/add/rbac/internal/app/role/dto.go.hbs +40 -0
- package/templates/add/rbac/internal/app/role/errors.go.hbs +39 -0
- package/templates/add/rbac/internal/app/role/handler.go.hbs +101 -0
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +9 -0
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +18 -0
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +8 -0
- package/templates/add/rbac/internal/app/role/repository.go.hbs +96 -0
- package/templates/add/rbac/internal/app/role/service.go.hbs +210 -0
- package/templates/add/rbac/internal/app/role/service_test.go.hbs +119 -0
- package/templates/add/rbac/internal/shared/middleware/authz.go.hbs +88 -0
- package/templates/add/rbac/internal/shared/middleware/authz_test.go.hbs +88 -0
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +15 -0
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +35 -0
- package/templates/add/worker/cmd/worker/main.go.hbs +77 -0
- package/templates/add/worker/internal/platform/cache/redis.go.hbs +18 -0
- package/templates/add/worker/internal/platform/mail/mail.go.hbs +48 -0
- package/templates/add/worker/internal/platform/mail/task.go.hbs +52 -0
- package/templates/add/worker/internal/platform/queue/client.go.hbs +31 -0
- package/templates/add/worker/internal/platform/queue/server.go.hbs +68 -0
- package/templates/create/base/.env.example.hbs +20 -0
- package/templates/create/base/.github/workflows/ci.yml.hbs +19 -4
- package/templates/create/base/.gitignore.hbs +2 -0
- package/templates/create/base/AGENTS.md.hbs +10 -12
- package/templates/create/base/Makefile.hbs +39 -8
- package/templates/create/base/README.md.hbs +52 -12
- package/templates/create/base/cmd/api/main.go.hbs +26 -1
- package/templates/create/base/internal/platform/database/database.go.hbs +53 -0
- package/templates/create/base/internal/shared/config/config.go.hbs +43 -1
- package/templates/create/base/internal/shared/middleware/cors.go.hbs +29 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +13 -1
- package/templates/create/base/migrations/embed.go.hbs +15 -0
- package/templates/create/features/docs/architecture.md.hbs +22 -0
- package/templates/create/features/docs/common/responses.yaml.hbs +15 -0
- package/templates/create/features/docs/observability/metrics.yaml.hbs +12 -0
- package/templates/create/features/docs/openapi.yaml.hbs +17 -5
- package/templates/create/features/docs/patterns.md.hbs +9 -6
- package/templates/create/features/docs/techstack.md.hbs +3 -0
- package/templates/create/features/observability/middleware/metrics.go.hbs +41 -0
- package/templates/create/features/observability/middleware/tracing.go.hbs +46 -0
- package/templates/create/features/observability/platform/telemetry/tracing.go.hbs +130 -0
- package/templates/generate/module/docs/item.yaml.hbs +3 -3
- package/templates/generate/module/handler.go.hbs +37 -6
- package/templates/generate/module/handler_test.go.hbs +113 -51
- package/templates/generate/module/migration.down.sql.hbs +1 -1
- package/templates/generate/module/migration.up.sql.hbs +1 -1
- package/templates/generate/module/minimal/handler.go.hbs +28 -4
- package/templates/generate/module/minimal/handler_test.go.hbs +5 -65
- package/templates/generate/module/minimal/service_test.go.hbs +39 -16
- package/templates/generate/module/model/model.go.hbs +7 -0
- package/templates/generate/module/permission.down.sql.hbs +5 -0
- package/templates/generate/module/permission.up.sql.hbs +4 -0
- package/templates/generate/module/repository_test.go.hbs +79 -0
- package/templates/generate/module/service.go.hbs +6 -4
- package/templates/generate/module/service_test.go.hbs +68 -17
- package/tests/integration/default-module.test.mjs +46 -0
- package/tests/integration/generator-naming.test.mjs +81 -0
- package/tests/integration/generator-unit-test-seams.test.mjs +91 -0
- package/tests/integration/legacy-method-compat.test.mjs +222 -0
- package/tests/integration/remove-module.test.mjs +58 -0
- package/tests/unit/naming.test.mjs +94 -0
- package/tests/unit/smoke-isolation.test.mjs +35 -0
- package/dist/utils/module-paths.js +0 -33
package/README.md
CHANGED
|
@@ -87,25 +87,26 @@ directory layout if missing).
|
|
|
87
87
|
### `generate module <name>` (alias `m`) — add a domain module
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
go-scaffold generate module orders #
|
|
91
|
-
go-scaffold generate module orders --
|
|
90
|
+
go-scaffold generate module orders # safe minimal module (default)
|
|
91
|
+
go-scaffold generate module orders --full # opt-in CRUD skeleton
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
`--full` scaffolds:
|
|
95
95
|
|
|
96
96
|
```text
|
|
97
|
-
internal/app/
|
|
97
|
+
internal/app/order/
|
|
98
98
|
├── 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
99
|
├── dto.go # request/response structs (empty stubs — add real fields yourself)
|
|
100
|
-
├── errors.go #
|
|
100
|
+
├── errors.go # ORDER_NOT_FOUND / ORDER_CONFLICT / ORDER_HAS_REFERENCES
|
|
101
101
|
├── repository.go # GORM data access
|
|
102
102
|
├── service.go # business logic + repository interface (mockable)
|
|
103
103
|
├── handler.go # Gin routes, registered under the project's API prefix
|
|
104
|
-
├── service_test.go # unit test,
|
|
105
|
-
|
|
104
|
+
├── service_test.go # unit test, function-backed repository stub
|
|
105
|
+
├── handler_test.go # HTTP unit test, service stub, no DB
|
|
106
|
+
└── repository_test.go # Postgres integration test against migrated schema
|
|
106
107
|
```
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
The default minimal mode scaffolds the same `model`/`errors`/`repository` (so `generate
|
|
109
110
|
method` always has a full data-access surface to call), but `dto`/`service`/
|
|
110
111
|
`handler` start empty — no default CRUD, no routes, just the plumbing
|
|
111
112
|
(`Register()`, the `repository` interface, `wrapFindErr`) that `generate
|
|
@@ -145,7 +146,7 @@ overwrites a method with the same name; picks a different one or errors.
|
|
|
145
146
|
| `--type` | Route | What's generated |
|
|
146
147
|
|---|---|---|
|
|
147
148
|
| `get --get-mode all` | `GET /<plural>/<kebab-name>` | reuses `FindAll` — TODO to add real filtering |
|
|
148
|
-
| `get --get-mode one --field <f>` | `GET /<plural>/<f>/:<f>` | a real `FindBy<F>` query added to the repository (+ its interface +
|
|
149
|
+
| `get --get-mode one --field <f>` | `GET /<plural>/<f>/:<f>` | a real `FindBy<F>` query added to the repository (+ its interface + function-backed repository test stub) |
|
|
149
150
|
| `post` | `POST /<plural>/<kebab-name>` | adds a body DTO; service is a TODO stub |
|
|
150
151
|
| `put` / `patch` | `<VERB> /<plural>/:id/<kebab-name>` | finds by id, TODO before saving (safe no-op until implemented) |
|
|
151
152
|
| `delete` | `DELETE /<plural>/:id/<kebab-name>` | TODO stub |
|
|
@@ -154,8 +155,84 @@ Business logic is always left as a `TODO`-marked stub that compiles and
|
|
|
154
155
|
returns a clean `500` rather than inventing behavior — see
|
|
155
156
|
`docs/architect/patterns.md` in the generated project.
|
|
156
157
|
|
|
157
|
-
`generate method`
|
|
158
|
-
`docs
|
|
158
|
+
When OpenAPI docs are enabled, `generate method` also creates a valid TODO stub
|
|
159
|
+
under `docs/<plural>/methods/` and wires the route into `docs/openapi.yaml`.
|
|
160
|
+
Replace its placeholder request/response schemas while implementing the TODO.
|
|
161
|
+
|
|
162
|
+
**Drift check** — `generate` type-checks the project (`go vet ./...`) before and
|
|
163
|
+
after it writes. If the project was fine beforehand and the generated code
|
|
164
|
+
doesn't compile, it stops with the compiler output instead of leaving you to
|
|
165
|
+
find it later:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
the generated code doesn't compile, but this project was fine a moment ago.
|
|
169
|
+
|
|
170
|
+
The most likely cause is drift: this project's internal/shared layer has been edited
|
|
171
|
+
since it was scaffolded, so the templates this CLI emits no longer match it.
|
|
172
|
+
|
|
173
|
+
scaffolded with: go-scaffold 0.1.2
|
|
174
|
+
this CLI: go-scaffold 0.3.0
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
That happens because `generate`'s templates are written against the `shared/`
|
|
178
|
+
layer `create` emits — editing that layer is normal work, but it moves the
|
|
179
|
+
project away from what this CLI's templates expect. `create` records its own
|
|
180
|
+
version in `go-scaffold.config.json` so the message can name both sides. A
|
|
181
|
+
project that was *already* broken (mid-refactor, or `go mod tidy` not run yet)
|
|
182
|
+
is left alone — only a passed-before/broken-after transition is reported. No Go
|
|
183
|
+
on `PATH` means the check is skipped.
|
|
184
|
+
|
|
185
|
+
### `generate migration <name>` (alias `mig`) — reserve a SQL migration pair
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
go-scaffold generate migration add_status_to_orders
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Creates timestamped `migrations/<version>_<name>.up.sql` and `.down.sql` TODO
|
|
192
|
+
stubs. The CLI reserves the names; you own the SQL and should apply it with
|
|
193
|
+
`make migrate-up` (or `migrate -path migrations -database "$DB_DSN" up`).
|
|
194
|
+
|
|
195
|
+
### `add worker` — add Redis-backed background work
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
go-scaffold add worker
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Adds Redis cache/queue support, async email delivery, and `cmd/worker`. It also
|
|
202
|
+
makes `/readyz` report unavailable when Redis is down. Run `go mod tidy` and
|
|
203
|
+
provide `REDIS_URL` before starting the API or worker.
|
|
204
|
+
|
|
205
|
+
### `add auth` — add email/password authentication
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
go-scaffold add auth
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Requires `add worker`. Adds JWT access tokens, Redis-backed refresh-token
|
|
212
|
+
rotation, registration/login/logout, password reset, email verification, and
|
|
213
|
+
Google OAuth routes. Apply the generated migrations; `AUTO_MIGRATE=true` is
|
|
214
|
+
convenient in development, while production should use `migrate up`.
|
|
215
|
+
|
|
216
|
+
### `add rbac` — add roles and permissions
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
go-scaffold add rbac
|
|
220
|
+
go-scaffold generate module secrets --auth --permission secret:manage
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Requires `add auth`. Adds role/permission administration, cached authorization
|
|
224
|
+
middleware, and role assignment. Its migration seeds the default roles and
|
|
225
|
+
permissions, so apply it with `migrate up`: AutoMigrate creates tables but does
|
|
226
|
+
not run SQL seed statements.
|
|
227
|
+
|
|
228
|
+
### Observability at project creation
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
go-scaffold create my-api --defaults --observability
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Opt-in observability adds Prometheus metrics at `/metrics` and OpenTelemetry
|
|
235
|
+
tracing. Tracing is disabled until `OTEL_EXPORTER_OTLP_ENDPOINT` is configured.
|
|
159
236
|
|
|
160
237
|
### `remove module <name>` (alias `rm m`) — drop a domain
|
|
161
238
|
|
|
@@ -165,9 +242,11 @@ go-scaffold rm m orders --yes # skip the confirm
|
|
|
165
242
|
```
|
|
166
243
|
|
|
167
244
|
The inverse of `generate module`: deletes `internal/app/<name>/` and reverses
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
245
|
+
the import/AutoMigrate/route in `main.go`, paths/schemas in `docs/openapi.yaml`,
|
|
246
|
+
and the per-module docs folder. **Existing migrations are preserved** because
|
|
247
|
+
production may already have recorded those immutable versions. The table/data
|
|
248
|
+
are also untouched; create a new `generate migration drop_<table>` migration
|
|
249
|
+
when removal is intentional. Restores the `_ = api` placeholder if it was the
|
|
171
250
|
last module, so the project still builds. Use this instead of hand-deleting
|
|
172
251
|
the folder — a partial hand-delete leaves stale wiring that duplicates on the
|
|
173
252
|
next `generate module` (which would panic gin at startup).
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.addAuth = addAuth;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const config_1 = require("../utils/config");
|
|
11
|
+
const template_renderer_1 = require("../utils/template-renderer");
|
|
12
|
+
const auth_manifest_1 = require("../templates/auth-manifest");
|
|
13
|
+
const auth_patcher_1 = require("../utils/auth-patcher");
|
|
14
|
+
const migrations_1 = require("../utils/migrations");
|
|
15
|
+
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
16
|
+
// URL (relative to the api prefix) -> docs file (relative to docs/) for every
|
|
17
|
+
// route `add auth` registers — kept next to AUTH_FILES's route list so the
|
|
18
|
+
// two are easy to eyeball together when a route changes.
|
|
19
|
+
const AUTH_OPENAPI_PATHS = [
|
|
20
|
+
{ urlPath: "/auth/register", file: "./auth/register.yaml" },
|
|
21
|
+
{ urlPath: "/auth/login", file: "./auth/login.yaml" },
|
|
22
|
+
{ urlPath: "/auth/refresh", file: "./auth/refresh.yaml" },
|
|
23
|
+
{ urlPath: "/auth/logout", file: "./auth/logout.yaml" },
|
|
24
|
+
{ urlPath: "/auth/forgot-password", file: "./auth/forgot-password.yaml" },
|
|
25
|
+
{ urlPath: "/auth/reset-password", file: "./auth/reset-password.yaml" },
|
|
26
|
+
{ urlPath: "/auth/verify-email", file: "./auth/verify-email.yaml" },
|
|
27
|
+
{ urlPath: "/auth/google/login", file: "./auth/google-login.yaml" },
|
|
28
|
+
{ urlPath: "/auth/google/callback", file: "./auth/google-callback.yaml" },
|
|
29
|
+
{ urlPath: "/users/me", file: "./auth/users-me.yaml" },
|
|
30
|
+
{ urlPath: "/users/me/resend-verification", file: "./auth/users-me-resend-verification.yaml" },
|
|
31
|
+
{ urlPath: "/users/me/logout-all", file: "./auth/users-me-logout-all.yaml" },
|
|
32
|
+
];
|
|
33
|
+
// addAuth scaffolds email/password authentication: a users+identities model
|
|
34
|
+
// pair, JWT access tokens, a Redis-backed refresh token store with
|
|
35
|
+
// rotation + reuse detection, and register/login/refresh/logout/me. No RBAC
|
|
36
|
+
// (no roles/permissions) — that's a separate opt-in on top of this, since
|
|
37
|
+
// most projects need "is this caller logged in" long before they need "can
|
|
38
|
+
// this caller do X".
|
|
39
|
+
async function addAuth(projectDir = process.cwd()) {
|
|
40
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
41
|
+
if (!config.features.worker) {
|
|
42
|
+
throw new Error("`go-scaffold add auth` requires `go-scaffold add worker` first — the refresh token store needs Redis");
|
|
43
|
+
}
|
|
44
|
+
const userDir = path_1.default.join(projectDir, "internal", "app", "user");
|
|
45
|
+
if (fs_extra_1.default.existsSync(userDir)) {
|
|
46
|
+
throw new Error(`${userDir} already exists — auth looks like it's already been added`);
|
|
47
|
+
}
|
|
48
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, auth_manifest_1.AUTH_FILES, { goModule: config.goModule });
|
|
49
|
+
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
50
|
+
fs_extra_1.default.ensureDirSync(migrationsDir);
|
|
51
|
+
const usersVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
52
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
|
|
53
|
+
{ template: "add/auth/migrations/create_users.up.sql.hbs", output: path_1.default.join("migrations", `${usersVersion}_create_users.up.sql`) },
|
|
54
|
+
{ template: "add/auth/migrations/create_users.down.sql.hbs", output: path_1.default.join("migrations", `${usersVersion}_create_users.down.sql`) },
|
|
55
|
+
], {});
|
|
56
|
+
// identities references users(id) — must apply strictly after it. A
|
|
57
|
+
// second newMigrationVersion() call, scanning the dir again now that the
|
|
58
|
+
// users pair is already written, guarantees a later (or same-second,
|
|
59
|
+
// bumped) timestamp rather than assuming +1 by hand.
|
|
60
|
+
const identitiesVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
61
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
|
|
62
|
+
{ template: "add/auth/migrations/create_identities.up.sql.hbs", output: path_1.default.join("migrations", `${identitiesVersion}_create_identities.up.sql`) },
|
|
63
|
+
{ template: "add/auth/migrations/create_identities.down.sql.hbs", output: path_1.default.join("migrations", `${identitiesVersion}_create_identities.down.sql`) },
|
|
64
|
+
], {});
|
|
65
|
+
(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", "main.go"), config.goModule);
|
|
67
|
+
patchEnvExample(path_1.default.join(projectDir, ".env.example"));
|
|
68
|
+
patchMakefile(path_1.default.join(projectDir, "Makefile"));
|
|
69
|
+
let docsMessage = "";
|
|
70
|
+
const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
|
|
71
|
+
if (config.features.openapiDocs && fs_extra_1.default.existsSync(openapiPath)) {
|
|
72
|
+
const docsEntries = [
|
|
73
|
+
{ template: "add/auth/docs/schemas.yaml.hbs", output: path_1.default.join("docs", "auth", "schemas.yaml") },
|
|
74
|
+
...AUTH_OPENAPI_PATHS.map(({ file }) => ({
|
|
75
|
+
template: `add/auth/docs/${path_1.default.basename(file)}.hbs`,
|
|
76
|
+
output: path_1.default.join("docs", "auth", path_1.default.basename(file)),
|
|
77
|
+
})),
|
|
78
|
+
];
|
|
79
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, docsEntries, {});
|
|
80
|
+
(0, openapi_patcher_1.patchOpenapiIndexRaw)(openapiPath, config.apiPrefix, AUTH_OPENAPI_PATHS);
|
|
81
|
+
docsMessage = "\ndocs: docs/auth/*.yaml, wired into docs/openapi.yaml";
|
|
82
|
+
}
|
|
83
|
+
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
84
|
+
(0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, auth: true } });
|
|
85
|
+
console.log(picocolors_1.default.green("\nadded internal/app/user/, internal/shared/middleware/auth.go, and cmd/seed"));
|
|
86
|
+
console.log("registered POST /auth/{register,login,refresh,logout,forgot-password,reset-password}, " +
|
|
87
|
+
"GET /auth/google/{login,callback}, and GET /users/me in cmd/api/main.go" +
|
|
88
|
+
docsMessage);
|
|
89
|
+
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
|
+
"seed an admin: SEED_ADMIN_EMAIL=... SEED_ADMIN_PASSWORD=... make seed"));
|
|
91
|
+
}
|
|
92
|
+
function patchMakefile(makefilePath) {
|
|
93
|
+
if (!fs_extra_1.default.existsSync(makefilePath))
|
|
94
|
+
return;
|
|
95
|
+
let content = fs_extra_1.default.readFileSync(makefilePath, "utf8");
|
|
96
|
+
if (content.includes("\nseed:\n"))
|
|
97
|
+
return; // already added
|
|
98
|
+
content = content.replace(/^\.PHONY: /m, ".PHONY: seed ");
|
|
99
|
+
const target = "\n# bootstrap an admin user (idempotent) — SEED_ADMIN_EMAIL/PASSWORD from the\n" +
|
|
100
|
+
"# environment, not .env, so a real secret never sits in a checked-in file.\n" +
|
|
101
|
+
"# --fixtures adds throwaway dev sample users, never use it outside dev.\n" +
|
|
102
|
+
"seed:\n" +
|
|
103
|
+
"\t@[ -f .env ] && export $$(grep -v '^#' .env | sed -E 's/[[:space:]]+#.*$//' | xargs); go run ./cmd/seed $(ARGS)\n";
|
|
104
|
+
content = content.replace(/\nbuild:/, `${target}\nbuild:`);
|
|
105
|
+
fs_extra_1.default.writeFileSync(makefilePath, content);
|
|
106
|
+
}
|
|
107
|
+
function patchEnvExample(envExamplePath) {
|
|
108
|
+
if (!fs_extra_1.default.existsSync(envExamplePath))
|
|
109
|
+
return;
|
|
110
|
+
let content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
|
|
111
|
+
if (content.includes("JWT_SECRET"))
|
|
112
|
+
return; // already added
|
|
113
|
+
content =
|
|
114
|
+
content.replace(/\n?$/, "\n") +
|
|
115
|
+
"\n# HS256 signing secret for access tokens — change this before deploying with APP_ENV=production\n" +
|
|
116
|
+
"JWT_SECRET=dev-secret-change-me\n" +
|
|
117
|
+
"JWT_ACCESS_TTL_MIN=15\n" +
|
|
118
|
+
"JWT_REFRESH_TTL_MIN=43200\n" +
|
|
119
|
+
"COOKIE_SECURE=false\n" +
|
|
120
|
+
"\nPASSWORD_RESET_TTL_MIN=30\n" +
|
|
121
|
+
"PASSWORD_RESET_URL=http://localhost:3000/reset-password\n" +
|
|
122
|
+
"\nEMAIL_VERIFY_TTL_MIN=1440\n" +
|
|
123
|
+
"EMAIL_VERIFY_URL=http://localhost:3000/verify-email\n" +
|
|
124
|
+
"\n# leave the Google vars unset to disable Google login (register/login/refresh still work)\n" +
|
|
125
|
+
"GOOGLE_CLIENT_ID=\n" +
|
|
126
|
+
"GOOGLE_CLIENT_SECRET=\n" +
|
|
127
|
+
"GOOGLE_REDIRECT_URL=\n";
|
|
128
|
+
fs_extra_1.default.writeFileSync(envExamplePath, content);
|
|
129
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -12,6 +12,7 @@ const create_manifest_1 = require("../templates/create-manifest");
|
|
|
12
12
|
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
|
+
const version_1 = require("../utils/version");
|
|
15
16
|
async function createProject(rawName, opts) {
|
|
16
17
|
const trimmed = (rawName ?? (await (0, create_wizard_1.promptProjectName)())).trim();
|
|
17
18
|
if (!trimmed)
|
|
@@ -26,7 +27,7 @@ async function createProject(rawName, opts) {
|
|
|
26
27
|
let features;
|
|
27
28
|
let apiPrefix;
|
|
28
29
|
if (opts.defaults) {
|
|
29
|
-
features = { docker: opts.docker ?? true, openapiDocs: opts.openapiDocs ?? true };
|
|
30
|
+
features = { docker: opts.docker ?? true, openapiDocs: opts.openapiDocs ?? true, observability: opts.observability ?? false };
|
|
30
31
|
apiPrefix = (0, naming_1.normalizeApiPrefix)(opts.apiPrefix ?? "v1");
|
|
31
32
|
const check = (0, naming_1.validateApiPrefix)(apiPrefix);
|
|
32
33
|
if (check !== true)
|
|
@@ -45,7 +46,7 @@ async function createProject(rawName, opts) {
|
|
|
45
46
|
await fs_extra_1.default.ensureDir(projectDir);
|
|
46
47
|
await (0, template_renderer_1.applyTemplateEntries)(projectDir, create_manifest_1.CREATE_MANIFEST, context);
|
|
47
48
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
48
|
-
(0, config_1.writeConfig)(projectDir, { projectName, goModule, apiPrefix, features });
|
|
49
|
+
(0, config_1.writeConfig)(projectDir, { projectName, goModule, apiPrefix, features, scaffoldVersion: (0, version_1.cliVersion)() });
|
|
49
50
|
console.log(picocolors_1.default.green(`\ncreated ${projectName}/`));
|
|
50
51
|
console.log(`\ncd ${projectName}`);
|
|
51
52
|
if (features.docker)
|
|
@@ -9,25 +9,50 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
9
9
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
10
|
const config_1 = require("../utils/config");
|
|
11
11
|
const naming_1 = require("../utils/naming");
|
|
12
|
+
const module_location_1 = require("../utils/module-location");
|
|
12
13
|
const template_renderer_1 = require("../utils/template-renderer");
|
|
13
14
|
const module_manifest_1 = require("../templates/module-manifest");
|
|
14
15
|
const main_patcher_1 = require("../utils/main-patcher");
|
|
15
16
|
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
16
17
|
const migrations_1 = require("../utils/migrations");
|
|
18
|
+
const gocheck_1 = require("../utils/gocheck");
|
|
17
19
|
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
20
|
+
const PERMISSION_CODE_PATTERN = /^[a-z][a-z0-9:_-]*$/;
|
|
18
21
|
async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
19
22
|
const config = (0, config_1.readConfig)(projectDir);
|
|
20
|
-
|
|
23
|
+
// --permission implies --auth (authz.Require must run after RequireAuth) —
|
|
24
|
+
// require both explicitly rather than silently turning one on, so the
|
|
25
|
+
// generated route's protection matches what the command line actually said.
|
|
26
|
+
if (opts.permission && !opts.auth) {
|
|
27
|
+
throw new Error("--permission requires --auth (permission checks run after auth) — pass both, e.g. --auth --permission products:manage");
|
|
28
|
+
}
|
|
29
|
+
if (opts.auth && !config.features.auth) {
|
|
30
|
+
throw new Error("--auth requires `go-scaffold add auth` first — there's no RequireAuth middleware yet");
|
|
31
|
+
}
|
|
32
|
+
if (opts.permission) {
|
|
33
|
+
if (!config.features.rbac) {
|
|
34
|
+
throw new Error("--permission requires `go-scaffold add rbac` first — there's no permissions table or authz middleware yet");
|
|
35
|
+
}
|
|
36
|
+
if (!PERMISSION_CODE_PATTERN.test(opts.permission)) {
|
|
37
|
+
throw new Error(`invalid permission code "${opts.permission}" — must start with a lowercase letter and contain only lowercase letters, digits, ':', '_', or '-'`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const naming = (0, module_location_1.resolveProjectModuleNaming)(projectDir, rawName ?? (await (0, generate_wizard_1.promptModuleName)()));
|
|
21
41
|
const modulePath = naming.pkg;
|
|
22
42
|
const moduleDir = path_1.default.join(projectDir, "internal", "app", modulePath);
|
|
23
43
|
if (fs_extra_1.default.existsSync(moduleDir) && fs_extra_1.default.readdirSync(moduleDir).length > 0) {
|
|
24
44
|
throw new Error(`${moduleDir} already exists — pick a different name or delete it first`);
|
|
25
45
|
}
|
|
46
|
+
// snapshot before writing anything, so assertNoDrift below can tell "we broke
|
|
47
|
+
// it" from "it was already broken"
|
|
48
|
+
const checkBefore = (0, gocheck_1.typeChecks)(projectDir);
|
|
26
49
|
const context = {
|
|
27
50
|
...naming,
|
|
28
51
|
goModule: config.goModule,
|
|
29
52
|
dbName: (0, naming_1.toDbName)(config.projectName),
|
|
30
53
|
modulePath,
|
|
54
|
+
auth: opts.auth,
|
|
55
|
+
permission: opts.permission,
|
|
31
56
|
};
|
|
32
57
|
const moduleFiles = opts.full ? module_manifest_1.MODULE_FILES : module_manifest_1.MODULE_FILES_MINIMAL;
|
|
33
58
|
const moduleEntries = moduleFiles.map((f) => ({
|
|
@@ -42,7 +67,7 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
42
67
|
fs_extra_1.default.readdirSync(migrationsDir).some((f) => f.endsWith(`_create_${naming.plural}.up.sql`));
|
|
43
68
|
let seq = "";
|
|
44
69
|
if (!migrationExists) {
|
|
45
|
-
seq = (0, migrations_1.
|
|
70
|
+
seq = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
46
71
|
const migrationEntries = [
|
|
47
72
|
{
|
|
48
73
|
template: "generate/module/migration.up.sql.hbs",
|
|
@@ -55,12 +80,33 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
55
80
|
];
|
|
56
81
|
await (0, template_renderer_1.applyTemplateEntries)(projectDir, migrationEntries, context);
|
|
57
82
|
}
|
|
83
|
+
// --permission needs the code to actually exist before any role can be
|
|
84
|
+
// granted it — SetPermissions validates against the real catalog and
|
|
85
|
+
// rejects unknown codes, so an ungenerated permission would leave the
|
|
86
|
+
// route permanently unreachable by anyone, admin included.
|
|
87
|
+
let permissionSeq = "";
|
|
88
|
+
if (opts.permission) {
|
|
89
|
+
permissionSeq = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
90
|
+
const permissionEntries = [
|
|
91
|
+
{
|
|
92
|
+
template: "generate/module/permission.up.sql.hbs",
|
|
93
|
+
output: path_1.default.join("migrations", `${permissionSeq}_add_${naming.plural}_permission.up.sql`),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
template: "generate/module/permission.down.sql.hbs",
|
|
97
|
+
output: path_1.default.join("migrations", `${permissionSeq}_add_${naming.plural}_permission.down.sql`),
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, permissionEntries, context);
|
|
101
|
+
}
|
|
58
102
|
const mainGoPath = path_1.default.join(projectDir, "cmd", "api", "main.go");
|
|
59
103
|
(0, main_patcher_1.patchMainGo)(mainGoPath, {
|
|
60
104
|
goModule: config.goModule,
|
|
61
105
|
modulePath,
|
|
62
106
|
pkg: naming.pkg,
|
|
63
107
|
pascalName: naming.pascalName,
|
|
108
|
+
auth: opts.auth,
|
|
109
|
+
permission: opts.permission,
|
|
64
110
|
});
|
|
65
111
|
let docsMessage = "";
|
|
66
112
|
const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
|
|
@@ -75,6 +121,7 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
75
121
|
docsMessage = `\ndocs: docs/${naming.plural}/{collection,item,schemas}.yaml, wired into docs/openapi.yaml`;
|
|
76
122
|
}
|
|
77
123
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
124
|
+
(0, gocheck_1.assertNoDrift)(projectDir, checkBefore, config);
|
|
78
125
|
const routePath = config.apiPrefix ? `/${config.apiPrefix}/${naming.plural}` : `/${naming.plural}`;
|
|
79
126
|
console.log(picocolors_1.default.green(`\ngenerated internal/app/${modulePath}/`));
|
|
80
127
|
if (opts.full) {
|
|
@@ -84,12 +131,24 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
84
131
|
console.log(`registered empty route group ${routePath} in cmd/api/main.go — ` +
|
|
85
132
|
`add endpoints with \`go-scaffold generate method ${naming.pkg} <name> --type ...\``);
|
|
86
133
|
}
|
|
134
|
+
if (opts.permission) {
|
|
135
|
+
console.log(`protected: requires a valid access token AND the "${opts.permission}" permission`);
|
|
136
|
+
}
|
|
137
|
+
else if (opts.auth) {
|
|
138
|
+
console.log("protected: requires a valid access token (no specific permission)");
|
|
139
|
+
}
|
|
140
|
+
else if (config.features.auth) {
|
|
141
|
+
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
|
+
}
|
|
87
143
|
if (seq) {
|
|
88
144
|
console.log(`migration: migrations/${seq}_create_${naming.plural}.{up,down}.sql`);
|
|
89
145
|
}
|
|
90
146
|
else {
|
|
91
147
|
console.log(`migration: reused existing migrations/*_create_${naming.plural}.{up,down}.sql`);
|
|
92
148
|
}
|
|
149
|
+
if (permissionSeq) {
|
|
150
|
+
console.log(`migration: migrations/${permissionSeq}_add_${naming.plural}_permission.{up,down}.sql (seeds the "${opts.permission}" permission — grant it to a role via PATCH /roles/:code/permissions)`);
|
|
151
|
+
}
|
|
93
152
|
if (docsMessage)
|
|
94
153
|
console.log(docsMessage);
|
|
95
154
|
console.log(picocolors_1.default.dim(`\nnext: add real fields to model.go/dto.go, run \`go build ./...\`, then apply the migration ` +
|
package/dist/commands/method.js
CHANGED
|
@@ -9,28 +9,61 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
9
9
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
10
|
const config_1 = require("../utils/config");
|
|
11
11
|
const naming_1 = require("../utils/naming");
|
|
12
|
+
const module_location_1 = require("../utils/module-location");
|
|
12
13
|
const method_patcher_1 = require("../utils/method-patcher");
|
|
14
|
+
const gocheck_1 = require("../utils/gocheck");
|
|
13
15
|
const template_renderer_1 = require("../utils/template-renderer");
|
|
16
|
+
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
14
17
|
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function routeHint(naming, method, type, apiPrefix, getMode, field) {
|
|
20
|
-
const base = apiPrefix ? `/${apiPrefix}/${naming.plural}` : `/${naming.plural}`;
|
|
18
|
+
// URL path registered in method-patcher.ts, excluding the project-wide API
|
|
19
|
+
// prefix so it can be passed directly to patchOpenapiIndexRaw.
|
|
20
|
+
function methodRoutePath(naming, method, type, getMode, field) {
|
|
21
|
+
const base = `/${naming.plural}`;
|
|
21
22
|
if (type === "get" && getMode === "all")
|
|
22
|
-
return
|
|
23
|
+
return `${base}/${method.pathSegment}`;
|
|
23
24
|
if (type === "get")
|
|
24
|
-
return
|
|
25
|
+
return `${base}/${(0, naming_1.toDbName)(field ?? "")}/{${(0, naming_1.toCamelCase)(field ?? "")}}`;
|
|
25
26
|
if (type === "post")
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
return `${base}/${method.pathSegment}`;
|
|
28
|
+
return `${base}/{id}/${method.pathSegment}`;
|
|
29
|
+
}
|
|
30
|
+
function routeHint(naming, method, type, apiPrefix, getMode, field) {
|
|
31
|
+
const path = methodRoutePath(naming, method, type, getMode, field);
|
|
32
|
+
const prefixed = apiPrefix ? `/${apiPrefix}${path}` : path;
|
|
33
|
+
return `${type.toUpperCase()} ${prefixed}`;
|
|
34
|
+
}
|
|
35
|
+
function methodOpenapiDocument(naming, method, type, getMode, field) {
|
|
36
|
+
const pathParameters = [];
|
|
37
|
+
if (type === "get" && getMode === "one") {
|
|
38
|
+
const parameter = (0, naming_1.toCamelCase)(field ?? "");
|
|
39
|
+
pathParameters.push("parameters:", ` - name: ${parameter}`, " in: path", " required: true", " schema:", " type: string");
|
|
40
|
+
}
|
|
41
|
+
else if (type === "put" || type === "patch" || type === "delete") {
|
|
42
|
+
pathParameters.push("parameters:", " - name: id", " in: path", " required: true", " schema:", " type: string", " format: uuid");
|
|
43
|
+
}
|
|
44
|
+
const operation = [
|
|
45
|
+
`${type}:`,
|
|
46
|
+
` summary: TODO document ${method.name}`,
|
|
47
|
+
` operationId: ${method.handlerName}${naming.pascalName}`,
|
|
48
|
+
` tags: [${naming.plural}]`,
|
|
49
|
+
];
|
|
50
|
+
if (type === "post") {
|
|
51
|
+
operation.push(" requestBody:", " required: true", " content:", " application/json:", " schema:", " type: object", " description: TODO define request fields");
|
|
52
|
+
}
|
|
53
|
+
const status = type === "delete" ? "204" : type === "post" ? "201" : "200";
|
|
54
|
+
operation.push(" responses:", ` \"${status}\":`, ` description: ${type === "delete" ? "completed or already absent" : "TODO define response"}`);
|
|
55
|
+
if (type !== "delete") {
|
|
56
|
+
operation.push(" content:", " application/json:", " schema:", " type: object", " description: TODO replace with the method response schema");
|
|
57
|
+
}
|
|
58
|
+
operation.push(" \"400\": { $ref: '../../common/responses.yaml#/ValidationError' }");
|
|
59
|
+
if (type === "get" || type === "put" || type === "patch") {
|
|
60
|
+
operation.push(" \"404\": { $ref: '../../common/responses.yaml#/NotFoundError' }");
|
|
61
|
+
}
|
|
62
|
+
return [...pathParameters, ...operation, ""].join("\n");
|
|
30
63
|
}
|
|
31
64
|
async function generateMethod(moduleNameArg, methodNameArg, opts, projectDir = process.cwd()) {
|
|
32
65
|
const config = (0, config_1.readConfig)(projectDir);
|
|
33
|
-
const naming = (0,
|
|
66
|
+
const naming = (0, module_location_1.resolveProjectModuleNaming)(projectDir, moduleNameArg ?? (await (0, generate_wizard_1.promptModuleName)()));
|
|
34
67
|
const modulePath = naming.pkg;
|
|
35
68
|
const moduleDir = path_1.default.join(projectDir, "internal", "app", modulePath);
|
|
36
69
|
const paths = {
|
|
@@ -59,12 +92,30 @@ async function generateMethod(moduleNameArg, methodNameArg, opts, projectDir = p
|
|
|
59
92
|
if (field)
|
|
60
93
|
(0, naming_1.assertNotGoKeyword)((0, naming_1.toCamelCase)(field), "lookup field");
|
|
61
94
|
const method = (0, naming_1.resolveMethodNaming)(methodNameArg ?? (await (0, generate_wizard_1.promptMethodName)()));
|
|
95
|
+
const docsRelativePath = config.features.openapiDocs
|
|
96
|
+
? `${naming.plural}/methods/${method.pathSegment}.yaml`
|
|
97
|
+
: undefined;
|
|
98
|
+
if (docsRelativePath && fs_extra_1.default.existsSync(path_1.default.join(projectDir, "docs", docsRelativePath))) {
|
|
99
|
+
throw new Error(`OpenAPI method document already exists: docs/${docsRelativePath}`);
|
|
100
|
+
}
|
|
101
|
+
const checkBefore = (0, gocheck_1.typeChecks)(projectDir);
|
|
62
102
|
(0, method_patcher_1.patchMethod)(paths, naming, method, { type, getMode, field }, config.goModule);
|
|
63
103
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
104
|
+
(0, gocheck_1.assertNoDrift)(projectDir, checkBefore, config);
|
|
105
|
+
if (docsRelativePath) {
|
|
106
|
+
const docsPath = path_1.default.join(projectDir, "docs", docsRelativePath);
|
|
107
|
+
fs_extra_1.default.outputFileSync(docsPath, methodOpenapiDocument(naming, method, type, getMode, field));
|
|
108
|
+
(0, openapi_patcher_1.patchOpenapiIndexRaw)(path_1.default.join(projectDir, "docs", "openapi.yaml"), config.apiPrefix, [
|
|
109
|
+
{
|
|
110
|
+
urlPath: methodRoutePath(naming, method, type, getMode, field),
|
|
111
|
+
file: `./${docsRelativePath}`,
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
64
115
|
console.log(picocolors_1.default.green(`\nadded "${method.name}" to internal/app/${modulePath}/`));
|
|
65
116
|
console.log(`route: ${routeHint(naming, method, type, config.apiPrefix, getMode, field)}`);
|
|
66
|
-
if (
|
|
67
|
-
console.log(picocolors_1.default.
|
|
117
|
+
if (docsRelativePath) {
|
|
118
|
+
console.log(picocolors_1.default.green(`docs: docs/${docsRelativePath} (wired into docs/openapi.yaml)`));
|
|
68
119
|
}
|
|
69
120
|
console.log(picocolors_1.default.dim(`\nnext: fill in the TODO in service.go, then \`go build ./...\` / \`go test ./...\``));
|
|
70
121
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateMigration = generateMigration;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const config_1 = require("../utils/config");
|
|
11
|
+
const migrations_1 = require("../utils/migrations");
|
|
12
|
+
const naming_1 = require("../utils/naming");
|
|
13
|
+
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
14
|
+
// generate migration doesn't know your schema, so unlike `generate module`
|
|
15
|
+
// it can't render real SQL — it only reserves the timestamped filename pair
|
|
16
|
+
// so two people adding a migration on the same day never collide, and stubs
|
|
17
|
+
// each with a TODO instead of leaving the CLI to guess at columns.
|
|
18
|
+
async function generateMigration(rawName, projectDir = process.cwd()) {
|
|
19
|
+
(0, config_1.readConfig)(projectDir); // throws with a clear message if this isn't a go-scaffold project
|
|
20
|
+
const name = (0, naming_1.toDbName)(rawName ?? (await (0, generate_wizard_1.promptMigrationName)()));
|
|
21
|
+
if (!name) {
|
|
22
|
+
throw new Error(`invalid migration name: "${rawName}" (must contain letters/numbers)`);
|
|
23
|
+
}
|
|
24
|
+
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
25
|
+
fs_extra_1.default.ensureDirSync(migrationsDir);
|
|
26
|
+
const version = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
27
|
+
const upPath = path_1.default.join(migrationsDir, `${version}_${name}.up.sql`);
|
|
28
|
+
const downPath = path_1.default.join(migrationsDir, `${version}_${name}.down.sql`);
|
|
29
|
+
fs_extra_1.default.writeFileSync(upPath, `-- TODO: write the up migration for ${name}\n`);
|
|
30
|
+
fs_extra_1.default.writeFileSync(downPath, `-- TODO: write the down migration for ${name} (reverses the up migration)\n`);
|
|
31
|
+
console.log(picocolors_1.default.green(`\ngenerated migrations/${version}_${name}.{up,down}.sql`));
|
|
32
|
+
console.log(picocolors_1.default.dim(`\nnext: write the SQL, then \`make migrate-up\` (dev) or apply it as a deploy step ` +
|
|
33
|
+
`(AUTO_MIGRATE=true also picks up model changes automatically in dev — this file matters most for prod)`));
|
|
34
|
+
}
|