@nakedev/go-scaffold 0.1.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.
Files changed (73) hide show
  1. package/README.md +223 -0
  2. package/bin/go-scaffold.js +2 -0
  3. package/dist/commands/create.js +57 -0
  4. package/dist/commands/generate.js +97 -0
  5. package/dist/commands/method.js +70 -0
  6. package/dist/commands/remove.js +72 -0
  7. package/dist/index.js +138 -0
  8. package/dist/prompts/create-wizard.js +43 -0
  9. package/dist/prompts/generate-wizard.js +68 -0
  10. package/dist/templates/create-manifest.js +110 -0
  11. package/dist/templates/module-manifest.js +28 -0
  12. package/dist/types.js +2 -0
  13. package/dist/utils/config.js +53 -0
  14. package/dist/utils/main-patcher.js +59 -0
  15. package/dist/utils/marker-patch.js +63 -0
  16. package/dist/utils/method-patcher.js +271 -0
  17. package/dist/utils/migrations.js +17 -0
  18. package/dist/utils/module-paths.js +33 -0
  19. package/dist/utils/naming.js +160 -0
  20. package/dist/utils/openapi-patcher.js +47 -0
  21. package/dist/utils/template-renderer.js +51 -0
  22. package/package.json +49 -0
  23. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +83 -0
  24. package/templates/create/base/.env.example.hbs +7 -0
  25. package/templates/create/base/.github/workflows/ci.yml.hbs +46 -0
  26. package/templates/create/base/.gitignore.hbs +5 -0
  27. package/templates/create/base/.golangci.yml.hbs +32 -0
  28. package/templates/create/base/.vscode/settings.json.hbs +11 -0
  29. package/templates/create/base/AGENTS.md.hbs +68 -0
  30. package/templates/create/base/CLAUDE.md.hbs +1 -0
  31. package/templates/create/base/Makefile.hbs +93 -0
  32. package/templates/create/base/README.md.hbs +143 -0
  33. package/templates/create/base/cmd/api/main.go.hbs +116 -0
  34. package/templates/create/base/go.mod.hbs +11 -0
  35. package/templates/create/base/internal/platform/database/database.go.hbs +28 -0
  36. package/templates/create/base/internal/shared/apperror/apperror.go.hbs +36 -0
  37. package/templates/create/base/internal/shared/config/config.go.hbs +46 -0
  38. package/templates/create/base/internal/shared/dberr/dberr.go.hbs +28 -0
  39. package/templates/create/base/internal/shared/httpx/httpx.go.hbs +37 -0
  40. package/templates/create/base/internal/shared/id/id.go.hbs +16 -0
  41. package/templates/create/base/internal/shared/middleware/error.go.hbs +33 -0
  42. package/templates/create/base/internal/shared/middleware/logger.go.hbs +23 -0
  43. package/templates/create/base/internal/shared/middleware/requestid.go.hbs +36 -0
  44. package/templates/create/base/internal/shared/pagination/pagination.go.hbs +39 -0
  45. package/templates/create/base/migrations/.gitkeep.hbs +0 -0
  46. package/templates/create/features/docker-compose.yml.hbs +14 -0
  47. package/templates/create/features/docs/architecture.md.hbs +99 -0
  48. package/templates/create/features/docs/common/parameters.yaml.hbs +13 -0
  49. package/templates/create/features/docs/common/responses.yaml.hbs +20 -0
  50. package/templates/create/features/docs/common/schemas.yaml.hbs +23 -0
  51. package/templates/create/features/docs/health/health-livez.yaml.hbs +13 -0
  52. package/templates/create/features/docs/health/health-readyz.yaml.hbs +21 -0
  53. package/templates/create/features/docs/openapi.yaml.hbs +33 -0
  54. package/templates/create/features/docs/patterns.md.hbs +119 -0
  55. package/templates/create/features/docs/techstack.md.hbs +38 -0
  56. package/templates/generate/module/docs/collection.yaml.hbs +36 -0
  57. package/templates/generate/module/docs/item.yaml.hbs +37 -0
  58. package/templates/generate/module/docs/schemas.yaml.hbs +13 -0
  59. package/templates/generate/module/dto.go.hbs +29 -0
  60. package/templates/generate/module/errors.go.hbs +28 -0
  61. package/templates/generate/module/handler.go.hbs +103 -0
  62. package/templates/generate/module/handler_test.go.hbs +105 -0
  63. package/templates/generate/module/migration.down.sql.hbs +1 -0
  64. package/templates/generate/module/migration.up.sql.hbs +5 -0
  65. package/templates/generate/module/minimal/dto.go.hbs +26 -0
  66. package/templates/generate/module/minimal/handler.go.hbs +24 -0
  67. package/templates/generate/module/minimal/handler_test.go.hbs +70 -0
  68. package/templates/generate/module/minimal/service.go.hbs +45 -0
  69. package/templates/generate/module/minimal/service_test.go.hbs +54 -0
  70. package/templates/generate/module/model/model.go.hbs +20 -0
  71. package/templates/generate/module/repository.go.hbs +49 -0
  72. package/templates/generate/module/service.go.hbs +97 -0
  73. package/templates/generate/module/service_test.go.hbs +65 -0
