@nakedev/go-scaffold 0.1.2 → 0.1.3
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 +75 -0
- package/dist/commands/auth.js +129 -0
- package/dist/commands/create.js +3 -2
- package/dist/commands/generate.js +59 -1
- package/dist/commands/method.js +3 -0
- package/dist/commands/migration.js +34 -0
- package/dist/commands/rbac.js +103 -0
- package/dist/commands/remove.js +19 -2
- package/dist/commands/worker.js +75 -0
- package/dist/index.js +66 -3
- 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/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/migrations.js +30 -8
- package/dist/utils/openapi-patcher.js +16 -0
- package/dist/utils/platform-patcher.js +59 -0
- package/dist/utils/rbac-patcher.js +277 -0
- package/dist/utils/version.js +24 -0
- package/package.json +2 -2
- 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 +4 -2
- package/templates/create/base/.gitignore.hbs +2 -0
- package/templates/create/base/Makefile.hbs +30 -5
- package/templates/create/base/README.md.hbs +36 -8
- 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 +13 -0
- 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/handler.go.hbs +20 -3
- package/templates/generate/module/handler_test.go.hbs +49 -6
- package/templates/generate/module/minimal/handler.go.hbs +21 -3
- package/templates/generate/module/minimal/handler_test.go.hbs +53 -6
- package/templates/generate/module/permission.down.sql.hbs +5 -0
- package/templates/generate/module/permission.up.sql.hbs +4 -0
- package/dist/utils/module-paths.js +0 -33
package/README.md
CHANGED
|
@@ -157,6 +157,81 @@ returns a clean `500` rather than inventing behavior — see
|
|
|
157
157
|
`generate method` prints the route it added but does **not** touch
|
|
158
158
|
`docs/openapi.yaml` — endpoint-specific spec entries stay hand-written.
|
|
159
159
|
|
|
160
|
+
**Drift check** — `generate` type-checks the project (`go vet ./...`) before and
|
|
161
|
+
after it writes. If the project was fine beforehand and the generated code
|
|
162
|
+
doesn't compile, it stops with the compiler output instead of leaving you to
|
|
163
|
+
find it later:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
the generated code doesn't compile, but this project was fine a moment ago.
|
|
167
|
+
|
|
168
|
+
The most likely cause is drift: this project's internal/shared layer has been edited
|
|
169
|
+
since it was scaffolded, so the templates this CLI emits no longer match it.
|
|
170
|
+
|
|
171
|
+
scaffolded with: go-scaffold 0.1.2
|
|
172
|
+
this CLI: go-scaffold 0.3.0
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
That happens because `generate`'s templates are written against the `shared/`
|
|
176
|
+
layer `create` emits — editing that layer is normal work, but it moves the
|
|
177
|
+
project away from what this CLI's templates expect. `create` records its own
|
|
178
|
+
version in `go-scaffold.config.json` so the message can name both sides. A
|
|
179
|
+
project that was *already* broken (mid-refactor, or `go mod tidy` not run yet)
|
|
180
|
+
is left alone — only a passed-before/broken-after transition is reported. No Go
|
|
181
|
+
on `PATH` means the check is skipped.
|
|
182
|
+
|
|
183
|
+
### `generate migration <name>` (alias `mig`) — reserve a SQL migration pair
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
go-scaffold generate migration add_status_to_orders
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Creates timestamped `migrations/<version>_<name>.up.sql` and `.down.sql` TODO
|
|
190
|
+
stubs. The CLI reserves the names; you own the SQL and should apply it with
|
|
191
|
+
`make migrate-up` (or `migrate -path migrations -database "$DB_DSN" up`).
|
|
192
|
+
|
|
193
|
+
### `add worker` — add Redis-backed background work
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
go-scaffold add worker
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Adds Redis cache/queue support, async email delivery, and `cmd/worker`. It also
|
|
200
|
+
makes `/readyz` report unavailable when Redis is down. Run `go mod tidy` and
|
|
201
|
+
provide `REDIS_URL` before starting the API or worker.
|
|
202
|
+
|
|
203
|
+
### `add auth` — add email/password authentication
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
go-scaffold add auth
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Requires `add worker`. Adds JWT access tokens, Redis-backed refresh-token
|
|
210
|
+
rotation, registration/login/logout, password reset, email verification, and
|
|
211
|
+
Google OAuth routes. Apply the generated migrations; `AUTO_MIGRATE=true` is
|
|
212
|
+
convenient in development, while production should use `migrate up`.
|
|
213
|
+
|
|
214
|
+
### `add rbac` — add roles and permissions
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
go-scaffold add rbac
|
|
218
|
+
go-scaffold generate module secrets --auth --permission secret:manage
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Requires `add auth`. Adds role/permission administration, cached authorization
|
|
222
|
+
middleware, and role assignment. Its migration seeds the default roles and
|
|
223
|
+
permissions, so apply it with `migrate up`: AutoMigrate creates tables but does
|
|
224
|
+
not run SQL seed statements.
|
|
225
|
+
|
|
226
|
+
### Observability at project creation
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
go-scaffold create my-api --defaults --observability
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Opt-in observability adds Prometheus metrics at `/metrics` and OpenTelemetry
|
|
233
|
+
tracing. Tracing is disabled until `OTEL_EXPORTER_OTLP_ENDPOINT` is configured.
|
|
234
|
+
|
|
160
235
|
### `remove module <name>` (alias `rm m`) — drop a domain
|
|
161
236
|
|
|
162
237
|
```bash
|
|
@@ -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)
|
|
@@ -14,20 +14,44 @@ const module_manifest_1 = require("../templates/module-manifest");
|
|
|
14
14
|
const main_patcher_1 = require("../utils/main-patcher");
|
|
15
15
|
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
16
16
|
const migrations_1 = require("../utils/migrations");
|
|
17
|
+
const gocheck_1 = require("../utils/gocheck");
|
|
17
18
|
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
19
|
+
const PERMISSION_CODE_PATTERN = /^[a-z][a-z0-9:_-]*$/;
|
|
18
20
|
async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
19
21
|
const config = (0, config_1.readConfig)(projectDir);
|
|
22
|
+
// --permission implies --auth (authz.Require must run after RequireAuth) —
|
|
23
|
+
// require both explicitly rather than silently turning one on, so the
|
|
24
|
+
// generated route's protection matches what the command line actually said.
|
|
25
|
+
if (opts.permission && !opts.auth) {
|
|
26
|
+
throw new Error("--permission requires --auth (permission checks run after auth) — pass both, e.g. --auth --permission products:manage");
|
|
27
|
+
}
|
|
28
|
+
if (opts.auth && !config.features.auth) {
|
|
29
|
+
throw new Error("--auth requires `go-scaffold add auth` first — there's no RequireAuth middleware yet");
|
|
30
|
+
}
|
|
31
|
+
if (opts.permission) {
|
|
32
|
+
if (!config.features.rbac) {
|
|
33
|
+
throw new Error("--permission requires `go-scaffold add rbac` first — there's no permissions table or authz middleware yet");
|
|
34
|
+
}
|
|
35
|
+
if (!PERMISSION_CODE_PATTERN.test(opts.permission)) {
|
|
36
|
+
throw new Error(`invalid permission code "${opts.permission}" — must start with a lowercase letter and contain only lowercase letters, digits, ':', '_', or '-'`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
20
39
|
const naming = (0, naming_1.resolveModuleNaming)(rawName ?? (await (0, generate_wizard_1.promptModuleName)()));
|
|
21
40
|
const modulePath = naming.pkg;
|
|
22
41
|
const moduleDir = path_1.default.join(projectDir, "internal", "app", modulePath);
|
|
23
42
|
if (fs_extra_1.default.existsSync(moduleDir) && fs_extra_1.default.readdirSync(moduleDir).length > 0) {
|
|
24
43
|
throw new Error(`${moduleDir} already exists — pick a different name or delete it first`);
|
|
25
44
|
}
|
|
45
|
+
// snapshot before writing anything, so assertNoDrift below can tell "we broke
|
|
46
|
+
// it" from "it was already broken"
|
|
47
|
+
const checkBefore = (0, gocheck_1.typeChecks)(projectDir);
|
|
26
48
|
const context = {
|
|
27
49
|
...naming,
|
|
28
50
|
goModule: config.goModule,
|
|
29
51
|
dbName: (0, naming_1.toDbName)(config.projectName),
|
|
30
52
|
modulePath,
|
|
53
|
+
auth: opts.auth,
|
|
54
|
+
permission: opts.permission,
|
|
31
55
|
};
|
|
32
56
|
const moduleFiles = opts.full ? module_manifest_1.MODULE_FILES : module_manifest_1.MODULE_FILES_MINIMAL;
|
|
33
57
|
const moduleEntries = moduleFiles.map((f) => ({
|
|
@@ -42,7 +66,7 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
42
66
|
fs_extra_1.default.readdirSync(migrationsDir).some((f) => f.endsWith(`_create_${naming.plural}.up.sql`));
|
|
43
67
|
let seq = "";
|
|
44
68
|
if (!migrationExists) {
|
|
45
|
-
seq = (0, migrations_1.
|
|
69
|
+
seq = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
46
70
|
const migrationEntries = [
|
|
47
71
|
{
|
|
48
72
|
template: "generate/module/migration.up.sql.hbs",
|
|
@@ -55,12 +79,33 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
55
79
|
];
|
|
56
80
|
await (0, template_renderer_1.applyTemplateEntries)(projectDir, migrationEntries, context);
|
|
57
81
|
}
|
|
82
|
+
// --permission needs the code to actually exist before any role can be
|
|
83
|
+
// granted it — SetPermissions validates against the real catalog and
|
|
84
|
+
// rejects unknown codes, so an ungenerated permission would leave the
|
|
85
|
+
// route permanently unreachable by anyone, admin included.
|
|
86
|
+
let permissionSeq = "";
|
|
87
|
+
if (opts.permission) {
|
|
88
|
+
permissionSeq = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
89
|
+
const permissionEntries = [
|
|
90
|
+
{
|
|
91
|
+
template: "generate/module/permission.up.sql.hbs",
|
|
92
|
+
output: path_1.default.join("migrations", `${permissionSeq}_add_${naming.plural}_permission.up.sql`),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
template: "generate/module/permission.down.sql.hbs",
|
|
96
|
+
output: path_1.default.join("migrations", `${permissionSeq}_add_${naming.plural}_permission.down.sql`),
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, permissionEntries, context);
|
|
100
|
+
}
|
|
58
101
|
const mainGoPath = path_1.default.join(projectDir, "cmd", "api", "main.go");
|
|
59
102
|
(0, main_patcher_1.patchMainGo)(mainGoPath, {
|
|
60
103
|
goModule: config.goModule,
|
|
61
104
|
modulePath,
|
|
62
105
|
pkg: naming.pkg,
|
|
63
106
|
pascalName: naming.pascalName,
|
|
107
|
+
auth: opts.auth,
|
|
108
|
+
permission: opts.permission,
|
|
64
109
|
});
|
|
65
110
|
let docsMessage = "";
|
|
66
111
|
const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
|
|
@@ -75,6 +120,7 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
75
120
|
docsMessage = `\ndocs: docs/${naming.plural}/{collection,item,schemas}.yaml, wired into docs/openapi.yaml`;
|
|
76
121
|
}
|
|
77
122
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
123
|
+
(0, gocheck_1.assertNoDrift)(projectDir, checkBefore, config);
|
|
78
124
|
const routePath = config.apiPrefix ? `/${config.apiPrefix}/${naming.plural}` : `/${naming.plural}`;
|
|
79
125
|
console.log(picocolors_1.default.green(`\ngenerated internal/app/${modulePath}/`));
|
|
80
126
|
if (opts.full) {
|
|
@@ -84,12 +130,24 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
84
130
|
console.log(`registered empty route group ${routePath} in cmd/api/main.go — ` +
|
|
85
131
|
`add endpoints with \`go-scaffold generate method ${naming.pkg} <name> --type ...\``);
|
|
86
132
|
}
|
|
133
|
+
if (opts.permission) {
|
|
134
|
+
console.log(`protected: requires a valid access token AND the "${opts.permission}" permission`);
|
|
135
|
+
}
|
|
136
|
+
else if (opts.auth) {
|
|
137
|
+
console.log("protected: requires a valid access token (no specific permission)");
|
|
138
|
+
}
|
|
139
|
+
else if (config.features.auth) {
|
|
140
|
+
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`));
|
|
141
|
+
}
|
|
87
142
|
if (seq) {
|
|
88
143
|
console.log(`migration: migrations/${seq}_create_${naming.plural}.{up,down}.sql`);
|
|
89
144
|
}
|
|
90
145
|
else {
|
|
91
146
|
console.log(`migration: reused existing migrations/*_create_${naming.plural}.{up,down}.sql`);
|
|
92
147
|
}
|
|
148
|
+
if (permissionSeq) {
|
|
149
|
+
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)`);
|
|
150
|
+
}
|
|
93
151
|
if (docsMessage)
|
|
94
152
|
console.log(docsMessage);
|
|
95
153
|
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
|
@@ -10,6 +10,7 @@ const picocolors_1 = __importDefault(require("picocolors"));
|
|
|
10
10
|
const config_1 = require("../utils/config");
|
|
11
11
|
const naming_1 = require("../utils/naming");
|
|
12
12
|
const method_patcher_1 = require("../utils/method-patcher");
|
|
13
|
+
const gocheck_1 = require("../utils/gocheck");
|
|
13
14
|
const template_renderer_1 = require("../utils/template-renderer");
|
|
14
15
|
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
15
16
|
// the actual URL the new route answers on — printed so the user can add the
|
|
@@ -59,8 +60,10 @@ async function generateMethod(moduleNameArg, methodNameArg, opts, projectDir = p
|
|
|
59
60
|
if (field)
|
|
60
61
|
(0, naming_1.assertNotGoKeyword)((0, naming_1.toCamelCase)(field), "lookup field");
|
|
61
62
|
const method = (0, naming_1.resolveMethodNaming)(methodNameArg ?? (await (0, generate_wizard_1.promptMethodName)()));
|
|
63
|
+
const checkBefore = (0, gocheck_1.typeChecks)(projectDir);
|
|
62
64
|
(0, method_patcher_1.patchMethod)(paths, naming, method, { type, getMode, field }, config.goModule);
|
|
63
65
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
66
|
+
(0, gocheck_1.assertNoDrift)(projectDir, checkBefore, config);
|
|
64
67
|
console.log(picocolors_1.default.green(`\nadded "${method.name}" to internal/app/${modulePath}/`));
|
|
65
68
|
console.log(`route: ${routeHint(naming, method, type, config.apiPrefix, getMode, field)}`);
|
|
66
69
|
if (config.features.openapiDocs) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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.addRbac = addRbac;
|
|
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 rbac_manifest_1 = require("../templates/rbac-manifest");
|
|
13
|
+
const migrations_1 = require("../utils/migrations");
|
|
14
|
+
const rbac_patcher_1 = require("../utils/rbac-patcher");
|
|
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 rbac` registers or adds onto the user handler.
|
|
18
|
+
const RBAC_OPENAPI_PATHS = [
|
|
19
|
+
{ urlPath: "/roles", file: "./rbac/roles.yaml" },
|
|
20
|
+
{ urlPath: "/roles/{code}/permissions", file: "./rbac/role-permissions.yaml" },
|
|
21
|
+
{ urlPath: "/roles/{code}", file: "./rbac/role.yaml" },
|
|
22
|
+
{ urlPath: "/permissions", file: "./rbac/permissions.yaml" },
|
|
23
|
+
{ urlPath: "/users", file: "./rbac/users.yaml" },
|
|
24
|
+
{ urlPath: "/users/{id}", file: "./rbac/user.yaml" },
|
|
25
|
+
{ urlPath: "/users/{id}/set-role", file: "./rbac/user-set-role.yaml" },
|
|
26
|
+
];
|
|
27
|
+
// addRbac layers role-based access control on top of `add auth`: a role
|
|
28
|
+
// domain (roles/permissions/role_permissions, admin-manageable), an Authz
|
|
29
|
+
// middleware with a cached role->permission lookup, a Role claim threaded
|
|
30
|
+
// through the JWT, and the admin endpoints that actually need it: listing/
|
|
31
|
+
// viewing other users (GET /users, GET /users/:id) and changing a user's
|
|
32
|
+
// role (PATCH /users/:id/set-role). Opt-in on top of an opt-in — most
|
|
33
|
+
// projects need "is this caller logged in" long before they need "can this
|
|
34
|
+
// caller do X".
|
|
35
|
+
async function addRbac(projectDir = process.cwd()) {
|
|
36
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
37
|
+
if (!config.features.auth) {
|
|
38
|
+
throw new Error("`go-scaffold add rbac` requires `go-scaffold add auth` first — there's no Role claim to check permissions against otherwise");
|
|
39
|
+
}
|
|
40
|
+
const roleDir = path_1.default.join(projectDir, "internal", "app", "role");
|
|
41
|
+
if (fs_extra_1.default.existsSync(roleDir)) {
|
|
42
|
+
throw new Error(`${roleDir} already exists — RBAC looks like it's already been added`);
|
|
43
|
+
}
|
|
44
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, rbac_manifest_1.RBAC_FILES, { goModule: config.goModule });
|
|
45
|
+
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
46
|
+
fs_extra_1.default.ensureDirSync(migrationsDir);
|
|
47
|
+
const version = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
48
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
|
|
49
|
+
{ template: "add/rbac/migrations/add_roles.up.sql.hbs", output: path_1.default.join("migrations", `${version}_add_roles.up.sql`) },
|
|
50
|
+
{ template: "add/rbac/migrations/add_roles.down.sql.hbs", output: path_1.default.join("migrations", `${version}_add_roles.down.sql`) },
|
|
51
|
+
], {});
|
|
52
|
+
(0, rbac_patcher_1.patchConfigForRbac)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
|
|
53
|
+
(0, rbac_patcher_1.patchUserModelForRbac)(path_1.default.join(projectDir, "internal", "app", "user", "model", "user.go"));
|
|
54
|
+
(0, rbac_patcher_1.patchMiddlewareAuthForRbac)(path_1.default.join(projectDir, "internal", "shared", "middleware", "auth.go"));
|
|
55
|
+
(0, rbac_patcher_1.patchUserJWTForRbac)(path_1.default.join(projectDir, "internal", "app", "user", "jwt.go"));
|
|
56
|
+
(0, rbac_patcher_1.patchUserServiceForRbac)(path_1.default.join(projectDir, "internal", "app", "user", "service.go"));
|
|
57
|
+
(0, rbac_patcher_1.patchUserServiceTestForRbac)(path_1.default.join(projectDir, "internal", "app", "user", "service_test.go"));
|
|
58
|
+
(0, rbac_patcher_1.patchUserDTOForRbac)(path_1.default.join(projectDir, "internal", "app", "user", "dto.go"));
|
|
59
|
+
(0, rbac_patcher_1.patchUserHandlerForRbac)(path_1.default.join(projectDir, "internal", "app", "user", "handler.go"), config.goModule);
|
|
60
|
+
(0, rbac_patcher_1.patchUserErrorsForRbac)(path_1.default.join(projectDir, "internal", "app", "user", "errors.go"));
|
|
61
|
+
(0, rbac_patcher_1.patchMainGoForRbac)(path_1.default.join(projectDir, "cmd", "api", "main.go"), config.goModule);
|
|
62
|
+
(0, rbac_patcher_1.patchCmdSeedForRbac)(path_1.default.join(projectDir, "cmd", "seed", "main.go"), config.goModule);
|
|
63
|
+
patchEnvExample(path_1.default.join(projectDir, ".env.example"));
|
|
64
|
+
let docsMessage = "";
|
|
65
|
+
const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
|
|
66
|
+
if (config.features.openapiDocs && fs_extra_1.default.existsSync(openapiPath)) {
|
|
67
|
+
const docsEntries = [
|
|
68
|
+
{ template: "add/rbac/docs/schemas.yaml.hbs", output: path_1.default.join("docs", "rbac", "schemas.yaml") },
|
|
69
|
+
...RBAC_OPENAPI_PATHS.map(({ file }) => ({
|
|
70
|
+
template: `add/rbac/docs/${path_1.default.basename(file)}.hbs`,
|
|
71
|
+
output: path_1.default.join("docs", "rbac", path_1.default.basename(file)),
|
|
72
|
+
})),
|
|
73
|
+
];
|
|
74
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, docsEntries, {});
|
|
75
|
+
(0, openapi_patcher_1.patchOpenapiIndexRaw)(openapiPath, config.apiPrefix, RBAC_OPENAPI_PATHS);
|
|
76
|
+
(0, rbac_patcher_1.patchAuthDocsForRbac)(path_1.default.join(projectDir, "docs", "auth", "schemas.yaml"));
|
|
77
|
+
docsMessage = "\ndocs: docs/rbac/*.yaml, wired into docs/openapi.yaml";
|
|
78
|
+
}
|
|
79
|
+
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
80
|
+
(0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, rbac: true } });
|
|
81
|
+
console.log(picocolors_1.default.green("\nadded internal/app/role/ and internal/shared/middleware/authz.go"));
|
|
82
|
+
console.log("registered GET /users, GET /users/:id, PATCH /users/:id/set-role, /roles, and /permissions in cmd/api/main.go" + docsMessage);
|
|
83
|
+
console.log(picocolors_1.default.yellow("\n⚠ AUTO_MIGRATE=true does NOT seed the role/permission data — it only creates the\n" +
|
|
84
|
+
" tables from the Go structs. The \"staff\"/\"admin\" roles and their permissions live\n" +
|
|
85
|
+
" in the migration's SQL (INSERT statements), which AutoMigrate never runs. Without\n" +
|
|
86
|
+
" applying it for real, `make seed` fails with \"unknown role code\" and nobody can be\n" +
|
|
87
|
+
" granted anything. Apply it before relying on RBAC, even in dev:\n" +
|
|
88
|
+
" migrate -path migrations -database \"$DB_DSN\" up (or: make migrate-up)"));
|
|
89
|
+
console.log(picocolors_1.default.dim("\nnext: go mod tidy, then SEED_ADMIN_EMAIL=... SEED_ADMIN_PASSWORD=... make seed to get an admin"));
|
|
90
|
+
}
|
|
91
|
+
function patchEnvExample(envExamplePath) {
|
|
92
|
+
if (!fs_extra_1.default.existsSync(envExamplePath))
|
|
93
|
+
return;
|
|
94
|
+
let content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
|
|
95
|
+
if (content.includes("AUTHZ_CACHE_TTL_MIN"))
|
|
96
|
+
return; // already added
|
|
97
|
+
content =
|
|
98
|
+
content.replace(/\n?$/, "\n") +
|
|
99
|
+
"\n# how long a role's permission set is cached before Authz.Require re-checks the DB —\n" +
|
|
100
|
+
"# a permission change takes effect for already-issued tokens within this window\n" +
|
|
101
|
+
"AUTHZ_CACHE_TTL_MIN=1\n";
|
|
102
|
+
fs_extra_1.default.writeFileSync(envExamplePath, content);
|
|
103
|
+
}
|
package/dist/commands/remove.js
CHANGED
|
@@ -34,6 +34,17 @@ async function removeModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
34
34
|
if (!ok)
|
|
35
35
|
throw new Error("removal cancelled");
|
|
36
36
|
}
|
|
37
|
+
// detect --auth/--permission from the generated handler.go itself (not
|
|
38
|
+
// stored anywhere else) — unpatchMainGo needs the exact same flags used at
|
|
39
|
+
// generate-time to reconstruct the identical line it's removing.
|
|
40
|
+
const handlerGoPath = path_1.default.join(moduleDir, "handler.go");
|
|
41
|
+
let auth;
|
|
42
|
+
let permission;
|
|
43
|
+
if (fs_extra_1.default.existsSync(handlerGoPath)) {
|
|
44
|
+
const handlerContent = fs_extra_1.default.readFileSync(handlerGoPath, "utf8");
|
|
45
|
+
auth = /jwtSecret\s+string/.test(handlerContent);
|
|
46
|
+
permission = handlerContent.match(/h\.authz\.Require\("([^"]+)"\)/)?.[1];
|
|
47
|
+
}
|
|
37
48
|
// 1. the domain package
|
|
38
49
|
fs_extra_1.default.removeSync(moduleDir);
|
|
39
50
|
// 2. main.go wiring
|
|
@@ -42,6 +53,8 @@ async function removeModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
42
53
|
modulePath,
|
|
43
54
|
pkg: naming.pkg,
|
|
44
55
|
pascalName: naming.pascalName,
|
|
56
|
+
auth,
|
|
57
|
+
permission,
|
|
45
58
|
});
|
|
46
59
|
// 3. openapi index + per-module docs
|
|
47
60
|
const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
|
|
@@ -49,12 +62,16 @@ async function removeModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
49
62
|
(0, openapi_patcher_1.unpatchOpenapiIndex)(openapiPath, naming, config.apiPrefix);
|
|
50
63
|
fs_extra_1.default.removeSync(path_1.default.join(projectDir, "docs", naming.plural));
|
|
51
64
|
}
|
|
52
|
-
// 4. migration files (up + down)
|
|
65
|
+
// 4. migration files (up + down) — including the --permission seed
|
|
66
|
+
// migration, if this module was generated with one.
|
|
53
67
|
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
54
68
|
const removedMigrations = [];
|
|
55
69
|
if (fs_extra_1.default.existsSync(migrationsDir)) {
|
|
56
70
|
for (const f of fs_extra_1.default.readdirSync(migrationsDir)) {
|
|
57
|
-
if (f.endsWith(`_create_${naming.plural}.up.sql`) ||
|
|
71
|
+
if (f.endsWith(`_create_${naming.plural}.up.sql`) ||
|
|
72
|
+
f.endsWith(`_create_${naming.plural}.down.sql`) ||
|
|
73
|
+
f.endsWith(`_add_${naming.plural}_permission.up.sql`) ||
|
|
74
|
+
f.endsWith(`_add_${naming.plural}_permission.down.sql`)) {
|
|
58
75
|
fs_extra_1.default.removeSync(path_1.default.join(migrationsDir, f));
|
|
59
76
|
removedMigrations.push(f);
|
|
60
77
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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.addWorker = addWorker;
|
|
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 worker_manifest_1 = require("../templates/worker-manifest");
|
|
13
|
+
const platform_patcher_1 = require("../utils/platform-patcher");
|
|
14
|
+
// addWorker scaffolds the async task processing subsystem: Redis
|
|
15
|
+
// (platform/cache), Asynq client/server (platform/queue), SMTP mail
|
|
16
|
+
// (platform/mail, with an email:send task type as the one thing the fresh
|
|
17
|
+
// worker actually handles), and cmd/worker itself. Opt-in — most projects
|
|
18
|
+
// don't need a queue on day one, and an empty worker with no task types
|
|
19
|
+
// registered is a stranger scaffold than just not having one.
|
|
20
|
+
async function addWorker(projectDir = process.cwd()) {
|
|
21
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
22
|
+
const queueDir = path_1.default.join(projectDir, "internal", "platform", "queue");
|
|
23
|
+
if (fs_extra_1.default.existsSync(queueDir)) {
|
|
24
|
+
throw new Error(`${queueDir} already exists — worker infrastructure looks like it's already been added`);
|
|
25
|
+
}
|
|
26
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, worker_manifest_1.WORKER_FILES, { goModule: config.goModule });
|
|
27
|
+
(0, platform_patcher_1.patchConfigForWorker)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
|
|
28
|
+
(0, platform_patcher_1.patchMainGoForWorker)(path_1.default.join(projectDir, "cmd", "api", "main.go"), config.goModule);
|
|
29
|
+
patchEnvExample(path_1.default.join(projectDir, ".env.example"));
|
|
30
|
+
patchMakefile(path_1.default.join(projectDir, "Makefile"));
|
|
31
|
+
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
32
|
+
(0, config_1.writeConfig)(projectDir, { ...config, features: { ...config.features, worker: true } });
|
|
33
|
+
console.log(picocolors_1.default.green("\nadded internal/platform/{cache,queue,mail}/ and cmd/worker/"));
|
|
34
|
+
console.log("wired Redis into cmd/api (readyz check) — cmd/api does not enqueue anything yet");
|
|
35
|
+
console.log(picocolors_1.default.dim("\nnext: make worker (separate terminal, or `make dev` runs both), then go build ./... to confirm"));
|
|
36
|
+
}
|
|
37
|
+
function patchEnvExample(envExamplePath) {
|
|
38
|
+
if (!fs_extra_1.default.existsSync(envExamplePath))
|
|
39
|
+
return;
|
|
40
|
+
let content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
|
|
41
|
+
if (content.includes("REDIS_URL"))
|
|
42
|
+
return; // already added
|
|
43
|
+
content =
|
|
44
|
+
content.replace(/\n?$/, "\n") +
|
|
45
|
+
"\nREDIS_URL=redis://localhost:6379/0\n" +
|
|
46
|
+
"\n# leave SMTP_HOST unset to log emails instead of sending them (dev default)\n" +
|
|
47
|
+
"SMTP_HOST=\n" +
|
|
48
|
+
"SMTP_PORT=587\n" +
|
|
49
|
+
"SMTP_USERNAME=\n" +
|
|
50
|
+
"SMTP_PASSWORD=\n" +
|
|
51
|
+
"SMTP_FROM=no-reply@example.local\n";
|
|
52
|
+
fs_extra_1.default.writeFileSync(envExamplePath, content);
|
|
53
|
+
}
|
|
54
|
+
function patchMakefile(makefilePath) {
|
|
55
|
+
if (!fs_extra_1.default.existsSync(makefilePath))
|
|
56
|
+
return;
|
|
57
|
+
let content = fs_extra_1.default.readFileSync(makefilePath, "utf8");
|
|
58
|
+
if (content.includes("\nworker:\n"))
|
|
59
|
+
return; // already added
|
|
60
|
+
content = content.replace(/^\.PHONY: /m, ".PHONY: dev worker ");
|
|
61
|
+
const targets = "\n# run both API + worker in one terminal — Ctrl+C kills both\n" +
|
|
62
|
+
"dev:\n" +
|
|
63
|
+
"\t@[ -f .env ] && export $$(grep -v '^#' .env | xargs); \\\n" +
|
|
64
|
+
"\t(trap 'kill 0' SIGINT SIGTERM; \\\n" +
|
|
65
|
+
"\t go run ./cmd/api & \\\n" +
|
|
66
|
+
"\t go run ./cmd/worker & \\\n" +
|
|
67
|
+
"\t wait)\n" +
|
|
68
|
+
"\n" +
|
|
69
|
+
"# background worker for async task processing (email, ...) — requires Redis.\n" +
|
|
70
|
+
"# Use `make dev` to run both in one terminal, or run this in a separate one.\n" +
|
|
71
|
+
"worker:\n" +
|
|
72
|
+
"\t@[ -f .env ] && export $$(grep -v '^#' .env | xargs); go run ./cmd/worker\n";
|
|
73
|
+
content = content.replace(/\nbuild:/, `${targets}\nbuild:`);
|
|
74
|
+
fs_extra_1.default.writeFileSync(makefilePath, content);
|
|
75
|
+
}
|