@nakedev/go-scaffold 0.4.3 → 0.5.1
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
|
@@ -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
|
-
// queryRepository is the outbound port for read-only use cases.
|
|
15
|
-
type queryRepository interface {
|
|
16
|
-
FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error)
|
|
17
|
-
FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error)
|
|
18
|
-
// go-scaffold:query-repository-interface
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// queryService is the inbound application port consumed by query routes.
|
|
22
|
-
type queryService interface {
|
|
23
|
-
// go-scaffold:query-interface
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// QueryHandler owns read-only use cases for {{pkg}}.
|
|
27
|
-
type QueryHandler struct {
|
|
28
|
-
repo queryRepository
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
func NewQueryHandler(repo queryRepository) *QueryHandler {
|
|
32
|
-
return &QueryHandler{repo: repo}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// unused until `generate method` adds a get-one 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(err)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// go-scaffold:query-methods
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
{{#if cqrs}}
|
|
4
|
-
// repository is the complete data dependency retained for the compatibility
|
|
5
|
-
// facade below. CQRS handlers consume the narrower command/query ports.
|
|
6
|
-
type repository interface {
|
|
7
|
-
commandRepository
|
|
8
|
-
queryRepository
|
|
9
|
-
// go-scaffold:repository-interface
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Service is a compatibility facade. New code should depend on
|
|
13
|
-
// commandService or queryService directly.
|
|
14
|
-
type Service struct {
|
|
15
|
-
commands commandService
|
|
16
|
-
queries queryService
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
func NewService(repo repository) *Service {
|
|
20
|
-
return &Service{
|
|
21
|
-
commands: NewCommandHandler(repo),
|
|
22
|
-
queries: NewQueryHandler(repo),
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// go-scaffold:service-methods
|
|
27
|
-
{{else}}
|
|
28
|
-
import (
|
|
29
|
-
"context"
|
|
30
|
-
"errors"
|
|
31
|
-
|
|
32
|
-
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
33
|
-
"{{goModule}}/internal/shared/apperror"
|
|
34
|
-
|
|
35
|
-
"github.com/google/uuid"
|
|
36
|
-
"gorm.io/gorm"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
// repository = what the service needs from the data layer (declared on the consumer side, so it can be mocked in tests)
|
|
40
|
-
// minimal module: no CRUD wired yet, but the full data-access surface is here so `generate method` has something to call
|
|
41
|
-
type repository interface {
|
|
42
|
-
Create(ctx context.Context, m *model.{{pascalName}}) error
|
|
43
|
-
FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error)
|
|
44
|
-
FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error)
|
|
45
|
-
Update(ctx context.Context, m *model.{{pascalName}}) error
|
|
46
|
-
Delete(ctx context.Context, id uuid.UUID) error
|
|
47
|
-
// go-scaffold:repository-interface
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Service = business logic for {{pkg}} (rules live here, it knows nothing about HTTP)
|
|
51
|
-
// minimal module: add methods with `go-scaffold generate method {{pkg}} <name> --type ...`
|
|
52
|
-
type Service struct {
|
|
53
|
-
repo repository
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
func NewService(repo repository) *Service {
|
|
57
|
-
return &Service{repo: repo}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// unused until `generate method` adds a get-one/patch/put/delete method that calls it
|
|
61
|
-
//
|
|
62
|
-
//nolint:unused
|
|
63
|
-
func wrapFindErr(err error) error {
|
|
64
|
-
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
65
|
-
return errNotFound()
|
|
66
|
-
}
|
|
67
|
-
return apperror.NewInternal(err)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// go-scaffold:service-methods
|
|
71
|
-
{{/if}}
|
|
@@ -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,62 +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
|
-
// queryRepository is the outbound port for read-only use cases. A separate
|
|
15
|
-
// port makes it possible to replace this with a projection/read model later
|
|
16
|
-
// without changing command handlers.
|
|
17
|
-
type queryRepository interface {
|
|
18
|
-
FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error)
|
|
19
|
-
FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error)
|
|
20
|
-
// go-scaffold:query-repository-interface
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// queryService is the inbound application port consumed by read endpoints.
|
|
24
|
-
type queryService interface {
|
|
25
|
-
List(context.Context, int, int) ([]model.{{pascalName}}, error)
|
|
26
|
-
Get(context.Context, uuid.UUID) (*model.{{pascalName}}, error)
|
|
27
|
-
// go-scaffold:query-interface
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// QueryHandler owns read-only use cases for {{pkg}}.
|
|
31
|
-
type QueryHandler struct {
|
|
32
|
-
repo queryRepository
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
func NewQueryHandler(repo queryRepository) *QueryHandler {
|
|
36
|
-
return &QueryHandler{repo: repo}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
func (h *QueryHandler) List(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
|
|
40
|
-
items, err := h.repo.FindAll(ctx, limit, offset)
|
|
41
|
-
if err != nil {
|
|
42
|
-
return nil, apperror.NewInternal(err)
|
|
43
|
-
}
|
|
44
|
-
return items, nil
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
func (h *QueryHandler) Get(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
|
|
48
|
-
m, err := h.repo.FindByID(ctx, id)
|
|
49
|
-
if err != nil {
|
|
50
|
-
return nil, wrapFindErr(err)
|
|
51
|
-
}
|
|
52
|
-
return m, nil
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
func wrapFindErr(err error) error {
|
|
56
|
-
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
57
|
-
return errNotFound()
|
|
58
|
-
}
|
|
59
|
-
return apperror.NewInternal(err)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// go-scaffold:query-methods
|
|
@@ -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
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
package {{pkg}}
|
|
2
|
-
|
|
3
|
-
{{#if cqrs}}
|
|
4
|
-
import (
|
|
5
|
-
"context"
|
|
6
|
-
|
|
7
|
-
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
8
|
-
|
|
9
|
-
"github.com/google/uuid"
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
// repository is the complete data dependency retained for the compatibility
|
|
13
|
-
// facade below. The actual CQRS handlers consume the narrower
|
|
14
|
-
// commandRepository/queryRepository ports declared in commands.go/queries.go.
|
|
15
|
-
type repository interface {
|
|
16
|
-
commandRepository
|
|
17
|
-
queryRepository
|
|
18
|
-
// go-scaffold:repository-interface
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Service is a compatibility facade for callers that still want one feature
|
|
22
|
-
// service. New code should depend on commandService or queryService instead,
|
|
23
|
-
// so the read and write paths remain independently replaceable.
|
|
24
|
-
type Service struct {
|
|
25
|
-
commands commandService
|
|
26
|
-
queries queryService
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
func NewService(repo repository) *Service {
|
|
30
|
-
return &Service{
|
|
31
|
-
commands: NewCommandHandler(repo),
|
|
32
|
-
queries: NewQueryHandler(repo),
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
func (s *Service) Create(ctx context.Context, in createInput) (*model.{{pascalName}}, error) {
|
|
37
|
-
return s.commands.Create(ctx, in)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
func (s *Service) List(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
|
|
41
|
-
return s.queries.List(ctx, limit, offset)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
func (s *Service) Get(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
|
|
45
|
-
return s.queries.Get(ctx, id)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
func (s *Service) Update(ctx context.Context, id uuid.UUID, in updateInput) (*model.{{pascalName}}, error) {
|
|
49
|
-
return s.commands.Update(ctx, id, in)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
|
|
53
|
-
return s.commands.Delete(ctx, id)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// go-scaffold:service-methods
|
|
57
|
-
{{else}}
|
|
58
|
-
import (
|
|
59
|
-
"context"
|
|
60
|
-
"errors"
|
|
61
|
-
|
|
62
|
-
"{{goModule}}/internal/app/{{modulePath}}/model"
|
|
63
|
-
"{{goModule}}/internal/shared/apperror"
|
|
64
|
-
"{{goModule}}/internal/shared/dberr"
|
|
65
|
-
"{{goModule}}/internal/shared/id"
|
|
66
|
-
|
|
67
|
-
"github.com/google/uuid"
|
|
68
|
-
"gorm.io/gorm"
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
// repository = what the service needs from the data layer (declared on the consumer side, so it can be mocked in tests)
|
|
72
|
-
type repository interface {
|
|
73
|
-
Create(ctx context.Context, m *model.{{pascalName}}) error
|
|
74
|
-
FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error)
|
|
75
|
-
FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error)
|
|
76
|
-
Update(ctx context.Context, m *model.{{pascalName}}) error
|
|
77
|
-
Delete(ctx context.Context, id uuid.UUID) error
|
|
78
|
-
// go-scaffold:repository-interface
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Service = business logic for {{pkg}} (rules live here, it knows nothing about HTTP)
|
|
82
|
-
// input shape validation happens in the handler (binding tags)
|
|
83
|
-
type Service struct {
|
|
84
|
-
repo repository
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
func NewService(repo repository) *Service {
|
|
88
|
-
return &Service{repo: repo}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
func (s *Service) Create(ctx context.Context, in createInput) (*model.{{pascalName}}, error) {
|
|
92
|
-
// TODO: set real fields from in
|
|
93
|
-
_ = in
|
|
94
|
-
m := &model.{{pascalName}}{ID: id.New()}
|
|
95
|
-
if err := s.repo.Create(ctx, m); err != nil {
|
|
96
|
-
if dberr.IsDuplicate(err) {
|
|
97
|
-
return nil, errConflict()
|
|
98
|
-
}
|
|
99
|
-
return nil, apperror.NewInternal(err)
|
|
100
|
-
}
|
|
101
|
-
return m, nil
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
func (s *Service) List(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
|
|
105
|
-
items, err := s.repo.FindAll(ctx, limit, offset)
|
|
106
|
-
if err != nil {
|
|
107
|
-
return nil, apperror.NewInternal(err)
|
|
108
|
-
}
|
|
109
|
-
return items, nil
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
func (s *Service) Get(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
|
|
113
|
-
m, err := s.repo.FindByID(ctx, id)
|
|
114
|
-
if err != nil {
|
|
115
|
-
return nil, wrapFindErr(err)
|
|
116
|
-
}
|
|
117
|
-
return m, nil
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
func (s *Service) Update(ctx context.Context, id uuid.UUID, in updateInput) (*model.{{pascalName}}, error) {
|
|
121
|
-
m, err := s.repo.FindByID(ctx, id)
|
|
122
|
-
if err != nil {
|
|
123
|
-
return nil, wrapFindErr(err)
|
|
124
|
-
}
|
|
125
|
-
// TODO: apply real fields from in before saving
|
|
126
|
-
// The version the client read comes from the request, not from the row we
|
|
127
|
-
// just loaded — comparing the row against itself would always succeed and
|
|
128
|
-
// defeat the check.
|
|
129
|
-
m.Version = in.Version
|
|
130
|
-
if err := s.repo.Update(ctx, m); err != nil {
|
|
131
|
-
if errors.Is(err, ErrStaleVersion) {
|
|
132
|
-
return nil, errStale()
|
|
133
|
-
}
|
|
134
|
-
if dberr.IsDuplicate(err) {
|
|
135
|
-
return nil, errConflict()
|
|
136
|
-
}
|
|
137
|
-
return nil, apperror.NewInternal(err)
|
|
138
|
-
}
|
|
139
|
-
return m, nil
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
|
|
143
|
-
if err := s.repo.Delete(ctx, id); err != nil {
|
|
144
|
-
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
145
|
-
return errNotFound()
|
|
146
|
-
}
|
|
147
|
-
// deleting a {{pkg}} that's still referenced elsewhere → FK RESTRICT trips = 409, not 500
|
|
148
|
-
if dberr.IsForeignKey(err) {
|
|
149
|
-
return errHasReferences()
|
|
150
|
-
}
|
|
151
|
-
return apperror.NewInternal(err)
|
|
152
|
-
}
|
|
153
|
-
return nil
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
func wrapFindErr(err error) error {
|
|
157
|
-
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
158
|
-
return errNotFound()
|
|
159
|
-
}
|
|
160
|
-
return apperror.NewInternal(err)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// go-scaffold:service-methods
|
|
164
|
-
{{/if}}
|