@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
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"net/http"
|
|
6
|
-
|
|
7
|
-
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
8
|
-
"{{goModule}}/internal/shared/httpx"
|
|
9
|
-
{{#if auth}}
|
|
10
|
-
"{{goModule}}/internal/shared/middleware"
|
|
11
|
-
{{/if}}
|
|
12
|
-
"{{goModule}}/internal/shared/pagination"
|
|
13
|
-
|
|
14
|
-
"github.com/gin-gonic/gin"
|
|
15
|
-
"github.com/google/uuid"
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
// service is the narrow application API required by this HTTP adapter. Keeping
|
|
19
|
-
// the dependency as an interface makes handler tests fast and database-free.
|
|
20
|
-
type service interface {
|
|
21
|
-
Create(context.Context, createInput) (*model.{{pascalName}}, error)
|
|
22
|
-
List(context.Context, int, int) ([]model.{{pascalName}}, error)
|
|
23
|
-
Get(context.Context, uuid.UUID) (*model.{{pascalName}}, error)
|
|
24
|
-
Update(context.Context, uuid.UUID, updateInput) (*model.{{pascalName}}, error)
|
|
25
|
-
Delete(context.Context, uuid.UUID) error
|
|
26
|
-
// go-scaffold:service-interface
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Handler = delivery for {{pkg}} (parses HTTP, calls the service, attaches errors for the middleware to render)
|
|
30
|
-
type Handler struct {
|
|
31
|
-
svc service
|
|
32
|
-
{{#if auth}}
|
|
33
|
-
jwtSecret string
|
|
34
|
-
{{/if}}
|
|
35
|
-
{{#if permission}}
|
|
36
|
-
authz *middleware.Authz
|
|
37
|
-
{{/if}}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func NewHandler(svc service{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *Handler {
|
|
41
|
-
return &Handler{
|
|
42
|
-
svc: svc,
|
|
43
|
-
{{#if auth}}
|
|
44
|
-
jwtSecret: jwtSecret,
|
|
45
|
-
{{/if}}
|
|
46
|
-
{{#if permission}}
|
|
47
|
-
authz: authz,
|
|
48
|
-
{{/if}}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Register wires {{pkg}}'s routes onto the router group (takes an IRouter so it can be nested under /v1)
|
|
53
|
-
func (h *Handler) Register(rg gin.IRouter) {
|
|
54
|
-
g := rg.Group("/{{plural}}"{{#if auth}}, middleware.RequireAuth(h.jwtSecret){{/if}}{{#if permission}}, h.authz.Require("{{permission}}"){{/if}})
|
|
55
|
-
g.POST("", h.create)
|
|
56
|
-
g.GET("", h.list)
|
|
57
|
-
g.GET("/:id", h.get)
|
|
58
|
-
g.PUT("/:id", h.update)
|
|
59
|
-
g.DELETE("/:id", h.delete)
|
|
60
|
-
// go-scaffold:handler-routes
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
func (h *Handler) create(c *gin.Context) {
|
|
64
|
-
var in createInput
|
|
65
|
-
if err := c.ShouldBindJSON(&in); err != nil {
|
|
66
|
-
c.Error(httpx.BindErr(err))
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
m, err := h.svc.Create(c.Request.Context(), in)
|
|
70
|
-
if err != nil {
|
|
71
|
-
c.Error(err)
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
c.JSON(http.StatusCreated, toResponse(m))
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
func (h *Handler) list(c *gin.Context) {
|
|
78
|
-
p := pagination.Parse(c)
|
|
79
|
-
items, err := h.svc.List(c.Request.Context(), p.Limit, p.Offset)
|
|
80
|
-
if err != nil {
|
|
81
|
-
c.Error(err)
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
out := make([]response, len(items))
|
|
85
|
-
for i := range items {
|
|
86
|
-
out[i] = toResponse(&items[i])
|
|
87
|
-
}
|
|
88
|
-
c.JSON(http.StatusOK, p.Response(out))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
func (h *Handler) get(c *gin.Context) {
|
|
92
|
-
id, ok := httpx.ParseID(c)
|
|
93
|
-
if !ok {
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
m, err := h.svc.Get(c.Request.Context(), id)
|
|
97
|
-
if err != nil {
|
|
98
|
-
c.Error(err)
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
c.JSON(http.StatusOK, toResponse(m))
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
func (h *Handler) update(c *gin.Context) {
|
|
105
|
-
id, ok := httpx.ParseID(c)
|
|
106
|
-
if !ok {
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
var in updateInput
|
|
110
|
-
if err := c.ShouldBindJSON(&in); err != nil {
|
|
111
|
-
c.Error(httpx.BindErr(err))
|
|
112
|
-
return
|
|
113
|
-
}
|
|
114
|
-
m, err := h.svc.Update(c.Request.Context(), id, in)
|
|
115
|
-
if err != nil {
|
|
116
|
-
c.Error(err)
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
c.JSON(http.StatusOK, toResponse(m))
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
func (h *Handler) delete(c *gin.Context) {
|
|
123
|
-
id, ok := httpx.ParseID(c)
|
|
124
|
-
if !ok {
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
if err := h.svc.Delete(c.Request.Context(), id); err != nil {
|
|
128
|
-
c.Error(err)
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
c.Status(http.StatusNoContent)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// go-scaffold:handler-funcs
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"bytes"
|
|
5
|
-
"context"
|
|
6
|
-
"net/http"
|
|
7
|
-
"net/http/httptest"
|
|
8
|
-
"testing"
|
|
9
|
-
{{#if auth}}
|
|
10
|
-
"time"
|
|
11
|
-
{{/if}}
|
|
12
|
-
|
|
13
|
-
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
14
|
-
"{{goModule}}/internal/shared/middleware"
|
|
15
|
-
|
|
16
|
-
"github.com/gin-gonic/gin"
|
|
17
|
-
{{#if auth}}
|
|
18
|
-
"github.com/golang-jwt/jwt/v5"
|
|
19
|
-
{{/if}}
|
|
20
|
-
"github.com/google/uuid"
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
// serviceStub keeps handler tests at the HTTP boundary. It exercises binding,
|
|
24
|
-
// routing, middleware, status codes, and serialization without a database.
|
|
25
|
-
type serviceStub struct {
|
|
26
|
-
// Embedding keeps this stub source-compatible when `generate method` adds a
|
|
27
|
-
// new operation to the handler's service interface. Base CRUD methods below
|
|
28
|
-
// still override the promoted concrete methods for focused unit tests.
|
|
29
|
-
*Service
|
|
30
|
-
createFn func(context.Context, createInput) (*model.{{pascalName}}, error)
|
|
31
|
-
listFn func(context.Context, int, int) ([]model.{{pascalName}}, error)
|
|
32
|
-
getFn func(context.Context, uuid.UUID) (*model.{{pascalName}}, error)
|
|
33
|
-
updateFn func(context.Context, uuid.UUID, updateInput) (*model.{{pascalName}}, error)
|
|
34
|
-
deleteFn func(context.Context, uuid.UUID) error
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
func (s *serviceStub) Create(ctx context.Context, in createInput) (*model.{{pascalName}}, error) {
|
|
38
|
-
if s.createFn == nil {
|
|
39
|
-
panic("unexpected service.Create call")
|
|
40
|
-
}
|
|
41
|
-
return s.createFn(ctx, in)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
func (s *serviceStub) List(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
|
|
45
|
-
if s.listFn == nil {
|
|
46
|
-
panic("unexpected service.List call")
|
|
47
|
-
}
|
|
48
|
-
return s.listFn(ctx, limit, offset)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
func (s *serviceStub) Get(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
|
|
52
|
-
if s.getFn == nil {
|
|
53
|
-
panic("unexpected service.Get call")
|
|
54
|
-
}
|
|
55
|
-
return s.getFn(ctx, id)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
func (s *serviceStub) Update(ctx context.Context, id uuid.UUID, in updateInput) (*model.{{pascalName}}, error) {
|
|
59
|
-
if s.updateFn == nil {
|
|
60
|
-
panic("unexpected service.Update call")
|
|
61
|
-
}
|
|
62
|
-
return s.updateFn(ctx, id, in)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
func (s *serviceStub) Delete(ctx context.Context, id uuid.UUID) error {
|
|
66
|
-
if s.deleteFn == nil {
|
|
67
|
-
panic("unexpected service.Delete call")
|
|
68
|
-
}
|
|
69
|
-
return s.deleteFn(ctx, id)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// go-scaffold:service-stub-methods
|
|
73
|
-
|
|
74
|
-
func setupHandlerTest(t *testing.T, svc service) *gin.Engine {
|
|
75
|
-
t.Helper()
|
|
76
|
-
gin.SetMode(gin.TestMode)
|
|
77
|
-
r := gin.New()
|
|
78
|
-
r.Use(middleware.RequestID(), middleware.Error(true))
|
|
79
|
-
{{#if permission}}
|
|
80
|
-
// The handler unit test verifies route composition, not the role repository.
|
|
81
|
-
authz := middleware.NewAuthz(func(_ context.Context, _ string) (map[string]struct{}, error) {
|
|
82
|
-
return map[string]struct{}{"{{permission}}": {}}, nil
|
|
83
|
-
}, time.Minute)
|
|
84
|
-
{{/if}}
|
|
85
|
-
NewHandler(svc{{#if auth}}, testJWTSecret{{/if}}{{#if permission}}, authz{{/if}}).Register(r)
|
|
86
|
-
return r
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
{{#if auth}}
|
|
90
|
-
const testJWTSecret = "test-secret"
|
|
91
|
-
|
|
92
|
-
func authHeader() string {
|
|
93
|
-
claims := jwt.MapClaims{
|
|
94
|
-
"typ": "access",
|
|
95
|
-
"sub": uuid.NewString(),
|
|
96
|
-
"role": "staff",
|
|
97
|
-
"exp": time.Now().Add(time.Hour).Unix(),
|
|
98
|
-
"iat": time.Now().Unix(),
|
|
99
|
-
}
|
|
100
|
-
tok, _ := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(testJWTSecret))
|
|
101
|
-
return "Bearer " + tok
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
{{/if}}
|
|
105
|
-
func doHandlerRequest(r *gin.Engine, method, requestPath, body string) *httptest.ResponseRecorder {
|
|
106
|
-
req := httptest.NewRequest(method, requestPath, bytes.NewBufferString(body))
|
|
107
|
-
req.Header.Set("Content-Type", "application/json")
|
|
108
|
-
{{#if auth}}
|
|
109
|
-
req.Header.Set("Authorization", authHeader())
|
|
110
|
-
{{/if}}
|
|
111
|
-
w := httptest.NewRecorder()
|
|
112
|
-
r.ServeHTTP(w, req)
|
|
113
|
-
return w
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// createBody is the JSON these tests POST. It starts empty because createInput
|
|
117
|
-
// starts empty — add a value here for every field you add to createInput in
|
|
118
|
-
// dto.go. Miss that and this test fails with a 400 the moment one of those
|
|
119
|
-
// fields is `binding:"required"`: the handler is fine, the fixture just no
|
|
120
|
-
// longer satisfies it.
|
|
121
|
-
const createBody = `{}`
|
|
122
|
-
|
|
123
|
-
func TestHandler_Create_OK(t *testing.T) {
|
|
124
|
-
svc := &serviceStub{
|
|
125
|
-
createFn: func(context.Context, createInput) (*model.{{pascalName}}, error) {
|
|
126
|
-
return &model.{{pascalName}}{ID: uuid.New()}, nil
|
|
127
|
-
},
|
|
128
|
-
}
|
|
129
|
-
r := setupHandlerTest(t, svc)
|
|
130
|
-
|
|
131
|
-
w := doHandlerRequest(r, http.MethodPost, "/{{plural}}", createBody)
|
|
132
|
-
|
|
133
|
-
if w.Code != http.StatusCreated {
|
|
134
|
-
t.Fatalf("want 201, got %d body=%s", w.Code, w.Body)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
func TestHandler_Get_NotFound(t *testing.T) {
|
|
139
|
-
svc := &serviceStub{
|
|
140
|
-
getFn: func(context.Context, uuid.UUID) (*model.{{pascalName}}, error) {
|
|
141
|
-
return nil, errNotFound()
|
|
142
|
-
},
|
|
143
|
-
}
|
|
144
|
-
r := setupHandlerTest(t, svc)
|
|
145
|
-
|
|
146
|
-
w := doHandlerRequest(r, http.MethodGet, "/{{plural}}/"+uuid.NewString(), "")
|
|
147
|
-
|
|
148
|
-
if w.Code != http.StatusNotFound {
|
|
149
|
-
t.Fatalf("want 404, got %d body=%s", w.Code, w.Body)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
func TestHandler_Get_InvalidID_DoesNotCallService(t *testing.T) {
|
|
154
|
-
r := setupHandlerTest(t, &serviceStub{})
|
|
155
|
-
|
|
156
|
-
w := doHandlerRequest(r, http.MethodGet, "/{{plural}}/not-a-uuid", "")
|
|
157
|
-
|
|
158
|
-
if w.Code != http.StatusBadRequest {
|
|
159
|
-
t.Fatalf("want 400, got %d body=%s", w.Code, w.Body)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
func TestHandler_Delete_OK(t *testing.T) {
|
|
164
|
-
svc := &serviceStub{
|
|
165
|
-
deleteFn: func(context.Context, uuid.UUID) error { return nil },
|
|
166
|
-
}
|
|
167
|
-
r := setupHandlerTest(t, svc)
|
|
168
|
-
|
|
169
|
-
w := doHandlerRequest(r, http.MethodDelete, "/{{plural}}/"+uuid.NewString(), "")
|
|
170
|
-
|
|
171
|
-
if w.Code != http.StatusNoContent {
|
|
172
|
-
t.Fatalf("want 204, got %d body=%s", w.Code, w.Body)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"time"
|
|
5
|
-
|
|
6
|
-
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
7
|
-
|
|
8
|
-
"github.com/google/uuid"
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
// response = the DTO sent out (kept separate from the model so a later DB column doesn't leak automatically)
|
|
12
|
-
// TODO: add fields as you generate methods that return more than id/created_at
|
|
13
|
-
// unused until `generate method` adds a method that returns one
|
|
14
|
-
//
|
|
15
|
-
//nolint:unused
|
|
16
|
-
type response struct {
|
|
17
|
-
ID uuid.UUID `json:"id"`
|
|
18
|
-
CreatedAt time.Time `json:"created_at"`
|
|
19
|
-
// clients send this back on an update so a concurrent save can be detected
|
|
20
|
-
Version int `json:"version"`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
//nolint:unused
|
|
24
|
-
func toResponse(m *model.{{pascalName}}) response {
|
|
25
|
-
return response{ID: m.ID, CreatedAt: m.CreatedAt, Version: m.Version}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// go-scaffold:dto
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
{{#if auth}}
|
|
5
|
-
"{{goModule}}/internal/shared/middleware"
|
|
6
|
-
|
|
7
|
-
{{/if}}
|
|
8
|
-
"github.com/gin-gonic/gin"
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
// service is intentionally empty until `generate method` adds the exact
|
|
12
|
-
// application operations required by this HTTP adapter.
|
|
13
|
-
type service interface {
|
|
14
|
-
// go-scaffold:service-interface
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Handler = delivery for {{pkg}} (parses HTTP, calls the service, attaches errors for the middleware to render)
|
|
18
|
-
type Handler struct {
|
|
19
|
-
svc service
|
|
20
|
-
{{#if auth}}
|
|
21
|
-
jwtSecret string
|
|
22
|
-
{{/if}}
|
|
23
|
-
{{#if permission}}
|
|
24
|
-
authz *middleware.Authz
|
|
25
|
-
{{/if}}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
func NewHandler(svc service{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *Handler {
|
|
29
|
-
return &Handler{
|
|
30
|
-
svc: svc,
|
|
31
|
-
{{#if auth}}
|
|
32
|
-
jwtSecret: jwtSecret,
|
|
33
|
-
{{/if}}
|
|
34
|
-
{{#if permission}}
|
|
35
|
-
authz: authz,
|
|
36
|
-
{{/if}}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Register wires {{pkg}}'s routes onto the router group (takes an IRouter so it can be nested under /v1)
|
|
41
|
-
// minimal module: no routes yet — add them with `go-scaffold generate method {{pkg}} <name> --type ...`
|
|
42
|
-
func (h *Handler) Register(rg gin.IRouter) {
|
|
43
|
-
g := rg.Group("/{{plural}}"{{#if auth}}, middleware.RequireAuth(h.jwtSecret){{/if}}{{#if permission}}, h.authz.Require("{{permission}}"){{/if}})
|
|
44
|
-
_ = g
|
|
45
|
-
// go-scaffold:handler-routes
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// go-scaffold:handler-funcs
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
// serviceStub embeds the concrete service so a generated method automatically
|
|
4
|
-
// satisfies the handler's narrow service interface. Add explicit function-backed
|
|
5
|
-
// overrides here when writing focused handler tests for that method.
|
|
6
|
-
//
|
|
7
|
-
//nolint:unused
|
|
8
|
-
type serviceStub struct {
|
|
9
|
-
*Service
|
|
10
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"errors"
|
|
6
|
-
|
|
7
|
-
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
8
|
-
"{{goModule}}/internal/shared/apperror"
|
|
9
|
-
|
|
10
|
-
"github.com/google/uuid"
|
|
11
|
-
"gorm.io/gorm"
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
// repository = what the service needs from the data layer (declared on the consumer side, so it can be mocked in tests)
|
|
15
|
-
// minimal module: no CRUD wired yet, but the full data-access surface is here so `generate method` has something to call
|
|
16
|
-
type repository interface {
|
|
17
|
-
Create(ctx context.Context, m *model.{{pascalName}}) error
|
|
18
|
-
FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error)
|
|
19
|
-
FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error)
|
|
20
|
-
Update(ctx context.Context, m *model.{{pascalName}}) error
|
|
21
|
-
Delete(ctx context.Context, id uuid.UUID) error
|
|
22
|
-
// go-scaffold:repository-interface
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Service = business logic for {{pkg}} (rules live here, it knows nothing about HTTP)
|
|
26
|
-
// minimal module: add methods with `go-scaffold generate method {{pkg}} <name> --type ...`
|
|
27
|
-
type Service struct {
|
|
28
|
-
repo repository
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
func NewService(repo repository) *Service {
|
|
32
|
-
return &Service{repo: repo}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// unused until `generate method` adds a get-one/patch/put/delete method that calls it
|
|
36
|
-
//
|
|
37
|
-
//nolint:unused
|
|
38
|
-
func wrapFindErr(err error) error {
|
|
39
|
-
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
40
|
-
return errNotFound()
|
|
41
|
-
}
|
|
42
|
-
return apperror.NewInternal()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// go-scaffold:service-methods
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"errors"
|
|
6
|
-
"testing"
|
|
7
|
-
|
|
8
|
-
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
9
|
-
"{{goModule}}/internal/shared/apperror"
|
|
10
|
-
|
|
11
|
-
"github.com/google/uuid"
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
// repositoryStub is ready for focused use-case tests added alongside generated
|
|
15
|
-
// methods. Every unset dependency panics so unexpected orchestration is visible.
|
|
16
|
-
//
|
|
17
|
-
//nolint:unused
|
|
18
|
-
type repositoryStub struct {
|
|
19
|
-
createFn func(context.Context, *model.{{pascalName}}) error
|
|
20
|
-
findAllFn func(context.Context, int, int) ([]model.{{pascalName}}, error)
|
|
21
|
-
findByIDFn func(context.Context, uuid.UUID) (*model.{{pascalName}}, error)
|
|
22
|
-
updateFn func(context.Context, *model.{{pascalName}}) error
|
|
23
|
-
deleteFn func(context.Context, uuid.UUID) error
|
|
24
|
-
// go-scaffold:repository-stub-fields
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
//nolint:unused
|
|
28
|
-
func (s *repositoryStub) Create(ctx context.Context, m *model.{{pascalName}}) error {
|
|
29
|
-
if s.createFn == nil {
|
|
30
|
-
panic("unexpected repository.Create call")
|
|
31
|
-
}
|
|
32
|
-
return s.createFn(ctx, m)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
//nolint:unused
|
|
36
|
-
func (s *repositoryStub) FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
|
|
37
|
-
if s.findAllFn == nil {
|
|
38
|
-
panic("unexpected repository.FindAll call")
|
|
39
|
-
}
|
|
40
|
-
return s.findAllFn(ctx, limit, offset)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
//nolint:unused
|
|
44
|
-
func (s *repositoryStub) FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
|
|
45
|
-
if s.findByIDFn == nil {
|
|
46
|
-
panic("unexpected repository.FindByID call")
|
|
47
|
-
}
|
|
48
|
-
return s.findByIDFn(ctx, id)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
//nolint:unused
|
|
52
|
-
func (s *repositoryStub) Update(ctx context.Context, m *model.{{pascalName}}) error {
|
|
53
|
-
if s.updateFn == nil {
|
|
54
|
-
panic("unexpected repository.Update call")
|
|
55
|
-
}
|
|
56
|
-
return s.updateFn(ctx, m)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
//nolint:unused
|
|
60
|
-
func (s *repositoryStub) Delete(ctx context.Context, id uuid.UUID) error {
|
|
61
|
-
if s.deleteFn == nil {
|
|
62
|
-
panic("unexpected repository.Delete call")
|
|
63
|
-
}
|
|
64
|
-
return s.deleteFn(ctx, id)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// go-scaffold:repository-stub-methods
|
|
68
|
-
|
|
69
|
-
//nolint:unused
|
|
70
|
-
func status(t *testing.T, err error) int {
|
|
71
|
-
t.Helper()
|
|
72
|
-
var appErr *apperror.AppError
|
|
73
|
-
if !errors.As(err, &appErr) {
|
|
74
|
-
t.Fatalf("expected *apperror.AppError, got %T: %v", err, err)
|
|
75
|
-
}
|
|
76
|
-
return appErr.HTTPStatus
|
|
77
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// Package model holds this domain's GORM tables — a folder (not a single
|
|
2
|
-
// file) so a domain with more than one table can add a file per table
|
|
3
|
-
// without cluttering the domain package's top level.
|
|
4
|
-
package model
|
|
5
|
-
|
|
6
|
-
import (
|
|
7
|
-
"time"
|
|
8
|
-
|
|
9
|
-
"github.com/google/uuid"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
// {{pascalName}} = domain model + GORM table
|
|
13
|
-
// TODO: add your real fields here.
|
|
14
|
-
// ID = UUID v7 generated app-side (service.Create via id.New) instead of a DB
|
|
15
|
-
// default, so the app has the id before insert without needing gen_random_uuid.
|
|
16
|
-
type {{pascalName}} struct {
|
|
17
|
-
ID uuid.UUID `json:"id" gorm:"type:uuid;primaryKey"`
|
|
18
|
-
CreatedAt time.Time `json:"created_at"`
|
|
19
|
-
UpdatedAt time.Time `json:"updated_at"`
|
|
20
|
-
// Version guards against two clients that both loaded this row overwriting
|
|
21
|
-
// each other: an update only applies if the version it carries still
|
|
22
|
-
// matches the stored one. Bumped by Repository.Update, never by hand.
|
|
23
|
-
Version int `json:"version" gorm:"not null;default:1"`
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// TableName pins the persistence contract to the versioned SQL migrations.
|
|
27
|
-
// Do not rely on GORM's English inflector: irregular/plural module names must
|
|
28
|
-
// resolve to exactly the same table in development and production.
|
|
29
|
-
//
|
|
30
|
-
// Schema-qualified ("{{schemaName}}", not just "{{tableName}}") so this
|
|
31
|
-
// domain's table can never collide with another domain's, and a raw SQL JOIN
|
|
32
|
-
// reaching into it from outside this package fails loudly instead of quietly
|
|
33
|
-
// coupling two domains together.
|
|
34
|
-
func ({{pascalName}}) TableName() string {
|
|
35
|
-
return "{{schemaName}}.{{tableName}}"
|
|
36
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"context"
|
|
5
|
-
"errors"
|
|
6
|
-
|
|
7
|
-
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
8
|
-
"{{goModule}}/internal/shared/tx"
|
|
9
|
-
|
|
10
|
-
"github.com/google/uuid"
|
|
11
|
-
"gorm.io/gorm"
|
|
12
|
-
)
|
|
13
|
-
|
|
14
|
-
// ErrStaleVersion means the row moved on since the caller read it — someone
|
|
15
|
-
// else saved first. Returned instead of overwriting their work.
|
|
16
|
-
var ErrStaleVersion = errors.New("stale version")
|
|
17
|
-
|
|
18
|
-
// Repository = data access for {{pkg}} (the only place that touches the DB for this domain)
|
|
19
|
-
// every method takes ctx, so a cancelled request cancels the query too — and
|
|
20
|
-
// so tx.From can pick up a transaction opened by the caller, letting two
|
|
21
|
-
// repositories commit together without changing any signature here.
|
|
22
|
-
type Repository struct {
|
|
23
|
-
db *gorm.DB
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
func NewRepository(db *gorm.DB) *Repository {
|
|
27
|
-
return &Repository{db: db}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
func (r *Repository) Create(ctx context.Context, m *model.{{pascalName}}) error {
|
|
31
|
-
return tx.From(ctx, r.db).WithContext(ctx).Create(m).Error
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
func (r *Repository) FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
|
|
35
|
-
var items []model.{{pascalName}}
|
|
36
|
-
err := tx.From(ctx, r.db).WithContext(ctx).Order("id desc").Limit(limit).Offset(offset).Find(&items).Error
|
|
37
|
-
return items, err
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func (r *Repository) FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
|
|
41
|
-
var m model.{{pascalName}}
|
|
42
|
-
// "id = ?" explicit — PK is a UUID, not an int, this avoids GORM misinterpreting the arg
|
|
43
|
-
if err := tx.From(ctx, r.db).WithContext(ctx).First(&m, "id = ?", id).Error; err != nil {
|
|
44
|
-
return nil, err
|
|
45
|
-
}
|
|
46
|
-
return &m, nil
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Update applies m only if the version it carries still matches the stored
|
|
50
|
-
// row, then bumps it. Two clients that both loaded version 3 can't silently
|
|
51
|
-
// overwrite each other: the second one gets ErrStaleVersion instead of the
|
|
52
|
-
// first one's changes disappearing with no error anywhere.
|
|
53
|
-
func (r *Repository) Update(ctx context.Context, m *model.{{pascalName}}) error {
|
|
54
|
-
expected := m.Version
|
|
55
|
-
m.Version = expected + 1
|
|
56
|
-
|
|
57
|
-
res := tx.From(ctx, r.db).WithContext(ctx).
|
|
58
|
-
Model(&model.{{pascalName}}{}).
|
|
59
|
-
Where("id = ? AND version = ?", m.ID, expected).
|
|
60
|
-
// Select("*") because Updates on a struct skips zero values, which
|
|
61
|
-
// would quietly ignore a field the caller meant to clear.
|
|
62
|
-
Select("*").Omit("id", "created_at").
|
|
63
|
-
Updates(m)
|
|
64
|
-
if res.Error != nil {
|
|
65
|
-
m.Version = expected
|
|
66
|
-
return res.Error
|
|
67
|
-
}
|
|
68
|
-
if res.RowsAffected == 0 {
|
|
69
|
-
m.Version = expected
|
|
70
|
-
return ErrStaleVersion
|
|
71
|
-
}
|
|
72
|
-
return nil
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Delete reports gorm.ErrRecordNotFound when the row wasn't there, so a
|
|
76
|
-
// DELETE on an id that never existed answers 404 like GET does — a plain
|
|
77
|
-
// GORM delete affects zero rows and returns no error, which had the same
|
|
78
|
-
// request answer 204 as a real deletion.
|
|
79
|
-
func (r *Repository) Delete(ctx context.Context, id uuid.UUID) error {
|
|
80
|
-
res := tx.From(ctx, r.db).WithContext(ctx).Delete(&model.{{pascalName}}{}, "id = ?", id)
|
|
81
|
-
if res.Error != nil {
|
|
82
|
-
return res.Error
|
|
83
|
-
}
|
|
84
|
-
if res.RowsAffected == 0 {
|
|
85
|
-
return gorm.ErrRecordNotFound
|
|
86
|
-
}
|
|
87
|
-
return nil
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Count is the total the list endpoint needs to say "page 3 of 12" — see
|
|
91
|
-
// pagination.ResponseWithTotal. Separate from FindAll because it costs a second
|
|
92
|
-
// query: an endpoint that only ever scrolls shouldn't pay for a count nobody
|
|
93
|
-
// reads.
|
|
94
|
-
//
|
|
95
|
-
// Apply the same WHERE clause here that FindAll uses once you add filtering,
|
|
96
|
-
// or the total will describe a different set than the rows.
|
|
97
|
-
func (r *Repository) Count(ctx context.Context) (int64, error) {
|
|
98
|
-
var total int64
|
|
99
|
-
err := tx.From(ctx, r.db).WithContext(ctx).Model(&model.{{pascalName}}{}).Count(&total).Error
|
|
100
|
-
return total, err
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// go-scaffold:repository-methods
|