@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
|
@@ -22,7 +22,8 @@ import (
|
|
|
22
22
|
// run is this binary's composition root — the only function that sees every
|
|
23
23
|
// module and wires them to each other. Modules never import one another; a
|
|
24
24
|
// domain that needs another's behaviour declares a narrow interface and gets
|
|
25
|
-
//
|
|
25
|
+
// its concrete implementation from the process composition package (see
|
|
26
|
+
// docs/architect/patterns.md).
|
|
26
27
|
//
|
|
27
28
|
// It returns an error rather than calling os.Exit, so every defer below runs
|
|
28
29
|
// on the way out and main() holds the only exit in the binary.
|
|
@@ -46,17 +47,20 @@ func run() error {
|
|
|
46
47
|
if err != nil {
|
|
47
48
|
return fmt.Errorf("db handle: %w", err)
|
|
48
49
|
}
|
|
50
|
+
defer func() {
|
|
51
|
+
if err := sqlDB.Close(); err != nil {
|
|
52
|
+
logger.Error("close db", "error", err)
|
|
53
|
+
}
|
|
54
|
+
}()
|
|
49
55
|
// go-scaffold:platform-init
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
if !cfg.IsProd() {
|
|
58
|
+
// Development boot creates tables but never the schema they live in — each
|
|
59
|
+
// domain gets its own (see the outbound postgres model's TableName), so it has to exist
|
|
60
|
+
// before the bootstrap runs. Production runs the versioned SQL migrations
|
|
61
|
+
// instead, so this entire branch is skipped there.
|
|
62
|
+
// go-scaffold:schemas
|
|
56
63
|
|
|
57
|
-
if cfg.AutoMigrate {
|
|
58
|
-
// ponytail: AutoMigrate is for dev only (add-only, locks the table once data grows)
|
|
59
|
-
// prod: set AUTO_MIGRATE=false and run golang-migrate as versioned SQL instead
|
|
60
64
|
if err := db.AutoMigrate(
|
|
61
65
|
// go-scaffold:models
|
|
62
66
|
); err != nil {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Package composition contains process-level wiring and cross-feature adapters.
|
|
2
|
+
//
|
|
3
|
+
// Feature-local construction belongs in internal/app/<feature>/composition.go.
|
|
4
|
+
// Keep this package focused on translating caller-owned ports to another
|
|
5
|
+
// feature's public application capabilities. Do not put business rules,
|
|
6
|
+
// persistence adapters, or HTTP handlers here.
|
|
7
|
+
package composition
|
|
@@ -20,7 +20,8 @@ import (
|
|
|
20
20
|
|
|
21
21
|
// Open connects to Postgres and sets the connection pool — it talks to a
|
|
22
22
|
// real external system, so it lives in platform/, not shared/.
|
|
23
|
-
//
|
|
23
|
+
// Production schema is managed via golang-migrate (migrations/); development
|
|
24
|
+
// may use the convenience path selected by APP_ENV in the composition root.
|
|
24
25
|
func Open(cfg config.Config) (*gorm.DB, error) {
|
|
25
26
|
// GORM's default logger reports ErrRecordNotFound at ERROR level, so an
|
|
26
27
|
// ordinary miss — every FindByID behind a 404, cmd/seed's "does this admin
|
|
@@ -58,8 +59,7 @@ var migrationVersionRe = regexp.MustCompile(`^(\d+)_.*\.up\.sql$`)
|
|
|
58
59
|
// golang-migrate CLI's own schema_migrations table) doesn't match the newest
|
|
59
60
|
// migration file baked into this binary — instead of booting against a stale
|
|
60
61
|
// or half-applied schema and failing later on whatever query hits the
|
|
61
|
-
// missing column first.
|
|
62
|
-
// this from that branch only.
|
|
62
|
+
// missing column first. Call this from the production branch only.
|
|
63
63
|
func CheckMigrationVersion(db *gorm.DB) error {
|
|
64
64
|
entries, err := migrations.FS.ReadDir(".")
|
|
65
65
|
if err != nil {
|
|
@@ -10,10 +10,15 @@ type AppError struct {
|
|
|
10
10
|
Message string `json:"message"`
|
|
11
11
|
Details any `json:"details,omitempty"`
|
|
12
12
|
RequestID string `json:"request_id,omitempty"` // filled in by the error middleware
|
|
13
|
+
cause error
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
func (e *AppError) Error() string { return e.Message }
|
|
16
17
|
|
|
18
|
+
// Unwrap keeps the technical cause available to structured logging and
|
|
19
|
+
// errors.Is/As without putting it in the JSON response.
|
|
20
|
+
func (e *AppError) Unwrap() error { return e.cause }
|
|
21
|
+
|
|
17
22
|
func New(status int, code, msg string) *AppError {
|
|
18
23
|
return &AppError{HTTPStatus: status, Code: code, Message: msg}
|
|
19
24
|
}
|
|
@@ -30,7 +35,15 @@ func NewConflict(msg string) *AppError {
|
|
|
30
35
|
return &AppError{HTTPStatus: http.StatusConflict, Code: "CONFLICT", Message: msg}
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
func NewInternal() *AppError {
|
|
38
|
+
func NewInternal(cause ...error) *AppError {
|
|
34
39
|
// ponytail: never leak internal details to the client, log separately in middleware
|
|
35
|
-
|
|
40
|
+
err := &AppError{HTTPStatus: http.StatusInternalServerError, Code: "INTERNAL", Message: "internal server error"}
|
|
41
|
+
if len(cause) > 0 {
|
|
42
|
+
err.cause = cause[0]
|
|
43
|
+
}
|
|
44
|
+
return err
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func NewNotImplemented() *AppError {
|
|
48
|
+
return &AppError{HTTPStatus: http.StatusNotImplemented, Code: "NOT_IMPLEMENTED", Message: "not implemented"}
|
|
36
49
|
}
|
|
@@ -14,7 +14,6 @@ type Config struct {
|
|
|
14
14
|
Port string
|
|
15
15
|
DBDSN string
|
|
16
16
|
LogLevel string
|
|
17
|
-
AutoMigrate bool
|
|
18
17
|
DBMaxOpenConns int
|
|
19
18
|
DBMaxIdleConns int
|
|
20
19
|
DBConnMaxLifetime time.Duration
|
|
@@ -56,13 +55,6 @@ func Load() Config {
|
|
|
56
55
|
panic(fmt.Sprintf("invalid APP_ENV %q — must be development or production", cfg.AppEnv))
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
// Defaults off in production, which is why it's set here rather than in
|
|
60
|
-
// the literal above — it needs a validated AppEnv first. AutoMigrate
|
|
61
|
-
// rewrites the schema at boot AND skips CheckMigrationVersion entirely
|
|
62
|
-
// (see main.go), so a deploy that merely forgets to set it used to get
|
|
63
|
-
// both. AUTO_MIGRATE=true still forces it on if you really mean it.
|
|
64
|
-
cfg.AutoMigrate = env("AUTO_MIGRATE", strconv.FormatBool(!cfg.IsProd())) == "true"
|
|
65
|
-
|
|
66
58
|
return cfg
|
|
67
59
|
}
|
|
68
60
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package middleware
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"net/http"
|
|
5
|
+
"net/http/httptest"
|
|
6
|
+
"testing"
|
|
7
|
+
|
|
8
|
+
"github.com/gin-gonic/gin"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func TestCORS_AllowsOnlyConfiguredExactOrigins(t *testing.T) {
|
|
12
|
+
gin.SetMode(gin.TestMode)
|
|
13
|
+
tests := []struct {
|
|
14
|
+
name string
|
|
15
|
+
origin string
|
|
16
|
+
wantAllowOrigin string
|
|
17
|
+
wantCredentials bool
|
|
18
|
+
}{
|
|
19
|
+
{name: "allowed exact origin", origin: "http://localhost:3000", wantAllowOrigin: "http://localhost:3000", wantCredentials: true},
|
|
20
|
+
{name: "different origin denied", origin: "https://evil.example", wantAllowOrigin: "", wantCredentials: false},
|
|
21
|
+
{name: "wildcard is not a substitute", origin: "*", wantAllowOrigin: "", wantCredentials: false},
|
|
22
|
+
}
|
|
23
|
+
for _, tt := range tests {
|
|
24
|
+
t.Run(tt.name, func(t *testing.T) {
|
|
25
|
+
router := gin.New()
|
|
26
|
+
router.Use(CORS([]string{"http://localhost:3000"}))
|
|
27
|
+
router.GET("/refresh", func(c *gin.Context) { c.Status(http.StatusNoContent) })
|
|
28
|
+
request := httptest.NewRequest(http.MethodGet, "/refresh", nil)
|
|
29
|
+
request.Header.Set("Origin", tt.origin)
|
|
30
|
+
response := httptest.NewRecorder()
|
|
31
|
+
router.ServeHTTP(response, request)
|
|
32
|
+
if got := response.Header().Get("Access-Control-Allow-Origin"); got != tt.wantAllowOrigin {
|
|
33
|
+
t.Fatalf("Access-Control-Allow-Origin = %q, want %q", got, tt.wantAllowOrigin)
|
|
34
|
+
}
|
|
35
|
+
if got := response.Header().Get("Access-Control-Allow-Credentials") == "true"; got != tt.wantCredentials {
|
|
36
|
+
t.Fatalf("Allow-Credentials = %v, want %v", got, tt.wantCredentials)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -29,14 +29,24 @@ func Error(exposeDetail bool) gin.HandlerFunc {
|
|
|
29
29
|
if !errors.As(err.Err, &appErr) {
|
|
30
30
|
// Unexpected error: log the real thing, answer the client generically.
|
|
31
31
|
slog.Error("unhandled error", "error", err.Err, "request_id", c.GetString(RequestIDKey))
|
|
32
|
-
appErr = apperror.NewInternal()
|
|
32
|
+
appErr = apperror.NewInternal(err.Err)
|
|
33
33
|
if exposeDetail {
|
|
34
34
|
appErr.Details = err.Err.Error()
|
|
35
35
|
}
|
|
36
|
-
} else
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
appErr.
|
|
36
|
+
} else {
|
|
37
|
+
// Internal AppErrors carry a technical cause for one structured log;
|
|
38
|
+
// known domain errors (validation, not-found, etc.) stay quiet.
|
|
39
|
+
if cause := appErr.Unwrap(); cause != nil {
|
|
40
|
+
slog.Error("request failed", "error", cause, "request_id", c.GetString(RequestIDKey))
|
|
41
|
+
if exposeDetail {
|
|
42
|
+
appErr.Details = cause.Error()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if !exposeDetail {
|
|
46
|
+
// Known AppError (e.g. VALIDATION_ERROR): still strip Details in
|
|
47
|
+
// prod, so a direct API call doesn't get field-level hints.
|
|
48
|
+
appErr.Details = nil
|
|
49
|
+
}
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
appErr.RequestID = c.GetString(RequestIDKey)
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Composition root
|
|
4
4
|
|
|
5
|
-
`cmd/api/main.go` decides the exit code and nothing else.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Modules never import one another.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
`cmd/api/main.go` decides the exit code and nothing else. `cmd/api/wiring.go`
|
|
6
|
+
owns process bootstrap and route registration; `internal/composition/` owns
|
|
7
|
+
reusable process-level adapters that connect feature ports.
|
|
8
|
+
|
|
9
|
+
Modules never import one another. Each generated domain owns its local
|
|
10
|
+
outbound adapter → service or command/query application handlers → inbound
|
|
11
|
+
adapter composition in `composition.go`; `internal/composition/` owns
|
|
12
|
+
cross-feature adapters and `wiring.go` supplies infrastructure, invokes those
|
|
13
|
+
adapters, and registers routes. A domain that needs another's behaviour
|
|
14
|
+
declares a narrow interface for exactly what it needs and receives its
|
|
15
|
+
concrete implementation from the process composition; `.golangci.yml` has a
|
|
16
|
+
depguard rule per domain that fails the build if anyone shortcuts that.
|
|
13
17
|
|
|
14
18
|
Two things the split buys. `run()` returns an error instead of calling
|
|
15
19
|
`os.Exit`, so the deferred cleanup below it actually runs — `os.Exit` skips
|
|
@@ -19,32 +23,49 @@ you open to understand the binary: `wiring.go` grows with the system, which is
|
|
|
19
23
|
what a composition root is for, while `main.go` stays put.
|
|
20
24
|
|
|
21
25
|
|
|
22
|
-
> Status: generated by `@nakedev/go-scaffold
|
|
23
|
-
> Scope:
|
|
26
|
+
> Status: generated by `@nakedev/go-scaffold`; refreshes as optional features are added
|
|
27
|
+
> Scope: current generated project configuration
|
|
24
28
|
|
|
25
29
|
## 1. Topology
|
|
26
30
|
|
|
27
|
-
**Decision:**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
**Decision:** `cmd/api` serves REST over Gin. Optional operational binaries
|
|
32
|
+
are separate entry points: `cmd/worker` for queued jobs and `cmd/seed` for
|
|
33
|
+
one-shot bootstrap tasks when those features are installed.
|
|
34
|
+
**Rationale:** The HTTP process stays independently deployable while worker and
|
|
35
|
+
seed lifecycles remain explicit. Adding either binary does not turn an API
|
|
36
|
+
handler or feature composition root into a process-wide god object.
|
|
32
37
|
|
|
33
38
|
## 2. Package Layout
|
|
34
39
|
|
|
35
40
|
```text
|
|
36
41
|
cmd/
|
|
37
|
-
└── api/
|
|
42
|
+
└── api/
|
|
43
|
+
├── main.go # process entrypoint and exit code
|
|
44
|
+
└── wiring.go # config, infrastructure, graceful shutdown, routes
|
|
38
45
|
internal/
|
|
39
|
-
├──
|
|
40
|
-
|
|
46
|
+
├── composition/ # process-level cross-feature adapters
|
|
47
|
+
├── platform/ # talks to real external systems
|
|
48
|
+
│ └── database/ # PostgreSQL connection and pool
|
|
41
49
|
├── shared/ # pure logic/framework glue, no I/O
|
|
42
50
|
│ ├── config/ apperror/ dberr/ httpx/ id/
|
|
43
51
|
│ └── middleware/ pagination/ tx/
|
|
44
52
|
└── app/ # domain packages (one per feature)
|
|
45
53
|
└── <domain>/
|
|
54
|
+
├── domain/ # business entities and errors
|
|
55
|
+
├── ports/ # consumer-owned application dependencies
|
|
56
|
+
├── application/ # service OR commands + queries
|
|
57
|
+
├── adapters/inbound/http/ # request/response DTOs + HTTP adapter
|
|
58
|
+
├── adapters/outbound/postgres/
|
|
59
|
+
└── composition.go # feature-local object graph
|
|
46
60
|
```
|
|
47
61
|
|
|
62
|
+
Installed process and platform additions:
|
|
63
|
+
{{#if auth}}- `cmd/seed` and `platform/authprovider/google/` for user bootstrap and Google OAuth/OIDC
|
|
64
|
+
{{/if}}{{#if worker}}- `cmd/worker` and `platform/{mail,queue}/` for queue/SMTP jobs{{#if (eq queue "asynq")}}, plus `platform/cache/` for Redis{{/if}}
|
|
65
|
+
{{/if}}{{#if observability}}- `platform/telemetry/` for the OTLP exporter
|
|
66
|
+
{{/if}}{{#if rbac}}- `shared/middleware/authz.go` and `app/role/` for RBAC policy
|
|
67
|
+
{{/if}}
|
|
68
|
+
|
|
48
69
|
**Decision:** `platform/` vs `shared/` is split by whether the code talks to
|
|
49
70
|
a real external system.
|
|
50
71
|
**Rationale:** Code that does I/O against Postgres/cache/queue/SMTP/S3 →
|
|
@@ -52,47 +73,80 @@ a real external system.
|
|
|
52
73
|
add a `shared/utils` or similarly generic package — name it after what it
|
|
53
74
|
actually does.
|
|
54
75
|
|
|
76
|
+
Optional feature boundaries are explicit: auth owns user authentication and
|
|
77
|
+
provider ports; RBAC owns role policy and authorization; worker owns queue and
|
|
78
|
+
mail adapters; observability owns metrics/tracing setup. They do not create a
|
|
79
|
+
second domain architecture.
|
|
80
|
+
|
|
55
81
|
## 3. API Style
|
|
56
82
|
|
|
57
83
|
**Decision:** REST over Gin, one handler package per domain, every route
|
|
58
84
|
grouped under{{#if apiPrefix}} `/{{apiPrefix}}`{{else}} no prefix{{/if}}.
|
|
59
|
-
**Rationale:** `
|
|
60
|
-
|
|
85
|
+
**Rationale:** `inbound HTTP adapter → application boundary → domain + ports →
|
|
86
|
+
outbound adapter`, one direction,
|
|
87
|
+
with delivery, application, and persistence concerns kept inside the feature
|
|
88
|
+
boundary — no cross-domain imports. The prefix is a
|
|
61
89
|
single project-wide choice made at `create` time (`--api-prefix`) — there is
|
|
62
90
|
no per-domain versioning; a domain that needs a real breaking change gets a
|
|
63
91
|
new domain package (or a new field on the existing DTO), not a duplicated
|
|
64
92
|
model pointed at the same table under a different URL.
|
|
65
93
|
|
|
66
|
-
## 4.
|
|
94
|
+
## 4. Application boundaries
|
|
95
|
+
|
|
96
|
+
**Decision:** New modules use the project defaults recorded in
|
|
97
|
+
`go-scaffold.config.json`: surface `{{defaultModuleSurface}}` and application
|
|
98
|
+
style `{{defaultApplicationStyle}}`. The `generate module` wizard presents
|
|
99
|
+
those values through Lean/CRUD/CQRS profiles, with Advanced for an explicit
|
|
100
|
+
custom combination. `--profile` or the supported axis flags can override the
|
|
101
|
+
defaults for one module.
|
|
102
|
+
**Rationale:** A feature with meaningful write invariants or a different read
|
|
103
|
+
shape benefits from separate `CommandHandler` and `QueryHandler` ports while
|
|
104
|
+
remaining inside this modular monolith. A small CRUD feature does not benefit
|
|
105
|
+
from empty command/query wrappers, so it keeps one service until its business
|
|
106
|
+
needs justify the split. CQRS here does not require a second database, broker,
|
|
107
|
+
or event-sourcing runtime.
|
|
108
|
+
|
|
109
|
+
When enabled, `application/commands.go` owns state-changing application use
|
|
110
|
+
cases, `application/queries.go` owns read use cases, and `composition.go`
|
|
111
|
+
constructs both. The HTTP adapter is still an inbound adapter and
|
|
112
|
+
`cmd/api/wiring.go` still selects infrastructure, invokes process-level
|
|
113
|
+
adapters from `internal/composition/`, and registers the feature. A service
|
|
114
|
+
module instead has one `application/service.go`; these
|
|
115
|
+
styles are exclusive inside a module but can coexist across the monolith.
|
|
116
|
+
|
|
117
|
+
The CQRS inbound adapter is wired through `NewHandler` with separate command
|
|
118
|
+
and query ports. It does not generate an `application/service.go` facade.
|
|
119
|
+
|
|
120
|
+
## 5. Response and Error Model
|
|
67
121
|
|
|
68
122
|
**Decision:** Central `apperror.AppError` (HTTP status + machine-readable
|
|
69
123
|
`code` + message), rendered once by `middleware.Error()`.
|
|
70
124
|
**Rationale:** Handlers only ever do `c.Error(err); return` — no `c.JSON` per
|
|
71
125
|
call site, no error-shape drift between domains.
|
|
72
126
|
|
|
73
|
-
- Domain-specific errors live in each
|
|
127
|
+
- Domain-specific errors live in each module's `domain/errors.go` (e.g.
|
|
74
128
|
`USER_NOT_FOUND`, not a bare `NOT_FOUND`)
|
|
75
129
|
- DB errors are classified once in `shared/dberr` (`IsDuplicate`,
|
|
76
130
|
`IsForeignKey`) and mapped to the right HTTP status per domain
|
|
77
131
|
|
|
78
|
-
##
|
|
132
|
+
## 6. Pagination
|
|
79
133
|
|
|
80
134
|
**Decision:** Shared `limit`/`offset` parsing and response envelope
|
|
81
135
|
(`shared/pagination`), used by every list endpoint.
|
|
82
136
|
**Rationale:** One implementation, one response shape (`{data, limit,
|
|
83
137
|
offset}`) — no per-domain reinvention.
|
|
84
138
|
|
|
85
|
-
##
|
|
139
|
+
## 7. Persistence
|
|
86
140
|
|
|
87
141
|
**Decision:** PostgreSQL + GORM, schema managed by
|
|
88
142
|
[golang-migrate](https://github.com/golang-migrate/migrate).
|
|
89
|
-
**Rationale:** `
|
|
90
|
-
|
|
91
|
-
|
|
143
|
+
**Rationale:** `APP_ENV=development` allows a convenience table bootstrap;
|
|
144
|
+
`APP_ENV=production` runs only after `migrate up` has applied the separate,
|
|
145
|
+
versioned, rollback-capable SQL migrations.
|
|
92
146
|
|
|
93
147
|
{{#if docker}}- `docker-compose.yml` provides the local Postgres instance
|
|
94
148
|
{{/if}}
|
|
95
|
-
##
|
|
149
|
+
## 8. IDs
|
|
96
150
|
|
|
97
151
|
**Decision:** UUID v7 for every entity, generated app-side
|
|
98
152
|
(`shared/id.New()`), not by a DB default.
|
|
@@ -101,7 +155,7 @@ splits versus random v4 under heavy writes — and the app has the ID before
|
|
|
101
155
|
insert, so it doesn't need `gen_random_uuid()`.
|
|
102
156
|
|
|
103
157
|
{{#if openapiDocs}}
|
|
104
|
-
##
|
|
158
|
+
## 9. API Documentation
|
|
105
159
|
|
|
106
160
|
**Decision:** Hand-written OpenAPI spec (`docs/openapi.yaml`), split by
|
|
107
161
|
domain module, read from the working copy only — the server serves no `/docs`
|
|
@@ -112,7 +166,7 @@ comment-generated (swaggo) if hand-updates start drifting.
|
|
|
112
166
|
{{/if}}
|
|
113
167
|
|
|
114
168
|
{{#if observability}}
|
|
115
|
-
##
|
|
169
|
+
## 10. Observability
|
|
116
170
|
|
|
117
171
|
**Decision:** Prometheus metrics (`GET /metrics`, request count + latency
|
|
118
172
|
per route) always-on when this feature is enabled; OpenTelemetry tracing
|
|
@@ -147,5 +201,11 @@ skipped by the `os.Exit` calls already in there.
|
|
|
147
201
|
|
|
148
202
|
- `go-scaffold generate module <name>` adds a new domain package and
|
|
149
203
|
wires it into `cmd/api/wiring.go`
|
|
150
|
-
-
|
|
151
|
-
|
|
204
|
+
- `go-scaffold check` validates the physical split, process-composition
|
|
205
|
+
boundary, layer dependencies, and exclusive service/CQRS choice for every
|
|
206
|
+
discovered module
|
|
207
|
+
- `go-scaffold add auth`, `add worker`, `add rbac`, and `add observability`
|
|
208
|
+
refresh this document while it still matches the generated version. A
|
|
209
|
+
hand-edited document is preserved and must be updated by its owner.
|
|
210
|
+
- This document describes package boundaries, not completed business behavior;
|
|
211
|
+
generated methods remain explicit TODO/501 stubs until implemented.
|