@nakedev/go-scaffold 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +598 -306
- package/dist/commands/auth.js +65 -23
- package/dist/commands/check.js +281 -0
- package/dist/commands/config.js +50 -0
- package/dist/commands/create.js +33 -2
- package/dist/commands/generate.js +29 -3
- package/dist/commands/method.js +74 -63
- package/dist/commands/migration.js +2 -2
- package/dist/commands/observability.js +4 -53
- package/dist/commands/rbac.js +21 -10
- package/dist/commands/undo.js +11 -3
- package/dist/commands/worker.js +15 -5
- package/dist/index.js +198 -59
- package/dist/prompts/auth-wizard.js +40 -6
- package/dist/prompts/create-wizard.js +42 -1
- package/dist/prompts/generate-wizard.js +89 -9
- package/dist/templates/auth-manifest.js +50 -19
- package/dist/templates/create-manifest.js +8 -0
- package/dist/templates/module-manifest.js +84 -26
- package/dist/templates/rbac-manifest.js +16 -11
- package/dist/templates/worker-manifest.js +4 -1
- package/dist/types.js +8 -0
- package/dist/utils/auth-patcher.js +124 -33
- package/dist/utils/config.js +167 -4
- package/dist/utils/docs-patcher.js +68 -0
- package/dist/utils/hexagonal-method-patcher.js +334 -0
- package/dist/utils/main-patcher.js +32 -30
- package/dist/utils/marker-patch.js +7 -1
- package/dist/utils/module-location.js +17 -11
- package/dist/utils/module-profile.js +32 -0
- package/dist/utils/platform-patcher.js +56 -7
- package/dist/utils/rbac-patcher.js +89 -210
- package/package.json +7 -2
- package/templates/add/auth/cmd/seed/main.go.hbs +15 -3
- package/templates/add/auth/docs/login.yaml.hbs +11 -1
- package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
- package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
- package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
- package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
- package/templates/add/auth/docs/register.yaml.hbs +7 -0
- package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
- package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
- package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
- package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
- package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
- package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/browser_policy.go.hbs +98 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
- package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
- package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
- package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
- package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
- package/templates/add/auth/internal/app/user/application/external_login.go.hbs +198 -0
- package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
- package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
- package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
- package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
- package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
- package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
- package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
- package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
- package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
- package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
- package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
- package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
- package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
- package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
- package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
- package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
- package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
- package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
- package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +10 -5
- package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
- package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
- package/templates/add/auth/migrations/create_users.up.sql.hbs +4 -3
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
- package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
- package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
- package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
- package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
- package/templates/add/rbac/internal/app/role/composition.go.hbs +48 -0
- package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
- package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
- package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
- package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
- package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
- package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +358 -121
- package/templates/create/base/.env.example.hbs +0 -1
- package/templates/create/base/.golangci.yml.hbs +2 -2
- package/templates/create/base/AGENTS.md.hbs +279 -67
- package/templates/create/base/Makefile.hbs +2 -1
- package/templates/create/base/README.md.hbs +115 -32
- package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
- package/templates/create/base/internal/composition/doc.go.hbs +7 -0
- package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
- package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
- package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
- package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
- package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
- package/templates/create/features/docs/architecture.md.hbs +92 -32
- package/templates/create/features/docs/patterns.md.hbs +137 -91
- package/templates/create/features/docs/techstack.md.hbs +18 -3
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
- package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
- package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
- package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
- package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
- package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
- package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
- package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
- package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
- package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
- package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
- package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
- package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
- package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
- package/templates/generate/module/migration.up.sql.hbs +1 -1
- package/dist/utils/method-patcher.js +0 -357
- package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
- package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
- package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
- package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
- package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
- package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
- package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
- package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
- package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
- package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
- package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
- package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
- package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
- package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
- package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
- package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
- package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
- package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
- package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
- package/templates/generate/module/dto.go.hbs +0 -36
- package/templates/generate/module/errors.go.hbs +0 -33
- package/templates/generate/module/handler.go.hbs +0 -134
- package/templates/generate/module/handler_test.go.hbs +0 -174
- package/templates/generate/module/minimal/dto.go.hbs +0 -28
- package/templates/generate/module/minimal/handler.go.hbs +0 -48
- package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
- package/templates/generate/module/minimal/service.go.hbs +0 -45
- package/templates/generate/module/minimal/service_test.go.hbs +0 -77
- package/templates/generate/module/model/model.go.hbs +0 -36
- package/templates/generate/module/repository.go.hbs +0 -103
- package/templates/generate/module/service.go.hbs +0 -108
- package/templates/generate/module/service_test.go.hbs +0 -161
package/dist/commands/auth.js
CHANGED
|
@@ -18,6 +18,8 @@ const migrations_1 = require("../utils/migrations");
|
|
|
18
18
|
const openapi_patcher_1 = require("../utils/openapi-patcher");
|
|
19
19
|
const gocheck_1 = require("../utils/gocheck");
|
|
20
20
|
const gomod_patcher_1 = require("../utils/gomod-patcher");
|
|
21
|
+
const auth_wizard_1 = require("../prompts/auth-wizard");
|
|
22
|
+
const docs_patcher_1 = require("../utils/docs-patcher");
|
|
21
23
|
// URL (relative to the api prefix) -> docs file (relative to docs/) for every
|
|
22
24
|
// route `add auth` registers — kept next to AUTH_FILES's route list so the
|
|
23
25
|
// two are easy to eyeball together when a route changes.
|
|
@@ -29,20 +31,26 @@ const AUTH_OPENAPI_PATHS = [
|
|
|
29
31
|
{ urlPath: "/auth/forgot-password", file: "./auth/forgot-password.yaml" },
|
|
30
32
|
{ urlPath: "/auth/reset-password", file: "./auth/reset-password.yaml" },
|
|
31
33
|
{ urlPath: "/auth/verify-email", file: "./auth/verify-email.yaml" },
|
|
32
|
-
{ urlPath: "/auth/
|
|
33
|
-
{ urlPath: "/auth/
|
|
34
|
+
{ urlPath: "/auth/{provider}/login", file: "./auth/provider-login.yaml" },
|
|
35
|
+
{ urlPath: "/auth/{provider}/exchange", file: "./auth/provider-exchange.yaml" },
|
|
34
36
|
{ urlPath: "/users/me", file: "./auth/users-me.yaml" },
|
|
35
37
|
{ urlPath: "/users/me/resend-verification", file: "./auth/users-me-resend-verification.yaml" },
|
|
36
38
|
{ urlPath: "/users/me/logout-all", file: "./auth/users-me-logout-all.yaml" },
|
|
39
|
+
{ urlPath: "/users/me/mfa", file: "./auth/users-me-mfa.yaml" },
|
|
40
|
+
{ urlPath: "/users/me/mfa/setup", file: "./auth/users-me-mfa-setup.yaml" },
|
|
41
|
+
{ urlPath: "/users/me/mfa/confirm", file: "./auth/users-me-mfa-confirm.yaml" },
|
|
42
|
+
{ urlPath: "/users/me/mfa/disable", file: "./auth/users-me-mfa-disable.yaml" },
|
|
43
|
+
{ urlPath: "/auth/mfa/verify", file: "./auth/mfa-verify.yaml" },
|
|
37
44
|
];
|
|
38
45
|
// addAuth scaffolds email/password authentication: a users+identities model
|
|
39
|
-
// pair, JWT access tokens, a
|
|
46
|
+
// pair, JWT access tokens, a selectable refresh token store with
|
|
40
47
|
// rotation + reuse detection, and register/login/refresh/logout/me. No RBAC
|
|
41
48
|
// (no roles/permissions) — that's a separate opt-in on top of this, since
|
|
42
49
|
// most projects need "is this caller logged in" long before they need "can
|
|
43
50
|
// this caller do X".
|
|
44
|
-
async function addAuth(store = "postgres", projectDir = process.cwd()) {
|
|
51
|
+
async function addAuth(store = "postgres", projectDir = process.cwd(), browserTopology = auth_wizard_1.DEFAULT_BROWSER_TOPOLOGY) {
|
|
45
52
|
const config = (0, config_1.readConfig)(projectDir);
|
|
53
|
+
const browser = (0, auth_wizard_1.validateBrowserTopology)(browserTopology);
|
|
46
54
|
// No longer a prerequisite. Without a worker the verification and reset mail
|
|
47
55
|
// goes out inline instead of through a queue — a real trade (those two
|
|
48
56
|
// endpoints then block on SMTP), but not one worth forcing a second binary
|
|
@@ -63,7 +71,11 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
|
|
|
63
71
|
(0, platform_patcher_1.patchConfigForSMTP)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
|
|
64
72
|
patchEnvExampleForSMTP(path_1.default.join(projectDir, ".env.example"));
|
|
65
73
|
}
|
|
66
|
-
await (0, template_renderer_1.applyTemplateEntries)(projectDir, (0, auth_manifest_1.authFiles)(store), {
|
|
74
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, (0, auth_manifest_1.authFiles)(store), {
|
|
75
|
+
goModule: config.goModule,
|
|
76
|
+
redis: store === "redis",
|
|
77
|
+
worker,
|
|
78
|
+
});
|
|
67
79
|
const migrationsDir = path_1.default.join(projectDir, "migrations");
|
|
68
80
|
fs_extra_1.default.ensureDirSync(migrationsDir);
|
|
69
81
|
const usersVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
@@ -71,6 +83,11 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
|
|
|
71
83
|
{ template: "add/auth/migrations/create_users.up.sql.hbs", output: path_1.default.join("migrations", `${usersVersion}_create_users.up.sql`) },
|
|
72
84
|
{ template: "add/auth/migrations/create_users.down.sql.hbs", output: path_1.default.join("migrations", `${usersVersion}_create_users.down.sql`) },
|
|
73
85
|
], {});
|
|
86
|
+
const mfaVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
87
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
|
|
88
|
+
{ template: "add/auth/migrations/create_mfa.up.sql.hbs", output: path_1.default.join("migrations", `${mfaVersion}_create_mfa.up.sql`) },
|
|
89
|
+
{ template: "add/auth/migrations/create_mfa.down.sql.hbs", output: path_1.default.join("migrations", `${mfaVersion}_create_mfa.down.sql`) },
|
|
90
|
+
], {});
|
|
74
91
|
// identities references users(id) — must apply strictly after it. A
|
|
75
92
|
// second newMigrationVersion() call, scanning the dir again now that the
|
|
76
93
|
// users pair is already written, guarantees a later (or same-second,
|
|
@@ -87,13 +104,13 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
|
|
|
87
104
|
{ template: "add/auth/migrations/create_login_throttle.up.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.up.sql`) },
|
|
88
105
|
{ template: "add/auth/migrations/create_login_throttle.down.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.down.sql`) },
|
|
89
106
|
], {});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
107
|
+
// Recovery tokens are always stored in Postgres so consumption and the user
|
|
108
|
+
// update can share one transaction, even when refresh rotation uses Redis.
|
|
109
|
+
const authTokensVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
|
|
110
|
+
await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
|
|
111
|
+
{ template: "add/auth/migrations/create_auth_tokens.up.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.up.sql`) },
|
|
112
|
+
{ template: "add/auth/migrations/create_auth_tokens.down.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.down.sql`) },
|
|
113
|
+
], {});
|
|
97
114
|
// Only meaningful when there is a worker; readConfig fills this from the
|
|
98
115
|
// adapter file on disk, so the only way it is still unknown is a project
|
|
99
116
|
// that has internal/platform/queue with neither adapter in it. Guessing
|
|
@@ -120,7 +137,7 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
|
|
|
120
137
|
// go-redis only when something in this project actually constructs a client
|
|
121
138
|
...(store === "redis" ? ["github.com/redis/go-redis/v9 v9.22.0"] : []),
|
|
122
139
|
]);
|
|
123
|
-
patchEnvExample(path_1.default.join(projectDir, ".env.example"));
|
|
140
|
+
patchEnvExample(path_1.default.join(projectDir, ".env.example"), browser);
|
|
124
141
|
patchMakefile(path_1.default.join(projectDir, "Makefile"));
|
|
125
142
|
let docsMessage = "";
|
|
126
143
|
const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
|
|
@@ -140,19 +157,30 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
|
|
|
140
157
|
// parse-only: jwt/oauth2/bcrypt aren't in go.mod until the `go mod tidy`
|
|
141
158
|
// printed below, so `go vet` can't be the gate here.
|
|
142
159
|
(0, gocheck_1.assertStillParses)(projectDir, parsedBefore, "added auth");
|
|
143
|
-
(0,
|
|
160
|
+
const staleDocs = (0, docs_patcher_1.refreshProjectDocs)(projectDir, config, { auth: true, authStore: store });
|
|
161
|
+
if (staleDocs.length)
|
|
162
|
+
docsMessage += (0, docs_patcher_1.docsRefreshWarning)(staleDocs, "add auth");
|
|
163
|
+
(0, config_1.writeConfig)(projectDir, {
|
|
164
|
+
...config,
|
|
165
|
+
features: { ...config.features, auth: true, authStore: store },
|
|
166
|
+
modules: {
|
|
167
|
+
...config.modules,
|
|
168
|
+
user: { surface: "minimal", applicationStyle: "service", boundary: "hexagonal", packageLayout: "split" },
|
|
169
|
+
},
|
|
170
|
+
});
|
|
144
171
|
console.log(picocolors_1.default.green("\nadded internal/app/user/, internal/shared/middleware/auth.go, and cmd/seed"));
|
|
145
172
|
console.log(worker
|
|
146
173
|
? "verification + password-reset mail goes through the queue"
|
|
147
174
|
: "verification + password-reset mail is sent inline (no worker) — run `add worker` later to move it onto the queue");
|
|
148
175
|
console.log(store === "postgres"
|
|
149
|
-
? "
|
|
150
|
-
: "tokens + rate-limit counters: Redis");
|
|
176
|
+
? "refresh + recovery tokens: Postgres (user_svc.auth_tokens), rate-limit counters in-process — no Redis"
|
|
177
|
+
: "refresh tokens + rate-limit counters: Redis; recovery tokens: Postgres (user_svc.auth_tokens)");
|
|
151
178
|
console.log("registered POST /auth/{register,login,refresh,logout,forgot-password,reset-password,verify-email}, " +
|
|
152
|
-
"GET /auth/
|
|
153
|
-
"POST /users/me/{resend-verification,logout-all}
|
|
179
|
+
"GET /auth/{provider}/login, POST /auth/{provider}/exchange, GET /users/me, and " +
|
|
180
|
+
"POST /users/me/{resend-verification,logout-all,mfa/setup,mfa/confirm,mfa/disable}, " +
|
|
181
|
+
"GET /users/me/mfa, and POST /auth/mfa/verify in cmd/api/wiring.go" +
|
|
154
182
|
docsMessage);
|
|
155
|
-
console.log(picocolors_1.default.dim("\nnext: go mod tidy, then apply the new migrations
|
|
183
|
+
console.log(picocolors_1.default.dim("\nnext: go mod tidy, then apply the new migrations with `make migrate-up` before production\n" +
|
|
156
184
|
"seed an admin: SEED_ADMIN_EMAIL=... SEED_ADMIN_PASSWORD=... make seed"));
|
|
157
185
|
}
|
|
158
186
|
function patchMakefile(makefilePath) {
|
|
@@ -183,7 +211,7 @@ function patchMakefile(makefilePath) {
|
|
|
183
211
|
content = content.replace(/\nbuild:/, () => `${target}\nbuild:`);
|
|
184
212
|
fs_extra_1.default.writeFileSync(makefilePath, content);
|
|
185
213
|
}
|
|
186
|
-
function patchEnvExample(envExamplePath) {
|
|
214
|
+
function patchEnvExample(envExamplePath, browserTopology) {
|
|
187
215
|
if (!fs_extra_1.default.existsSync(envExamplePath))
|
|
188
216
|
return;
|
|
189
217
|
let content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
|
|
@@ -195,13 +223,15 @@ function patchEnvExample(envExamplePath) {
|
|
|
195
223
|
"JWT_SECRET=dev-secret-change-me\n" +
|
|
196
224
|
"JWT_ACCESS_TTL_MIN=15\n" +
|
|
197
225
|
"JWT_REFRESH_TTL_MIN=43200\n" +
|
|
198
|
-
"
|
|
226
|
+
"JWT_REFRESH_MAX_TTL_MIN=43200\n" +
|
|
227
|
+
"OAUTH_STATE_TTL_MIN=10\n" +
|
|
228
|
+
`COOKIE_SECURE=${browserTopology === "cross-site" ? "true" : "false"}\n` +
|
|
199
229
|
"# strict | lax | none — the refresh cookie's SameSite. Keep strict while the\n" +
|
|
200
230
|
"# frontend is the same site as this API (localhost:3000 -> localhost:8080 is,\n" +
|
|
201
231
|
"# and so is app.example.com -> api.example.com). A frontend on a different\n" +
|
|
202
232
|
"# site entirely needs none, together with COOKIE_SECURE=true, or the browser\n" +
|
|
203
233
|
"# never sends the cookie to /auth/refresh and sessions die at every expiry.\n" +
|
|
204
|
-
"
|
|
234
|
+
`COOKIE_SAMESITE=${browserTopology === "cross-site" ? "none" : "strict"}\n` +
|
|
205
235
|
"\nPASSWORD_RESET_TTL_MIN=30\n" +
|
|
206
236
|
"PASSWORD_RESET_URL=http://localhost:3000/reset-password\n" +
|
|
207
237
|
"\nEMAIL_VERIFY_TTL_MIN=1440\n" +
|
|
@@ -209,7 +239,19 @@ function patchEnvExample(envExamplePath) {
|
|
|
209
239
|
"\n# leave the Google vars unset to disable Google login (register/login/refresh still work)\n" +
|
|
210
240
|
"GOOGLE_CLIENT_ID=\n" +
|
|
211
241
|
"GOOGLE_CLIENT_SECRET=\n" +
|
|
212
|
-
"
|
|
242
|
+
"# exact browser callback URI registered with the provider (frontend-owned route)\n" +
|
|
243
|
+
"GOOGLE_OAUTH_REDIRECT_URI=\n" +
|
|
244
|
+
"\n# cookie/CORS deployment topology; the frontend owns its provider callback route\n" +
|
|
245
|
+
`AUTH_BROWSER_TOPOLOGY=${browserTopology}\n` +
|
|
246
|
+
"\n# MFA is globally off by default. When enabled, set a base64-encoded 32-byte\n" +
|
|
247
|
+
"# AES-256 key (for example: openssl rand -base64 32). Users still opt in\n" +
|
|
248
|
+
"# individually through /users/me/mfa/setup and /users/me/mfa/confirm.\n" +
|
|
249
|
+
"AUTH_MFA_ENABLED=false\n" +
|
|
250
|
+
"MFA_ISSUER=go-scaffold\n" +
|
|
251
|
+
"MFA_ENCRYPTION_KEY=\n" +
|
|
252
|
+
"MFA_CHALLENGE_TTL_MIN=5\n" +
|
|
253
|
+
"MFA_TOTP_WINDOW=1\n" +
|
|
254
|
+
"MFA_RECOVERY_CODE_COUNT=10\n";
|
|
213
255
|
fs_extra_1.default.writeFileSync(envExamplePath, content);
|
|
214
256
|
}
|
|
215
257
|
// ensureRedis adds internal/platform/cache + its config/env/main.go wiring
|
|
@@ -0,0 +1,281 @@
|
|
|
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.checkProject = checkProject;
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const config_1 = require("../utils/config");
|
|
11
|
+
const forbiddenByLayer = {
|
|
12
|
+
domain: [
|
|
13
|
+
/github\.com\/gin-gonic\/gin/,
|
|
14
|
+
/gorm\.io\//,
|
|
15
|
+
/net\/http/,
|
|
16
|
+
/internal\/(shared|platform)\//,
|
|
17
|
+
/redis/,
|
|
18
|
+
],
|
|
19
|
+
application: [
|
|
20
|
+
/github\.com\/gin-gonic\/gin/,
|
|
21
|
+
/gorm\.io\//,
|
|
22
|
+
/database\/sql/,
|
|
23
|
+
/internal\/platform\//,
|
|
24
|
+
/internal\/shared\/(apperror|httpx|middleware|tx|dberr)\//,
|
|
25
|
+
/redis/,
|
|
26
|
+
],
|
|
27
|
+
ports: [
|
|
28
|
+
/github\.com\/gin-gonic\/gin/,
|
|
29
|
+
/gorm\.io\//,
|
|
30
|
+
/database\/sql/,
|
|
31
|
+
/internal\/platform\//,
|
|
32
|
+
/internal\/shared\/(apperror|httpx|middleware|tx|dberr)\//,
|
|
33
|
+
/redis/,
|
|
34
|
+
],
|
|
35
|
+
inbound: [/gorm\.io\//, /internal\/platform\//],
|
|
36
|
+
outbound: [/github\.com\/gin-gonic\/gin/, /internal\/shared\/httpx\//, /internal\/shared\/middleware\//],
|
|
37
|
+
};
|
|
38
|
+
// A split module is not hexagonal merely because its folders have the right
|
|
39
|
+
// names. Keep dependencies flowing toward the application core. Composition
|
|
40
|
+
// is the only module-local exception because it is the explicit wiring edge.
|
|
41
|
+
const forbiddenInternalLayers = {
|
|
42
|
+
domain: ["application", "ports", "inbound", "outbound", "composition"],
|
|
43
|
+
ports: ["application", "inbound", "outbound", "composition"],
|
|
44
|
+
application: ["inbound", "outbound", "composition"],
|
|
45
|
+
inbound: ["outbound", "composition"],
|
|
46
|
+
outbound: ["application", "inbound", "composition"],
|
|
47
|
+
};
|
|
48
|
+
function goFiles(dir) {
|
|
49
|
+
if (!fs_extra_1.default.existsSync(dir))
|
|
50
|
+
return [];
|
|
51
|
+
const files = [];
|
|
52
|
+
for (const entry of fs_extra_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
53
|
+
const entryPath = path_1.default.join(dir, entry.name);
|
|
54
|
+
if (entry.isDirectory())
|
|
55
|
+
files.push(...goFiles(entryPath));
|
|
56
|
+
else if (entry.isFile() && entry.name.endsWith(".go"))
|
|
57
|
+
files.push(entryPath);
|
|
58
|
+
}
|
|
59
|
+
return files;
|
|
60
|
+
}
|
|
61
|
+
function directoriesNamed(dir, names) {
|
|
62
|
+
if (!fs_extra_1.default.existsSync(dir))
|
|
63
|
+
return [];
|
|
64
|
+
const found = [];
|
|
65
|
+
for (const entry of fs_extra_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
66
|
+
if (!entry.isDirectory())
|
|
67
|
+
continue;
|
|
68
|
+
const entryPath = path_1.default.join(dir, entry.name);
|
|
69
|
+
if (names.has(entry.name))
|
|
70
|
+
found.push(entryPath);
|
|
71
|
+
found.push(...directoriesNamed(entryPath, names));
|
|
72
|
+
}
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
function imports(content) {
|
|
76
|
+
const found = [];
|
|
77
|
+
for (const match of content.matchAll(/^\s*(?:import\s+)?(?:[._\w]+\s+)?"([^"]+)"\s*$/gm))
|
|
78
|
+
found.push(match[1]);
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
81
|
+
function internalLayer(imported, moduleName) {
|
|
82
|
+
const match = imported.match(/(?:^|\/)internal\/app\/([^/]+)\/(.+)$/);
|
|
83
|
+
if (!match || match[1] !== moduleName)
|
|
84
|
+
return undefined;
|
|
85
|
+
const relative = match[2];
|
|
86
|
+
if (relative === "domain" || relative.startsWith("domain/"))
|
|
87
|
+
return "domain";
|
|
88
|
+
if (relative === "application" || relative.startsWith("application/"))
|
|
89
|
+
return "application";
|
|
90
|
+
if (relative === "ports" || relative.startsWith("ports/"))
|
|
91
|
+
return "ports";
|
|
92
|
+
if (relative === "adapters/inbound" || relative.startsWith("adapters/inbound/"))
|
|
93
|
+
return "inbound";
|
|
94
|
+
if (relative === "adapters/outbound" || relative.startsWith("adapters/outbound/"))
|
|
95
|
+
return "outbound";
|
|
96
|
+
if (relative === "composition.go" || relative === "composition")
|
|
97
|
+
return "composition";
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
function siblingModule(imported, current) {
|
|
101
|
+
const match = imported.match(/(?:^|\/)internal\/app\/([^/]+)(?:\/|$)/);
|
|
102
|
+
if (!match || match[1] === current)
|
|
103
|
+
return undefined;
|
|
104
|
+
return match[1];
|
|
105
|
+
}
|
|
106
|
+
function appImport(imported) {
|
|
107
|
+
const match = imported.match(/(?:^|\/)internal\/app\/([^/]+)(?:\/(.*))?$/);
|
|
108
|
+
if (!match)
|
|
109
|
+
return undefined;
|
|
110
|
+
return { module: match[1], privatePath: match[2] ?? "" };
|
|
111
|
+
}
|
|
112
|
+
function filesForLayer(moduleDir, layer) {
|
|
113
|
+
if (layer === "composition")
|
|
114
|
+
return goFiles(moduleDir).filter((file) => path_1.default.basename(file) === "composition.go");
|
|
115
|
+
const relative = layer === "inbound"
|
|
116
|
+
? path_1.default.join("adapters", "inbound", "http")
|
|
117
|
+
: layer === "outbound"
|
|
118
|
+
? path_1.default.join("adapters", "outbound")
|
|
119
|
+
: layer;
|
|
120
|
+
return goFiles(path_1.default.join(moduleDir, relative));
|
|
121
|
+
}
|
|
122
|
+
function moduleNames(projectDir, config) {
|
|
123
|
+
const appDir = path_1.default.join(projectDir, "internal", "app");
|
|
124
|
+
const discovered = fs_extra_1.default.existsSync(appDir)
|
|
125
|
+
? fs_extra_1.default.readdirSync(appDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
|
|
126
|
+
: [];
|
|
127
|
+
return [...new Set([...Object.keys(config.modules), ...discovered])].sort();
|
|
128
|
+
}
|
|
129
|
+
function expectedFiles(moduleDir, module) {
|
|
130
|
+
const required = [
|
|
131
|
+
"composition.go",
|
|
132
|
+
"domain/entity.go",
|
|
133
|
+
"domain/errors.go",
|
|
134
|
+
"ports/repository.go",
|
|
135
|
+
"application/dto.go",
|
|
136
|
+
"adapters/inbound/http/dto.go",
|
|
137
|
+
"adapters/inbound/http/handler.go",
|
|
138
|
+
"adapters/outbound/postgres/model.go",
|
|
139
|
+
"adapters/outbound/postgres/repository.go",
|
|
140
|
+
];
|
|
141
|
+
if (module.applicationStyle === "service")
|
|
142
|
+
required.push("application/service.go");
|
|
143
|
+
else
|
|
144
|
+
required.push("application/commands.go", "application/queries.go");
|
|
145
|
+
return required.map((file) => path_1.default.join(moduleDir, file));
|
|
146
|
+
}
|
|
147
|
+
function checkModule(projectDir, config, name, module) {
|
|
148
|
+
const errors = [];
|
|
149
|
+
const moduleDir = path_1.default.join(projectDir, "internal", "app", name);
|
|
150
|
+
if (!module) {
|
|
151
|
+
errors.push(`internal/app/${name}: missing module metadata in go-scaffold.config.json`);
|
|
152
|
+
return errors;
|
|
153
|
+
}
|
|
154
|
+
if (module.boundary !== "hexagonal" || module.packageLayout !== "split") {
|
|
155
|
+
errors.push(`internal/app/${name}: module must declare boundary=hexagonal and packageLayout=split`);
|
|
156
|
+
}
|
|
157
|
+
for (const forbidden of directoriesNamed(moduleDir, new Set(["model"]))) {
|
|
158
|
+
errors.push(`${path_1.default.relative(projectDir, forbidden)}: feature-level model package is forbidden; keep persistence models in the outbound adapter`);
|
|
159
|
+
}
|
|
160
|
+
for (const forbidden of directoriesNamed(moduleDir, new Set(["compat"]))) {
|
|
161
|
+
errors.push(`${path_1.default.relative(projectDir, forbidden)}: feature-level compat directory is forbidden; keep legacy compatibility out of the canonical split architecture`);
|
|
162
|
+
}
|
|
163
|
+
for (const file of expectedFiles(moduleDir, module)) {
|
|
164
|
+
if (!fs_extra_1.default.existsSync(file))
|
|
165
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: required by the split module contract`);
|
|
166
|
+
}
|
|
167
|
+
const applicationDir = path_1.default.join(moduleDir, "application");
|
|
168
|
+
if (module.applicationStyle === "cqrs" && fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "service.go"))) {
|
|
169
|
+
errors.push(`internal/app/${name}: CQRS module must not contain application/service.go; use commands.go and queries.go`);
|
|
170
|
+
}
|
|
171
|
+
if (module.applicationStyle === "cqrs" && fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "service_test.go"))) {
|
|
172
|
+
errors.push(`internal/app/${name}: CQRS module must not contain application/service_test.go; use cqrs_test.go`);
|
|
173
|
+
}
|
|
174
|
+
if (module.applicationStyle === "service" && (fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "commands.go")) || fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "queries.go")))) {
|
|
175
|
+
errors.push(`internal/app/${name}: service module must not contain application/commands.go or queries.go`);
|
|
176
|
+
}
|
|
177
|
+
if (module.applicationStyle === "service" && fs_extra_1.default.existsSync(path_1.default.join(applicationDir, "cqrs_test.go"))) {
|
|
178
|
+
errors.push(`internal/app/${name}: service module must not contain application/cqrs_test.go; use service_test.go`);
|
|
179
|
+
}
|
|
180
|
+
for (const [layer, patterns] of Object.entries(forbiddenByLayer)) {
|
|
181
|
+
for (const file of filesForLayer(moduleDir, layer)) {
|
|
182
|
+
const content = fs_extra_1.default.readFileSync(file, "utf8");
|
|
183
|
+
for (const imported of imports(content)) {
|
|
184
|
+
const rule = patterns.find((pattern) => pattern.test(imported));
|
|
185
|
+
if (rule)
|
|
186
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: ${layer} layer imports forbidden dependency ${imported}`);
|
|
187
|
+
const importedLayer = internalLayer(imported, name);
|
|
188
|
+
if (importedLayer && forbiddenInternalLayers[layer].includes(importedLayer)) {
|
|
189
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: ${layer} layer imports forbidden internal layer ${imported}`);
|
|
190
|
+
}
|
|
191
|
+
const other = siblingModule(imported, name);
|
|
192
|
+
if (other) {
|
|
193
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: imports sibling module ${other}; use a port and process composition`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (layer === "application" && path_1.default.basename(file) === "dto.go" && content.includes("json:")) {
|
|
197
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: application DTOs must be transport-neutral; keep JSON tags in adapters/inbound/http/dto.go`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
for (const file of filesForLayer(moduleDir, "composition")) {
|
|
202
|
+
const content = fs_extra_1.default.readFileSync(file, "utf8");
|
|
203
|
+
for (const imported of imports(content)) {
|
|
204
|
+
const other = siblingModule(imported, name);
|
|
205
|
+
if (other) {
|
|
206
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: imports sibling module ${other}; use a port and process composition`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const file of goFiles(moduleDir)) {
|
|
211
|
+
const content = fs_extra_1.default.readFileSync(file, "utf8");
|
|
212
|
+
for (const imported of imports(content)) {
|
|
213
|
+
if (/\/internal\/app\/[^/]+\/model(?:\/|$)/.test(imported)) {
|
|
214
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: imports a forbidden feature-level model package ${imported}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const rootGoFiles = fs_extra_1.default.existsSync(moduleDir)
|
|
219
|
+
? fs_extra_1.default
|
|
220
|
+
.readdirSync(moduleDir, { withFileTypes: true })
|
|
221
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".go"))
|
|
222
|
+
.map((entry) => path_1.default.join(moduleDir, entry.name))
|
|
223
|
+
: [];
|
|
224
|
+
for (const file of rootGoFiles) {
|
|
225
|
+
if (path_1.default.basename(file) !== "composition.go") {
|
|
226
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: root module package may only contain composition.go`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return errors;
|
|
230
|
+
}
|
|
231
|
+
function checkProcessComposition(projectDir) {
|
|
232
|
+
const errors = [];
|
|
233
|
+
const compositionDir = path_1.default.join(projectDir, "internal", "composition");
|
|
234
|
+
const docPath = path_1.default.join(compositionDir, "doc.go");
|
|
235
|
+
if (!fs_extra_1.default.existsSync(docPath)) {
|
|
236
|
+
errors.push("internal/composition/doc.go: required process-level composition package");
|
|
237
|
+
}
|
|
238
|
+
for (const file of goFiles(compositionDir)) {
|
|
239
|
+
const content = fs_extra_1.default.readFileSync(file, "utf8");
|
|
240
|
+
for (const imported of imports(content)) {
|
|
241
|
+
const feature = appImport(imported);
|
|
242
|
+
if (feature?.privatePath) {
|
|
243
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: process composition must import the public internal/app/${feature.module} package, not private path ${imported}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// wiring.go is allowed to see every feature package because it is the
|
|
248
|
+
// executable's root. Other cmd/api files must not become hidden
|
|
249
|
+
// cross-feature adapters; those belong here so integration tests can reuse
|
|
250
|
+
// the same mapping without importing package main.
|
|
251
|
+
const apiDir = path_1.default.join(projectDir, "cmd", "api");
|
|
252
|
+
for (const file of goFiles(apiDir)) {
|
|
253
|
+
const base = path_1.default.basename(file);
|
|
254
|
+
if (base === "main.go" || base === "wiring.go")
|
|
255
|
+
continue;
|
|
256
|
+
for (const imported of imports(fs_extra_1.default.readFileSync(file, "utf8"))) {
|
|
257
|
+
if (appImport(imported)) {
|
|
258
|
+
errors.push(`${path_1.default.relative(projectDir, file)}: process-level feature adapters belong in internal/composition; keep cmd/api for entrypoint and wiring`);
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return errors;
|
|
264
|
+
}
|
|
265
|
+
function checkProject(projectDir = process.cwd()) {
|
|
266
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
267
|
+
const errors = [];
|
|
268
|
+
if (config.architecture.style !== "modular-monolith")
|
|
269
|
+
errors.push("architecture.style must be modular-monolith");
|
|
270
|
+
if (config.architecture.boundary !== "hexagonal")
|
|
271
|
+
errors.push("architecture.boundary must be hexagonal");
|
|
272
|
+
if (config.architecture.packageLayout !== "split")
|
|
273
|
+
errors.push("architecture.packageLayout must be split");
|
|
274
|
+
errors.push(...checkProcessComposition(projectDir));
|
|
275
|
+
for (const name of moduleNames(projectDir, config))
|
|
276
|
+
errors.push(...checkModule(projectDir, config, name, config.modules[name]));
|
|
277
|
+
if (errors.length) {
|
|
278
|
+
throw new Error(`${picocolors_1.default.red("architecture check failed")}:\n${errors.map((error) => ` - ${error}`).join("\n")}`);
|
|
279
|
+
}
|
|
280
|
+
console.log(picocolors_1.default.green(`architecture check passed: ${moduleNames(projectDir, config).length} split module(s), hexagonal boundary`));
|
|
281
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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.configureProject = configureProject;
|
|
7
|
+
exports.showProjectConfig = showProjectConfig;
|
|
8
|
+
exports.validateProjectConfig = validateProjectConfig;
|
|
9
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
10
|
+
const generate_wizard_1 = require("../prompts/generate-wizard");
|
|
11
|
+
const interactive_1 = require("../prompts/interactive");
|
|
12
|
+
const config_1 = require("../utils/config");
|
|
13
|
+
const module_profile_1 = require("../utils/module-profile");
|
|
14
|
+
/**
|
|
15
|
+
* Interactive project-level defaults. Existing modules are not rewritten: the
|
|
16
|
+
* wizard only controls what a future `generate module` receives when the user
|
|
17
|
+
* chooses `--defaults` or accepts the generated-module prompt defaults.
|
|
18
|
+
*/
|
|
19
|
+
async function configureProject(projectDir = process.cwd()) {
|
|
20
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
21
|
+
console.log("\nConfigure future generated modules:\n");
|
|
22
|
+
const profile = await (0, generate_wizard_1.promptModuleProfile)((0, module_profile_1.moduleProfileFor)(config.architecture.defaultModuleSurface, config.architecture.defaultApplicationStyle));
|
|
23
|
+
const { moduleSurface: defaultModuleSurface, applicationStyle: defaultApplicationStyle } = profile === "advanced"
|
|
24
|
+
? await (0, generate_wizard_1.promptAdvancedModuleArchitecture)(config.architecture.defaultModuleSurface, config.architecture.defaultApplicationStyle)
|
|
25
|
+
: (0, generate_wizard_1.moduleArchitectureForProfile)(profile);
|
|
26
|
+
console.log("\nSummary:");
|
|
27
|
+
console.log(` Default module profile: ${(0, generate_wizard_1.moduleProfileDescription)(defaultModuleSurface, defaultApplicationStyle)}`);
|
|
28
|
+
console.log(` Default module surface: ${defaultModuleSurface}`);
|
|
29
|
+
console.log(` Default application style: ${defaultApplicationStyle}`);
|
|
30
|
+
console.log(" Existing modules: unchanged");
|
|
31
|
+
const proceed = await (0, interactive_1.confirm)({ message: "Save these project defaults?", default: true });
|
|
32
|
+
if (!proceed)
|
|
33
|
+
throw new Error("configuration cancelled");
|
|
34
|
+
(0, config_1.writeConfig)(projectDir, {
|
|
35
|
+
...config,
|
|
36
|
+
architecture: {
|
|
37
|
+
...config.architecture,
|
|
38
|
+
defaultModuleSurface,
|
|
39
|
+
defaultApplicationStyle,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
console.log(picocolors_1.default.green("\nupdated go-scaffold.config.json"));
|
|
43
|
+
}
|
|
44
|
+
function showProjectConfig(projectDir = process.cwd()) {
|
|
45
|
+
console.log(JSON.stringify((0, config_1.readConfig)(projectDir), null, 2));
|
|
46
|
+
}
|
|
47
|
+
function validateProjectConfig(projectDir = process.cwd()) {
|
|
48
|
+
const config = (0, config_1.readConfig)(projectDir);
|
|
49
|
+
console.log(picocolors_1.default.green(`valid go-scaffold.config.json (schema ${config.schemaVersion})`));
|
|
50
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -13,7 +13,10 @@ const config_1 = require("../utils/config");
|
|
|
13
13
|
const naming_1 = require("../utils/naming");
|
|
14
14
|
const create_wizard_1 = require("../prompts/create-wizard");
|
|
15
15
|
const version_1 = require("../utils/version");
|
|
16
|
+
const types_1 = require("../types");
|
|
17
|
+
const config_2 = require("../utils/config");
|
|
16
18
|
const observability_1 = require("./observability");
|
|
19
|
+
const module_profile_1 = require("../utils/module-profile");
|
|
17
20
|
async function createProject(rawName, opts) {
|
|
18
21
|
const trimmed = (rawName ?? (await (0, create_wizard_1.promptProjectName)())).trim();
|
|
19
22
|
if (!trimmed)
|
|
@@ -27,6 +30,13 @@ async function createProject(rawName, opts) {
|
|
|
27
30
|
}
|
|
28
31
|
let features;
|
|
29
32
|
let apiPrefix;
|
|
33
|
+
let architecture;
|
|
34
|
+
const moduleProfile = (0, config_1.parseModuleProfile)(opts.moduleProfile, "--module-profile");
|
|
35
|
+
const moduleSurface = (0, config_1.parseModuleSurface)(opts.moduleSurface);
|
|
36
|
+
const applicationStyle = (0, config_1.parseApplicationStyle)(opts.applicationStyle);
|
|
37
|
+
if (moduleProfile && (moduleSurface || applicationStyle)) {
|
|
38
|
+
throw new Error("--module-profile cannot be combined with --module-surface or --application-style — choose one configuration style");
|
|
39
|
+
}
|
|
30
40
|
if (opts.defaults) {
|
|
31
41
|
features = { docker: opts.docker ?? true, openapiDocs: opts.openapiDocs ?? true, observability: opts.observability ?? false };
|
|
32
42
|
// "" to match what the wizard's Enter now gives. --defaults means "don't
|
|
@@ -37,16 +47,27 @@ async function createProject(rawName, opts) {
|
|
|
37
47
|
const check = (0, naming_1.validateApiPrefix)(apiPrefix);
|
|
38
48
|
if (check !== true)
|
|
39
49
|
throw new Error(check);
|
|
50
|
+
const profileArchitecture = moduleProfile ? (0, module_profile_1.architectureForModuleProfile)(moduleProfile) : undefined;
|
|
51
|
+
architecture = {
|
|
52
|
+
...types_1.DEFAULT_ARCHITECTURE_CONFIG,
|
|
53
|
+
...(profileArchitecture ? { defaultModuleSurface: profileArchitecture.moduleSurface } : {}),
|
|
54
|
+
...(profileArchitecture ? { defaultApplicationStyle: profileArchitecture.applicationStyle } : {}),
|
|
55
|
+
...(!profileArchitecture && moduleSurface ? { defaultModuleSurface: moduleSurface } : {}),
|
|
56
|
+
...(!profileArchitecture && applicationStyle ? { defaultApplicationStyle: applicationStyle } : {}),
|
|
57
|
+
};
|
|
40
58
|
}
|
|
41
59
|
else {
|
|
42
60
|
// commander gives `--no-x` options a default of true, and there is no
|
|
43
61
|
// `--docker`/`--openapi-docs` to pass, so false here can only mean the
|
|
44
62
|
// caller opted out explicitly. undefined leaves the question to the wizard.
|
|
45
|
-
({ features, apiPrefix } = await (0, create_wizard_1.runCreateWizard)({
|
|
63
|
+
({ features, apiPrefix, architecture } = await (0, create_wizard_1.runCreateWizard)({
|
|
46
64
|
docker: opts.docker === false ? false : undefined,
|
|
47
65
|
openapiDocs: opts.openapiDocs === false ? false : undefined,
|
|
48
66
|
observability: opts.observability === true ? true : undefined,
|
|
49
67
|
apiPrefix: opts.apiPrefix,
|
|
68
|
+
moduleProfile,
|
|
69
|
+
moduleSurface,
|
|
70
|
+
applicationStyle,
|
|
50
71
|
}));
|
|
51
72
|
}
|
|
52
73
|
const context = {
|
|
@@ -54,12 +75,22 @@ async function createProject(rawName, opts) {
|
|
|
54
75
|
goModule,
|
|
55
76
|
dbName: (0, naming_1.toDbName)(projectName),
|
|
56
77
|
apiPrefix,
|
|
78
|
+
...architecture,
|
|
57
79
|
...features,
|
|
58
80
|
};
|
|
59
81
|
await fs_extra_1.default.ensureDir(projectDir);
|
|
60
82
|
await (0, template_renderer_1.applyTemplateEntries)(projectDir, create_manifest_1.CREATE_MANIFEST, context);
|
|
61
83
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
62
|
-
(0, config_1.writeConfig)(projectDir, {
|
|
84
|
+
(0, config_1.writeConfig)(projectDir, {
|
|
85
|
+
schemaVersion: config_2.CONFIG_SCHEMA_VERSION,
|
|
86
|
+
projectName,
|
|
87
|
+
goModule,
|
|
88
|
+
apiPrefix,
|
|
89
|
+
features,
|
|
90
|
+
architecture,
|
|
91
|
+
modules: {},
|
|
92
|
+
scaffoldVersion: (0, version_1.cliVersion)(),
|
|
93
|
+
});
|
|
63
94
|
// Composed the same way a user would do it by hand — `create` always
|
|
64
95
|
// renders the plain base, then this layers the same patches `add
|
|
65
96
|
// observability` would apply on an existing project, so the two paths
|
|
@@ -59,8 +59,18 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
59
59
|
modulePath,
|
|
60
60
|
auth: opts.auth,
|
|
61
61
|
permission: opts.permission,
|
|
62
|
+
full: opts.full,
|
|
63
|
+
cqrs: opts.cqrs,
|
|
64
|
+
moduleSurface: opts.full ? "crud" : "minimal",
|
|
65
|
+
applicationStyle: opts.cqrs ? "cqrs" : "service",
|
|
62
66
|
};
|
|
63
|
-
const moduleFiles = opts.
|
|
67
|
+
const moduleFiles = opts.cqrs
|
|
68
|
+
? opts.full
|
|
69
|
+
? module_manifest_1.MODULE_FILES_CQRS
|
|
70
|
+
: module_manifest_1.MODULE_FILES_MINIMAL_CQRS
|
|
71
|
+
: opts.full
|
|
72
|
+
? module_manifest_1.MODULE_FILES
|
|
73
|
+
: module_manifest_1.MODULE_FILES_MINIMAL;
|
|
64
74
|
const moduleEntries = moduleFiles.map((f) => ({
|
|
65
75
|
template: f.template,
|
|
66
76
|
output: path_1.default.join("internal", "app", modulePath, f.output),
|
|
@@ -132,8 +142,24 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
132
142
|
}
|
|
133
143
|
(0, template_renderer_1.gofmtTree)(projectDir);
|
|
134
144
|
(0, gocheck_1.assertNoDrift)(projectDir, checkBefore, config);
|
|
145
|
+
(0, config_1.writeConfig)(projectDir, {
|
|
146
|
+
...config,
|
|
147
|
+
modules: {
|
|
148
|
+
...config.modules,
|
|
149
|
+
[modulePath]: {
|
|
150
|
+
surface: opts.full ? "crud" : "minimal",
|
|
151
|
+
applicationStyle: opts.cqrs ? "cqrs" : "service",
|
|
152
|
+
boundary: "hexagonal",
|
|
153
|
+
packageLayout: "split",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
135
157
|
const routePath = config.apiPrefix ? `/${config.apiPrefix}/${naming.plural}` : `/${naming.plural}`;
|
|
136
158
|
console.log(picocolors_1.default.green(`\ngenerated internal/app/${modulePath}/`));
|
|
159
|
+
if (opts.cqrs) {
|
|
160
|
+
console.log("application boundary: CQRS command/query handlers in application/commands.go and application/queries.go");
|
|
161
|
+
}
|
|
162
|
+
console.log(`recorded module defaults: ${opts.full ? "crud" : "minimal"} + ${opts.cqrs ? "cqrs" : "service"}`);
|
|
137
163
|
if (opts.full) {
|
|
138
164
|
console.log(`registered route ${routePath} in cmd/api/wiring.go`);
|
|
139
165
|
}
|
|
@@ -161,6 +187,6 @@ async function generateModule(rawName, opts, projectDir = process.cwd()) {
|
|
|
161
187
|
}
|
|
162
188
|
if (docsMessage)
|
|
163
189
|
console.log(docsMessage);
|
|
164
|
-
console.log(picocolors_1.default.dim(`\nnext: add real fields to
|
|
165
|
-
`(
|
|
190
|
+
console.log(picocolors_1.default.dim(`\nnext: add real fields to domain/entity.go, application/dto.go, adapters/inbound/http/dto.go, and the outbound persistence model, then run \`go build ./...\` and apply the migration ` +
|
|
191
|
+
`(use the development bootstrap locally, or \`migrate -path migrations -database "$DB_DSN" up\` before production)`));
|
|
166
192
|
}
|