@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Coding Patterns: {{projectName}}
|
|
2
2
|
|
|
3
|
-
> Status: generated by `@nakedev/go-scaffold
|
|
4
|
-
> Last
|
|
3
|
+
> Status: generated by `@nakedev/go-scaffold`; current generated-project conventions
|
|
4
|
+
> Last reviewed: generated at project creation and refreshed with feature docs
|
|
5
5
|
|
|
6
6
|
## Domain Package Shape
|
|
7
7
|
|
|
@@ -10,56 +10,92 @@ like this — copy the shape by hand if you ever add one without the CLI:
|
|
|
10
10
|
|
|
11
11
|
```text
|
|
12
12
|
internal/app/<domain>/
|
|
13
|
-
├──
|
|
14
|
-
│
|
|
15
|
-
|
|
16
|
-
├──
|
|
17
|
-
|
|
18
|
-
├──
|
|
19
|
-
├──
|
|
20
|
-
├──
|
|
21
|
-
├──
|
|
22
|
-
|
|
13
|
+
├── domain/
|
|
14
|
+
│ ├── entity.go # business state and invariants; no Gin/GORM/HTTP
|
|
15
|
+
│ └── errors.go # domain error catalog (<DOMAIN>_NOT_FOUND, ...)
|
|
16
|
+
├── ports/
|
|
17
|
+
│ └── repository.go # consumer-owned persistence/application interfaces
|
|
18
|
+
├── application/
|
|
19
|
+
│ ├── dto.go # transport-neutral use-case input/output types
|
|
20
|
+
│ ├── service.go # service-style use-case orchestration
|
|
21
|
+
│ ├── commands.go # CQRS writes, when selected
|
|
22
|
+
│ ├── queries.go # CQRS reads, when selected
|
|
23
|
+
│ └── *_test.go # application unit tests, no DB
|
|
24
|
+
├── adapters/
|
|
25
|
+
│ ├── inbound/http/
|
|
26
|
+
│ │ ├── dto.go # JSON/binding DTOs and application/HTTP mapping
|
|
27
|
+
│ │ ├── handler.go # Gin routing, error/response mapping
|
|
28
|
+
│ │ └── handler_test.go # HTTP adapter tests, no DB
|
|
29
|
+
│ └── outbound/postgres/
|
|
30
|
+
│ ├── model.go # persistence model + explicit domain mapping
|
|
31
|
+
│ ├── repository.go # GORM adapter; the only DB access in the module
|
|
32
|
+
│ └── repository_test.go # Postgres integration tests
|
|
33
|
+
└── composition.go # feature-local adapter → application → handler wiring
|
|
23
34
|
```
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
`service.go` and `commands.go`/`queries.go` are mutually exclusive inside a
|
|
37
|
+
module. Service and CQRS can coexist across different modules in the same
|
|
38
|
+
modular monolith. The root package contains only `composition.go`; other code
|
|
39
|
+
must import the specific boundary package it belongs to.
|
|
40
|
+
|
|
41
|
+
### Domain and persistence model
|
|
42
|
+
|
|
43
|
+
- `domain/entity.go` owns business state and invariants. It must not import
|
|
44
|
+
Gin, GORM, SQL drivers, Redis, HTTP, or environment configuration.
|
|
45
|
+
- `adapters/outbound/postgres/model.go` owns persistence-only fields and
|
|
46
|
+
explicit `toDomain`/`fromDomain` mapping. A database column or GORM tag must
|
|
47
|
+
not silently become part of the business API.
|
|
48
|
+
- A domain with more than one table should add one persistence model per table
|
|
49
|
+
in the outbound adapter, with the corresponding domain type or value object
|
|
50
|
+
kept in `domain/`.
|
|
35
51
|
- `TableName()` returns a schema-qualified name (`order_svc.orders`, not
|
|
36
52
|
`orders`) — every domain gets its own Postgres schema, created by its own
|
|
37
53
|
migration (`CREATE SCHEMA IF NOT EXISTS`) and by `cmd/api/wiring.go` before
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
raw SQL `JOIN` from reaching into a domain it doesn't own without at least
|
|
42
|
-
naming the schema it's crossing into.
|
|
54
|
+
the development table bootstrap runs. A cross-domain FK is still fine — see
|
|
55
|
+
the FK rules below — this only stops one domain's table from silently
|
|
56
|
+
colliding with another's.
|
|
43
57
|
|
|
44
58
|
## Layer Conventions
|
|
45
59
|
|
|
46
60
|
### Handler
|
|
47
|
-
- Owns routing, request binding, and calling the service
|
|
61
|
+
- Owns routing, request binding, and calling the application service or
|
|
62
|
+
command/query port
|
|
48
63
|
- No business logic — `c.Error(err); return` on failure, nothing more
|
|
49
64
|
- Reads pagination via `pagination.Parse(c)`, wraps list responses with
|
|
50
65
|
`p.Response(out)`
|
|
51
66
|
- Reads the `:id` param via `httpx.ParseID(c)`
|
|
52
67
|
|
|
68
|
+
### Commands and queries (opt-in CQRS)
|
|
69
|
+
|
|
70
|
+
Use the project default recorded in `go-scaffold.config.json`, or override it
|
|
71
|
+
per module with `--cqrs`, when the feature has distinct write invariants, read
|
|
72
|
+
projections, consistency requirements, or scaling needs. `commands.go` owns
|
|
73
|
+
state-changing application ports and handlers; `queries.go` owns read-only
|
|
74
|
+
ports and handlers. `composition.go` constructs both, and the HTTP adapter
|
|
75
|
+
selects the matching port for each route. They may share the same
|
|
76
|
+
repository/database in this modular monolith — separate storage or event
|
|
77
|
+
sourcing is not implied.
|
|
78
|
+
|
|
79
|
+
The generated CQRS application has separate `CommandPort` and `QueryPort`
|
|
80
|
+
interfaces and separate handlers. Its HTTP adapter receives both through
|
|
81
|
+
`NewHandler`; there is no generated `application/service.go` facade to hide the
|
|
82
|
+
split. New feature code should depend on the narrower port it actually needs.
|
|
83
|
+
`generate module --defaults` uses the project default; choose the single service
|
|
84
|
+
path for simple CRUD and CQRS only when the feature earns the extra boundary.
|
|
85
|
+
|
|
53
86
|
### Service
|
|
54
87
|
- Contains the business logic, knows nothing about HTTP
|
|
55
|
-
-
|
|
56
|
-
this is what makes it mockable in `
|
|
88
|
+
- Depends on consumer-owned interfaces from `ports/` for what it needs from
|
|
89
|
+
the data layer — this is what makes it mockable in `application/*_test.go`
|
|
90
|
+
without a DB
|
|
57
91
|
- Generates the ID itself via `id.New()` before calling `repository.Create`
|
|
58
|
-
-
|
|
59
|
-
|
|
92
|
+
- Returns domain errors and never constructs an HTTP response. The outbound
|
|
93
|
+
adapter maps driver errors with `dberr.IsDuplicate` /
|
|
94
|
+
`dberr.IsForeignKey` before they reach the application boundary.
|
|
60
95
|
|
|
61
|
-
### Repository
|
|
62
|
-
-
|
|
96
|
+
### Repository adapter
|
|
97
|
+
- `adapters/outbound/postgres/repository.go` is the only module code that talks
|
|
98
|
+
to GORM
|
|
63
99
|
- Every method takes `ctx context.Context` first, so a cancelled request
|
|
64
100
|
cancels the query
|
|
65
101
|
- Every query starts from `tx.From(ctx, r.db).WithContext(ctx)`, never from
|
|
@@ -75,16 +111,18 @@ internal/app/<domain>/
|
|
|
75
111
|
and GORM can misinterpret a bare struct arg
|
|
76
112
|
|
|
77
113
|
### DTOs
|
|
78
|
-
- `
|
|
79
|
-
|
|
80
|
-
|
|
114
|
+
- `application/dto.go` owns transport-neutral use-case inputs and output read
|
|
115
|
+
models. `adapters/inbound/http/dto.go` owns JSON names, binding/validation
|
|
116
|
+
tags, and explicit request/response mapping at the HTTP boundary.
|
|
117
|
+
- When `generate method` adds a POST input, it adds both the application input
|
|
118
|
+
and an inbound HTTP DTO/mapping seam. Fill both deliberately; do not bind
|
|
119
|
+
Gin JSON directly into an application type.
|
|
81
120
|
|
|
82
121
|
### Error Catalog
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`apperror.NewNotFound()` directly from a handler
|
|
122
|
+
- Domain sentinels live in `domain/errors.go`; they are values used with
|
|
123
|
+
`errors.Is`, not HTTP errors or mutable request state.
|
|
124
|
+
- The inbound adapter maps those sentinels to domain-specific HTTP codes
|
|
125
|
+
(`ORDER_NOT_FOUND`); domain/application code never constructs an HTTP error.
|
|
88
126
|
|
|
89
127
|
## Optimistic Locking
|
|
90
128
|
|
|
@@ -103,13 +141,13 @@ and `response` echoes it back: the client round-trips the value, and an
|
|
|
103
141
|
update that arrives without one is rejected rather than treated as a blind
|
|
104
142
|
overwrite.
|
|
105
143
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`
|
|
112
|
-
|
|
144
|
+
The inbound adapter maps `ErrStaleVersion` to the domain's `<DOMAIN>_STALE`
|
|
145
|
+
HTTP 409 response — a lost update is a client problem to retry, not a 500. A
|
|
146
|
+
minimal module has the same `Repository.Update` guard waiting for its first
|
|
147
|
+
update use case. Keep the chain intact when you add one by hand:
|
|
148
|
+
`errors.Is(err, domain.ErrStaleVersion)` → adapter HTTP mapping, and never set
|
|
149
|
+
`Version` on the persistence model yourself — `Repository.Update` owns the
|
|
150
|
+
bump.
|
|
113
151
|
|
|
114
152
|
## Domains With a Foreign Key (Relations) — 3 Rules
|
|
115
153
|
|
|
@@ -121,20 +159,21 @@ if this project has one):
|
|
|
121
159
|
associations / belongs-to — that's what keeps one domain package from
|
|
122
160
|
importing another.
|
|
123
161
|
2. **Declare the FK constraint in migration SQL**
|
|
124
|
-
(`REFERENCES ... ON DELETE ...`), not a GORM tag —
|
|
125
|
-
create the constraint, which would make dev and prod schemas diverge.
|
|
126
|
-
3. **Map the FK error to the right status**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
catch the error — a pre-check
|
|
162
|
+
(`REFERENCES ... ON DELETE ...`), not a GORM tag — the development bootstrap
|
|
163
|
+
doesn't create the constraint, which would make dev and prod schemas diverge.
|
|
164
|
+
3. **Map the FK error to the right status** in the outbound adapter with
|
|
165
|
+
`dberr.IsForeignKey`, then let the inbound adapter render it — inserting a
|
|
166
|
+
reference to a missing parent, or deleting a parent that still has children,
|
|
167
|
+
is a client error (409/422), not a 500. Don't pre-check existence before
|
|
168
|
+
insert; let the DB enforce it atomically and catch the error — a pre-check
|
|
169
|
+
has a TOCTOU race.
|
|
131
170
|
|
|
132
171
|
## Calling Another Domain's Logic
|
|
133
172
|
|
|
134
173
|
For behavior, not just a data reference (e.g. `order` needs `user`'s email
|
|
135
|
-
to put on a receipt) — the caller's `service.go
|
|
136
|
-
|
|
137
|
-
`repository` interface.
|
|
174
|
+
to put on a receipt) — the caller's application boundary (`application/service.go`,
|
|
175
|
+
`application/commands.go`, or `application/queries.go`) declares its own narrow interface for exactly
|
|
176
|
+
what it needs, the same way it already declares a `repository` interface.
|
|
138
177
|
|
|
139
178
|
**The interface speaks in the caller's own terms.** No domain package ever
|
|
140
179
|
imports another domain package — `golangci-lint`'s `depguard` rules enforce
|
|
@@ -142,33 +181,33 @@ this, and it is the same rule that makes rule 1 above work. So the interface
|
|
|
142
181
|
names primitives, or types the caller owns, never `user.Response`:
|
|
143
182
|
|
|
144
183
|
```go
|
|
145
|
-
// order/service.go
|
|
184
|
+
// order/application/service.go
|
|
146
185
|
type userLookup interface {
|
|
147
186
|
EmailOf(ctx context.Context, id uuid.UUID) (string, error)
|
|
148
187
|
}
|
|
149
188
|
```
|
|
150
189
|
|
|
151
|
-
|
|
152
|
-
|
|
190
|
+
The feature keeps its own repository/application/handler composition in
|
|
191
|
+
`composition.go`; `cmd/api/wiring.go` supplies the DB and registers routes.
|
|
192
|
+
Process-level adapters for cross-feature capabilities live in
|
|
193
|
+
`internal/composition/`. For example, auth and RBAC are composed locally and
|
|
194
|
+
the root passes only the role feature's public capabilities:
|
|
153
195
|
|
|
154
196
|
```go
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if err != nil {
|
|
160
|
-
return "", err
|
|
161
|
-
}
|
|
162
|
-
return u.Email, nil
|
|
163
|
-
},
|
|
164
|
-
))
|
|
197
|
+
roleComposition := role.NewCompositionFromDB(db, cfg.JWTSecret, cfg.AuthzCacheTTL)
|
|
198
|
+
user.NewHandlerFromDB(
|
|
199
|
+
db, cfg, roleComposition.Service, roleComposition.Authz,
|
|
200
|
+
).Register(api)
|
|
165
201
|
```
|
|
166
202
|
|
|
167
|
-
|
|
168
|
-
interface
|
|
203
|
+
For ordinary cross-domain behaviour, the caller still declares a narrow
|
|
204
|
+
interface next to its application boundary. Its local composition accepts that
|
|
205
|
+
port, while `internal/composition/` supplies an adapter; the process root never
|
|
206
|
+
reaches into the callee's repository or handler. A func-to-interface shim is
|
|
207
|
+
one small option:
|
|
169
208
|
|
|
170
209
|
```go
|
|
171
|
-
// order/service.go
|
|
210
|
+
// order/application/service.go
|
|
172
211
|
type UserLookupFunc func(ctx context.Context, id uuid.UUID) (string, error)
|
|
173
212
|
|
|
174
213
|
func (f UserLookupFunc) EmailOf(ctx context.Context, id uuid.UUID) (string, error) {
|
|
@@ -183,6 +222,10 @@ can never silently ripple into `order`. The adapter is also the honest place
|
|
|
183
222
|
to notice you are reaching for something that should have been the caller's
|
|
184
223
|
own data.
|
|
185
224
|
|
|
225
|
+
Cross-feature integration tests belong under `internal/integration`, where
|
|
226
|
+
they may compose real feature services and adapters. A feature's own unit and
|
|
227
|
+
repository tests stay inside its package and must not import another feature.
|
|
228
|
+
|
|
186
229
|
- **One direction only.** If `user` would need to call back into `order`,
|
|
187
230
|
don't wire it both ways — either the two belong in one domain, or the
|
|
188
231
|
callback needs an event/queue, not a direct call.
|
|
@@ -198,30 +241,33 @@ own data.
|
|
|
198
241
|
go-scaffold generate method <domain> <name> --type <get|post|put|patch|delete> [--get-mode all|one] [--field <name>]
|
|
199
242
|
```
|
|
200
243
|
|
|
201
|
-
Patches `handler.go
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
placeholder fields.
|
|
244
|
+
Patches `adapters/inbound/http/handler.go`, its request DTO seam, and the application boundary (or
|
|
245
|
+
`application/commands.go`/`application/queries.go` for a CQRS module; plus
|
|
246
|
+
`ports/repository.go` and the outbound repository adapter/test seam for a
|
|
247
|
+
`get --get-mode one --field` lookup) in place, at the
|
|
248
|
+
`// go-scaffold:*` marker comments near the end of each file. **Don't delete
|
|
249
|
+
those markers** — they're where the next `generate method` call inserts. The
|
|
250
|
+
method body is always left as a compiling `TODO` rather than guessing at
|
|
251
|
+
business logic — same spirit as `generate module`'s placeholder fields.
|
|
209
252
|
|
|
210
253
|
## Testing Conventions
|
|
211
254
|
|
|
212
255
|
- Unit and integration tests live in the same directory as the code under
|
|
213
256
|
test (Go convention) — never a separate `test/` folder. `test/` is only
|
|
214
257
|
for e2e black-box suites or fixtures.
|
|
215
|
-
- `service_test.go` uses a function-backed repository stub so each
|
|
216
|
-
method has independent behavior and argument assertions.
|
|
217
|
-
- `handler_test.go`
|
|
218
|
-
|
|
258
|
+
- `application/service_test.go` uses a function-backed repository stub so each
|
|
259
|
+
dependency method has independent behavior and argument assertions.
|
|
260
|
+
- `adapters/inbound/http/handler_test.go` checks route composition; focused
|
|
261
|
+
HTTP tests can inject an application port and never need Postgres.
|
|
262
|
+
- `adapters/outbound/postgres/repository_test.go` runs against a migrated
|
|
263
|
+
Postgres database inside a
|
|
219
264
|
transaction that's rolled back after each test. Local runs may skip when
|
|
220
265
|
`TEST_DB_DSN` is unset; CI sets `REQUIRE_TEST_DB=true` so it must run.
|
|
221
266
|
|
|
222
267
|
## Docs Maintenance
|
|
223
268
|
|
|
224
|
-
- This document describes the
|
|
225
|
-
|
|
226
|
-
-
|
|
227
|
-
|
|
269
|
+
- This document describes the conventions used by generated domains and the
|
|
270
|
+
canonical split emitted by `go-scaffold`.
|
|
271
|
+
- When a shared generator contract changes, update the templates and add a
|
|
272
|
+
regression test. Generated projects with hand-edited docs are left alone by
|
|
273
|
+
incremental feature refreshes and must be updated by their owners.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Tech Stack: {{projectName}}
|
|
2
2
|
|
|
3
|
-
> Status: generated by `@nakedev/go-scaffold
|
|
3
|
+
> Status: generated by `@nakedev/go-scaffold`; refreshes as optional features are added
|
|
4
4
|
|
|
5
5
|
## Stack Overview
|
|
6
6
|
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
| Logging | `log/slog`, JSON handler |
|
|
16
16
|
| Migrations | [golang-migrate](https://github.com/golang-migrate/migrate) |
|
|
17
17
|
| Testing | stdlib `testing`, real Postgres for integration tests |
|
|
18
|
+
| Authentication | {{#if auth}}JWT access/refresh sessions, password recovery, email verification, provider login, and MFA{{else}}optional via `go-scaffold add auth`{{/if}} |
|
|
19
|
+
| Authorization | {{#if rbac}}RBAC role/permission middleware{{else}}optional RBAC via `go-scaffold add rbac`{{/if}} |
|
|
20
|
+
| Background jobs | {{#if worker}}{{queue}} queue backend with SMTP mail and `cmd/worker`{{else}}optional via `go-scaffold add worker`{{/if}} |
|
|
18
21
|
|
|
19
22
|
## Scaffolded Capabilities
|
|
20
23
|
|
|
@@ -24,6 +27,9 @@
|
|
|
24
27
|
- Docker Compose (local Postgres): `{{#if docker}}enabled{{else}}disabled{{/if}}`
|
|
25
28
|
- OpenAPI docs (`docs/openapi.yaml`, working-copy only — never served over HTTP): `{{#if openapiDocs}}enabled{{else}}disabled{{/if}}`
|
|
26
29
|
- Metrics + tracing (Prometheus `/metrics`, OpenTelemetry for Gin + GORM, `cmd/api` only): `{{#if observability}}enabled{{else}}disabled{{/if}}`
|
|
30
|
+
- Authentication and browser OAuth: `{{#if auth}}enabled{{else}}disabled{{/if}}`
|
|
31
|
+
- Background worker and mail: `{{#if worker}}enabled{{else}}disabled{{/if}}`
|
|
32
|
+
- RBAC roles, permissions, and authorization: `{{#if rbac}}enabled{{else}}disabled{{/if}}`
|
|
27
33
|
- API route prefix: `{{#if apiPrefix}}/{{apiPrefix}}{{else}}(none){{/if}}`
|
|
28
34
|
- CI (`.github/workflows/ci.yml` — build, vet, gofmt check, golangci-lint, `go test` with a real Postgres service): always enabled
|
|
29
35
|
|
|
@@ -31,8 +37,14 @@
|
|
|
31
37
|
|
|
32
38
|
{{#if docker}}- Local Postgres comes from `docker-compose.yml`
|
|
33
39
|
{{else}}- No Docker scaffolding included — bring your own Postgres and point `DB_DSN` at it
|
|
34
|
-
{{/if}}
|
|
35
|
-
|
|
40
|
+
{{/if}}{{#if worker}}{{#if (eq queue "river")}}- Run `make river-migrate` before relying on queued jobs in a fresh database;
|
|
41
|
+
run `make river-migrate-test` as well when `REQUIRE_TEST_DB=true` runs the
|
|
42
|
+
River worker round-trip test.{{/if}}{{/if}}
|
|
43
|
+
- `APP_ENV=development` enables the convenience table bootstrap;
|
|
44
|
+
`APP_ENV=production` requires `migrate up` as a deploy step instead
|
|
45
|
+
{{#if worker}}{{#if (eq queue "river")}}- River uses the database/sql driver and polls for jobs instead of receiving
|
|
46
|
+
PostgreSQL LISTEN/NOTIFY wakeups; this is deliberate for a shared GORM pool.
|
|
47
|
+
{{/if}}{{/if}}
|
|
36
48
|
{{#if openapiDocs}}- `docs/openapi.yaml` is hand-written, not generated — update it whenever an
|
|
37
49
|
endpoint or DTO changes
|
|
38
50
|
{{/if}}{{#if observability}}- `OTEL_EXPORTER_OTLP_ENDPOINT` unset (dev default) means tracing exports
|
|
@@ -44,3 +56,6 @@
|
|
|
44
56
|
resolves them to the versions that feature was built against instead of
|
|
45
57
|
whatever happened to be newest that day. The full list is `go.mod`; this
|
|
46
58
|
table only covers what `create` itself brings in
|
|
59
|
+
- Auth, worker, and RBAC are opt-in. Their `add` commands update this snapshot
|
|
60
|
+
when the generated file is untouched; hand-edited docs are left alone and
|
|
61
|
+
require a manual update.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"time"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/app/{{modulePath}}/application"
|
|
7
|
+
|
|
8
|
+
"github.com/google/uuid"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// response is the HTTP representation of the application output. Keep JSON
|
|
12
|
+
// names and protocol-only fields at this inbound boundary.
|
|
13
|
+
type response struct {
|
|
14
|
+
ID uuid.UUID `json:"id"`
|
|
15
|
+
CreatedAt time.Time `json:"created_at"`
|
|
16
|
+
Version int `json:"version"`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func toResponse(out application.Response) response {
|
|
20
|
+
return response{ID: out.ID, CreatedAt: out.CreatedAt, Version: out.Version}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Request DTOs belong to the inbound adapter. Keep JSON names, validation
|
|
24
|
+
// tags, and request-to-command mapping here; application inputs stay usable
|
|
25
|
+
// from jobs, tests, and other non-HTTP callers.
|
|
26
|
+
type createInput struct {
|
|
27
|
+
// TODO: mirror the request fields added to application.CreateInput.
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type updateInput struct {
|
|
31
|
+
Version int `json:"version" binding:"required,min=1"`
|
|
32
|
+
// TODO: mirror the update fields added to application.UpdateInput.
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func toCreateInput(in createInput) application.CreateInput {
|
|
36
|
+
// TODO: map request fields explicitly as the application contract evolves.
|
|
37
|
+
_ = in
|
|
38
|
+
return application.CreateInput{}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func toUpdateInput(in updateInput) application.UpdateInput {
|
|
42
|
+
return application.UpdateInput{Version: in.Version}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// go-scaffold:dto
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package httpadapter
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"time"
|
|
5
|
+
|
|
6
|
+
"{{goModule}}/internal/app/{{modulePath}}/application"
|
|
7
|
+
|
|
8
|
+
"github.com/google/uuid"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// response is the HTTP representation of the application output. Keep JSON
|
|
12
|
+
// names and protocol-only fields at this inbound boundary.
|
|
13
|
+
//nolint:unused // generate method uses this seam when the first endpoint is added.
|
|
14
|
+
type response struct {
|
|
15
|
+
ID uuid.UUID `json:"id"`
|
|
16
|
+
CreatedAt time.Time `json:"created_at"`
|
|
17
|
+
Version int `json:"version"`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//nolint:unused // generate method uses this seam when the first endpoint is added.
|
|
21
|
+
func toResponse(out application.Response) response {
|
|
22
|
+
return response{ID: out.ID, CreatedAt: out.CreatedAt, Version: out.Version}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A lean module has no HTTP body endpoints yet. The marker gives
|
|
26
|
+
// `generate method` a stable place to add request DTOs when a POST is earned.
|
|
27
|
+
|
|
28
|
+
// go-scaffold:dto
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Package httpadapter is the Gin inbound adapter for {{pkg}}.
|
|
2
|
+
package httpadapter
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"errors"
|
|
6
|
+
"net/http"
|
|
7
|
+
|
|
8
|
+
"{{goModule}}/internal/app/{{modulePath}}/application"
|
|
9
|
+
"{{goModule}}/internal/app/{{modulePath}}/domain"
|
|
10
|
+
"{{goModule}}/internal/shared/apperror"
|
|
11
|
+
"{{goModule}}/internal/shared/httpx"
|
|
12
|
+
{{#if auth}} "{{goModule}}/internal/shared/middleware"
|
|
13
|
+
{{/if}}
|
|
14
|
+
"{{goModule}}/internal/shared/pagination"
|
|
15
|
+
|
|
16
|
+
"github.com/gin-gonic/gin"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
type Handler struct {
|
|
20
|
+
{{#if cqrs}}
|
|
21
|
+
commands application.CommandPort
|
|
22
|
+
queries application.QueryPort
|
|
23
|
+
{{else}}
|
|
24
|
+
svc application.ServicePort
|
|
25
|
+
{{/if}}
|
|
26
|
+
{{#if auth}}
|
|
27
|
+
jwtSecret string
|
|
28
|
+
{{/if}}
|
|
29
|
+
{{#if permission}}
|
|
30
|
+
authz *middleware.Authz
|
|
31
|
+
{{/if}}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
{{#if cqrs}}
|
|
35
|
+
func NewHandler(commands application.CommandPort, queries application.QueryPort{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *Handler {
|
|
36
|
+
h := &Handler{commands: commands, queries: queries}
|
|
37
|
+
{{#if auth}}
|
|
38
|
+
h.jwtSecret = jwtSecret
|
|
39
|
+
{{/if}}
|
|
40
|
+
{{#if permission}}
|
|
41
|
+
h.authz = authz
|
|
42
|
+
{{/if}}
|
|
43
|
+
return h
|
|
44
|
+
}
|
|
45
|
+
{{else}}
|
|
46
|
+
func NewHandler(svc application.ServicePort{{#if auth}}, jwtSecret string{{/if}}{{#if permission}}, authz *middleware.Authz{{/if}}) *Handler {
|
|
47
|
+
h := &Handler{svc: svc}
|
|
48
|
+
{{#if auth}}
|
|
49
|
+
h.jwtSecret = jwtSecret
|
|
50
|
+
{{/if}}
|
|
51
|
+
{{#if permission}}
|
|
52
|
+
h.authz = authz
|
|
53
|
+
{{/if}}
|
|
54
|
+
return h
|
|
55
|
+
}
|
|
56
|
+
{{/if}}
|
|
57
|
+
|
|
58
|
+
func (h *Handler) Register(rg gin.IRouter) {
|
|
59
|
+
g := rg.Group("/{{plural}}"{{#if auth}}, middleware.RequireAuth(h.jwtSecret){{/if}}{{#if permission}}, h.authz.Require("{{permission}}"){{/if}})
|
|
60
|
+
g.POST("", h.create)
|
|
61
|
+
g.GET("", h.list)
|
|
62
|
+
g.GET("/:id", h.get)
|
|
63
|
+
g.PUT("/:id", h.update)
|
|
64
|
+
g.DELETE("/:id", h.delete)
|
|
65
|
+
// go-scaffold:handler-routes
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func appError(err error) error {
|
|
69
|
+
switch {
|
|
70
|
+
case errors.Is(err, domain.ErrNotFound):
|
|
71
|
+
return applicationError(http.StatusNotFound, "{{errorPrefix}}_NOT_FOUND", "{{pkg}} not found")
|
|
72
|
+
case errors.Is(err, domain.ErrConflict):
|
|
73
|
+
return applicationError(http.StatusConflict, "{{errorPrefix}}_CONFLICT", "{{pkg}} already exists")
|
|
74
|
+
case errors.Is(err, domain.ErrStaleVersion):
|
|
75
|
+
return applicationError(http.StatusConflict, "{{errorPrefix}}_STALE", "{{pkg}} was modified by someone else — reload and try again")
|
|
76
|
+
case errors.Is(err, domain.ErrHasReferences):
|
|
77
|
+
return applicationError(http.StatusConflict, "{{errorPrefix}}_HAS_REFERENCES", "{{pkg}} still has related records")
|
|
78
|
+
case errors.Is(err, domain.ErrNotImplemented):
|
|
79
|
+
return applicationError(http.StatusNotImplemented, "NOT_IMPLEMENTED", "not implemented")
|
|
80
|
+
default:
|
|
81
|
+
return apperror.NewInternal(err)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func applicationError(status int, code, message string) *apperror.AppError {
|
|
86
|
+
// The HTTP adapter is the only layer allowed to create an HTTP error.
|
|
87
|
+
return apperror.New(status, code, message)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func (h *Handler) create(c *gin.Context) {
|
|
91
|
+
var in createInput
|
|
92
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
93
|
+
c.Error(httpx.BindErr(err))
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
{{#if cqrs}}
|
|
97
|
+
m, err := h.commands.Create(c.Request.Context(), toCreateInput(in))
|
|
98
|
+
{{else}}
|
|
99
|
+
m, err := h.svc.Create(c.Request.Context(), toCreateInput(in))
|
|
100
|
+
{{/if}}
|
|
101
|
+
if err != nil {
|
|
102
|
+
c.Error(appError(err))
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
c.JSON(http.StatusCreated, toResponse(application.ToResponse(m)))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func (h *Handler) list(c *gin.Context) {
|
|
109
|
+
p := pagination.Parse(c)
|
|
110
|
+
{{#if cqrs}}
|
|
111
|
+
items, err := h.queries.List(c.Request.Context(), p.Limit, p.Offset)
|
|
112
|
+
{{else}}
|
|
113
|
+
items, err := h.svc.List(c.Request.Context(), p.Limit, p.Offset)
|
|
114
|
+
{{/if}}
|
|
115
|
+
if err != nil {
|
|
116
|
+
c.Error(appError(err))
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
out := make([]response, len(items))
|
|
120
|
+
for i := range items {
|
|
121
|
+
out[i] = toResponse(application.ToResponse(&items[i]))
|
|
122
|
+
}
|
|
123
|
+
c.JSON(http.StatusOK, p.Response(out))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
func (h *Handler) get(c *gin.Context) {
|
|
127
|
+
id, ok := httpx.ParseID(c)
|
|
128
|
+
if !ok {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
{{#if cqrs}}
|
|
132
|
+
m, err := h.queries.Get(c.Request.Context(), id)
|
|
133
|
+
{{else}}
|
|
134
|
+
m, err := h.svc.Get(c.Request.Context(), id)
|
|
135
|
+
{{/if}}
|
|
136
|
+
if err != nil {
|
|
137
|
+
c.Error(appError(err))
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
c.JSON(http.StatusOK, toResponse(application.ToResponse(m)))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func (h *Handler) update(c *gin.Context) {
|
|
144
|
+
id, ok := httpx.ParseID(c)
|
|
145
|
+
if !ok {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
var in updateInput
|
|
149
|
+
if err := c.ShouldBindJSON(&in); err != nil {
|
|
150
|
+
c.Error(httpx.BindErr(err))
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
{{#if cqrs}}
|
|
154
|
+
m, err := h.commands.Update(c.Request.Context(), id, toUpdateInput(in))
|
|
155
|
+
{{else}}
|
|
156
|
+
m, err := h.svc.Update(c.Request.Context(), id, toUpdateInput(in))
|
|
157
|
+
{{/if}}
|
|
158
|
+
if err != nil {
|
|
159
|
+
c.Error(appError(err))
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
c.JSON(http.StatusOK, toResponse(application.ToResponse(m)))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
func (h *Handler) delete(c *gin.Context) {
|
|
166
|
+
id, ok := httpx.ParseID(c)
|
|
167
|
+
if !ok {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
{{#if cqrs}}
|
|
171
|
+
err := h.commands.Delete(c.Request.Context(), id)
|
|
172
|
+
{{else}}
|
|
173
|
+
err := h.svc.Delete(c.Request.Context(), id)
|
|
174
|
+
{{/if}}
|
|
175
|
+
if err != nil {
|
|
176
|
+
c.Error(appError(err))
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
c.Status(http.StatusNoContent)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// go-scaffold:handler-funcs
|