ga-plugins-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config-patcher.d.ts +20 -50
- package/dist/config-patcher.d.ts.map +1 -1
- package/dist/config-patcher.js +138 -102
- package/dist/config-patcher.js.map +1 -1
- package/dist/index.js +41 -5
- package/dist/index.js.map +1 -1
- package/dist/installer.d.ts +0 -18
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +19 -39
- package/dist/installer.js.map +1 -1
- package/dist/types.d.ts +10 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/uninstaller.d.ts +0 -23
- package/dist/uninstaller.d.ts.map +1 -1
- package/dist/uninstaller.js +22 -68
- package/dist/uninstaller.js.map +1 -1
- package/package.json +3 -2
- package/plugins/go-reviewer/.claude-plugin/plugin.json +12 -0
- package/plugins/go-reviewer/commands/go-review.md +424 -0
- package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/README.md +236 -0
- package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/main.go +678 -0
- package/plugins/go-scaffolder/.claude-plugin/plugin.json +12 -0
- package/plugins/go-scaffolder/commands/scaffold-service.md +802 -0
- package/plugins/go-scaffolder/reference-service/.env.example +27 -0
- package/plugins/go-scaffolder/reference-service/Dockerfile +55 -0
- package/plugins/go-scaffolder/reference-service/REFERENCE-SERVICE-NOTICE.md +104 -0
- package/plugins/go-scaffolder/reference-service/cmd/server/main.go +266 -0
- package/plugins/go-scaffolder/reference-service/config/config.go +67 -0
- package/plugins/go-scaffolder/reference-service/go.mod +17 -0
- package/plugins/go-scaffolder/reference-service/internal/domain/booking.go +118 -0
- package/plugins/go-scaffolder/reference-service/internal/handler/booking.go +242 -0
- package/plugins/go-scaffolder/reference-service/internal/handler/booking_test.go +451 -0
- package/plugins/go-scaffolder/reference-service/internal/repository/booking_postgres.go +124 -0
- package/plugins/go-scaffolder/reference-service/internal/usecase/booking.go +181 -0
- package/plugins/go-standards/.claude-plugin/plugin.json +22 -0
- package/plugins/go-standards/commands/go-standards-check.md +232 -0
- package/plugins/go-standards/skills/concurrency.md +336 -0
- package/plugins/go-standards/skills/config.md +267 -0
- package/plugins/go-standards/skills/error-handling.md +286 -0
- package/plugins/go-standards/skills/http-chi.md +390 -0
- package/plugins/go-standards/skills/logging-observability.md +340 -0
- package/plugins/go-standards/skills/naming-and-style.md +315 -0
- package/plugins/go-standards/skills/project-layout.md +313 -0
- package/plugins/go-standards/skills/testing.md +366 -0
- package/plugins/java2go-porter/.claude-plugin/plugin.json +21 -0
- package/plugins/java2go-porter/agents/analyzer.md +232 -0
- package/plugins/java2go-porter/agents/reviewer.md +241 -0
- package/plugins/java2go-porter/agents/test-pairer.md +365 -0
- package/plugins/java2go-porter/agents/translator.md +419 -0
- package/plugins/java2go-porter/commands/port-java-service.md +149 -0
- package/plugins/java2go-porter/skills/idiom-mapping.md +75 -0
- package/plugins/migration-safety/.claude-plugin/plugin.json +20 -0
- package/plugins/migration-safety/commands/gen-characterization-test.md +452 -0
- package/plugins/migration-safety/commands/strangler-plan.md +356 -0
- package/plugins/migration-safety/skills/strangler-fig.md +382 -0
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
# Command: scaffold-service
|
|
2
|
+
|
|
3
|
+
Generate a complete, production-ready thin Go microservice wired to go-ga-lib. The wizard asks exactly what dependencies the service needs and imports ONLY those packages — no unused imports, no dead code.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1: Interactive Wizard
|
|
8
|
+
|
|
9
|
+
Ask ALL questions in order before writing any files. Collect all answers first, then generate everything at once.
|
|
10
|
+
|
|
11
|
+
### Q1 — Service name
|
|
12
|
+
|
|
13
|
+
Ask:
|
|
14
|
+
|
|
15
|
+
> "Domain / service name? (e.g. booking, payment, notification)"
|
|
16
|
+
|
|
17
|
+
Record as `<service-name>`. This becomes:
|
|
18
|
+
- The output folder name: `<service-name>-service/`
|
|
19
|
+
- Part of the default module path: `github.com/zokypesch/<service-name>-service`
|
|
20
|
+
- Package names throughout the code
|
|
21
|
+
|
|
22
|
+
### Q2 — Service type
|
|
23
|
+
|
|
24
|
+
Ask:
|
|
25
|
+
|
|
26
|
+
> "Service type?
|
|
27
|
+
> (1) API server — HTTP only, no message consumption
|
|
28
|
+
> (2) Message consumer — consumes messages, no HTTP API (worker process)
|
|
29
|
+
> (3) Both — HTTP API + message consumption"
|
|
30
|
+
|
|
31
|
+
Record as `<service-type>`. Values: `api`, `consumer`, `both`.
|
|
32
|
+
|
|
33
|
+
### Q3 — SQL database
|
|
34
|
+
|
|
35
|
+
Ask:
|
|
36
|
+
|
|
37
|
+
> "SQL database?
|
|
38
|
+
> (1) MySQL 8.4
|
|
39
|
+
> (2) PostgreSQL 18.4
|
|
40
|
+
> (3) None"
|
|
41
|
+
|
|
42
|
+
Record as `<sql-choice>`. Values: `mysql`, `postgres`, `none`.
|
|
43
|
+
|
|
44
|
+
### Q4 — NoSQL stores
|
|
45
|
+
|
|
46
|
+
Ask:
|
|
47
|
+
|
|
48
|
+
> "NoSQL store(s)? Select all that apply (comma-separated numbers, e.g. 1,3):
|
|
49
|
+
> (1) Redis
|
|
50
|
+
> (2) Elasticsearch
|
|
51
|
+
> (3) MongoDB
|
|
52
|
+
> (4) Cassandra
|
|
53
|
+
> (5) ClickHouse
|
|
54
|
+
> (6) pgvector
|
|
55
|
+
> (7) Neo4j
|
|
56
|
+
> (8) None"
|
|
57
|
+
|
|
58
|
+
Record as `<nosql-choices>` (a list). If the user picks 8 or leaves blank, set to empty list.
|
|
59
|
+
|
|
60
|
+
### Q5 — Message broker (only if `<service-type>` is `consumer` or `both`)
|
|
61
|
+
|
|
62
|
+
If `<service-type>` is `api`, skip this question and set `<broker>` to `none`.
|
|
63
|
+
|
|
64
|
+
Otherwise ask:
|
|
65
|
+
|
|
66
|
+
> "Message broker?
|
|
67
|
+
> (1) Kafka
|
|
68
|
+
> (2) RabbitMQ
|
|
69
|
+
> (3) ActiveMQ
|
|
70
|
+
> (4) NSQ"
|
|
71
|
+
|
|
72
|
+
Record as `<broker>`. Values: `kafka`, `rabbitmq`, `activemq`, `nsq`.
|
|
73
|
+
|
|
74
|
+
### Q6 — HTTP port
|
|
75
|
+
|
|
76
|
+
Ask:
|
|
77
|
+
|
|
78
|
+
> "HTTP port? (press Enter for default: 8080)"
|
|
79
|
+
|
|
80
|
+
Record as `<http-port>`. Default: `8080`. Only used if `<service-type>` is `api` or `both`.
|
|
81
|
+
|
|
82
|
+
### Q7 — Go module path
|
|
83
|
+
|
|
84
|
+
Ask:
|
|
85
|
+
|
|
86
|
+
> "Go module path for this service? (press Enter for default: github.com/zokypesch/<service-name>-service)"
|
|
87
|
+
|
|
88
|
+
Record as `<module-path>`. Default: `github.com/zokypesch/<service-name>-service`.
|
|
89
|
+
|
|
90
|
+
### Q8 — go-ga-lib version
|
|
91
|
+
|
|
92
|
+
Ask:
|
|
93
|
+
|
|
94
|
+
> "go-ga-lib version to pin?
|
|
95
|
+
> Enter a git tag (e.g. v1.0.0) for a production service.
|
|
96
|
+
> Enter 'local' to use a replace directive pointing to the local go-ga-lib clone (D:/project/asyst/go-ga-lib).
|
|
97
|
+
> (default: local)"
|
|
98
|
+
|
|
99
|
+
Record as `<ga-lib-version>`. Default: `local`.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Step 2: Confirm Before Generating
|
|
104
|
+
|
|
105
|
+
Print a confirmation summary:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
Generating <service-name>-service with:
|
|
109
|
+
Module: <module-path>
|
|
110
|
+
Type: <service-type>
|
|
111
|
+
SQL: <sql-choice>
|
|
112
|
+
NoSQL: <nosql-choices or "none">
|
|
113
|
+
Broker: <broker or "n/a">
|
|
114
|
+
Port: <http-port or "n/a (consumer only)">
|
|
115
|
+
go-ga-lib: <ga-lib-version>
|
|
116
|
+
Output dir: <service-name>-service/
|
|
117
|
+
|
|
118
|
+
Proceed? (Y/n)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If the user says n or no, restart from Q1.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Step 3: Generate Files
|
|
126
|
+
|
|
127
|
+
Generate ALL files listed below. Apply the DEPENDENCY RULES table strictly — if a dependency was not chosen, its import does NOT appear anywhere in the generated code.
|
|
128
|
+
|
|
129
|
+
### DEPENDENCY RULES TABLE
|
|
130
|
+
|
|
131
|
+
| Dependency | Chosen when | go-ga-lib subpackage to import | Env vars generated |
|
|
132
|
+
|---|---|---|---|
|
|
133
|
+
| PostgreSQL | `<sql-choice>` == `postgres` | `github.com/zokypesch/go-ga-lib/sql` | DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME, DB_SSLMODE |
|
|
134
|
+
| MySQL | `<sql-choice>` == `mysql` | `github.com/zokypesch/go-ga-lib/sql/mysql` | DB_HOST, DB_PORT, DB_USER, DB_PASS, DB_NAME |
|
|
135
|
+
| Redis | `nosql-choices` contains `1` | `github.com/zokypesch/go-ga-lib/nosql/redis` | REDIS_ADDR, REDIS_PASSWORD, REDIS_DB |
|
|
136
|
+
| Elasticsearch | `nosql-choices` contains `2` | `github.com/zokypesch/go-ga-lib/nosql/elasticsearch` | ES_ADDRS, ES_USERNAME, ES_PASSWORD |
|
|
137
|
+
| MongoDB | `nosql-choices` contains `3` | `github.com/zokypesch/go-ga-lib/nosql/mongo` | MONGO_URI, MONGO_DATABASE |
|
|
138
|
+
| Cassandra | `nosql-choices` contains `4` | `github.com/zokypesch/go-ga-lib/nosql/cassandra` | CASSANDRA_HOSTS, CASSANDRA_KEYSPACE |
|
|
139
|
+
| ClickHouse | `nosql-choices` contains `5` | `github.com/zokypesch/go-ga-lib/nosql/clickhouse` | CLICKHOUSE_ADDR, CLICKHOUSE_DATABASE |
|
|
140
|
+
| pgvector | `nosql-choices` contains `6` | `github.com/zokypesch/go-ga-lib/nosql/pgvector` | PGVECTOR_HOST, PGVECTOR_PORT, PGVECTOR_USER, PGVECTOR_PASS, PGVECTOR_NAME |
|
|
141
|
+
| Neo4j | `nosql-choices` contains `7` | `github.com/zokypesch/go-ga-lib/nosql/neo4j` | NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD |
|
|
142
|
+
| Kafka | `<broker>` == `kafka` | `github.com/zokypesch/go-ga-lib/broker/kafka` | KAFKA_BROKERS, KAFKA_GROUP_ID, KAFKA_TOPIC |
|
|
143
|
+
| RabbitMQ | `<broker>` == `rabbitmq` | `github.com/zokypesch/go-ga-lib/broker/rabbitmq` | RABBITMQ_URL, RABBITMQ_QUEUE |
|
|
144
|
+
| ActiveMQ | `<broker>` == `activemq` | `github.com/zokypesch/go-ga-lib/broker/activemq` | ACTIVEMQ_URL, ACTIVEMQ_QUEUE |
|
|
145
|
+
| NSQ | `<broker>` == `nsq` | `github.com/zokypesch/go-ga-lib/broker/nsq` | NSQ_ADDR, NSQ_TOPIC, NSQ_CHANNEL |
|
|
146
|
+
|
|
147
|
+
Always import regardless of choices:
|
|
148
|
+
- `github.com/zokypesch/go-ga-lib/config` — config loading
|
|
149
|
+
- `github.com/zokypesch/go-ga-lib/observability` — OTel tracing + Prometheus + Loki
|
|
150
|
+
- `github.com/zokypesch/go-ga-lib/logger` — structured zap logger
|
|
151
|
+
|
|
152
|
+
Import only if `<service-type>` is `api` or `both`:
|
|
153
|
+
- `github.com/zokypesch/go-ga-lib/httpx` — chi router factory + JSON/error helpers
|
|
154
|
+
- `github.com/zokypesch/go-ga-lib/server` — HTTP server with graceful shutdown
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### File: `<service-name>-service/go.mod`
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
module <module-path>
|
|
162
|
+
|
|
163
|
+
go 1.23
|
|
164
|
+
|
|
165
|
+
require (
|
|
166
|
+
github.com/go-chi/chi/v5 v5.2.1
|
|
167
|
+
github.com/zokypesch/go-ga-lib <version-line>
|
|
168
|
+
go.uber.org/zap v1.27.0
|
|
169
|
+
github.com/google/uuid v1.6.0
|
|
170
|
+
github.com/jmoiron/sqlx v1.4.0
|
|
171
|
+
github.com/stretchr/testify v1.9.0
|
|
172
|
+
github.com/caarlos0/env/v11 v11.3.1
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`<version-line>` rules:
|
|
177
|
+
- If `<ga-lib-version>` == `local`: write `v0.0.0` AND add the replace directive below the require block:
|
|
178
|
+
```
|
|
179
|
+
replace github.com/zokypesch/go-ga-lib => D:/project/asyst/go-ga-lib
|
|
180
|
+
```
|
|
181
|
+
- If `<ga-lib-version>` is a tag (e.g. `v1.0.0`): write that tag as the version, NO replace directive.
|
|
182
|
+
|
|
183
|
+
Only include driver dependencies that match chosen dependencies:
|
|
184
|
+
- postgres: add `github.com/lib/pq v1.10.9`
|
|
185
|
+
- mysql: add `github.com/go-sql-driver/mysql v1.8.1`
|
|
186
|
+
- mongo: add `go.mongodb.org/mongo-driver v1.15.0`
|
|
187
|
+
- redis: add `github.com/redis/go-redis/v9 v9.5.1`
|
|
188
|
+
- kafka: add `github.com/IBM/sarama v1.43.2`
|
|
189
|
+
- rabbitmq: add `github.com/rabbitmq/amqp091-go v1.10.0`
|
|
190
|
+
- nsq: add `github.com/nsqio/go-nsq v1.1.0`
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### File: `<service-name>-service/config/config.go`
|
|
195
|
+
|
|
196
|
+
```go
|
|
197
|
+
package config
|
|
198
|
+
|
|
199
|
+
import (
|
|
200
|
+
"fmt"
|
|
201
|
+
"github.com/caarlos0/env/v11"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
// Config holds all configuration for the <service-name> service.
|
|
205
|
+
// All values come from environment variables — no config files at runtime.
|
|
206
|
+
type Config struct {
|
|
207
|
+
// Application
|
|
208
|
+
Port int `env:"PORT" envDefault:"<http-port>"`
|
|
209
|
+
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
|
|
210
|
+
ServiceName string `env:"SERVICE_NAME" envDefault:"<service-name>-service"`
|
|
211
|
+
ServiceVersion string `env:"SERVICE_VERSION" envDefault:"0.1.0"`
|
|
212
|
+
ShutdownTimeout int `env:"SHUTDOWN_TIMEOUT" envDefault:"10"` // seconds
|
|
213
|
+
|
|
214
|
+
// OpenTelemetry
|
|
215
|
+
OTLPEndpoint string `env:"OTLP_ENDPOINT" envDefault:"localhost:4317"`
|
|
216
|
+
|
|
217
|
+
// [ONLY include the blocks below that match chosen dependencies]
|
|
218
|
+
// PostgreSQL block — only if sql-choice == postgres
|
|
219
|
+
DBHost string `env:"DB_HOST" required:"true"`
|
|
220
|
+
DBPort string `env:"DB_PORT" envDefault:"5432"`
|
|
221
|
+
DBUser string `env:"DB_USER" required:"true"`
|
|
222
|
+
DBPass string `env:"DB_PASS" required:"true"`
|
|
223
|
+
DBName string `env:"DB_NAME" required:"true"`
|
|
224
|
+
DBSSLMode string `env:"DB_SSLMODE" envDefault:"disable"`
|
|
225
|
+
|
|
226
|
+
// Redis block — only if Redis chosen
|
|
227
|
+
RedisAddr string `env:"REDIS_ADDR" envDefault:"localhost:6379"`
|
|
228
|
+
RedisPassword string `env:"REDIS_PASSWORD" envDefault:""`
|
|
229
|
+
RedisDB int `env:"REDIS_DB" envDefault:"0"`
|
|
230
|
+
|
|
231
|
+
// [Continue pattern for each chosen dependency per DEPENDENCY RULES TABLE]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Load parses environment variables into Config.
|
|
235
|
+
// Returns an error if any required variable is missing or invalid.
|
|
236
|
+
func Load() (*Config, error) {
|
|
237
|
+
cfg := &Config{}
|
|
238
|
+
if err := env.Parse(cfg); err != nil {
|
|
239
|
+
return nil, fmt.Errorf("loading config: %w", err)
|
|
240
|
+
}
|
|
241
|
+
return cfg, nil
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// DSN builds the PostgreSQL connection string. [Only include if postgres chosen]
|
|
245
|
+
// Never log the output of this method.
|
|
246
|
+
func (c *Config) DSN() string {
|
|
247
|
+
return fmt.Sprintf(
|
|
248
|
+
"host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
|
|
249
|
+
c.DBHost, c.DBPort, c.DBName, c.DBUser, c.DBPass, c.DBSSLMode,
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// String returns a safe representation with secrets redacted.
|
|
254
|
+
func (c *Config) String() string {
|
|
255
|
+
return fmt.Sprintf(
|
|
256
|
+
"Config{Port:%d ServiceName:%s ServiceVersion:%s LogLevel:%s}",
|
|
257
|
+
c.Port, c.ServiceName, c.ServiceVersion, c.LogLevel,
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
RULE: Only generate config fields for chosen dependencies. If SQL is None, no DB_ fields. If no NoSQL chosen, no NoSQL fields. If no broker, no broker fields.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### File: `<service-name>-service/internal/domain/<service-name>.go`
|
|
267
|
+
|
|
268
|
+
Generate the domain layer:
|
|
269
|
+
|
|
270
|
+
```go
|
|
271
|
+
package domain
|
|
272
|
+
|
|
273
|
+
import (
|
|
274
|
+
"context"
|
|
275
|
+
"errors"
|
|
276
|
+
"time"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
// Sentinel errors — map these to HTTP status codes in the handler layer.
|
|
280
|
+
var (
|
|
281
|
+
ErrNotFound = errors.New("<service-name> not found")
|
|
282
|
+
ErrAlreadyExists = errors.New("<service-name> already exists")
|
|
283
|
+
ErrInvalidInput = errors.New("invalid input")
|
|
284
|
+
ErrUnauthorized = errors.New("unauthorized")
|
|
285
|
+
ErrForbidden = errors.New("forbidden")
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// <ServiceName> is the core domain entity.
|
|
289
|
+
type <ServiceName> struct {
|
|
290
|
+
ID string `db:"id" json:"id"`
|
|
291
|
+
// Add 3-5 realistic fields for the domain (e.g. for booking: CustomerID, FlightID, Status)
|
|
292
|
+
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
|
293
|
+
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// <ServiceName>Repository defines the data access contract.
|
|
297
|
+
type <ServiceName>Repository interface {
|
|
298
|
+
Create(ctx context.Context, entity *<ServiceName>) error
|
|
299
|
+
GetByID(ctx context.Context, id string) (*<ServiceName>, error)
|
|
300
|
+
Update(ctx context.Context, entity *<ServiceName>) error
|
|
301
|
+
List(ctx context.Context, limit, offset int) ([]*<ServiceName>, error)
|
|
302
|
+
Delete(ctx context.Context, id string) error
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// <ServiceName>Usecase defines the business logic contract.
|
|
306
|
+
type <ServiceName>Usecase interface {
|
|
307
|
+
Create(ctx context.Context, input Create<ServiceName>Input) (*<ServiceName>, error)
|
|
308
|
+
GetByID(ctx context.Context, id string) (*<ServiceName>, error)
|
|
309
|
+
Update(ctx context.Context, id string, input Update<ServiceName>Input) (*<ServiceName>, error)
|
|
310
|
+
List(ctx context.Context, limit, offset int) ([]*<ServiceName>, error)
|
|
311
|
+
Delete(ctx context.Context, id string) error
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Create<ServiceName>Input is the payload for creating a new entity.
|
|
315
|
+
type Create<ServiceName>Input struct {
|
|
316
|
+
// Fields derived from <service-name> domain
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Update<ServiceName>Input is the payload for updating an existing entity.
|
|
320
|
+
type Update<ServiceName>Input struct {
|
|
321
|
+
// Fields derived from <service-name> domain (all optional via pointers)
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### File: `<service-name>-service/internal/repository/<service-name>_<sql-engine>.go`
|
|
328
|
+
|
|
329
|
+
[Only generate if `<sql-choice>` != `none`]
|
|
330
|
+
|
|
331
|
+
```go
|
|
332
|
+
package repository
|
|
333
|
+
|
|
334
|
+
import (
|
|
335
|
+
"context"
|
|
336
|
+
"database/sql"
|
|
337
|
+
"fmt"
|
|
338
|
+
|
|
339
|
+
"github.com/jmoiron/sqlx"
|
|
340
|
+
"<module-path>/internal/domain"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
type <sqlEngine><ServiceName>Repo struct {
|
|
344
|
+
db *sqlx.DB
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
func New<SQLEngine><ServiceName>Repo(db *sqlx.DB) domain.<ServiceName>Repository {
|
|
348
|
+
return &<sqlEngine><ServiceName>Repo{db: db}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
func (r *<sqlEngine><ServiceName>Repo) Create(ctx context.Context, entity *domain.<ServiceName>) error {
|
|
352
|
+
query := `INSERT INTO <service-name>s (id, ..., created_at, updated_at)
|
|
353
|
+
VALUES (:id, ..., :created_at, :updated_at)`
|
|
354
|
+
_, err := r.db.NamedExecContext(ctx, query, entity)
|
|
355
|
+
if err != nil {
|
|
356
|
+
return fmt.Errorf("repository.Create: %w", err)
|
|
357
|
+
}
|
|
358
|
+
return nil
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
func (r *<sqlEngine><ServiceName>Repo) GetByID(ctx context.Context, id string) (*domain.<ServiceName>, error) {
|
|
362
|
+
entity := &domain.<ServiceName>{}
|
|
363
|
+
err := r.db.GetContext(ctx, entity, `SELECT * FROM <service-name>s WHERE id = $1`, id)
|
|
364
|
+
if err != nil {
|
|
365
|
+
if err == sql.ErrNoRows {
|
|
366
|
+
return nil, domain.ErrNotFound
|
|
367
|
+
}
|
|
368
|
+
return nil, fmt.Errorf("repository.GetByID: %w", err)
|
|
369
|
+
}
|
|
370
|
+
return entity, nil
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// [Implement Update, List, Delete following the same pattern]
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
If `<sql-choice>` is `mysql`, use `?` placeholders. If `postgres`, use `$1`, `$2`, etc.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### File: `<service-name>-service/internal/usecase/<service-name>.go`
|
|
381
|
+
|
|
382
|
+
```go
|
|
383
|
+
package usecase
|
|
384
|
+
|
|
385
|
+
import (
|
|
386
|
+
"context"
|
|
387
|
+
"fmt"
|
|
388
|
+
"time"
|
|
389
|
+
|
|
390
|
+
"github.com/google/uuid"
|
|
391
|
+
"go.uber.org/zap"
|
|
392
|
+
|
|
393
|
+
"<module-path>/internal/domain"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
type <service-name>Usecase struct {
|
|
397
|
+
repo domain.<ServiceName>Repository
|
|
398
|
+
logger *zap.Logger
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
func New<ServiceName>Usecase(repo domain.<ServiceName>Repository, logger *zap.Logger) domain.<ServiceName>Usecase {
|
|
402
|
+
return &<service-name>Usecase{repo: repo, logger: logger}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func (uc *<service-name>Usecase) Create(ctx context.Context, input domain.Create<ServiceName>Input) (*domain.<ServiceName>, error) {
|
|
406
|
+
// Validate input
|
|
407
|
+
// [add real validation here]
|
|
408
|
+
|
|
409
|
+
entity := &domain.<ServiceName>{
|
|
410
|
+
ID: uuid.NewString(),
|
|
411
|
+
CreatedAt: time.Now().UTC(),
|
|
412
|
+
UpdatedAt: time.Now().UTC(),
|
|
413
|
+
// map input fields
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if err := uc.repo.Create(ctx, entity); err != nil {
|
|
417
|
+
return nil, fmt.Errorf("usecase.Create: %w", err)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
uc.logger.Info("created <service-name>", zap.String("id", entity.ID))
|
|
421
|
+
return entity, nil
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// [Implement GetByID, Update, List, Delete with proper error wrapping and logging]
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
### File: `<service-name>-service/internal/handler/<service-name>.go`
|
|
430
|
+
|
|
431
|
+
[Only generate if `<service-type>` is `api` or `both`]
|
|
432
|
+
|
|
433
|
+
```go
|
|
434
|
+
package handler
|
|
435
|
+
|
|
436
|
+
import (
|
|
437
|
+
"encoding/json"
|
|
438
|
+
"errors"
|
|
439
|
+
"io"
|
|
440
|
+
"net/http"
|
|
441
|
+
"strconv"
|
|
442
|
+
|
|
443
|
+
"github.com/go-chi/chi/v5"
|
|
444
|
+
"github.com/go-chi/chi/v5/middleware"
|
|
445
|
+
"go.uber.org/zap"
|
|
446
|
+
|
|
447
|
+
"<module-path>/internal/domain"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
const maxBodyBytes = 1 << 20 // 1 MB
|
|
451
|
+
|
|
452
|
+
type <ServiceName>Handler struct {
|
|
453
|
+
uc domain.<ServiceName>Usecase
|
|
454
|
+
logger *zap.Logger
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
func New<ServiceName>Handler(uc domain.<ServiceName>Usecase, logger *zap.Logger) *<ServiceName>Handler {
|
|
458
|
+
return &<ServiceName>Handler{uc: uc, logger: logger}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Routes mounts all <service-name> routes on the provided chi.Router.
|
|
462
|
+
func (h *<ServiceName>Handler) Routes(r chi.Router) {
|
|
463
|
+
r.Get("/", h.List)
|
|
464
|
+
r.Post("/", h.Create)
|
|
465
|
+
r.Get("/{id}", h.GetByID)
|
|
466
|
+
r.Put("/{id}", h.Update)
|
|
467
|
+
r.Delete("/{id}", h.Delete)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
func (h *<ServiceName>Handler) Create(w http.ResponseWriter, r *http.Request) {
|
|
471
|
+
var input domain.Create<ServiceName>Input
|
|
472
|
+
if err := decodeJSON(r, &input); err != nil {
|
|
473
|
+
respondErrorStatus(w, http.StatusBadRequest, err.Error(), "BAD_REQUEST")
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
result, err := h.uc.Create(r.Context(), input)
|
|
477
|
+
if err != nil {
|
|
478
|
+
h.logger.Error("create <service-name>", zap.Error(err),
|
|
479
|
+
zap.String("request_id", middleware.GetReqID(r.Context())))
|
|
480
|
+
respondError(w, err)
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
respondJSON(w, http.StatusCreated, result)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// [Implement GetByID, Update, List, Delete following the same thin-handler pattern]
|
|
487
|
+
|
|
488
|
+
// --- helpers ---
|
|
489
|
+
|
|
490
|
+
func decodeJSON(r *http.Request, dst any) error {
|
|
491
|
+
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
|
|
492
|
+
dec := json.NewDecoder(r.Body)
|
|
493
|
+
dec.DisallowUnknownFields()
|
|
494
|
+
if err := dec.Decode(dst); err != nil {
|
|
495
|
+
if errors.Is(err, io.EOF) {
|
|
496
|
+
return errors.New("request body must not be empty")
|
|
497
|
+
}
|
|
498
|
+
return fmt.Errorf("decoding request: %w", err)
|
|
499
|
+
}
|
|
500
|
+
return nil
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
type errorBody struct {
|
|
504
|
+
Error string `json:"error"`
|
|
505
|
+
Code string `json:"code"`
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
func respondJSON(w http.ResponseWriter, status int, data any) {
|
|
509
|
+
w.Header().Set("Content-Type", "application/json")
|
|
510
|
+
w.WriteHeader(status)
|
|
511
|
+
if data != nil {
|
|
512
|
+
_ = json.NewEncoder(w).Encode(data)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
func respondError(w http.ResponseWriter, err error) {
|
|
517
|
+
switch {
|
|
518
|
+
case errors.Is(err, domain.ErrNotFound):
|
|
519
|
+
respondErrorStatus(w, http.StatusNotFound, err.Error(), "NOT_FOUND")
|
|
520
|
+
case errors.Is(err, domain.ErrAlreadyExists):
|
|
521
|
+
respondErrorStatus(w, http.StatusConflict, err.Error(), "CONFLICT")
|
|
522
|
+
case errors.Is(err, domain.ErrInvalidInput):
|
|
523
|
+
respondErrorStatus(w, http.StatusBadRequest, err.Error(), "BAD_REQUEST")
|
|
524
|
+
case errors.Is(err, domain.ErrUnauthorized):
|
|
525
|
+
respondErrorStatus(w, http.StatusUnauthorized, err.Error(), "UNAUTHORIZED")
|
|
526
|
+
case errors.Is(err, domain.ErrForbidden):
|
|
527
|
+
respondErrorStatus(w, http.StatusForbidden, err.Error(), "FORBIDDEN")
|
|
528
|
+
default:
|
|
529
|
+
respondErrorStatus(w, http.StatusInternalServerError, "internal server error", "INTERNAL")
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
func respondErrorStatus(w http.ResponseWriter, status int, message, code string) {
|
|
534
|
+
w.Header().Set("Content-Type", "application/json")
|
|
535
|
+
w.WriteHeader(status)
|
|
536
|
+
_ = json.NewEncoder(w).Encode(errorBody{Error: message, Code: code})
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
### File: `<service-name>-service/internal/consumer/<service-name>_consumer.go`
|
|
543
|
+
|
|
544
|
+
[Only generate if `<service-type>` is `consumer` or `both`]
|
|
545
|
+
|
|
546
|
+
Generate a consumer struct that:
|
|
547
|
+
- Imports ONLY the chosen broker subpackage from go-ga-lib
|
|
548
|
+
- Has a `Start(ctx context.Context) error` method
|
|
549
|
+
- Has a `processMessage(ctx context.Context, msg []byte) error` method with example logic
|
|
550
|
+
- Stops cleanly when the context is cancelled
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
### File: `<service-name>-service/cmd/server/main.go`
|
|
555
|
+
|
|
556
|
+
```go
|
|
557
|
+
package main
|
|
558
|
+
|
|
559
|
+
import (
|
|
560
|
+
"context"
|
|
561
|
+
"fmt"
|
|
562
|
+
"log"
|
|
563
|
+
"net/http"
|
|
564
|
+
"os"
|
|
565
|
+
"os/signal"
|
|
566
|
+
"syscall"
|
|
567
|
+
"time"
|
|
568
|
+
|
|
569
|
+
"github.com/go-chi/chi/v5"
|
|
570
|
+
"github.com/go-chi/chi/v5/middleware"
|
|
571
|
+
"go.uber.org/zap"
|
|
572
|
+
|
|
573
|
+
"<module-path>/config"
|
|
574
|
+
"<module-path>/internal/domain"
|
|
575
|
+
"<module-path>/internal/handler" // [only if api or both]
|
|
576
|
+
"<module-path>/internal/repository" // [only if sql != none]
|
|
577
|
+
"<module-path>/internal/usecase"
|
|
578
|
+
// [Add chosen go-ga-lib subpackage imports per DEPENDENCY RULES TABLE]
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
func main() {
|
|
582
|
+
// 1. Load config — fail fast if required vars are missing
|
|
583
|
+
cfg, err := config.Load()
|
|
584
|
+
if err != nil {
|
|
585
|
+
log.Fatalf("loading config: %v", err)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// 2. Initialize logger
|
|
589
|
+
// [call go-ga-lib/logger.New with cfg.LogLevel, cfg.ServiceName, cfg.ServiceVersion]
|
|
590
|
+
|
|
591
|
+
// 3. Initialize observability (OTel tracing + Prometheus + Loki)
|
|
592
|
+
// [call go-ga-lib/observability.Init with cfg.OTLPEndpoint, cfg.ServiceName, cfg.ServiceVersion]
|
|
593
|
+
// defer shutdown
|
|
594
|
+
|
|
595
|
+
// 4. Initialize chosen dependencies (only include blocks for chosen deps):
|
|
596
|
+
|
|
597
|
+
// [SQL block — only if sql != none]
|
|
598
|
+
// db, err := <go-ga-lib/sql>.Open(cfg.DSN())
|
|
599
|
+
// if err != nil { logger.Fatal("opening db", zap.Error(err)) }
|
|
600
|
+
// defer db.Close()
|
|
601
|
+
|
|
602
|
+
// [Redis block — only if Redis chosen]
|
|
603
|
+
// rdb := <go-ga-lib/nosql/redis>.NewClient(...)
|
|
604
|
+
// defer rdb.Close()
|
|
605
|
+
|
|
606
|
+
// [Broker block — only if consumer or both]
|
|
607
|
+
// brokerClient := <go-ga-lib/broker/<broker>>.NewClient(...)
|
|
608
|
+
// defer brokerClient.Close()
|
|
609
|
+
|
|
610
|
+
// 5. Wire layers
|
|
611
|
+
// repo := repository.New<SQLEngine><ServiceName>Repo(db) // [only if sql != none]
|
|
612
|
+
// uc := usecase.New<ServiceName>Usecase(repo, logger)
|
|
613
|
+
|
|
614
|
+
// 6. Start consumer (only if consumer or both)
|
|
615
|
+
// consumer := consumer.New<ServiceName>Consumer(brokerClient, uc, logger)
|
|
616
|
+
// go consumer.Start(ctx)
|
|
617
|
+
|
|
618
|
+
// 7. Start HTTP server (only if api or both)
|
|
619
|
+
r := chi.NewRouter()
|
|
620
|
+
r.Use(middleware.RequestID)
|
|
621
|
+
r.Use(middleware.RealIP)
|
|
622
|
+
r.Use(middleware.Recoverer)
|
|
623
|
+
|
|
624
|
+
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
625
|
+
w.Header().Set("Content-Type", "application/json")
|
|
626
|
+
w.WriteHeader(http.StatusOK)
|
|
627
|
+
fmt.Fprint(w, `{"status":"ok"}`)
|
|
628
|
+
})
|
|
629
|
+
r.Get("/ready", func(w http.ResponseWriter, r *http.Request) {
|
|
630
|
+
// [ping each dependency; return 503 if any fail]
|
|
631
|
+
w.Header().Set("Content-Type", "application/json")
|
|
632
|
+
w.WriteHeader(http.StatusOK)
|
|
633
|
+
fmt.Fprint(w, `{"status":"ready"}`)
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
r.Route("/api/v1", func(r chi.Router) {
|
|
637
|
+
<service-name>Handler := handler.New<ServiceName>Handler(uc, logger)
|
|
638
|
+
r.Route("/<service-name>s", func(r chi.Router) {
|
|
639
|
+
<service-name>Handler.Routes(r)
|
|
640
|
+
})
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
srv := &http.Server{
|
|
644
|
+
Addr: fmt.Sprintf(":%d", cfg.Port),
|
|
645
|
+
Handler: r,
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// 8. Graceful shutdown
|
|
649
|
+
quit := make(chan os.Signal, 1)
|
|
650
|
+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
651
|
+
|
|
652
|
+
go func() {
|
|
653
|
+
logger.Info("server starting", zap.Int("port", cfg.Port))
|
|
654
|
+
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
655
|
+
logger.Fatal("server failed", zap.Error(err))
|
|
656
|
+
}
|
|
657
|
+
}()
|
|
658
|
+
|
|
659
|
+
<-quit
|
|
660
|
+
logger.Info("shutting down")
|
|
661
|
+
|
|
662
|
+
shutdownCtx, cancel := context.WithTimeout(context.Background(),
|
|
663
|
+
time.Duration(cfg.ShutdownTimeout)*time.Second)
|
|
664
|
+
defer cancel()
|
|
665
|
+
|
|
666
|
+
if err := srv.Shutdown(shutdownCtx); err != nil {
|
|
667
|
+
logger.Error("shutdown error", zap.Error(err))
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// [close each initialized dependency in reverse init order]
|
|
671
|
+
logger.Info("server stopped")
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
### File: `<service-name>-service/Dockerfile`
|
|
678
|
+
|
|
679
|
+
Multi-stage Dockerfile. Stage 1 builds, stage 2 is distroless minimal runtime.
|
|
680
|
+
|
|
681
|
+
```dockerfile
|
|
682
|
+
# syntax=docker/dockerfile:1
|
|
683
|
+
|
|
684
|
+
# ---- Stage 1: Build ----
|
|
685
|
+
FROM golang:1.23-alpine AS builder
|
|
686
|
+
|
|
687
|
+
RUN apk add --no-cache git ca-certificates tzdata
|
|
688
|
+
|
|
689
|
+
WORKDIR /app
|
|
690
|
+
|
|
691
|
+
COPY go.mod go.sum ./
|
|
692
|
+
RUN go mod download
|
|
693
|
+
|
|
694
|
+
COPY . .
|
|
695
|
+
|
|
696
|
+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
|
697
|
+
go build -trimpath -ldflags="-s -w" \
|
|
698
|
+
-o /app/server ./cmd/server
|
|
699
|
+
|
|
700
|
+
# ---- Stage 2: Runtime ----
|
|
701
|
+
FROM gcr.io/distroless/static:nonroot
|
|
702
|
+
|
|
703
|
+
COPY --from=builder /app/server /server
|
|
704
|
+
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
705
|
+
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
|
706
|
+
|
|
707
|
+
USER nonroot:nonroot
|
|
708
|
+
|
|
709
|
+
EXPOSE <http-port>
|
|
710
|
+
|
|
711
|
+
ENTRYPOINT ["/server"]
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
### File: `<service-name>-service/.env.example`
|
|
717
|
+
|
|
718
|
+
Generate only the env vars relevant to chosen dependencies. Always include the core app/OTel block.
|
|
719
|
+
|
|
720
|
+
```bash
|
|
721
|
+
# Copy to .env for local development. Never commit .env.
|
|
722
|
+
|
|
723
|
+
# --- Application ---
|
|
724
|
+
PORT=<http-port>
|
|
725
|
+
LOG_LEVEL=info # debug | info | warn | error
|
|
726
|
+
SERVICE_NAME=<service-name>-service
|
|
727
|
+
SERVICE_VERSION=0.1.0
|
|
728
|
+
SHUTDOWN_TIMEOUT=10 # seconds to wait for in-flight requests on SIGTERM
|
|
729
|
+
|
|
730
|
+
# --- OpenTelemetry (Grafana Tempo) ---
|
|
731
|
+
OTLP_ENDPOINT=localhost:4317
|
|
732
|
+
|
|
733
|
+
# [Only include sections below for chosen dependencies]
|
|
734
|
+
|
|
735
|
+
# --- PostgreSQL ---
|
|
736
|
+
DB_HOST=localhost
|
|
737
|
+
DB_PORT=5432
|
|
738
|
+
DB_USER=<service-name>_user
|
|
739
|
+
DB_PASS=changeme
|
|
740
|
+
DB_NAME=<service-name>_db
|
|
741
|
+
DB_SSLMODE=disable # use 'require' in production
|
|
742
|
+
|
|
743
|
+
# --- Redis ---
|
|
744
|
+
REDIS_ADDR=localhost:6379
|
|
745
|
+
REDIS_PASSWORD= # leave empty for no-auth local dev
|
|
746
|
+
REDIS_DB=0
|
|
747
|
+
|
|
748
|
+
# [Continue pattern for each chosen dependency]
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## Step 4: Post-generation Summary
|
|
754
|
+
|
|
755
|
+
After writing all files, print:
|
|
756
|
+
|
|
757
|
+
```
|
|
758
|
+
Generated: <service-name>-service/
|
|
759
|
+
|
|
760
|
+
go.mod (module <module-path>)
|
|
761
|
+
config/config.go (env config)
|
|
762
|
+
internal/domain/<service-name>.go (interfaces + structs)
|
|
763
|
+
internal/repository/<service-name>_<db>.go (if sql chosen)
|
|
764
|
+
internal/usecase/<service-name>.go (business logic)
|
|
765
|
+
internal/handler/<service-name>.go (HTTP handlers, if api or both)
|
|
766
|
+
internal/consumer/<service-name>_consumer.go (if consumer or both)
|
|
767
|
+
cmd/server/main.go (entry point)
|
|
768
|
+
Dockerfile
|
|
769
|
+
.env.example
|
|
770
|
+
|
|
771
|
+
Dependencies wired:
|
|
772
|
+
SQL: <sql-choice>
|
|
773
|
+
NoSQL: <nosql-choices or "none">
|
|
774
|
+
Broker: <broker or "n/a">
|
|
775
|
+
|
|
776
|
+
Next steps:
|
|
777
|
+
1. Copy .env.example to .env and fill in real values
|
|
778
|
+
2. Run: go mod tidy
|
|
779
|
+
3. Run: go build ./...
|
|
780
|
+
4. See reference-service/ for a complete working exemplar
|
|
781
|
+
|
|
782
|
+
NOTE: If you used 'local' for go-ga-lib, the go.mod contains a replace directive.
|
|
783
|
+
Switch to a pinned version before committing to the shared repository:
|
|
784
|
+
1. Remove the replace directive
|
|
785
|
+
2. Set: require github.com/zokypesch/go-ga-lib v<tag>
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
---
|
|
789
|
+
|
|
790
|
+
## Constraints (strictly enforced)
|
|
791
|
+
|
|
792
|
+
- NEVER import a go-ga-lib subpackage that was not chosen.
|
|
793
|
+
- NEVER add a Config field for an env var that was not chosen.
|
|
794
|
+
- NEVER add a `.env.example` line for an unchosen dependency.
|
|
795
|
+
- NEVER generate a consumer file if `<service-type>` is `api`.
|
|
796
|
+
- NEVER generate an HTTP handler if `<service-type>` is `consumer`.
|
|
797
|
+
- The `replace` directive only appears in `go.mod` when `<ga-lib-version>` == `local`. Production services MUST NOT have a replace directive.
|
|
798
|
+
- All generated Go files must compile (no placeholder syntax errors, no missing imports).
|
|
799
|
+
- Use `context.Context` as the first argument of every repository and usecase method.
|
|
800
|
+
- All repository methods must map `sql.ErrNoRows` to `domain.ErrNotFound`.
|
|
801
|
+
- All usecase methods must wrap errors with `fmt.Errorf("usecase.Method: %w", err)`.
|
|
802
|
+
- Dockerfile stage 2 must use `gcr.io/distroless/static:nonroot` — never `alpine` or `ubuntu` in the runtime stage.
|