@nakedev/go-scaffold 0.4.3 → 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 +567 -513
- package/dist/commands/auth.js +12 -1
- package/dist/commands/check.js +281 -0
- package/dist/commands/create.js +2 -1
- package/dist/commands/generate.js +5 -2
- package/dist/commands/method.js +66 -70
- package/dist/commands/observability.js +4 -53
- package/dist/commands/rbac.js +19 -8
- package/dist/commands/undo.js +6 -3
- package/dist/commands/worker.js +14 -4
- package/dist/index.js +13 -1
- package/dist/templates/auth-manifest.js +46 -45
- package/dist/templates/create-manifest.js +4 -0
- package/dist/templates/module-manifest.js +82 -60
- package/dist/templates/rbac-manifest.js +15 -11
- package/dist/templates/worker-manifest.js +4 -1
- package/dist/types.js +2 -0
- package/dist/utils/auth-patcher.js +22 -22
- package/dist/utils/config.js +24 -5
- package/dist/utils/docs-patcher.js +68 -0
- package/dist/utils/hexagonal-method-patcher.js +334 -0
- package/dist/utils/main-patcher.js +3 -3
- package/dist/utils/module-location.js +17 -11
- package/dist/utils/platform-patcher.js +27 -0
- package/dist/utils/rbac-patcher.js +73 -216
- package/package.json +1 -1
- package/templates/add/auth/cmd/seed/main.go.hbs +2 -0
- package/templates/add/auth/internal/app/user/{browser_policy.go.hbs → adapters/inbound/http/browser_policy.go.hbs} +3 -3
- package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
- package/templates/add/auth/internal/app/user/{handler.go.hbs → adapters/inbound/http/handler.go.hbs} +103 -8
- package/templates/add/auth/internal/app/user/{handler_local.go.hbs → adapters/inbound/http/handler_local.go.hbs} +8 -7
- package/templates/add/auth/internal/app/user/{handler_mfa.go.hbs → adapters/inbound/http/handler_mfa.go.hbs} +6 -6
- package/templates/add/auth/internal/app/user/{handler_oauth.go.hbs → adapters/inbound/http/handler_oauth.go.hbs} +17 -17
- package/templates/add/auth/internal/app/user/{handler_recovery.go.hbs → adapters/inbound/http/handler_recovery.go.hbs} +4 -4
- package/templates/add/auth/internal/app/user/{handler_test.go.hbs → adapters/inbound/http/handler_test.go.hbs} +52 -31
- package/templates/add/auth/internal/app/user/{handler_user.go.hbs → adapters/inbound/http/handler_user.go.hbs} +4 -4
- package/templates/add/auth/internal/app/user/{session_cookie.go.hbs → adapters/inbound/http/session_cookie.go.hbs} +5 -3
- 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/{mfa_store.go.hbs → adapters/outbound/postgres/mfa_store.go.hbs} +22 -20
- package/templates/add/auth/internal/app/user/{mfa_store_test.go.hbs → adapters/outbound/postgres/mfa_store_test.go.hbs} +7 -7
- 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/{tokenstore_pg.go.hbs → adapters/outbound/postgres/tokenstore_pg.go.hbs} +36 -32
- package/templates/add/auth/internal/app/user/{tokenstore_pg_test.go.hbs → adapters/outbound/postgres/tokenstore_pg_test.go.hbs} +10 -3
- package/templates/add/auth/internal/app/user/{tokenstore_recovery.go.hbs → adapters/outbound/postgres/tokenstore_recovery.go.hbs} +29 -3
- package/templates/add/auth/internal/app/user/{tokenstore_redis.go.hbs → adapters/outbound/redis/tokenstore.go.hbs} +43 -36
- package/templates/add/auth/internal/app/user/{tokenstore_redis_test.go.hbs → adapters/outbound/redis/tokenstore_test.go.hbs} +14 -3
- 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/{external_login.go.hbs → application/external_login.go.hbs} +30 -40
- package/templates/add/auth/internal/app/user/{jwt.go.hbs → application/jwt.go.hbs} +6 -3
- package/templates/add/auth/internal/app/user/{local_auth.go.hbs → application/local_auth.go.hbs} +17 -19
- package/templates/add/auth/internal/app/user/{mfa_service.go.hbs → application/mfa_service.go.hbs} +27 -28
- package/templates/add/auth/internal/app/user/{mfa_service_test.go.hbs → application/mfa_service_test.go.hbs} +12 -11
- package/templates/add/auth/internal/app/user/application/oauth.go.hbs +3 -3
- package/templates/add/auth/internal/app/user/{provider_test.go.hbs → application/provider_test.go.hbs} +55 -56
- package/templates/add/auth/internal/app/user/application/recovery.go.hbs +16 -47
- package/templates/add/auth/internal/app/user/{recovery_service.go.hbs → application/recovery_service.go.hbs} +13 -15
- package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
- package/templates/add/auth/internal/app/user/{service_test.go.hbs → application/service_test.go.hbs} +94 -75
- package/templates/add/auth/internal/app/user/{sessions.go.hbs → application/sessions.go.hbs} +14 -14
- package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
- package/templates/add/auth/internal/app/user/{user_query.go.hbs → application/user_query.go.hbs} +16 -16
- package/templates/add/auth/internal/app/user/composition.go.hbs +89 -86
- 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/migrations/create_auth_tokens.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
- package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -1
- 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 +23 -10
- 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 +54 -36
- package/templates/create/base/.golangci.yml.hbs +2 -2
- package/templates/create/base/AGENTS.md.hbs +55 -31
- package/templates/create/base/README.md.hbs +77 -22
- package/templates/create/base/cmd/api/wiring.go.hbs +3 -2
- package/templates/create/base/internal/composition/doc.go.hbs +7 -0
- package/templates/create/features/docs/architecture.md.hbs +64 -26
- package/templates/create/features/docs/patterns.md.hbs +117 -90
- package/templates/create/features/docs/techstack.md.hbs +17 -2
- 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 -537
- package/templates/add/auth/internal/app/user/contracts.go.hbs +0 -88
- package/templates/add/auth/internal/app/user/dto.go.hbs +0 -134
- package/templates/add/auth/internal/app/user/errors.go.hbs +0 -68
- package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -45
- package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -32
- package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
- package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +0 -17
- package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +0 -20
- package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +0 -17
- package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -31
- package/templates/add/auth/internal/app/user/repository.go.hbs +0 -139
- package/templates/add/auth/internal/app/user/service.go.hbs +0 -135
- package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -58
- 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/commands.go.hbs +0 -95
- package/templates/generate/module/composition.go.hbs +0 -23
- package/templates/generate/module/cqrs_test.go.hbs +0 -7
- 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 -179
- package/templates/generate/module/handler_test.go.hbs +0 -174
- package/templates/generate/module/minimal/commands.go.hbs +0 -34
- package/templates/generate/module/minimal/dto.go.hbs +0 -28
- package/templates/generate/module/minimal/handler.go.hbs +0 -82
- package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
- package/templates/generate/module/minimal/queries.go.hbs +0 -45
- package/templates/generate/module/minimal/service.go.hbs +0 -71
- 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/queries.go.hbs +0 -62
- package/templates/generate/module/repository.go.hbs +0 -103
- package/templates/generate/module/service.go.hbs +0 -164
- package/templates/generate/module/service_test.go.hbs +0 -161
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Package httpadapter is the Gin inbound adapter for {{pkg}}.
|
|
2
|
+
package httpadapter
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"errors"
|
|
6
|
+
"net/http"
|
|
7
|
+
|
|
8
|
+
"{{goModule}}/internal/app/{{modulePath}}/application"
|
|
9
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
10
|
+
"{{goModule}}/internal/shared/apperror"
|
|
11
|
+
"{{goModule}}/internal/shared/httpx"
|
|
12
|
+
{{#if auth}} "{{goModule}}/internal/shared/middleware"
|
|
13
|
+
{{/if}}
|
|
14
|
+
"{{goModule}}/internal/shared/pagination"
|
|
15
|
+
|
|
16
|
+
"github.com/gin-gonic/gin"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
type Handler struct {
|
|
20
|
+
{{#if cqrs}}
|
|
21
|
+
commands application.CommandPort
|
|
22
|
+
queries application.QueryPort
|
|
23
|
+
{{else}}
|
|
24
|
+
svc application.ServicePort
|
|
25
|
+
{{/if}}
|
|
26
|
+
{{#if auth}}
|
|
27
|
+
jwtSecret string
|
|
28
|
+
{{/if}}
|
|
29
|
+
{{#if permission}}
|
|
30
|
+
authz *middleware.Authz
|
|
31
|
+
{{/if}}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
{{#if cqrs}}
|
|
35
|
+
func NewHandler(commands application.CommandPort, queries application.QueryPort{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *Handler {
|
|
36
|
+
h := &Handler{commands: commands, queries: queries}
|
|
37
|
+
{{#if auth}}
|
|
38
|
+
h.jwtSecret = jwtSecret
|
|
39
|
+
{{/if}}
|
|
40
|
+
{{#if permission}}
|
|
41
|
+
h.authz = authz
|
|
42
|
+
{{/if}}
|
|
43
|
+
return h
|
|
44
|
+
}
|
|
45
|
+
{{else}}
|
|
46
|
+
func NewHandler(svc application.ServicePort{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *Handler {
|
|
47
|
+
h := &Handler{svc: svc}
|
|
48
|
+
{{#if auth}}
|
|
49
|
+
h.jwtSecret = jwtSecret
|
|
50
|
+
{{/if}}
|
|
51
|
+
{{#if permission}}
|
|
52
|
+
h.authz = authz
|
|
53
|
+
{{/if}}
|
|
54
|
+
return h
|
|
55
|
+
}
|
|
56
|
+
{{/if}}
|
|
57
|
+
|
|
58
|
+
func (h *Handler) Register(rg gin.IRouter) {
|
|
59
|
+
g := rg.Group("/{{plural}}"{{#if auth}}, middleware.RequireAuth(h.jwtSecret){{/if}}{{#if permission}}, h.authz.Require("{{permission}}"){{/if}})
|
|
60
|
+
g.POST("", h.create)
|
|
61
|
+
g.GET("", h.list)
|
|
62
|
+
g.GET("/:id", h.get)
|
|
63
|
+
g.PUT("/:id", h.update)
|
|
64
|
+
g.DELETE("/:id", h.delete)
|
|
65
|
+
// go-scaffold:handler-routes
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func appError(err error) error {
|
|
69
|
+
switch {
|
|
70
|
+
case errors.Is(err, domain.ErrNotFound):
|
|
71
|
+
return applicationError(http.StatusNotFound, "{{errorPrefix}}_NOT_FOUND", "{{pkg}} not found")
|
|
72
|
+
case errors.Is(err, domain.ErrConflict):
|
|
73
|
+
return applicationError(http.StatusConflict, "{{errorPrefix}}_CONFLICT", "{{pkg}} already exists")
|
|
74
|
+
case errors.Is(err, domain.ErrStaleVersion):
|
|
75
|
+
return applicationError(http.StatusConflict, "{{errorPrefix}}_STALE", "{{pkg}} was modified by someone else — reload and try again")
|
|
76
|
+
case errors.Is(err, domain.ErrHasReferences):
|
|
77
|
+
return applicationError(http.StatusConflict, "{{errorPrefix}}_HAS_REFERENCES", "{{pkg}} still has related records")
|
|
78
|
+
case errors.Is(err, domain.ErrNotImplemented):
|
|
79
|
+
return applicationError(http.StatusNotImplemented, "NOT_IMPLEMENTED", "not implemented")
|
|
80
|
+
default:
|
|
81
|
+
return apperror.NewInternal(err)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func applicationError(status int, code, message string) *apperror.AppError {
|
|
86
|
+
// The HTTP adapter is the only layer allowed to create an HTTP error.
|
|
87
|
+
return apperror.New(status, code, message)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func (h *Handler) create(c *gin.Context) {
|
|
91
|
+
var in createInput
|
|
92
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
93
|
+
c.Error(httpx.BindErr(err))
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
{{#if cqrs}}
|
|
97
|
+
m, err := h.commands.Create(c.Request.Context(), toCreateInput(in))
|
|
98
|
+
{{else}}
|
|
99
|
+
m, err := h.svc.Create(c.Request.Context(), toCreateInput(in))
|
|
100
|
+
{{/if}}
|
|
101
|
+
if err != nil {
|
|
102
|
+
c.Error(appError(err))
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
c.JSON(http.StatusCreated, toResponse(application.ToResponse(m)))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func (h *Handler) list(c *gin.Context) {
|
|
109
|
+
p := pagination.Parse(c)
|
|
110
|
+
{{#if cqrs}}
|
|
111
|
+
items, err := h.queries.List(c.Request.Context(), p.Limit, p.Offset)
|
|
112
|
+
{{else}}
|
|
113
|
+
items, err := h.svc.List(c.Request.Context(), p.Limit, p.Offset)
|
|
114
|
+
{{/if}}
|
|
115
|
+
if err != nil {
|
|
116
|
+
c.Error(appError(err))
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
out := make([]response, len(items))
|
|
120
|
+
for i := range items {
|
|
121
|
+
out[i] = toResponse(application.ToResponse(&items[i]))
|
|
122
|
+
}
|
|
123
|
+
c.JSON(http.StatusOK, p.Response(out))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func (h *Handler) get(c *gin.Context) {
|
|
127
|
+
id, ok := httpx.ParseID(c)
|
|
128
|
+
if !ok {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
{{#if cqrs}}
|
|
132
|
+
m, err := h.queries.Get(c.Request.Context(), id)
|
|
133
|
+
{{else}}
|
|
134
|
+
m, err := h.svc.Get(c.Request.Context(), id)
|
|
135
|
+
{{/if}}
|
|
136
|
+
if err != nil {
|
|
137
|
+
c.Error(appError(err))
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
c.JSON(http.StatusOK, toResponse(application.ToResponse(m)))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func (h *Handler) update(c *gin.Context) {
|
|
144
|
+
id, ok := httpx.ParseID(c)
|
|
145
|
+
if !ok {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
var in updateInput
|
|
149
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
150
|
+
c.Error(httpx.BindErr(err))
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
{{#if cqrs}}
|
|
154
|
+
m, err := h.commands.Update(c.Request.Context(), id, toUpdateInput(in))
|
|
155
|
+
{{else}}
|
|
156
|
+
m, err := h.svc.Update(c.Request.Context(), id, toUpdateInput(in))
|
|
157
|
+
{{/if}}
|
|
158
|
+
if err != nil {
|
|
159
|
+
c.Error(appError(err))
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
c.JSON(http.StatusOK, toResponse(application.ToResponse(m)))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func (h *Handler) delete(c *gin.Context) {
|
|
166
|
+
id, ok := httpx.ParseID(c)
|
|
167
|
+
if !ok {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
{{#if cqrs}}
|
|
171
|
+
err := h.commands.Delete(c.Request.Context(), id)
|
|
172
|
+
{{else}}
|
|
173
|
+
err := h.svc.Delete(c.Request.Context(), id)
|
|
174
|
+
{{/if}}
|
|
175
|
+
if err != nil {
|
|
176
|
+
c.Error(appError(err))
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
c.Status(http.StatusNoContent)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// go-scaffold:handler-funcs
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Package httpadapter is the Gin inbound adapter for {{pkg}}.
|
|
2
|
+
package httpadapter
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"errors"
|
|
6
|
+
"net/http"
|
|
7
|
+
|
|
8
|
+
"{{goModule}}/internal/app/{{modulePath}}/application"
|
|
9
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
10
|
+
"{{goModule}}/internal/shared/apperror"
|
|
11
|
+
{{#if auth}}
|
|
12
|
+
"{{goModule}}/internal/shared/middleware"
|
|
13
|
+
{{/if}}
|
|
14
|
+
|
|
15
|
+
"github.com/gin-gonic/gin"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
type Handler struct {
|
|
19
|
+
{{#if cqrs}}
|
|
20
|
+
commands application.CommandPort
|
|
21
|
+
queries application.QueryPort
|
|
22
|
+
{{else}}
|
|
23
|
+
svc application.ServicePort
|
|
24
|
+
{{/if}}
|
|
25
|
+
{{#if auth}}
|
|
26
|
+
jwtSecret string
|
|
27
|
+
{{/if}}
|
|
28
|
+
{{#if permission}}
|
|
29
|
+
authz *middleware.Authz
|
|
30
|
+
{{/if}}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
{{#if cqrs}}
|
|
34
|
+
func NewHandler(commands application.CommandPort, queries application.QueryPort{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *Handler {
|
|
35
|
+
h := &Handler{commands: commands, queries: queries}
|
|
36
|
+
{{#if auth}}
|
|
37
|
+
h.jwtSecret = jwtSecret
|
|
38
|
+
{{/if}}
|
|
39
|
+
{{#if permission}}
|
|
40
|
+
h.authz = authz
|
|
41
|
+
{{/if}}
|
|
42
|
+
return h
|
|
43
|
+
}
|
|
44
|
+
{{else}}
|
|
45
|
+
func NewHandler(svc application.ServicePort{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *Handler {
|
|
46
|
+
h := &Handler{svc: svc}
|
|
47
|
+
{{#if auth}}
|
|
48
|
+
h.jwtSecret = jwtSecret
|
|
49
|
+
{{/if}}
|
|
50
|
+
{{#if permission}}
|
|
51
|
+
h.authz = authz
|
|
52
|
+
{{/if}}
|
|
53
|
+
return h
|
|
54
|
+
}
|
|
55
|
+
{{/if}}
|
|
56
|
+
|
|
57
|
+
// Register wires the module's route group. Add endpoints with
|
|
58
|
+
// `go-scaffold generate method {{pkg}} <name> --type ...`.
|
|
59
|
+
func (h *Handler) Register(rg gin.IRouter) {
|
|
60
|
+
g := rg.Group("/{{plural}}"{{#if auth}}, middleware.RequireAuth(h.jwtSecret){{/if}}{{#if permission}}, h.authz.Require("{{permission}}"){{/if}})
|
|
61
|
+
_ = g
|
|
62
|
+
_ = appError
|
|
63
|
+
// go-scaffold:handler-routes
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func appError(err error) error {
|
|
67
|
+
switch {
|
|
68
|
+
case errors.Is(err, domain.ErrNotFound):
|
|
69
|
+
return apperror.New(http.StatusNotFound, "{{errorPrefix}}_NOT_FOUND", "{{pkg}} not found")
|
|
70
|
+
case errors.Is(err, domain.ErrConflict):
|
|
71
|
+
return apperror.New(http.StatusConflict, "{{errorPrefix}}_CONFLICT", "{{pkg}} already exists")
|
|
72
|
+
case errors.Is(err, domain.ErrStaleVersion):
|
|
73
|
+
return apperror.New(http.StatusConflict, "{{errorPrefix}}_STALE", "{{pkg}} was modified by someone else — reload and try again")
|
|
74
|
+
case errors.Is(err, domain.ErrHasReferences):
|
|
75
|
+
return apperror.New(http.StatusConflict, "{{errorPrefix}}_HAS_REFERENCES", "{{pkg}} still has related records")
|
|
76
|
+
case errors.Is(err, domain.ErrNotImplemented):
|
|
77
|
+
return apperror.New(http.StatusNotImplemented, "NOT_IMPLEMENTED", "not implemented")
|
|
78
|
+
default:
|
|
79
|
+
return apperror.NewInternal(err)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// go-scaffold:handler-funcs
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"github.com/gin-gonic/gin"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func TestHandler_RegisterCRUD(t *testing.T) {
|
|
10
|
+
gin.SetMode(gin.TestMode)
|
|
11
|
+
r := gin.New()
|
|
12
|
+
{{#if cqrs}}
|
|
13
|
+
h := NewHandler(nil, nil{{#if auth}}, "test-secret"{{/if}}{{#if permission}}, nil{{/if}})
|
|
14
|
+
{{else}}
|
|
15
|
+
h := NewHandler(nil{{#if auth}}, "test-secret"{{/if}}{{#if permission}}, nil{{/if}})
|
|
16
|
+
{{/if}}
|
|
17
|
+
h.Register(r)
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
{{#if permission}}
|
|
5
|
+
"context"
|
|
6
|
+
"time"
|
|
7
|
+
{{/if}}
|
|
8
|
+
"testing"
|
|
9
|
+
|
|
10
|
+
{{#if permission}}
|
|
11
|
+
"{{goModule}}/internal/shared/middleware"
|
|
12
|
+
{{/if}}
|
|
13
|
+
"github.com/gin-gonic/gin"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func TestHandler_Register(t *testing.T) {
|
|
17
|
+
gin.SetMode(gin.TestMode)
|
|
18
|
+
r := gin.New()
|
|
19
|
+
{{#if permission}}
|
|
20
|
+
authz := middleware.NewAuthz(func(context.Context, string) (map[string]struct{}, error) {
|
|
21
|
+
return map[string]struct{}{"{{permission}}": {}}, nil
|
|
22
|
+
}, time.Minute)
|
|
23
|
+
{{/if}}
|
|
24
|
+
{{#if cqrs}}
|
|
25
|
+
h := NewHandler(nil, nil{{#if auth}}, "test-secret"{{/if}}{{#if permission}}, authz{{/if}})
|
|
26
|
+
{{else}}
|
|
27
|
+
h := NewHandler(nil{{#if auth}}, "test-secret"{{/if}}{{#if permission}}, authz{{/if}})
|
|
28
|
+
{{/if}}
|
|
29
|
+
h.Register(r)
|
|
30
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Package postgres is the {{pkg}} outbound adapter. Its GORM model is not
|
|
2
|
+
// exported as a domain type; conversion happens at this boundary.
|
|
3
|
+
package postgres
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"time"
|
|
7
|
+
|
|
8
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
9
|
+
|
|
10
|
+
"github.com/google/uuid"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type {{pascalName}}Model struct {
|
|
14
|
+
ID uuid.UUID `gorm:"type:uuid;primaryKey"`
|
|
15
|
+
CreatedAt time.Time
|
|
16
|
+
UpdatedAt time.Time
|
|
17
|
+
Version int `gorm:"not null;default:1"`
|
|
18
|
+
// TODO: keep persistence-only fields here and map them explicitly.
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func ({{pascalName}}Model) TableName() string {
|
|
22
|
+
return "{{schemaName}}.{{tableName}}"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func toDomain(m *{{pascalName}}Model) *domain.{{pascalName}} {
|
|
26
|
+
if m == nil {
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
return &domain.{{pascalName}}{ID: m.ID, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt, Version: m.Version}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func fromDomain(m *domain.{{pascalName}}) *{{pascalName}}Model {
|
|
33
|
+
if m == nil {
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
return &{{pascalName}}Model{ID: m.ID, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt, Version: m.Version}
|
|
37
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
package postgres
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
|
|
7
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
8
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
9
|
+
"{{goModule}}/internal/shared/dberr"
|
|
10
|
+
"{{goModule}}/internal/shared/tx"
|
|
11
|
+
|
|
12
|
+
"github.com/google/uuid"
|
|
13
|
+
"gorm.io/gorm"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
var _ ports.Repository = (*Repository)(nil)
|
|
17
|
+
|
|
18
|
+
type Repository struct {
|
|
19
|
+
db *gorm.DB
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func NewRepository(db *gorm.DB) *Repository {
|
|
23
|
+
return &Repository{db: db}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func (r *Repository) Create(ctx context.Context, m *domain.{{pascalName}}) error {
|
|
27
|
+
if err := tx.From(ctx, r.db).WithContext(ctx).Create(fromDomain(m)).Error; err != nil {
|
|
28
|
+
return mapDatabaseError(err)
|
|
29
|
+
}
|
|
30
|
+
return nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func (r *Repository) FindAll(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
|
|
34
|
+
var rows []{{pascalName}}Model
|
|
35
|
+
if err := tx.From(ctx, r.db).WithContext(ctx).Order("id desc").Limit(limit).Offset(offset).Find(&rows).Error; err != nil {
|
|
36
|
+
return nil, mapDatabaseError(err)
|
|
37
|
+
}
|
|
38
|
+
items := make([]domain.{{pascalName}}, len(rows))
|
|
39
|
+
for i := range rows {
|
|
40
|
+
items[i] = *toDomain(&rows[i])
|
|
41
|
+
}
|
|
42
|
+
return items, nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func (r *Repository) FindByID(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
|
|
46
|
+
var row {{pascalName}}Model
|
|
47
|
+
if err := tx.From(ctx, r.db).WithContext(ctx).First(&row, "id = ?", id).Error; err != nil {
|
|
48
|
+
return nil, mapDatabaseError(err)
|
|
49
|
+
}
|
|
50
|
+
return toDomain(&row), nil
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func (r *Repository) Update(ctx context.Context, m *domain.{{pascalName}}) error {
|
|
54
|
+
expected := m.Version
|
|
55
|
+
row := fromDomain(m)
|
|
56
|
+
row.Version = expected + 1
|
|
57
|
+
result := tx.From(ctx, r.db).WithContext(ctx).
|
|
58
|
+
Model(&{{pascalName}}Model{}).
|
|
59
|
+
Where("id = ? AND version = ?", m.ID, expected).
|
|
60
|
+
Select("*").Omit("id", "created_at").Updates(row)
|
|
61
|
+
if result.Error != nil {
|
|
62
|
+
return mapDatabaseError(result.Error)
|
|
63
|
+
}
|
|
64
|
+
if result.RowsAffected == 0 {
|
|
65
|
+
return domain.ErrStaleVersion
|
|
66
|
+
}
|
|
67
|
+
m.Version = row.Version
|
|
68
|
+
return nil
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func (r *Repository) Delete(ctx context.Context, id uuid.UUID) error {
|
|
72
|
+
result := tx.From(ctx, r.db).WithContext(ctx).Delete(&{{pascalName}}Model{}, "id = ?", id)
|
|
73
|
+
if result.Error != nil {
|
|
74
|
+
return mapDatabaseError(result.Error)
|
|
75
|
+
}
|
|
76
|
+
if result.RowsAffected == 0 {
|
|
77
|
+
return domain.ErrNotFound
|
|
78
|
+
}
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func mapDatabaseError(err error) error {
|
|
83
|
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
84
|
+
return domain.ErrNotFound
|
|
85
|
+
}
|
|
86
|
+
if dberr.IsDuplicate(err) {
|
|
87
|
+
return domain.ErrConflict
|
|
88
|
+
}
|
|
89
|
+
if dberr.IsForeignKey(err) {
|
|
90
|
+
return domain.ErrHasReferences
|
|
91
|
+
}
|
|
92
|
+
return err
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// go-scaffold:repository-methods
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
package
|
|
1
|
+
package postgres
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
@@ -6,7 +6,7 @@ import (
|
|
|
6
6
|
"os"
|
|
7
7
|
"testing"
|
|
8
8
|
|
|
9
|
-
"{{goModule}}/internal/app/{{modulePath}}/
|
|
9
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
10
10
|
|
|
11
11
|
"github.com/google/uuid"
|
|
12
12
|
"gorm.io/driver/postgres"
|
|
@@ -15,7 +15,7 @@ import (
|
|
|
15
15
|
|
|
16
16
|
// repositoryDBForTest expects the test database schema to come from the same
|
|
17
17
|
// versioned SQL migrations used in production. Unit tests stay database-free;
|
|
18
|
-
// CI sets REQUIRE_TEST_DB=true so an unavailable
|
|
18
|
+
// CI sets REQUIRE_TEST_DB=true so an unavailable or unmigrated database fails
|
|
19
19
|
// instead of becoming a false-green skip.
|
|
20
20
|
func repositoryDBForTest(t *testing.T) *gorm.DB {
|
|
21
21
|
t.Helper()
|
|
@@ -62,7 +62,7 @@ func TestRepository_CreateFindDelete(t *testing.T) {
|
|
|
62
62
|
repo := NewRepository(repositoryDBForTest(t))
|
|
63
63
|
ctx := context.Background()
|
|
64
64
|
id := uuid.New()
|
|
65
|
-
item := &
|
|
65
|
+
item := &domain.{{pascalName}}{ID: id, Version: 1}
|
|
66
66
|
|
|
67
67
|
if err := repo.Create(ctx, item); err != nil {
|
|
68
68
|
t.Fatalf("create: %v", err)
|
|
@@ -85,13 +85,13 @@ func TestRepository_CreateFindDelete(t *testing.T) {
|
|
|
85
85
|
func TestRepository_Update_RefusesStaleVersion(t *testing.T) {
|
|
86
86
|
repo := NewRepository(repositoryDBForTest(t))
|
|
87
87
|
ctx := context.Background()
|
|
88
|
-
item := &
|
|
88
|
+
item := &domain.{{pascalName}}{ID: uuid.New(), Version: 1}
|
|
89
89
|
|
|
90
90
|
if err := repo.Create(ctx, item); err != nil {
|
|
91
91
|
t.Fatalf("create: %v", err)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
first := &
|
|
94
|
+
first := &domain.{{pascalName}}{ID: item.ID, Version: item.Version}
|
|
95
95
|
if err := repo.Update(ctx, first); err != nil {
|
|
96
96
|
t.Fatalf("first update: %v", err)
|
|
97
97
|
}
|
|
@@ -99,8 +99,8 @@ func TestRepository_Update_RefusesStaleVersion(t *testing.T) {
|
|
|
99
99
|
t.Fatalf("want version bumped to %d, got %d", item.Version+1, first.Version)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
stale := &
|
|
103
|
-
if err := repo.Update(ctx, stale); !errors.Is(err, ErrStaleVersion) {
|
|
102
|
+
stale := &domain.{{pascalName}}{ID: item.ID, Version: item.Version}
|
|
103
|
+
if err := repo.Update(ctx, stale); !errors.Is(err, domain.ErrStaleVersion) {
|
|
104
104
|
t.Fatalf("want ErrStaleVersion for the second writer, got %v", err)
|
|
105
105
|
}
|
|
106
106
|
if stale.Version != item.Version {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
6
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
7
|
+
"{{goModule}}/internal/shared/id"
|
|
8
|
+
|
|
9
|
+
"github.com/google/uuid"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type CommandPort interface {
|
|
13
|
+
Create(context.Context, CreateInput) (*domain.{{pascalName}}, error)
|
|
14
|
+
Update(context.Context, uuid.UUID, UpdateInput) (*domain.{{pascalName}}, error)
|
|
15
|
+
Delete(context.Context, uuid.UUID) error
|
|
16
|
+
// go-scaffold:command-interface
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type CommandHandler struct {
|
|
20
|
+
repo ports.CommandRepository
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func NewCommandHandler(repo ports.CommandRepository) *CommandHandler {
|
|
24
|
+
return &CommandHandler{repo: repo}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func (h *CommandHandler) Create(ctx context.Context, in CreateInput) (*domain.{{pascalName}}, error) {
|
|
28
|
+
// TODO: map in to real domain fields and enforce invariants.
|
|
29
|
+
_ = in
|
|
30
|
+
m := &domain.{{pascalName}}{ID: id.New(), Version: 1}
|
|
31
|
+
if err := h.repo.Create(ctx, m); err != nil {
|
|
32
|
+
return nil, err
|
|
33
|
+
}
|
|
34
|
+
return m, nil
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func (h *CommandHandler) Update(ctx context.Context, id uuid.UUID, in UpdateInput) (*domain.{{pascalName}}, error) {
|
|
38
|
+
m, err := h.repo.FindByID(ctx, id)
|
|
39
|
+
if err != nil {
|
|
40
|
+
return nil, err
|
|
41
|
+
}
|
|
42
|
+
// TODO: map in to real domain fields before saving.
|
|
43
|
+
m.Version = in.Version
|
|
44
|
+
if err := h.repo.Update(ctx, m); err != nil {
|
|
45
|
+
return nil, err
|
|
46
|
+
}
|
|
47
|
+
return m, nil
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func (h *CommandHandler) Delete(ctx context.Context, id uuid.UUID) error {
|
|
51
|
+
return h.repo.Delete(ctx, id)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// go-scaffold:command-methods
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
7
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type CommandPort interface {
|
|
11
|
+
// go-scaffold:command-interface
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type CommandHandler struct {
|
|
15
|
+
repo ports.CommandRepository
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func NewCommandHandler(repo ports.CommandRepository) *CommandHandler {
|
|
19
|
+
return &CommandHandler{repo: repo}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var _ context.Context
|
|
23
|
+
var _ *domain.{{pascalName}}
|
|
24
|
+
|
|
25
|
+
// go-scaffold:command-methods
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
8
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
9
|
+
|
|
10
|
+
"github.com/google/uuid"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// repositoryStub is shared by command and query tests. It intentionally
|
|
14
|
+
// exposes one function per operation so a focused test can assert exactly
|
|
15
|
+
// which side of the CQRS boundary was called.
|
|
16
|
+
type repositoryStub struct {
|
|
17
|
+
createFn func(context.Context, *domain.{{pascalName}}) error
|
|
18
|
+
findAllFn func(context.Context, int, int) ([]domain.{{pascalName}}, error)
|
|
19
|
+
findByIDFn func(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
20
|
+
updateFn func(context.Context, *domain.{{pascalName}}) error
|
|
21
|
+
deleteFn func(context.Context, uuid.UUID) error
|
|
22
|
+
// go-scaffold:repository-stub-fields
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func (s *repositoryStub) Create(ctx context.Context, m *domain.{{pascalName}}) error {
|
|
26
|
+
if s.createFn == nil {
|
|
27
|
+
panic("unexpected repository.Create call")
|
|
28
|
+
}
|
|
29
|
+
return s.createFn(ctx, m)
|
|
30
|
+
}
|
|
31
|
+
func (s *repositoryStub) FindAll(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
|
|
32
|
+
if s.findAllFn == nil {
|
|
33
|
+
panic("unexpected repository.FindAll call")
|
|
34
|
+
}
|
|
35
|
+
return s.findAllFn(ctx, limit, offset)
|
|
36
|
+
}
|
|
37
|
+
func (s *repositoryStub) FindByID(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
|
|
38
|
+
if s.findByIDFn == nil {
|
|
39
|
+
panic("unexpected repository.FindByID call")
|
|
40
|
+
}
|
|
41
|
+
return s.findByIDFn(ctx, id)
|
|
42
|
+
}
|
|
43
|
+
func (s *repositoryStub) Update(ctx context.Context, m *domain.{{pascalName}}) error {
|
|
44
|
+
if s.updateFn == nil {
|
|
45
|
+
panic("unexpected repository.Update call")
|
|
46
|
+
}
|
|
47
|
+
return s.updateFn(ctx, m)
|
|
48
|
+
}
|
|
49
|
+
func (s *repositoryStub) Delete(ctx context.Context, id uuid.UUID) error {
|
|
50
|
+
if s.deleteFn == nil {
|
|
51
|
+
panic("unexpected repository.Delete call")
|
|
52
|
+
}
|
|
53
|
+
return s.deleteFn(ctx, id)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var _ ports.CommandRepository = (*repositoryStub)(nil)
|
|
57
|
+
var _ ports.QueryRepository = (*repositoryStub)(nil)
|
|
58
|
+
|
|
59
|
+
func TestCQRSHandlersCompose(t *testing.T) {
|
|
60
|
+
repo := &repositoryStub{}
|
|
61
|
+
if NewCommandHandler(repo) == nil || NewQueryHandler(repo) == nil {
|
|
62
|
+
t.Fatal("expected command and query handlers")
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// go-scaffold:repository-stub-methods
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"time"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
7
|
+
|
|
8
|
+
"github.com/google/uuid"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// CreateInput and UpdateInput are transport-neutral application inputs. JSON
|
|
12
|
+
// names and binding/validation tags stay in adapters/inbound/http/dto.go.
|
|
13
|
+
type CreateInput struct {
|
|
14
|
+
// TODO: add command fields and validate them in the application/domain.
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type UpdateInput struct {
|
|
18
|
+
// Version is the optimistic-concurrency version the client last observed.
|
|
19
|
+
Version int
|
|
20
|
+
// TODO: add update fields.
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Response is a transport-neutral application output. The inbound adapter
|
|
24
|
+
// maps it to its protocol response before serialization.
|
|
25
|
+
type Response struct {
|
|
26
|
+
ID uuid.UUID
|
|
27
|
+
CreatedAt time.Time
|
|
28
|
+
Version int
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func ToResponse(m *domain.{{pascalName}}) Response {
|
|
32
|
+
return Response{ID: m.ID, CreatedAt: m.CreatedAt, Version: m.Version}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// go-scaffold:dto
|