@nakedev/go-scaffold 0.5.5 → 0.8.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 +16 -2
- package/dist/commands/auth.js +6 -1
- package/dist/commands/check.js +5 -1
- package/dist/commands/method.js +27 -1
- package/dist/index.js +20 -6
- package/dist/prompts/auth-wizard.js +33 -1
- package/dist/templates/create-manifest.js +8 -0
- package/dist/utils/hexagonal-method-patcher.js +79 -20
- package/package.json +1 -1
- package/templates/add/auth/docs/login.yaml.hbs +9 -1
- package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +12 -2
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +1 -0
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +97 -13
- package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository_test.go.hbs +169 -5
- package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +44 -2
- package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +2 -1
- package/templates/add/auth/internal/app/user/application/service.go.hbs +1 -1
- package/templates/add/auth/internal/app/user/application/service_test.go.hbs +71 -12
- package/templates/add/auth/internal/app/user/application/user_query.go.hbs +5 -4
- package/templates/add/auth/internal/app/user/domain/errors.go.hbs +4 -0
- package/templates/add/auth/internal/app/user/ports/repository.go.hbs +21 -3
- package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +10 -0
- package/templates/add/rbac/docs/roles.yaml.hbs +2 -1
- package/templates/add/rbac/docs/users.yaml.hbs +2 -1
- package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +6 -2
- package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +21 -4
- package/templates/add/rbac/internal/app/role/application/dto.go.hbs +9 -1
- package/templates/add/rbac/internal/app/role/application/service.go.hbs +6 -6
- package/templates/add/rbac/internal/app/role/application/service_test.go.hbs +19 -1
- package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +13 -1
- package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +1 -1
- package/templates/create/base/AGENTS.md.hbs +9 -1
- package/templates/create/base/README.md.hbs +2 -1
- package/templates/create/base/internal/shared/dbq/dbq.go.hbs +47 -0
- package/templates/create/base/internal/shared/dbq/dbq_test.go.hbs +66 -0
- package/templates/create/base/internal/shared/pagination/pagination.go.hbs +19 -2
- package/templates/create/features/docs/architecture.md.hbs +9 -6
- package/templates/create/features/docs/common/parameters.yaml.hbs +5 -0
- package/templates/create/features/docs/common/schemas.yaml.hbs +1 -0
- package/templates/create/features/docs/patterns.md.hbs +60 -0
- package/templates/generate/module/docs/collection.yaml.hbs +2 -1
- package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +8 -3
- package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +27 -4
- package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +3 -3
- package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +3 -3
- package/templates/generate/module/hexagonal/application/service.crud.go.hbs +3 -3
- package/templates/generate/module/hexagonal/application/service_test.go.hbs +4 -3
- package/templates/generate/module/hexagonal/ports/repository.go.hbs +18 -2
|
@@ -7,6 +7,7 @@ import (
|
|
|
7
7
|
|
|
8
8
|
"{{goModule}}/internal/app/{{modulePath}}/application"
|
|
9
9
|
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
10
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
10
11
|
"{{goModule}}/internal/shared/apperror"
|
|
11
12
|
"{{goModule}}/internal/shared/httpx"
|
|
12
13
|
{{#if auth}} "{{goModule}}/internal/shared/middleware"
|
|
@@ -107,10 +108,14 @@ func (h *Handler) create(c *gin.Context) {
|
|
|
107
108
|
|
|
108
109
|
func (h *Handler) list(c *gin.Context) {
|
|
109
110
|
p := pagination.Parse(c)
|
|
111
|
+
// One struct all the way down, so this module's own filters are read off
|
|
112
|
+
// the query string here and added as fields on ports.ListFilter — never as
|
|
113
|
+
// extra arguments on the three signatures below.
|
|
114
|
+
filter := ports.ListFilter{Search: p.Search, Limit: p.Limit, Offset: p.Offset}
|
|
110
115
|
{{#if cqrs}}
|
|
111
|
-
items, err := h.queries.List(c.Request.Context(),
|
|
116
|
+
items, total, err := h.queries.List(c.Request.Context(), filter)
|
|
112
117
|
{{else}}
|
|
113
|
-
items, err := h.svc.List(c.Request.Context(),
|
|
118
|
+
items, total, err := h.svc.List(c.Request.Context(), filter)
|
|
114
119
|
{{/if}}
|
|
115
120
|
if err != nil {
|
|
116
121
|
c.Error(appError(err))
|
|
@@ -120,7 +125,7 @@ func (h *Handler) list(c *gin.Context) {
|
|
|
120
125
|
for i := range items {
|
|
121
126
|
out[i] = toResponse(application.ToResponse(&items[i]))
|
|
122
127
|
}
|
|
123
|
-
c.JSON(http.StatusOK, p.
|
|
128
|
+
c.JSON(http.StatusOK, p.ResponseWithTotal(out, total))
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
func (h *Handler) get(c *gin.Context) {
|
|
@@ -6,6 +6,7 @@ import (
|
|
|
6
6
|
|
|
7
7
|
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
8
8
|
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
9
|
+
"{{goModule}}/internal/shared/dbq"
|
|
9
10
|
"{{goModule}}/internal/shared/dberr"
|
|
10
11
|
"{{goModule}}/internal/shared/tx"
|
|
11
12
|
|
|
@@ -30,16 +31,38 @@ func (r *Repository) Create(ctx context.Context, m *domain.{{pascalName}}) error
|
|
|
30
31
|
return nil
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
// FindAll answers one page of the filter and how many rows it matched
|
|
35
|
+
// altogether. Two queries, because a count over a LIMITed query would only
|
|
36
|
+
// ever count the page.
|
|
37
|
+
func (r *Repository) FindAll(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
|
|
38
|
+
// A closure rather than one *gorm.DB reused twice: conditions accumulate
|
|
39
|
+
// on the value, so the count would silently inherit the page's LIMIT.
|
|
40
|
+
matching := func() *gorm.DB {
|
|
41
|
+
q := tx.From(ctx, r.db).WithContext(ctx).Model(&{{pascalName}}Model{})
|
|
42
|
+
// TODO: name the columns this list is searched by. dbq.Search is a
|
|
43
|
+
// no-op until it has at least one, so ?q= is accepted and ignored
|
|
44
|
+
// until then. Add this module's own filters beside it.
|
|
45
|
+
q = dbq.Search(q, filter.Search /*, "name", "code" */)
|
|
46
|
+
return q
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
var total int64
|
|
50
|
+
if err := matching().Count(&total).Error; err != nil {
|
|
51
|
+
return nil, 0, mapDatabaseError(err)
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
var rows []{{pascalName}}Model
|
|
35
|
-
|
|
36
|
-
|
|
55
|
+
// created_at orders it, id breaks the ties: without the tiebreaker two
|
|
56
|
+
// rows written in the same instant can swap between page 1 and page 2,
|
|
57
|
+
// showing one twice and hiding the other.
|
|
58
|
+
if err := matching().Order("created_at desc, id").Limit(filter.Limit).Offset(filter.Offset).Find(&rows).Error; err != nil {
|
|
59
|
+
return nil, 0, mapDatabaseError(err)
|
|
37
60
|
}
|
|
38
61
|
items := make([]domain.{{pascalName}}, len(rows))
|
|
39
62
|
for i := range rows {
|
|
40
63
|
items[i] = *toDomain(&rows[i])
|
|
41
64
|
}
|
|
42
|
-
return items, nil
|
|
65
|
+
return items, total, nil
|
|
43
66
|
}
|
|
44
67
|
|
|
45
68
|
func (r *Repository) FindByID(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
|
|
@@ -15,7 +15,7 @@ import (
|
|
|
15
15
|
// which side of the CQRS boundary was called.
|
|
16
16
|
type repositoryStub struct {
|
|
17
17
|
createFn func(context.Context, *domain.{{pascalName}}) error
|
|
18
|
-
findAllFn func(context.Context,
|
|
18
|
+
findAllFn func(context.Context, ports.ListFilter) ([]domain.{{pascalName}}, int64, error)
|
|
19
19
|
findByIDFn func(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
20
20
|
updateFn func(context.Context, *domain.{{pascalName}}) error
|
|
21
21
|
deleteFn func(context.Context, uuid.UUID) error
|
|
@@ -28,11 +28,11 @@ func (s *repositoryStub) Create(ctx context.Context, m *domain.{{pascalName}}) e
|
|
|
28
28
|
}
|
|
29
29
|
return s.createFn(ctx, m)
|
|
30
30
|
}
|
|
31
|
-
func (s *repositoryStub) FindAll(ctx context.Context,
|
|
31
|
+
func (s *repositoryStub) FindAll(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
|
|
32
32
|
if s.findAllFn == nil {
|
|
33
33
|
panic("unexpected repository.FindAll call")
|
|
34
34
|
}
|
|
35
|
-
return s.findAllFn(ctx,
|
|
35
|
+
return s.findAllFn(ctx, filter)
|
|
36
36
|
}
|
|
37
37
|
func (s *repositoryStub) FindByID(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
|
|
38
38
|
if s.findByIDFn == nil {
|
|
@@ -9,7 +9,7 @@ import (
|
|
|
9
9
|
)
|
|
10
10
|
|
|
11
11
|
type QueryPort interface {
|
|
12
|
-
List(context.Context,
|
|
12
|
+
List(context.Context, ports.ListFilter) ([]domain.{{pascalName}}, int64, error)
|
|
13
13
|
Get(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
14
14
|
// go-scaffold:query-interface
|
|
15
15
|
}
|
|
@@ -22,8 +22,8 @@ func NewQueryHandler(repo ports.QueryRepository) *QueryHandler {
|
|
|
22
22
|
return &QueryHandler{repo: repo}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
func (h *QueryHandler) List(ctx context.Context,
|
|
26
|
-
return h.repo.FindAll(ctx,
|
|
25
|
+
func (h *QueryHandler) List(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
|
|
26
|
+
return h.repo.FindAll(ctx, filter)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
func (h *QueryHandler) Get(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
|
|
@@ -13,7 +13,7 @@ import (
|
|
|
13
13
|
|
|
14
14
|
type ServicePort interface {
|
|
15
15
|
Create(context.Context, CreateInput) (*domain.{{pascalName}}, error)
|
|
16
|
-
List(context.Context,
|
|
16
|
+
List(context.Context, ports.ListFilter) ([]domain.{{pascalName}}, int64, error)
|
|
17
17
|
Get(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
18
18
|
Update(context.Context, uuid.UUID, UpdateInput) (*domain.{{pascalName}}, error)
|
|
19
19
|
Delete(context.Context, uuid.UUID) error
|
|
@@ -38,8 +38,8 @@ func (s *Service) Create(ctx context.Context, in CreateInput) (*domain.{{pascalN
|
|
|
38
38
|
return m, nil
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
func (s *Service) List(ctx context.Context,
|
|
42
|
-
return s.repo.FindAll(ctx,
|
|
41
|
+
func (s *Service) List(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
|
|
42
|
+
return s.repo.FindAll(ctx, filter)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
func (s *Service) Get(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
|
|
@@ -5,6 +5,7 @@ import (
|
|
|
5
5
|
"testing"
|
|
6
6
|
|
|
7
7
|
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
8
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
8
9
|
|
|
9
10
|
"github.com/google/uuid"
|
|
10
11
|
)
|
|
@@ -15,7 +16,7 @@ import (
|
|
|
15
16
|
// orchestration.
|
|
16
17
|
type repositoryStub struct {
|
|
17
18
|
createFn func(context.Context, *domain.{{pascalName}}) error
|
|
18
|
-
findAllFn func(context.Context,
|
|
19
|
+
findAllFn func(context.Context, ports.ListFilter) ([]domain.{{pascalName}}, int64, error)
|
|
19
20
|
findByIDFn func(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
20
21
|
updateFn func(context.Context, *domain.{{pascalName}}) error
|
|
21
22
|
deleteFn func(context.Context, uuid.UUID) error
|
|
@@ -28,11 +29,11 @@ func (s *repositoryStub) Create(ctx context.Context, m *domain.{{pascalName}}) e
|
|
|
28
29
|
}
|
|
29
30
|
return s.createFn(ctx, m)
|
|
30
31
|
}
|
|
31
|
-
func (s *repositoryStub) FindAll(ctx context.Context,
|
|
32
|
+
func (s *repositoryStub) FindAll(ctx context.Context, filter ports.ListFilter) ([]domain.{{pascalName}}, int64, error) {
|
|
32
33
|
if s.findAllFn == nil {
|
|
33
34
|
panic("unexpected repository.FindAll call")
|
|
34
35
|
}
|
|
35
|
-
return s.findAllFn(ctx,
|
|
36
|
+
return s.findAllFn(ctx, filter)
|
|
36
37
|
}
|
|
37
38
|
func (s *repositoryStub) FindByID(ctx context.Context, id uuid.UUID) (*domain.{{pascalName}}, error) {
|
|
38
39
|
if s.findByIDFn == nil {
|
|
@@ -11,10 +11,26 @@ import (
|
|
|
11
11
|
"github.com/google/uuid"
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
+
// ListFilter is the whole question the list endpoint asks. The zero value is
|
|
15
|
+
// "the first page of everything", so a caller only sets what it narrows by.
|
|
16
|
+
//
|
|
17
|
+
// Put this module's own filters here as fields rather than as arguments: one
|
|
18
|
+
// struct travels handler -> application -> repository, so adding a filter
|
|
19
|
+
// later is a field and not a fourth parameter on three signatures.
|
|
20
|
+
type ListFilter struct {
|
|
21
|
+
// Search is ?q= as shared/pagination parsed it, empty when the caller
|
|
22
|
+
// sent none.
|
|
23
|
+
Search string
|
|
24
|
+
Limit int
|
|
25
|
+
Offset int
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
// Repository is the complete persistence port for service-style modules.
|
|
15
29
|
type Repository interface {
|
|
16
30
|
Create(context.Context, *domain.{{pascalName}}) error
|
|
17
|
-
FindAll
|
|
31
|
+
// FindAll answers one page and the total the filter matched, which is what
|
|
32
|
+
// a client needs to draw "page 3 of 12".
|
|
33
|
+
FindAll(context.Context, ListFilter) ([]domain.{{pascalName}}, int64, error)
|
|
18
34
|
FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
19
35
|
Update(context.Context, *domain.{{pascalName}}) error
|
|
20
36
|
Delete(context.Context, uuid.UUID) error
|
|
@@ -32,7 +48,7 @@ type CommandRepository interface {
|
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
type QueryRepository interface {
|
|
35
|
-
FindAll(context.Context,
|
|
51
|
+
FindAll(context.Context, ListFilter) ([]domain.{{pascalName}}, int64, error)
|
|
36
52
|
FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
37
53
|
// go-scaffold:query-repository-interface
|
|
38
54
|
}
|