@@ -0,0 +1,49 @@
1
+ package {{pkg}}
2
+
3
+ import (
4
+ "context"
5
+
6
+ "{{goModule}}/internal/app/{{modulePath}}/model"
7
+
8
+ "github.com/google/uuid"
9
+ "gorm.io/gorm"
10
+ )
11
+
12
+ // Repository = data access for {{pkg}} (the only place that touches the DB for this domain)
13
+ // every method takes ctx, so a cancelled request cancels the query too
14
+ type Repository struct {
15
+ db *gorm.DB
16
+ }
17
+
18
+ func NewRepository(db *gorm.DB) *Repository {
19
+ return &Repository{db: db}
20
+ }
21
+
22
+ func (r *Repository) Create(ctx context.Context, m *model.{{pascalName}}) error {
23
+ return r.db.WithContext(ctx).Create(m).Error
24
+ }
25
+
26
+ func (r *Repository) FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
27
+ var items []model.{{pascalName}}
28
+ err := r.db.WithContext(ctx).Order("id desc").Limit(limit).Offset(offset).Find(&items).Error
29
+ return items, err
30
+ }
31
+
32
+ func (r *Repository) FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
33
+ var m model.{{pascalName}}
34
+ // "id = ?" explicit — PK is a UUID, not an int, this avoids GORM misinterpreting the arg
35
+ if err := r.db.WithContext(ctx).First(&m, "id = ?", id).Error; err != nil {
36
+ return nil, err
37
+ }
38
+ return &m, nil
39
+ }
40
+
41
+ func (r *Repository) Update(ctx context.Context, m *model.{{pascalName}}) error {
42
+ return r.db.WithContext(ctx).Save(m).Error
43
+ }
44
+
45
+ func (r *Repository) Delete(ctx context.Context, id uuid.UUID) error {
46
+ return r.db.WithContext(ctx).Delete(&model.{{pascalName}}{}, "id = ?", id).Error
47
+ }
48
+
49
+ // go-scaffold:repository-methods
@@ -0,0 +1,97 @@
1
+ package {{pkg}}
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+
7
+ "{{goModule}}/internal/app/{{modulePath}}/model"
8
+ "{{goModule}}/internal/shared/apperror"
9
+ "{{goModule}}/internal/shared/dberr"
10
+ "{{goModule}}/internal/shared/id"
11
+
12
+ "github.com/google/uuid"
13
+ "gorm.io/gorm"
14
+ )
15
+
16
+ // repository = what the service needs from the data layer (declared on the consumer side, so it can be mocked in tests)
17
+ type repository interface {
18
+ Create(ctx context.Context, m *model.{{pascalName}}) error
19
+ FindAll(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error)
20
+ FindByID(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error)
21
+ Update(ctx context.Context, m *model.{{pascalName}}) error
22
+ Delete(ctx context.Context, id uuid.UUID) error
23
+ // go-scaffold:repository-interface
24
+ }
25
+
26
+ // Service = business logic for {{pkg}} (rules live here, it knows nothing about HTTP)
27
+ // input shape validation happens in the handler (binding tags)
28
+ type Service struct {
29
+ repo repository
30
+ }
31
+
32
+ func NewService(repo repository) *Service {
33
+ return &Service{repo: repo}
34
+ }
35
+
36
+ func (s *Service) Create(ctx context.Context) (*model.{{pascalName}}, error) {
37
+ // TODO: accept and set real fields from createInput
38
+ m := &model.{{pascalName}}{ID: id.New()}
39
+ if err := s.repo.Create(ctx, m); err != nil {
40
+ if dberr.IsDuplicate(err) {
41
+ return nil, errConflict()
42
+ }
43
+ return nil, apperror.NewInternal()
44
+ }
45
+ return m, nil
46
+ }
47
+
48
+ func (s *Service) List(ctx context.Context, limit, offset int) ([]model.{{pascalName}}, error) {
49
+ items, err := s.repo.FindAll(ctx, limit, offset)
50
+ if err != nil {
51
+ return nil, apperror.NewInternal()
52
+ }
53
+ return items, nil
54
+ }
55
+
56
+ func (s *Service) Get(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
57
+ m, err := s.repo.FindByID(ctx, id)
58
+ if err != nil {
59
+ return nil, wrapFindErr(err)
60
+ }
61
+ return m, nil
62
+ }
63
+
64
+ func (s *Service) Update(ctx context.Context, id uuid.UUID) (*model.{{pascalName}}, error) {
65
+ m, err := s.repo.FindByID(ctx, id)
66
+ if err != nil {
67
+ return nil, wrapFindErr(err)
68
+ }
69
+ // TODO: apply real fields from updateInput before saving
70
+ if err := s.repo.Update(ctx, m); err != nil {
71
+ if dberr.IsDuplicate(err) {
72
+ return nil, errConflict()
73
+ }
74
+ return nil, apperror.NewInternal()
75
+ }
76
+ return m, nil
77
+ }
78
+
79
+ func (s *Service) Delete(ctx context.Context, id uuid.UUID) error {
80
+ if err := s.repo.Delete(ctx, id); err != nil {
81
+ // deleting a {{pkg}} that's still referenced elsewhere → FK RESTRICT trips = 409, not 500
82
+ if dberr.IsForeignKey(err) {
83
+ return errHasReferences()
84
+ }
85
+ return apperror.NewInternal()
86
+ }
87
+ return nil
88
+ }
89
+
90
+ func wrapFindErr(err error) error {
91
+ if errors.Is(err, gorm.ErrRecordNotFound) {
92
+ return errNotFound()
93
+ }
94
+ return apperror.NewInternal()
95
+ }
96
+
97
+ // go-scaffold:service-methods
@@ -0,0 +1,65 @@
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
+ "gorm.io/gorm"
13
+ )
14
+
15
+ // fakeRepo = mock of the repository interface, so the service can be tested without a DB
16
+ type fakeRepo struct {
17
+ err error
18
+ m *model.{{pascalName}}
19
+ }
20
+
21
+ func (f *fakeRepo) Create(context.Context, *model.{{pascalName}}) error { return f.err }
22
+ func (f *fakeRepo) FindAll(context.Context, int, int) ([]model.{{pascalName}}, error) { return nil, f.err }
23
+ func (f *fakeRepo) FindByID(context.Context, uuid.UUID) (*model.{{pascalName}}, error) {
24
+ if f.err != nil {
25
+ return nil, f.err
26
+ }
27
+ return f.m, nil
28
+ }
29
+ func (f *fakeRepo) Update(context.Context, *model.{{pascalName}}) error { return f.err }
30
+ func (f *fakeRepo) Delete(context.Context, uuid.UUID) error { return f.err }
31
+
32
+ // go-scaffold:fake-repo-methods
33
+
34
+ func status(t *testing.T, err error) int {
35
+ t.Helper()
36
+ var appErr *apperror.AppError
37
+ if !errors.As(err, &appErr) {
38
+ t.Fatalf("expected *apperror.AppError, got %T: %v", err, err)
39
+ }
40
+ return appErr.HTTPStatus
41
+ }
42
+
43
+ func TestService_Create_Duplicate(t *testing.T) {
44
+ svc := NewService(&fakeRepo{err: gorm.ErrDuplicatedKey})
45
+ _, err := svc.Create(context.Background())
46
+ if got := status(t, err); got != 409 {
47
+ t.Fatalf("want 409 conflict, got %d", got)
48
+ }
49
+ }
50
+
51
+ func TestService_Get_NotFound(t *testing.T) {
52
+ svc := NewService(&fakeRepo{err: gorm.ErrRecordNotFound})
53
+ _, err := svc.Get(context.Background(), uuid.New())
54
+ if got := status(t, err); got != 404 {
55
+ t.Fatalf("want 404, got %d", got)
56
+ }
57
+ }
58
+
59
+ func TestService_List_DBError_Becomes500(t *testing.T) {
60
+ svc := NewService(&fakeRepo{err: errors.New("connection refused")})
61
+ _, err := svc.List(context.Background(), 20, 0)
62
+ if got := status(t, err); got != 500 {
63
+ t.Fatalf("want 500, got %d", got)
64
+ }
65
+ }