@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,29 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
7
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
// ServicePort is the inbound application port used by the HTTP adapter.
|
|
11
|
+
type ServicePort interface {
|
|
12
|
+
// go-scaffold:service-interface
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Service orchestrates {{pkg}} use cases and depends only on ports.
|
|
16
|
+
type Service struct {
|
|
17
|
+
repo ports.Repository
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func NewService(repo ports.Repository) *Service {
|
|
21
|
+
return &Service{repo: repo}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Keep these imports and the port visible in a lean module before its first
|
|
25
|
+
// method is generated. Methods are added to this file by `generate method`.
|
|
26
|
+
var _ context.Context
|
|
27
|
+
var _ *domain.{{pascalName}}
|
|
28
|
+
|
|
29
|
+
// go-scaffold:service-methods
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package application
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
8
|
+
|
|
9
|
+
"github.com/google/uuid"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// repositoryStub exposes one function per dependency operation. Focused
|
|
13
|
+
// application tests can configure only the calls they expect; an unexpected
|
|
14
|
+
// call panics instead of silently returning a zero value that hides broken
|
|
15
|
+
// orchestration.
|
|
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
|
+
func TestService_ComposesWithRepositoryPort(t *testing.T) {
|
|
57
|
+
if NewService(&repositoryStub{}) == nil {
|
|
58
|
+
t.Fatal("expected service")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// go-scaffold:repository-stub-methods
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package {{pkg}}
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
httpadapter "{{goModule}}/internal/app/{{modulePath}}/adapters/inbound/http"
|
|
5
|
+
"{{goModule}}/internal/app/{{modulePath}}/adapters/outbound/postgres"
|
|
6
|
+
"{{goModule}}/internal/app/{{modulePath}}/application"
|
|
7
|
+
{{#unless cqrs}}
|
|
8
|
+
"{{goModule}}/internal/app/{{modulePath}}/ports"
|
|
9
|
+
{{/unless}}
|
|
10
|
+
"gorm.io/gorm"
|
|
11
|
+
{{#if permission}} "{{goModule}}/internal/shared/middleware"
|
|
12
|
+
{{/if}}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// NewHandlerFromDB is this module's composition root. The process root owns
|
|
16
|
+
// the database handle; this package owns the feature's adapter graph.
|
|
17
|
+
func NewHandlerFromDB(db *gorm.DB{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *httpadapter.Handler {
|
|
18
|
+
repo := postgres.NewRepository(db)
|
|
19
|
+
{{#if cqrs}}
|
|
20
|
+
commands := application.NewCommandHandler(repo)
|
|
21
|
+
queries := application.NewQueryHandler(repo)
|
|
22
|
+
return httpadapter.NewHandler(commands, queries{{#if auth}}, jwtSecret{{/if}}{{#if permission}}, authz{{/if}})
|
|
23
|
+
{{else}}
|
|
24
|
+
var service ports.Repository = repo
|
|
25
|
+
return httpadapter.NewHandler(application.NewService(service){{#if auth}}, jwtSecret{{/if}}{{#if permission}}, authz{{/if}})
|
|
26
|
+
{{/if}}
|
|
27
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Package domain contains {{pkg}}'s business state and invariants. It does
|
|
2
|
+
// not import Gin, GORM, SQL drivers, or HTTP concerns.
|
|
3
|
+
package domain
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"time"
|
|
7
|
+
|
|
8
|
+
"github.com/google/uuid"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// {{pascalName}} is the domain entity. Persistence and transport models are
|
|
12
|
+
// deliberately separate so a database column or JSON field cannot silently
|
|
13
|
+
// become part of the business API.
|
|
14
|
+
type {{pascalName}} struct {
|
|
15
|
+
ID uuid.UUID
|
|
16
|
+
CreatedAt time.Time
|
|
17
|
+
UpdatedAt time.Time
|
|
18
|
+
Version int
|
|
19
|
+
// TODO: add business fields and invariants here.
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package domain
|
|
2
|
+
|
|
3
|
+
import "errors"
|
|
4
|
+
|
|
5
|
+
var (
|
|
6
|
+
ErrNotFound = errors.New("{{pkg}} not found")
|
|
7
|
+
ErrConflict = errors.New("{{pkg}} conflicts with an existing record")
|
|
8
|
+
ErrStaleVersion = errors.New("{{pkg}} was modified by someone else")
|
|
9
|
+
ErrHasReferences = errors.New("{{pkg}} still has related records")
|
|
10
|
+
ErrNotImplemented = errors.New("{{pkg}} operation is not implemented")
|
|
11
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Package ports declares the dependencies owned by the {{pkg}} application.
|
|
2
|
+
// Adapters implement these interfaces; application code never imports a
|
|
3
|
+
// database driver.
|
|
4
|
+
package ports
|
|
5
|
+
|
|
6
|
+
import (
|
|
7
|
+
"context"
|
|
8
|
+
|
|
9
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
10
|
+
|
|
11
|
+
"github.com/google/uuid"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Repository is the complete persistence port for service-style modules.
|
|
15
|
+
type Repository interface {
|
|
16
|
+
Create(context.Context, *domain.{{pascalName}}) error
|
|
17
|
+
FindAll(context.Context, int, int) ([]domain.{{pascalName}}, error)
|
|
18
|
+
FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
19
|
+
Update(context.Context, *domain.{{pascalName}}) error
|
|
20
|
+
Delete(context.Context, uuid.UUID) error
|
|
21
|
+
// go-scaffold:repository-interface
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// CommandRepository and QueryRepository keep CQRS handlers honest: a write
|
|
25
|
+
// use case cannot grow a read dependency by accident, and vice versa.
|
|
26
|
+
type CommandRepository interface {
|
|
27
|
+
Create(context.Context, *domain.{{pascalName}}) error
|
|
28
|
+
FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
29
|
+
Update(context.Context, *domain.{{pascalName}}) error
|
|
30
|
+
Delete(context.Context, uuid.UUID) error
|
|
31
|
+
// go-scaffold:command-repository-interface
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type QueryRepository interface {
|
|
35
|
+
FindAll(context.Context, int, int) ([]domain.{{pascalName}}, error)
|
|
36
|
+
FindByID(context.Context, uuid.UUID) (*domain.{{pascalName}}, error)
|
|
37
|
+
// go-scaffold:query-repository-interface
|
|
38
|
+
}
|
|
@@ -1,357 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.assertMethodAbsent = assertMethodAbsent;
|
|
7
|
-
exports.patchMethod = patchMethod;
|
|
8
|
-
exports.markersPresent = markersPresent;
|
|
9
|
-
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
-
const marker_patch_1 = require("./marker-patch");
|
|
11
|
-
const naming_1 = require("./naming");
|
|
12
|
-
const DTO_MARKER = "// go-scaffold:dto";
|
|
13
|
-
const REPO_INTERFACE_MARKER = "// go-scaffold:repository-interface";
|
|
14
|
-
const REPO_IMPL_MARKER = "// go-scaffold:repository-methods";
|
|
15
|
-
const SERVICE_MARKER = "// go-scaffold:service-methods";
|
|
16
|
-
const SERVICE_INTERFACE_MARKER = "// go-scaffold:service-interface";
|
|
17
|
-
const HANDLER_ROUTES_MARKER = "// go-scaffold:handler-routes";
|
|
18
|
-
const HANDLER_FUNCS_MARKER = "// go-scaffold:handler-funcs";
|
|
19
|
-
const REPOSITORY_STUB_FIELDS_MARKER = "// go-scaffold:repository-stub-fields";
|
|
20
|
-
const REPOSITORY_STUB_METHODS_MARKER = "// go-scaffold:repository-stub-methods";
|
|
21
|
-
const LEGACY_FAKE_REPO_METHODS_MARKER = "// go-scaffold:fake-repo-methods";
|
|
22
|
-
const UNUSED_G_LINE = "\t_ = g\n";
|
|
23
|
-
// writeHandler ensures whatever packages the new handler code references are
|
|
24
|
-
// imported (a minimal module starts with only "gin" imported) and drops the
|
|
25
|
-
// `_ = g` placeholder once a real route makes it unnecessary — a no-op on a
|
|
26
|
-
// full module, which already imports everything and never has that line.
|
|
27
|
-
function writeHandler(handlerPath, content, goModule, needs) {
|
|
28
|
-
for (const pkg of needs) {
|
|
29
|
-
const importPath = pkg.startsWith("net/") ? pkg : `${goModule}/internal/shared/${pkg}`;
|
|
30
|
-
content = (0, marker_patch_1.ensureImport)(content, importPath);
|
|
31
|
-
}
|
|
32
|
-
fs_extra_1.default.writeFileSync(handlerPath, content.replace(UNUSED_G_LINE, ""));
|
|
33
|
-
}
|
|
34
|
-
function assertNotDuplicate(content, needle, what) {
|
|
35
|
-
if (content.includes(needle)) {
|
|
36
|
-
throw new Error(`${what} already exists — pick a different method name`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function routeCall(type) {
|
|
40
|
-
return { get: "GET", post: "POST", put: "PUT", patch: "PATCH", delete: "DELETE" }[type];
|
|
41
|
-
}
|
|
42
|
-
// assertMethodAbsent is the same check patchMethod runs, exported so callers
|
|
43
|
-
// can reach it before their own pre-flight checks. A name that already exists
|
|
44
|
-
// is the cause the caller needs to hear about, so it has to be reported ahead
|
|
45
|
-
// of anything downstream of it (the OpenAPI document that name would collide
|
|
46
|
-
// with, for one) rather than after.
|
|
47
|
-
function assertMethodAbsent(paths, method) {
|
|
48
|
-
const handlerSig = `func (h *Handler) ${method.handlerName}(`;
|
|
49
|
-
const serviceSig = `func (s *Service) ${method.pascalName}(`;
|
|
50
|
-
assertNotDuplicate(fs_extra_1.default.readFileSync(paths.handlerPath, "utf8"), handlerSig, `handler method "${method.handlerName}"`);
|
|
51
|
-
assertNotDuplicate(fs_extra_1.default.readFileSync(paths.servicePath, "utf8"), serviceSig, `service method "${method.pascalName}"`);
|
|
52
|
-
}
|
|
53
|
-
function patchMethod(paths, naming, method, opts, goModule) {
|
|
54
|
-
assertMethodAbsent(paths, method);
|
|
55
|
-
if (opts.type === "get" && opts.getMode === "all") {
|
|
56
|
-
patchGetAll(paths, naming, method, goModule);
|
|
57
|
-
}
|
|
58
|
-
else if (opts.type === "get") {
|
|
59
|
-
if (!opts.field)
|
|
60
|
-
throw new Error("--field is required for --type get --get-mode one");
|
|
61
|
-
if (opts.field.toLowerCase() === "id") {
|
|
62
|
-
throw new Error('--field cannot be "id" — GET /:id already exists as the default lookup');
|
|
63
|
-
}
|
|
64
|
-
patchGetOne(paths, naming, method, opts.field, goModule);
|
|
65
|
-
}
|
|
66
|
-
else if (opts.type === "post") {
|
|
67
|
-
patchPost(paths, naming, method, goModule);
|
|
68
|
-
}
|
|
69
|
-
else if (opts.type === "put" || opts.type === "patch") {
|
|
70
|
-
patchResourceAction(paths, naming, method, opts.type, goModule);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
patchDelete(paths, method, goModule);
|
|
74
|
-
}
|
|
75
|
-
patchHandlerServiceInterface(paths.handlerPath, naming, method, opts, goModule);
|
|
76
|
-
}
|
|
77
|
-
function patchHandlerServiceInterface(handlerPath, naming, method, opts, goModule) {
|
|
78
|
-
let signature;
|
|
79
|
-
let needsModel = true;
|
|
80
|
-
let needsUUID = false;
|
|
81
|
-
if (opts.type === "get" && opts.getMode === "all") {
|
|
82
|
-
signature = `${method.pascalName}(context.Context, int, int) ([]model.${naming.pascalName}, error)`;
|
|
83
|
-
}
|
|
84
|
-
else if (opts.type === "get") {
|
|
85
|
-
signature = `${method.pascalName}(context.Context, string) (*model.${naming.pascalName}, error)`;
|
|
86
|
-
}
|
|
87
|
-
else if (opts.type === "post") {
|
|
88
|
-
signature = `${method.pascalName}(context.Context, ${method.pascalName}Input) (*model.${naming.pascalName}, error)`;
|
|
89
|
-
}
|
|
90
|
-
else if (opts.type === "put" || opts.type === "patch") {
|
|
91
|
-
signature = `${method.pascalName}(context.Context, uuid.UUID) (*model.${naming.pascalName}, error)`;
|
|
92
|
-
needsUUID = true;
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
signature = `${method.pascalName}(context.Context, uuid.UUID) error`;
|
|
96
|
-
needsModel = false;
|
|
97
|
-
needsUUID = true;
|
|
98
|
-
}
|
|
99
|
-
let handler = fs_extra_1.default.readFileSync(handlerPath, "utf8");
|
|
100
|
-
if (!(0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER)) {
|
|
101
|
-
// Projects scaffolded before the narrow service interface stored *Service
|
|
102
|
-
// directly. The concrete type already exposes generated methods, so there
|
|
103
|
-
// is no interface declaration to patch and no migration is required.
|
|
104
|
-
if (handler.includes("svc *Service"))
|
|
105
|
-
return;
|
|
106
|
-
throw new Error(`marker "${SERVICE_INTERFACE_MARKER}" not found and Handler does not use legacy *Service wiring`);
|
|
107
|
-
}
|
|
108
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, SERVICE_INTERFACE_MARKER, signature);
|
|
109
|
-
handler = (0, marker_patch_1.ensureImport)(handler, "context");
|
|
110
|
-
if (needsModel) {
|
|
111
|
-
handler = (0, marker_patch_1.ensureImport)(handler, `${goModule}/internal/app/${naming.pkg}/model`);
|
|
112
|
-
}
|
|
113
|
-
if (needsUUID) {
|
|
114
|
-
handler = (0, marker_patch_1.ensureImport)(handler, "github.com/google/uuid");
|
|
115
|
-
}
|
|
116
|
-
fs_extra_1.default.writeFileSync(handlerPath, handler);
|
|
117
|
-
}
|
|
118
|
-
function patchGetAll(paths, naming, method, goModule) {
|
|
119
|
-
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
120
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.GET("/${method.pathSegment}", h.${method.handlerName})`);
|
|
121
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
122
|
-
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
123
|
-
`\tp := pagination.Parse(c)`,
|
|
124
|
-
`\titems, err := h.svc.${method.pascalName}(c.Request.Context(), p.Limit, p.Offset)`,
|
|
125
|
-
`\tif err != nil {`,
|
|
126
|
-
`\t\tc.Error(err)`,
|
|
127
|
-
`\t\treturn`,
|
|
128
|
-
`\t}`,
|
|
129
|
-
`\tout := make([]response, len(items))`,
|
|
130
|
-
`\tfor i := range items {`,
|
|
131
|
-
`\t\tout[i] = toResponse(&items[i])`,
|
|
132
|
-
`\t}`,
|
|
133
|
-
`\tc.JSON(http.StatusOK, p.Response(out))`,
|
|
134
|
-
`}`,
|
|
135
|
-
``,
|
|
136
|
-
].join("\n"));
|
|
137
|
-
writeHandler(paths.handlerPath, handler, goModule, ["net/http", "pagination"]);
|
|
138
|
-
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
139
|
-
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
140
|
-
`func (s *Service) ${method.pascalName}(ctx context.Context, limit, offset int) ([]model.${naming.pascalName}, error) {`,
|
|
141
|
-
`\t// TODO: add real filtering for "${method.name}" — currently reuses FindAll`,
|
|
142
|
-
`\titems, err := s.repo.FindAll(ctx, limit, offset)`,
|
|
143
|
-
`\tif err != nil {`,
|
|
144
|
-
`\t\treturn nil, apperror.NewInternal()`,
|
|
145
|
-
`\t}`,
|
|
146
|
-
`\treturn items, nil`,
|
|
147
|
-
`}`,
|
|
148
|
-
``,
|
|
149
|
-
].join("\n"));
|
|
150
|
-
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
151
|
-
}
|
|
152
|
-
function patchGetOne(paths, naming, method, rawField, goModule) {
|
|
153
|
-
const fieldParam = (0, naming_1.toCamelCase)(rawField);
|
|
154
|
-
const fieldPascal = (0, naming_1.toPascalCase)(rawField);
|
|
155
|
-
const fieldColumn = (0, naming_1.toDbName)(rawField);
|
|
156
|
-
let repo = fs_extra_1.default.readFileSync(paths.repositoryPath, "utf8");
|
|
157
|
-
assertNotDuplicate(repo, `FindBy${fieldPascal}(`, `repository method "FindBy${fieldPascal}"`);
|
|
158
|
-
repo = (0, marker_patch_1.insertBeforeMarker)(repo, REPO_IMPL_MARKER, [
|
|
159
|
-
`func (r *Repository) FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
|
|
160
|
-
`\tvar m model.${naming.pascalName}`,
|
|
161
|
-
`\t// TODO: add the "${fieldColumn}" column and an index on it — this query`,
|
|
162
|
-
`\t// compiles against any table and only fails when it runs.`,
|
|
163
|
-
`\t//`,
|
|
164
|
-
`\t// tx.From, like every other method here: a lookup that reached for r.db`,
|
|
165
|
-
`\t// directly would read outside a transaction its caller had opened, and`,
|
|
166
|
-
`\t// so miss that transaction's own uncommitted writes.`,
|
|
167
|
-
`\tif err := tx.From(ctx, r.db).WithContext(ctx).First(&m, "${fieldColumn} = ?", ${fieldParam}).Error; err != nil {`,
|
|
168
|
-
`\t\treturn nil, err`,
|
|
169
|
-
`\t}`,
|
|
170
|
-
`\treturn &m, nil`,
|
|
171
|
-
`}`,
|
|
172
|
-
``,
|
|
173
|
-
].join("\n"));
|
|
174
|
-
fs_extra_1.default.writeFileSync(paths.repositoryPath, repo);
|
|
175
|
-
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
176
|
-
service = (0, marker_patch_1.insertBeforeMarker)(service, REPO_INTERFACE_MARKER, `FindBy${fieldPascal}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error)`);
|
|
177
|
-
// The repository interface just grew, so the test double needs a matching
|
|
178
|
-
// method or focused service tests stop compiling. New projects use a
|
|
179
|
-
// function-backed stub; projects scaffolded before that refactor retain the
|
|
180
|
-
// old fakeRepo marker and error behavior.
|
|
181
|
-
let serviceTest = fs_extra_1.default.readFileSync(paths.serviceTestPath, "utf8");
|
|
182
|
-
if ((0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER) &&
|
|
183
|
-
(0, marker_patch_1.hasMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER)) {
|
|
184
|
-
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_FIELDS_MARKER, `findBy${fieldPascal}Fn func(context.Context, string) (*model.${naming.pascalName}, error)`);
|
|
185
|
-
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, REPOSITORY_STUB_METHODS_MARKER, [
|
|
186
|
-
`//nolint:unused`,
|
|
187
|
-
`func (s *repositoryStub) FindBy${fieldPascal}(ctx context.Context, value string) (*model.${naming.pascalName}, error) {`,
|
|
188
|
-
`\tif s.findBy${fieldPascal}Fn == nil {`,
|
|
189
|
-
`\t\tpanic("unexpected repository.FindBy${fieldPascal} call")`,
|
|
190
|
-
`\t}`,
|
|
191
|
-
`\treturn s.findBy${fieldPascal}Fn(ctx, value)`,
|
|
192
|
-
`}`,
|
|
193
|
-
``,
|
|
194
|
-
].join("\n"));
|
|
195
|
-
}
|
|
196
|
-
else if ((0, marker_patch_1.hasMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER)) {
|
|
197
|
-
serviceTest = (0, marker_patch_1.insertBeforeMarker)(serviceTest, LEGACY_FAKE_REPO_METHODS_MARKER, [
|
|
198
|
-
`//nolint:unused`,
|
|
199
|
-
`func (f *fakeRepo) FindBy${fieldPascal}(context.Context, string) (*model.${naming.pascalName}, error) {`,
|
|
200
|
-
`\tif f.err != nil {`,
|
|
201
|
-
`\t\treturn nil, f.err`,
|
|
202
|
-
`\t}`,
|
|
203
|
-
`\treturn f.m, nil`,
|
|
204
|
-
`}`,
|
|
205
|
-
``,
|
|
206
|
-
].join("\n"));
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
throw new Error(`neither current repository stub markers nor legacy "${LEGACY_FAKE_REPO_METHODS_MARKER}" found in ${paths.serviceTestPath}`);
|
|
210
|
-
}
|
|
211
|
-
fs_extra_1.default.writeFileSync(paths.serviceTestPath, serviceTest);
|
|
212
|
-
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
213
|
-
`func (s *Service) ${method.pascalName}(ctx context.Context, ${fieldParam} string) (*model.${naming.pascalName}, error) {`,
|
|
214
|
-
`\tm, err := s.repo.FindBy${fieldPascal}(ctx, ${fieldParam})`,
|
|
215
|
-
`\tif err != nil {`,
|
|
216
|
-
`\t\treturn nil, wrapFindErr(err)`,
|
|
217
|
-
`\t}`,
|
|
218
|
-
`\treturn m, nil`,
|
|
219
|
-
`}`,
|
|
220
|
-
``,
|
|
221
|
-
].join("\n"));
|
|
222
|
-
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
223
|
-
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
224
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.GET("/${fieldColumn}/:${fieldParam}", h.${method.handlerName})`);
|
|
225
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
226
|
-
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
227
|
-
`\t${fieldParam} := c.Param("${fieldParam}")`,
|
|
228
|
-
`\tm, err := h.svc.${method.pascalName}(c.Request.Context(), ${fieldParam})`,
|
|
229
|
-
`\tif err != nil {`,
|
|
230
|
-
`\t\tc.Error(err)`,
|
|
231
|
-
`\t\treturn`,
|
|
232
|
-
`\t}`,
|
|
233
|
-
`\tc.JSON(http.StatusOK, toResponse(m))`,
|
|
234
|
-
`}`,
|
|
235
|
-
``,
|
|
236
|
-
].join("\n"));
|
|
237
|
-
writeHandler(paths.handlerPath, handler, goModule, ["net/http"]);
|
|
238
|
-
}
|
|
239
|
-
function patchPost(paths, naming, method, goModule) {
|
|
240
|
-
const inputName = `${method.pascalName}Input`;
|
|
241
|
-
let dto = fs_extra_1.default.readFileSync(paths.dtoPath, "utf8");
|
|
242
|
-
assertNotDuplicate(dto, `type ${inputName} struct`, `DTO "${inputName}"`);
|
|
243
|
-
dto = (0, marker_patch_1.insertBeforeMarker)(dto, DTO_MARKER, [`type ${inputName} struct {`, `\t// TODO: add request fields`, `}`, ``].join("\n"));
|
|
244
|
-
fs_extra_1.default.writeFileSync(paths.dtoPath, dto);
|
|
245
|
-
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
246
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.POST("/${method.pathSegment}", h.${method.handlerName})`);
|
|
247
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
248
|
-
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
249
|
-
`\tvar in ${inputName}`,
|
|
250
|
-
`\tif err := c.ShouldBindJSON(&in); err != nil {`,
|
|
251
|
-
`\t\tc.Error(httpx.BindErr(err))`,
|
|
252
|
-
`\t\treturn`,
|
|
253
|
-
`\t}`,
|
|
254
|
-
`\tm, err := h.svc.${method.pascalName}(c.Request.Context(), in)`,
|
|
255
|
-
`\tif err != nil {`,
|
|
256
|
-
`\t\tc.Error(err)`,
|
|
257
|
-
`\t\treturn`,
|
|
258
|
-
`\t}`,
|
|
259
|
-
`\tc.JSON(http.StatusCreated, toResponse(m))`,
|
|
260
|
-
`}`,
|
|
261
|
-
``,
|
|
262
|
-
].join("\n"));
|
|
263
|
-
writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
|
|
264
|
-
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
265
|
-
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
266
|
-
`func (s *Service) ${method.pascalName}(ctx context.Context, in ${inputName}) (*model.${naming.pascalName}, error) {`,
|
|
267
|
-
`\t// TODO: implement "${method.name}" — this stub does nothing yet`,
|
|
268
|
-
`\t_ = in`,
|
|
269
|
-
`\treturn nil, apperror.NewInternal()`,
|
|
270
|
-
`}`,
|
|
271
|
-
``,
|
|
272
|
-
].join("\n"));
|
|
273
|
-
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
274
|
-
}
|
|
275
|
-
function patchResourceAction(paths, naming, method, type, goModule) {
|
|
276
|
-
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
277
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.${routeCall(type)}("/:id/${method.pathSegment}", h.${method.handlerName})`);
|
|
278
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
279
|
-
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
280
|
-
`\tid, ok := httpx.ParseID(c)`,
|
|
281
|
-
`\tif !ok {`,
|
|
282
|
-
`\t\treturn`,
|
|
283
|
-
`\t}`,
|
|
284
|
-
`\tm, err := h.svc.${method.pascalName}(c.Request.Context(), id)`,
|
|
285
|
-
`\tif err != nil {`,
|
|
286
|
-
`\t\tc.Error(err)`,
|
|
287
|
-
`\t\treturn`,
|
|
288
|
-
`\t}`,
|
|
289
|
-
`\tc.JSON(http.StatusOK, toResponse(m))`,
|
|
290
|
-
`}`,
|
|
291
|
-
``,
|
|
292
|
-
].join("\n"));
|
|
293
|
-
writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
|
|
294
|
-
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
295
|
-
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
296
|
-
`func (s *Service) ${method.pascalName}(ctx context.Context, id uuid.UUID) (*model.${naming.pascalName}, error) {`,
|
|
297
|
-
`\tm, err := s.repo.FindByID(ctx, id)`,
|
|
298
|
-
`\tif err != nil {`,
|
|
299
|
-
`\t\treturn nil, wrapFindErr(err)`,
|
|
300
|
-
`\t}`,
|
|
301
|
-
`\t// TODO: implement "${method.name}" — currently a no-op save`,
|
|
302
|
-
`\t// The version compared here is the one just read, so this only`,
|
|
303
|
-
`\t// catches a writer that lands between the read above and this write.`,
|
|
304
|
-
`\t// It is NOT the check Update does: that one compares against the version`,
|
|
305
|
-
`\t// the *client* last saw, which is what catches someone submitting a`,
|
|
306
|
-
`\t// form built from a copy that has since gone stale. If this endpoint`,
|
|
307
|
-
`\t// needs that, give it an input struct carrying Version and assign it`,
|
|
308
|
-
`\t// to m.Version here, the way service.go's Update does.`,
|
|
309
|
-
`\tif err := s.repo.Update(ctx, m); err != nil {`,
|
|
310
|
-
`\t\tif errors.Is(err, ErrStaleVersion) {`,
|
|
311
|
-
`\t\t\treturn nil, errStale()`,
|
|
312
|
-
`\t\t}`,
|
|
313
|
-
`\t\treturn nil, apperror.NewInternal()`,
|
|
314
|
-
`\t}`,
|
|
315
|
-
`\treturn m, nil`,
|
|
316
|
-
`}`,
|
|
317
|
-
``,
|
|
318
|
-
].join("\n"));
|
|
319
|
-
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
320
|
-
}
|
|
321
|
-
function patchDelete(paths, method, goModule) {
|
|
322
|
-
let handler = fs_extra_1.default.readFileSync(paths.handlerPath, "utf8");
|
|
323
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_ROUTES_MARKER, `g.DELETE("/:id/${method.pathSegment}", h.${method.handlerName})`);
|
|
324
|
-
handler = (0, marker_patch_1.insertBeforeMarker)(handler, HANDLER_FUNCS_MARKER, [
|
|
325
|
-
`func (h *Handler) ${method.handlerName}(c *gin.Context) {`,
|
|
326
|
-
`\tid, ok := httpx.ParseID(c)`,
|
|
327
|
-
`\tif !ok {`,
|
|
328
|
-
`\t\treturn`,
|
|
329
|
-
`\t}`,
|
|
330
|
-
`\tif err := h.svc.${method.pascalName}(c.Request.Context(), id); err != nil {`,
|
|
331
|
-
`\t\tc.Error(err)`,
|
|
332
|
-
`\t\treturn`,
|
|
333
|
-
`\t}`,
|
|
334
|
-
`\tc.Status(http.StatusNoContent)`,
|
|
335
|
-
`}`,
|
|
336
|
-
``,
|
|
337
|
-
].join("\n"));
|
|
338
|
-
writeHandler(paths.handlerPath, handler, goModule, ["net/http", "httpx"]);
|
|
339
|
-
let service = fs_extra_1.default.readFileSync(paths.servicePath, "utf8");
|
|
340
|
-
service = (0, marker_patch_1.insertBeforeMarker)(service, SERVICE_MARKER, [
|
|
341
|
-
`func (s *Service) ${method.pascalName}(ctx context.Context, id uuid.UUID) error {`,
|
|
342
|
-
`\t// TODO: implement "${method.name}" — this stub does nothing yet`,
|
|
343
|
-
`\treturn apperror.NewInternal()`,
|
|
344
|
-
`}`,
|
|
345
|
-
``,
|
|
346
|
-
].join("\n"));
|
|
347
|
-
fs_extra_1.default.writeFileSync(paths.servicePath, service);
|
|
348
|
-
}
|
|
349
|
-
function markersPresent(handlerPath, servicePath) {
|
|
350
|
-
const handler = fs_extra_1.default.readFileSync(handlerPath, "utf8");
|
|
351
|
-
const service = fs_extra_1.default.readFileSync(servicePath, "utf8");
|
|
352
|
-
return ((0, marker_patch_1.hasMarker)(handler, HANDLER_ROUTES_MARKER) &&
|
|
353
|
-
(0, marker_patch_1.hasMarker)(handler, HANDLER_FUNCS_MARKER) &&
|
|
354
|
-
((0, marker_patch_1.hasMarker)(handler, SERVICE_INTERFACE_MARKER) || handler.includes("svc *Service")) &&
|
|
355
|
-
(0, marker_patch_1.hasMarker)(service, SERVICE_MARKER) &&
|
|
356
|
-
(0, marker_patch_1.hasMarker)(service, REPO_INTERFACE_MARKER));
|
|
357
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
get:
|
|
2
|
-
summary: Google OAuth callback
|
|
3
|
-
description: Redirect target Google sends the browser back to after consent — not called directly by API clients.
|
|
4
|
-
operationId: googleCallback
|
|
5
|
-
tags: [auth]
|
|
6
|
-
security: []
|
|
7
|
-
parameters:
|
|
8
|
-
- name: code
|
|
9
|
-
in: query
|
|
10
|
-
required: true
|
|
11
|
-
schema: { type: string }
|
|
12
|
-
- name: state
|
|
13
|
-
in: query
|
|
14
|
-
required: true
|
|
15
|
-
schema: { type: string }
|
|
16
|
-
responses:
|
|
17
|
-
"200":
|
|
18
|
-
description: ok, refresh_token set as an httpOnly cookie
|
|
19
|
-
content:
|
|
20
|
-
application/json:
|
|
21
|
-
schema: { $ref: './schemas.yaml#/AuthResponse' }
|
|
22
|
-
"401": { $ref: '../common/responses.yaml#/UnauthorizedError' }
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
package user
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"time"
|
|
5
|
-
|
|
6
|
-
"{{goModule}}/internal/app/user/model"
|
|
7
|
-
|
|
8
|
-
"github.com/google/uuid"
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
type registerInput struct {
|
|
12
|
-
Email string `json:"email" binding:"required,email"`
|
|
13
|
-
Password string `json:"password" binding:"required,min=8"`
|
|
14
|
-
Name string `json:"name" binding:"required"`
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type loginInput struct {
|
|
18
|
-
Email string `json:"email" binding:"required,email"`
|
|
19
|
-
Password string `json:"password" binding:"required"`
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type forgotPasswordInput struct {
|
|
23
|
-
Email string `json:"email" binding:"required,email"`
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
type resetPasswordInput struct {
|
|
27
|
-
Token string `json:"token" binding:"required"`
|
|
28
|
-
NewPassword string `json:"new_password" binding:"required,min=8"`
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type verifyEmailInput struct {
|
|
32
|
-
Token string `json:"token" binding:"required"`
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// authResponse is what the service returns internally (both tokens); the
|
|
36
|
-
// handler sends the refresh token as an httpOnly cookie instead of echoing
|
|
37
|
-
// it in the body, so meResponse never carries it — see toCookieResponse.
|
|
38
|
-
type authResponse struct {
|
|
39
|
-
AccessToken string `json:"access_token"`
|
|
40
|
-
RefreshToken string `json:"-"`
|
|
41
|
-
TokenType string `json:"token_type"`
|
|
42
|
-
ExpiresIn int `json:"expires_in"`
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
type authCookieResponse struct {
|
|
46
|
-
AccessToken string `json:"access_token"`
|
|
47
|
-
TokenType string `json:"token_type"`
|
|
48
|
-
ExpiresIn int `json:"expires_in"`
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
func toCookieResponse(a *authResponse) authCookieResponse {
|
|
52
|
-
return authCookieResponse{AccessToken: a.AccessToken, TokenType: a.TokenType, ExpiresIn: a.ExpiresIn}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
type meResponse struct {
|
|
56
|
-
ID uuid.UUID `json:"id"`
|
|
57
|
-
Email string `json:"email"`
|
|
58
|
-
Name string `json:"name"`
|
|
59
|
-
AvatarURL string `json:"avatar_url"`
|
|
60
|
-
EmailVerified bool `json:"email_verified"`
|
|
61
|
-
// go-scaffold:user-me-fields
|
|
62
|
-
CreatedAt time.Time `json:"created_at"`
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
func toMeResponse(u *model.User) meResponse {
|
|
66
|
-
return meResponse{
|
|
67
|
-
ID: u.ID,
|
|
68
|
-
Email: u.Email,
|
|
69
|
-
Name: u.Name,
|
|
70
|
-
AvatarURL: u.AvatarURL,
|
|
71
|
-
EmailVerified: u.EmailVerified,
|
|
72
|
-
CreatedAt: u.CreatedAt,
|
|
73
|
-
// go-scaffold:user-me-values
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// go-scaffold:user-dto
|