@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
|
@@ -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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"time"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
7
|
+
|
|
8
|
+
"github.com/google/uuid"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// A lean module starts with no use-case input type. `generate method` adds
|
|
12
|
+
// the exact input beside the endpoint that needs it.
|
|
13
|
+
// Response is a transport-neutral application output. The inbound adapter
|
|
14
|
+
// maps it to its protocol response before serialization.
|
|
15
|
+
type Response struct {
|
|
16
|
+
ID uuid.UUID
|
|
17
|
+
CreatedAt time.Time
|
|
18
|
+
Version int
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func ToResponse(m *domain.{{pascalName}}) Response {
|
|
22
|
+
return Response{ID: m.ID, CreatedAt: m.CreatedAt, Version: m.Version}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// go-scaffold:dto
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
6
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
7
|
+
|
|
8
|
+
"github.com/google/uuid"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
type QueryPort interface {
|
|
12
|
+
List(context.Context, int, int) ([]domain.{{pascalName}}, error)
|
|
13
|
+
Get(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
14
|
+
// go-scaffold:query-interface
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type QueryHandler struct {
|
|
18
|
+
repo ports.QueryRepository
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func NewQueryHandler(repo ports.QueryRepository) *QueryHandler {
|
|
22
|
+
return &QueryHandler{repo: repo}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func (h *QueryHandler) List(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
|
|
26
|
+
return h.repo.FindAll(ctx, limit, offset)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func (h *QueryHandler) Get(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
|
|
30
|
+
return h.repo.FindByID(ctx, id)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// go-scaffold:query-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 QueryPort interface {
|
|
11
|
+
// go-scaffold:query-interface
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type QueryHandler struct {
|
|
15
|
+
repo ports.QueryRepository
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func NewQueryHandler(repo ports.QueryRepository) *QueryHandler {
|
|
19
|
+
return &QueryHandler{repo: repo}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var _ context.Context
|
|
23
|
+
var _ *domain.{{pascalName}}
|
|
24
|
+
|
|
25
|
+
// go-scaffold:query-methods
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
|
|
7
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
8
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
9
|
+
"{{goModule}}/internal/shared/id"
|
|
10
|
+
|
|
11
|
+
"github.com/google/uuid"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type ServicePort interface {
|
|
15
|
+
Create(context.Context, CreateInput) (*domain.{{pascalName}}, error)
|
|
16
|
+
List(context.Context, int, int) ([]domain.{{pascalName}}, error)
|
|
17
|
+
Get(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
18
|
+
Update(context.Context, uuid.UUID, UpdateInput) (*domain.{{pascalName}}, error)
|
|
19
|
+
Delete(context.Context, uuid.UUID) error
|
|
20
|
+
// go-scaffold:service-interface
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Service struct {
|
|
24
|
+
repo ports.Repository
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func NewService(repo ports.Repository) *Service {
|
|
28
|
+
return &Service{repo: repo}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func (s *Service) Create(ctx context.Context, in CreateInput) (*domain.{{pascalName}}, error) {
|
|
32
|
+
// TODO: map in to real domain fields and enforce invariants.
|
|
33
|
+
_ = in
|
|
34
|
+
m := &domain.{{pascalName}}{ID: id.New(), Version: 1}
|
|
35
|
+
if err := s.repo.Create(ctx, m); err != nil {
|
|
36
|
+
return nil, err
|
|
37
|
+
}
|
|
38
|
+
return m, nil
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func (s *Service) List(ctx context.Context, limit, offset int) ([]domain.{{pascalName}}, error) {
|
|
42
|
+
return s.repo.FindAll(ctx, limit, offset)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func (s *Service) Get(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
|
|
46
|
+
return s.repo.FindByID(ctx, id)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func (s *Service) Update(ctx context.Context, id uuid.UUID, in UpdateInput) (*domain.{{pascalName}}, error) {
|
|
50
|
+
m, err := s.repo.FindByID(ctx, id)
|
|
51
|
+
if err != nil {
|
|
52
|
+
return nil, err
|
|
53
|
+
}
|
|
54
|
+
// TODO: map in to real domain fields before saving.
|
|
55
|
+
m.Version = in.Version
|
|
56
|
+
if err := s.repo.Update(ctx, m); err != nil {
|
|
57
|
+
return nil, err
|
|
58
|
+
}
|
|
59
|
+
return m, nil
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
|
|
63
|
+
if err := s.repo.Delete(ctx, id); err != nil {
|
|
64
|
+
return err
|
|
65
|
+
}
|
|
66
|
+
return nil
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// IsNotFound is useful to non-HTTP callers without exposing persistence
|
|
70
|
+
// errors. The inbound HTTP adapter maps the same domain sentinel to 404.
|
|
71
|
+
func IsNotFound(err error) bool { return errors.Is(err, domain.ErrNotFound) }
|
|
72
|
+
|
|
73
|
+
// go-scaffold:service-methods
|