gorig-cli 1.0.24 → 1.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +112 -22
  2. package/commands/create.js +267 -112
  3. package/commands/doc.js +21 -8
  4. package/commands/init.js +316 -255
  5. package/commands/skill.js +55 -17
  6. package/package.json +1 -1
  7. package/templates/config.go.ejs +6 -5
  8. package/templates/config.yaml.ejs +3 -38
  9. package/templates/controller.go.ejs +11 -42
  10. package/templates/crud.controller.go.ejs +71 -0
  11. package/templates/crud.doc.md.ejs +60 -0
  12. package/templates/crud.dto.go.ejs +103 -0
  13. package/templates/crud.integration.init_test.go.ejs +17 -0
  14. package/templates/crud.integration_test.go.ejs +66 -0
  15. package/templates/crud.model.go.ejs +25 -0
  16. package/templates/crud.module.README.md.ejs +70 -0
  17. package/templates/crud.mongo.config.yaml.ejs +19 -0
  18. package/templates/crud.mysql.config.yaml.ejs +14 -0
  19. package/templates/crud.router.go.ejs +18 -0
  20. package/templates/crud.service.go.ejs +179 -0
  21. package/templates/crud.test.go.ejs +52 -0
  22. package/templates/dto.go.ejs +16 -0
  23. package/templates/gitignore.ejs +18 -49
  24. package/templates/hello.controller.go.ejs +19 -0
  25. package/templates/hello.router.go.ejs +12 -0
  26. package/templates/hello.service.go.ejs +31 -0
  27. package/templates/hello.test.go.ejs +34 -0
  28. package/templates/init.go.ejs +1 -27
  29. package/templates/main.go.ejs +2 -5
  30. package/templates/model.go.ejs +3 -4
  31. package/templates/project.README.md.ejs +35 -0
  32. package/templates/router.go.ejs +4 -8
  33. package/templates/service.go.ejs +25 -77
  34. package/templates/skills/gorig-backend/SKILL.md +89 -0
  35. package/templates/skills/gorig-backend/agents/openai.yaml +4 -0
  36. package/templates/skills/gorig-backend/assets/api-doc-template.md +59 -0
  37. package/templates/skills/gorig-backend/assets/gorig.gitignore +65 -0
  38. package/templates/skills/gorig-backend/assets/module-readme-template.md +58 -0
  39. package/templates/skills/gorig-backend/references/advanced-data-access.md +275 -0
  40. package/templates/skills/gorig-backend/references/auth-security.md +194 -0
  41. package/templates/skills/gorig-backend/references/business-scenarios.md +155 -0
  42. package/templates/skills/gorig-backend/references/cache.md +301 -0
  43. package/templates/skills/gorig-backend/references/capability-matrix.md +37 -0
  44. package/templates/skills/gorig-backend/references/configuration.md +48 -0
  45. package/templates/skills/gorig-backend/references/framework-api.md +190 -0
  46. package/templates/skills/gorig-backend/references/messaging.md +143 -0
  47. package/templates/skills/gorig-backend/references/onboarding-files.md +46 -0
  48. package/templates/skills/gorig-backend/references/outbound-http.md +162 -0
  49. package/templates/skills/gorig-backend/references/persistent-crud.md +332 -0
  50. package/templates/skills/gorig-backend/references/project-bootstrap.md +128 -0
  51. package/templates/skills/gorig-backend/references/scheduled-tasks.md +231 -0
  52. package/templates/skills/gorig-backend/references/service-lifecycle.md +51 -0
  53. package/templates/skills/gorig-backend/references/source-map.md +43 -0
  54. package/templates/skills/gorig-backend/references/source-policy.md +58 -0
  55. package/templates/skills/gorig-backend/references/sse.md +121 -0
  56. package/templates/skills/gorig-backend/references/testing.md +171 -0
  57. package/templates/skills/gorig-backend/scripts/check-source-links.sh +48 -0
  58. package/templates/skills/gorig-backend/scripts/detect-gorig-context.sh +67 -0
  59. package/templates/skills/gorig-backend/scripts/verify-basic-project.sh +108 -0
  60. package/npm_publish copy.sh +0 -65
  61. package/templates/internal.go.ejs +0 -49
  62. package/templates/req.go.ejs +0 -8
  63. package/templates/resp.go.ejs +0 -8
  64. package/templates/service.pub.go.ejs +0 -22
  65. package/templates/skills/claude/gorig-backend/SKILL.md +0 -766
  66. package/templates/skills/claude/gorig-backend/assets/api-doc-template.md +0 -50
  67. package/templates/skills/claude/gorig-backend/assets/module-readme-template.md +0 -46
  68. package/templates/skills/claude/gorig-backend/references/onboarding-files.md +0 -44
  69. package/templates/skills/codex/gorig-backend/SKILL.md +0 -766
  70. package/templates/skills/codex/gorig-backend/agents/openai.yaml +0 -4
  71. package/templates/skills/codex/gorig-backend/assets/api-doc-template.md +0 -50
  72. package/templates/skills/codex/gorig-backend/assets/module-readme-template.md +0 -46
  73. package/templates/skills/codex/gorig-backend/references/onboarding-files.md +0 -44
@@ -1,766 +0,0 @@
1
- ---
2
- name: gorig-backend
3
- description: Senior Go backend development expert for the gorig framework. Use when tasks involve implementing or reviewing gorig services, routers, controllers, services, models, domain modules, dx-based data access, cache, cronx scheduled tasks, messagex pub/sub, SSE streaming, API contract changes, test additions, or module documentation updates. Apply this skill when strict adherence to local gorig/news code patterns and real API behavior is required.
4
- ---
5
-
6
- # Gorig Backend
7
-
8
- Implement backend work in gorig with strict framework conventions, deterministic workflow, and mandatory confirmation before coding.
9
-
10
- ## Load Context First
11
- Read `~/.codex/skills/gorig-backend/references/onboarding-files.md`.
12
- Build a concrete mental model from local code before proposing changes.
13
- Treat code behavior as the source of truth for all framework APIs and helper methods.
14
- State uncertainty explicitly and ask focused questions instead of guessing.
15
-
16
- ## Confirm Change Plan Before Coding
17
- Provide a concise plan and wait for explicit user confirmation before editing code.
18
- Include:
19
- - Business objective and boundary.
20
- - Data structures and persistence impact.
21
- - Route list with request/response and error code shape.
22
- - File-level change list in `Router -> Controller -> Service -> Model/Domainx`.
23
- - Test and documentation update plan.
24
-
25
- Do not start implementation until confirmation is received.
26
-
27
- ## Implement With Framework Rules
28
- Follow these hard constraints:
29
- - Register services with `serv.RegisterService`; start with `bootstrap.StartUp()`.
30
- - Register routes with `httpx.RegisterRouter`; reuse existing `httpx` middleware.
31
- - Add `defer apix.HandlePanic(ctx)` in controllers.
32
- - Parse request params with `apix.BindParams` or `apix.GetParam*`.
33
- - Return responses with `apix.HandleData`.
34
- - Keep business logic in service layer.
35
- - Use `utils/errors` for errors.
36
- - Use `utils/logger` or global logger helpers for logging.
37
- - Do not use `fmt.Println` for runtime logging.
38
- - Prefer `domainx/dx` APIs for DB operations; align usage with `test/dx_test.go`.
39
- - Keep names short and clear; avoid long camel-case identifiers.
40
- - Use snake_case for storage fields and CamelCase for model fields.
41
- - Keep module layout under `domain/<module>/{router.go, controller.go, service.go, model/*.go}`.
42
- - Put shared logic in `domain/common`.
43
-
44
- ## Data Access Patterns
45
-
46
- ### Model layer — minimal structure
47
- The model `D` struct only needs fields and `DConfig()`. Do NOT add `M` type alias or `NewM()` factory:
48
- ```go
49
- // CORRECT
50
- type D struct {
51
- AgentID string `bson:"agent_id" json:"agent_id"`
52
- // ...
53
- }
54
- func (d *D) DConfig() (domainx.ConType, string, string) {
55
- return domainx.Mongo, global.QuantSageDB, Table
56
- }
57
-
58
- // WRONG — do not add these
59
- type M = domainx.Complex[D]
60
- func NewM() *M { return domainx.UseComplex[D](...) }
61
- ```
62
-
63
- ### Index registration — always in service.go init()
64
- Indexes are registered in `service.go` via `domainx.AutoMigrate`, never in the model file.
65
- Fields are automatically prefixed with `data.` by the framework — pass bare field names.
66
- `AutoMigrate` accepts `dx.On[T](ctx).Complex()` as the ConTable factory:
67
- ```go
68
- func init() {
69
- domainx.AutoMigrate(
70
- func() domainx.ConTable { return dx.On[model.D](context.Background()).Complex() },
71
- domainx.CtIdx(domainx.Idx, "field_a", "field_b", "field_c"), // compound index
72
- domainx.CtIdx(domainx.Unique, "unique_field"), // unique index
73
- domainx.CtIdx(domainx.Spatial2D, "lat", "lng"), // 2D spatial index
74
- )
75
- }
76
- ```
77
-
78
- ### dx query API — filters
79
-
80
- All filter methods accept an optional `ignore bool` trailing param. When `true`, the filter is skipped if value is zero/empty — useful for optional query params:
81
- ```go
82
- dx.On[model.D](ctx).Eq("status", status, status == "") // skip filter if status is empty
83
- ```
84
-
85
- **Comparison operators:**
86
- ```go
87
- .Eq("field", value) // ==
88
- .Ne("field", value) // !=
89
- .Gt("field", value) // >
90
- .Gte("field", value) // >=
91
- .Lt("field", value) // <
92
- .Lte("field", value) // <=
93
- .Like("field", "keyword") // LIKE / regex contains
94
- .In("field", []T{...}) // IN list
95
- .NotIn("field", []T{...}) // NOT IN list
96
- .NEmpty("field") // field is not empty / not null
97
- .WithID(id) // shorthand for Eq("id", id)
98
- ```
99
-
100
- **Array field operators** (for `[]string` / JSON array fields):
101
- ```go
102
- .Has("tags", "go") // array contains "go"
103
- .HasAny("tags", []string{"go","rust"}) // array contains any of the values
104
- .HasAll("tags", []string{"go","rust"}) // array contains all of the values
105
- ```
106
-
107
- **Geo-spatial operators** (Mongo only):
108
- ```go
109
- .Near("lat", "lng", lat, lng, distanceMeters) // separate lat/lng fields
110
- .NearLoc("location", lat, lng, distanceMeters) // embedded GeoJSON location field
111
- ```
112
-
113
- **Dynamic condition composition** (when conditions are built in a loop):
114
- ```go
115
- // Build Matches dynamically, then pass to AddMatches
116
- m := domainx.NewMatches().
117
- Eq("type", t).
118
- Gte("score", minScore, minScore == 0) // ignore param works here too
119
- dx.On[model.D](ctx).AddMatches(m).Sort("id").Find()
120
- ```
121
-
122
- ### dx query API — sorting & projection
123
- ```go
124
- .Sort("created_at") // default descending
125
- .Sort("created_at", true) // ascending (true = ASC)
126
- .Sort("id") // sort by id descending
127
-
128
- .Select("id", "name") // fetch only these fields
129
- .Omit("secret_field") // exclude these fields
130
- ```
131
-
132
- ### dx query API — read operations
133
- ```go
134
- // Get single record (by ID or by filters)
135
- item, err := dx.On[model.D](ctx).WithID(id).Get()
136
- item, err := dx.On[model.D](ctx).Eq("code", code).Sort("id").Get()
137
- if err != nil { return nil, err }
138
- if item.IsNil() { /* not found */ }
139
- _ = item.Data // *model.D
140
-
141
- // Find multiple records
142
- results, err := dx.On[model.D](ctx).Eq("status", "active").Sort("id").Find()
143
- if err != nil { return nil, err }
144
- for _, m := range results.List() { // List() returns []*model.D
145
- _ = m
146
- }
147
-
148
- // Count
149
- count, err := dx.On[model.D](ctx).Eq("type", t).Count()
150
-
151
- // Sum a numeric field
152
- total, err := dx.On[model.D](ctx).Eq("user_id", uid).Sum("amount")
153
-
154
- // Existence check (works with WithID or filters)
155
- exists, err := dx.On[model.D](ctx).Eq("email", email).Exists()
156
-
157
- // Cursor-based pagination (lastID = 0 on first page)
158
- pageResp, err := dx.On[model.D](ctx).Eq("user_id", uid).Sort("id").Page(page, size, lastID)
159
- // pageResp.Result *[]*domainx.Complex[D]
160
- // pageResp.Total.Get()
161
- // pageResp.LastID
162
- // pageResp.Page, pageResp.Size
163
-
164
- // Iterate over filtered results one by one
165
- err := dx.On[model.D](ctx).Eq("flag", true).FindEach(func(item *domainx.Complex[model.D]) *errors.Error {
166
- // process item.Data
167
- return nil
168
- })
169
-
170
- // Batch-paginated full scan (1000 per batch, safe for large collections)
171
- err := dx.On[model.D](ctx).Eq("type", t).AllEach(func(item *domainx.Complex[model.D]) *errors.Error {
172
- // process item.Data
173
- return nil
174
- })
175
- ```
176
-
177
- ### dx query API — write operations
178
- ```go
179
- // Save (insert or update based on ID)
180
- id, err := dx.On[model.D](ctx, &d).Save()
181
-
182
- // Manual ID generation before save
183
- id, err := dx.On[model.D](ctx, &d).GenerateID().Save()
184
-
185
- // Update single field (by ID or by filters)
186
- err := dx.On[model.D](ctx).WithID(id).Update("status", "done")
187
- err := dx.On[model.D](ctx).Eq("code", code).Update("count", newCount)
188
-
189
- // Update multiple fields at once
190
- err := dx.On[model.D](ctx).WithID(id).Updates(map[string]interface{}{
191
- "status": "done",
192
- "count": newCount,
193
- })
194
-
195
- // Delete (by ID or by filters)
196
- err := dx.On[model.D](ctx).WithID(id).Delete()
197
- err := dx.On[model.D](ctx).Eq("expired", true).Delete()
198
- ```
199
-
200
- ## Cache Package
201
-
202
- Use `cache.New[T]` to create a typed cache backed by Memory, JSON file, SQLite, or Redis:
203
-
204
- ```go
205
- import "github.com/jom-io/gorig/cache"
206
-
207
- // In-memory (go-cache), args: defaultExpiration, cleanupInterval
208
- c := cache.New[MyStruct](cache.Memory, 5*time.Minute, 10*time.Minute)
209
-
210
- // JSON file (persisted to .cache/<name>.json)
211
- c := cache.New[MyStruct](cache.JSON, "my_cache_name")
212
-
213
- // SQLite (persisted to .cache/<name>.db)
214
- c := cache.New[MyStruct](cache.Sqlite, "my_cache_name")
215
-
216
- // Redis (uses configured Redis instance)
217
- c := cache.New[MyStruct](cache.Redis)
218
- ```
219
-
220
- **Cache interface operations:**
221
- ```go
222
- err := c.Set("key", value, 5*time.Minute) // 0 = no expiry
223
- val, err := c.Get("key") // returns (T, error); error on miss
224
- ok, err := c.Exists("key")
225
- err = c.Del("key")
226
- err = c.Expire("key", 2*time.Minute) // update TTL
227
- n, err := c.Incr("counter") // atomic increment, returns new int64
228
- err = c.Flush() // clear all keys
229
- err = c.RPush("queue", value) // push to list tail
230
- val, err = c.BRPop(timeout, "queue") // blocking pop from list tail
231
- ```
232
-
233
- **Multi-level cache with singleflight** (prevents cache stampede):
234
- ```go
235
- l1 := cache.New[MyStruct](cache.Memory, time.Minute, time.Minute)
236
- l2 := cache.New[MyStruct](cache.Redis)
237
- tool := cache.NewCacheTool[MyStruct](ctx, []cache.Cache[MyStruct]{l1, l2}, func(key string) (MyStruct, error) {
238
- // loader: called only on full miss, result stored in all layers
239
- return fetchFromDB(key)
240
- })
241
- val, err := tool.Get("key", 5*time.Minute)
242
- tool.Set("key", val, 5*time.Minute)
243
- tool.Delete("key")
244
- ```
245
-
246
- **SQLite paged cache** — for time-series or queryable local storage with auto-indexed fields:
247
- ```go
248
- // Struct tags: `idx:"field"` = single index, `idx_group:"group_name"` = compound index
249
- type Stat struct {
250
- At int64 `json:"at" idx:"at"`
251
- Method string `json:"method" idx_group:"method_uri"`
252
- URI string `json:"uri" idx_group:"method_uri"`
253
- Count int64 `json:"count"`
254
- }
255
- pg, err := cache.NewSQLiteCachePage[Stat]("stat_cache")
256
- ```
257
-
258
- ## Scheduled Tasks (cronx)
259
-
260
- `cronx` is a service registered automatically. Enable it by including `"CRON"` in service startup or via `bootstrap.StartUp()`.
261
-
262
- ```go
263
- import "github.com/jom-io/gorig/cronx"
264
-
265
- // Standard cron expression — with optional timeout context
266
- cronx.AddCronTask("0 * * * * *", func(ctx context.Context) {
267
- // ctx is canceled on timeout; check ctx.Done() for long tasks
268
- }, 30*time.Second) // optional: task-level timeout
269
-
270
- // Interval-based shorthand
271
- cronx.AddEveryTask(5*time.Minute, func(ctx context.Context) {
272
- // runs every 5 minutes
273
- })
274
-
275
- // One-shot delayed task (runs once after delay)
276
- cronx.AddDelayTask(10*time.Second, func(ctx context.Context) {
277
- // runs once 10s after registration
278
- })
279
-
280
- // One-shot at specific time
281
- cronx.AddOnceTask(time.Now().Add(time.Hour), func(ctx context.Context) {
282
- // runs once at the specified time, then removes itself
283
- })
284
- ```
285
-
286
- **Built-in behaviors:**
287
- - Panic recovery: panics are caught, logged, and sent to DingTalk.
288
- - Timeout: if timeout is set and exceeded, `ctx` is canceled and an error is logged.
289
- - Duplicate deduplication: same spec+func combination is ignored with a warning.
290
- - `AddTask(spec, func())` is deprecated — use `AddCronTask` for context support.
291
-
292
- **Lifecycle** (if managing manually without bootstrap):
293
- ```go
294
- go cronx.Startup("CRON", "")
295
- // ...
296
- cronx.Shutdown("CRON", context.Background())
297
- ```
298
-
299
- ## Message Broker (messagex)
300
-
301
- In-process pub/sub (Local) or Redis-backed for cross-process messaging.
302
-
303
- ```go
304
- import "github.com/jom-io/gorig/mid/messagex"
305
-
306
- // Get broker instance
307
- svc := messagex.Ins(messagex.Local) // in-memory
308
- svc := messagex.Ins(messagex.Redis) // Redis-backed
309
-
310
- // Or use package-level helpers (Local broker)
311
- messagex.RegisterTopic(...)
312
- messagex.PublishNewMsg(...)
313
- messagex.UnSubscribe(...)
314
- ```
315
-
316
- **Concurrent subscriber** (messages dispatched in parallel):
317
- ```go
318
- subID, err := svc.RegisterTopic("order.created", func(msg *messagex.Message) *errors.Error {
319
- name := msg.GetValue("name") // interface{}
320
- age := msg.GetValueInt64("age") // int64
321
- amt := msg.GetValueFloat64("amount") // float64
322
- return nil
323
- })
324
- defer svc.UnRegisterTopic("order.created", subID)
325
- ```
326
-
327
- **Sequential subscriber** (messages processed in publish order, one at a time):
328
- ```go
329
- subID, err := svc.RegisterTopicSeq("order.created", func(msg *messagex.Message) *errors.Error {
330
- // handler called serially; return error to trigger retry/DLQ
331
- return nil
332
- }, messagex.WithMaxRetry(3), messagex.WithRetryIntervals(500*time.Millisecond))
333
- ```
334
-
335
- **Publish:**
336
- ```go
337
- // payload can be any struct or map[string]any
338
- svc.PublishNewMsg(ctx, "order.created", OrderCreatedEvent{UserID: uid, Amount: 99.9})
339
- ```
340
-
341
- **Dead Letter Queue (sequential only):**
342
- ```go
343
- // Replay up to 10 failed messages back into the topic queue
344
- err := svc.ReplayDLQ("order.created", 10) // limit <= 0 = replay all
345
- ```
346
-
347
- **Multiple subscribers** — all registered subscribers receive every published message independently.
348
-
349
- ## SSE — Server-Sent Events
350
-
351
- Use `ssex` middleware and helpers for streaming responses:
352
-
353
- ```go
354
- import "github.com/jom-io/gorig/httpx/ssex"
355
-
356
- // Route registration — SSE endpoint must be GET
357
- router.GET("/stream/events", ssex.Mid(), controller.StreamEvents)
358
-
359
- // In controller — ssex.Mid() sets headers and enforces GET-only (returns 405 otherwise)
360
- func (c *Controller) StreamEvents(ctx *gin.Context) {
361
- err := ssex.SendOK(ctx, "update", map[string]interface{}{"value": 42})
362
- if err != nil { return }
363
-
364
- err = ssex.SendError(ctx, "update", "something went wrong")
365
- if err != nil { return }
366
- }
367
- ```
368
-
369
- ## Logging
370
-
371
- Use `utils/logger` everywhere. Never use `fmt.Println` for runtime output.
372
-
373
- ```go
374
- import (
375
- "github.com/jom-io/gorig/utils/logger"
376
- "go.uber.org/zap"
377
- )
378
- ```
379
-
380
- **Log levels:**
381
- ```go
382
- logger.Debug(ctx, "cache miss", zap.String("key", key))
383
- logger.Info(ctx, "order created", zap.Int64("id", id), zap.Float64("amount", amount))
384
- logger.Warn(ctx, "retry attempt", zap.Int("attempt", n), zap.Error(err))
385
- logger.Error(ctx, "db write failed", zap.Error(err))
386
- logger.DPanic(ctx, "unexpected state") // panic in dev, error in prod
387
- ```
388
-
389
- - All calls auto-inject `traceID` and `userID` from ctx — pass ctx from controller all the way down.
390
- - Passing `nil` ctx is safe; traceID will be `"no traceid"`.
391
- - `*gin.Context` is accepted directly as ctx.
392
-
393
- **Create a standalone context with trace ID** (for background jobs / init):
394
- ```go
395
- ctx := logger.NewCtx() // auto-generates xid trace ID
396
- ctx := logger.NewCtx("my-trace-id") // custom trace ID
397
- traceID := logger.GetTraceID(ctx)
398
- ```
399
-
400
- **Structured fields** — always use zap typed constructors, never `zap.Any` for typed values:
401
- ```go
402
- zap.String("field", val)
403
- zap.Int64("id", id)
404
- zap.Float64("amount", amt)
405
- zap.Bool("ok", flag)
406
- zap.Error(err) // for error values
407
- zap.Any("data", obj) // fallback for unknown types
408
- ```
409
-
410
- **Log files** are written to `.logs/commons/commons.jsonl` by default (lumberjack rotation).
411
- Config keys in yaml: `logger.commons.root`, `logger.commons.level`, `logger.commons.file`.
412
-
413
- ### Scenario 6 — Logging across layers
414
-
415
- ```go
416
- // controller.go — ctx carries traceID + userID from middleware
417
- func (c *Controller) Create(ctx *gin.Context) {
418
- defer apix.HandlePanic(ctx)
419
- logger.Info(ctx, "create request received")
420
-
421
- userID := apix.GetParamInt64(ctx, "user_id")
422
- id, err := Create(ctx, userID, 99.9)
423
- if err != nil {
424
- logger.Error(ctx, "create failed", zap.Error(err))
425
- apix.HandleData(ctx, nil, err)
426
- return
427
- }
428
- logger.Info(ctx, "create success", zap.Int64("id", id))
429
- apix.HandleData(ctx, id, nil)
430
- }
431
-
432
- // service.go — forward ctx, never create a new one mid-request
433
- func Create(ctx context.Context, userID int64, amount float64) (int64, *errors.Error) {
434
- logger.Debug(ctx, "saving order", zap.Int64("user_id", userID))
435
- d := model.D{UserID: userID, Amount: amount, Status: "pending"}
436
- id, err := dx.On[model.D](ctx, &d).Save()
437
- if err != nil {
438
- logger.Error(ctx, "save order failed", zap.Int64("user_id", userID), zap.Error(err))
439
- return 0, err
440
- }
441
- return id, nil
442
- }
443
-
444
- // cronx / background — use logger.NewCtx() since there is no request ctx
445
- func syncExpiredOrders(ctx context.Context) {
446
- logger.Info(ctx, "sync started")
447
- count, err := cancelExpired(ctx)
448
- if err != nil {
449
- logger.Error(ctx, "sync failed", zap.Error(err))
450
- return
451
- }
452
- logger.Info(ctx, "sync done", zap.Int64("cancelled", count))
453
- }
454
- ```
455
-
456
- ## Middleware
457
-
458
- ```go
459
- import "github.com/jom-io/gorig/httpx"
460
-
461
- // Panic recovery middleware — returns HTTP 500 with JSON error body
462
- router.Use(httpx.Recovery())
463
- ```
464
-
465
- ## Execution Strategy
466
-
467
- Implement directly using Read/Edit/Write tools.
468
- Verify with `go build ./... && go test ./test/... -v` after every change.
469
-
470
- ## Maintain Documentation
471
- When API changes exist, create or update `doc/<module>.md` using `~/.codex/skills/gorig-backend/assets/api-doc-template.md`.
472
- When module behavior changes, update `domain/<module>/README.md` using `~/.codex/skills/gorig-backend/assets/module-readme-template.md`.
473
-
474
- ## Complete Scenario Examples
475
-
476
- ### Scenario 1 — CRUD module (HTTP API)
477
-
478
- ```
479
- domain/order/
480
- ├── router.go
481
- ├── controller.go
482
- ├── service.go
483
- └── model/
484
- └── order.go
485
- ```
486
-
487
- **model/order.go**
488
- ```go
489
- package model
490
-
491
- import "github.com/jom-io/gorig/domainx"
492
-
493
- const Table = "order"
494
-
495
- type D struct {
496
- UserID int64 `bson:"user_id" json:"user_id"`
497
- Status string `bson:"status" json:"status"`
498
- Amount float64 `bson:"amount" json:"amount"`
499
- }
500
-
501
- func (d *D) DConfig() (domainx.ConType, string, string) {
502
- return domainx.Mongo, "main", Table
503
- }
504
- ```
505
-
506
- **service.go**
507
- ```go
508
- package order
509
-
510
- import (
511
- "context"
512
- "github.com/jom-io/gorig/domainx"
513
- "github.com/jom-io/gorig/domainx/dx"
514
- "github.com/jom-io/gorig/utils/errors"
515
- "yourproject/domain/order/model"
516
- )
517
-
518
- func init() {
519
- domainx.AutoMigrate(
520
- func() domainx.ConTable { return dx.On[model.D](context.Background()).Complex() },
521
- domainx.CtIdx(domainx.Idx, "user_id", "status"),
522
- )
523
- }
524
-
525
- func Create(ctx context.Context, userID int64, amount float64) (int64, *errors.Error) {
526
- d := model.D{UserID: userID, Status: "pending", Amount: amount}
527
- id, err := dx.On[model.D](ctx, &d).Save()
528
- if err != nil {
529
- return 0, err
530
- }
531
- return id, nil
532
- }
533
-
534
- func ListByUser(ctx context.Context, userID int64) ([]*model.D, *errors.Error) {
535
- results, err := dx.On[model.D](ctx).Eq("user_id", userID).Sort("id").Find()
536
- if err != nil {
537
- return nil, err
538
- }
539
- return results.List(), nil
540
- }
541
-
542
- func Complete(ctx context.Context, id int64) *errors.Error {
543
- return dx.On[model.D](ctx).WithID(id).Update("status", "done")
544
- }
545
- ```
546
-
547
- **controller.go**
548
- ```go
549
- package order
550
-
551
- import (
552
- "github.com/gin-gonic/gin"
553
- "github.com/jom-io/gorig/apix"
554
- )
555
-
556
- type Controller struct{}
557
-
558
- func (c *Controller) Create(ctx *gin.Context) {
559
- defer apix.HandlePanic(ctx)
560
- userID := apix.GetParamInt64(ctx, "user_id")
561
- amount := apix.GetParamFloat64(ctx, "amount")
562
- id, err := Create(ctx, userID, amount)
563
- apix.HandleData(ctx, id, err)
564
- }
565
-
566
- func (c *Controller) List(ctx *gin.Context) {
567
- defer apix.HandlePanic(ctx)
568
- userID := apix.GetParamInt64(ctx, "user_id")
569
- list, err := ListByUser(ctx, userID)
570
- apix.HandleData(ctx, list, err)
571
- }
572
- ```
573
-
574
- **router.go**
575
- ```go
576
- package order
577
-
578
- import "github.com/jom-io/gorig/httpx"
579
-
580
- func init() {
581
- c := &Controller{}
582
- httpx.RegisterRouter(func(r httpx.Router) {
583
- g := r.Group("/order")
584
- g.POST("/create", c.Create)
585
- g.GET("/list", c.List)
586
- })
587
- }
588
- ```
589
-
590
- ---
591
-
592
- ### Scenario 2 — Scheduled task
593
-
594
- ```go
595
- package sync
596
-
597
- import (
598
- "context"
599
- "github.com/jom-io/gorig/cronx"
600
- "yourproject/domain/order"
601
- )
602
-
603
- func init() {
604
- // Run every day at 02:00
605
- cronx.AddCronTask("0 0 2 * * *", syncExpiredOrders, 5*time.Minute)
606
- }
607
-
608
- func syncExpiredOrders(ctx context.Context) {
609
- // business logic; ctx is canceled after 5 min timeout
610
- _ = order.CancelExpired(ctx)
611
- }
612
- ```
613
-
614
- ---
615
-
616
- ### Scenario 3 — Event-driven with messagex
617
-
618
- ```go
619
- // Publisher (in order service)
620
- func Create(ctx context.Context, ...) (int64, *errors.Error) {
621
- // ... save logic ...
622
- messagex.PublishNewMsg(ctx, "order.created", map[string]any{
623
- "order_id": id,
624
- "user_id": userID,
625
- })
626
- return id, nil
627
- }
628
-
629
- // Subscriber (in notify service init)
630
- func init() {
631
- messagex.RegisterTopic("order.created", func(msg *messagex.Message) *errors.Error {
632
- orderID := msg.GetValueInt64("order_id")
633
- return sendPushNotification(orderID)
634
- })
635
- }
636
- ```
637
-
638
- ---
639
-
640
- ### Scenario 4 — SSE streaming endpoint
641
-
642
- ```go
643
- // router.go
644
- g.GET("/stream/status", ssex.Mid(), c.StreamStatus)
645
-
646
- // controller.go
647
- func (c *Controller) StreamStatus(ctx *gin.Context) {
648
- defer apix.HandlePanic(ctx)
649
- orderID := apix.GetParamInt64(ctx, "order_id")
650
-
651
- status, err := order.GetStatus(ctx, orderID)
652
- if err != nil {
653
- ssex.SendError(ctx, "status", err.Error())
654
- return
655
- }
656
- ssex.SendOK(ctx, "status", map[string]any{"status": status})
657
- }
658
- ```
659
-
660
- ---
661
-
662
- ### Scenario 5 — Cache-aside pattern
663
-
664
- ```go
665
- var orderCache = cache.New[model.D](cache.Redis)
666
-
667
- func GetByID(ctx context.Context, id int64) (*model.D, *errors.Error) {
668
- key := fmt.Sprintf("order:%d", id)
669
-
670
- // L1: Redis cache
671
- if cached, err := orderCache.Get(key); err == nil {
672
- return &cached, nil
673
- }
674
-
675
- // L2: DB
676
- result, err := dx.On[model.D](ctx).WithID(id).Get()
677
- if err != nil || result.IsNil() {
678
- return nil, err
679
- }
680
- _ = orderCache.Set(key, *result.Data, 5*time.Minute)
681
- return result.Data, nil
682
- }
683
- ```
684
-
685
- ---
686
-
687
- ## Go Test Convention
688
-
689
- **All tests live in `test/`, `package test`. Never place test files inside domain packages.**
690
-
691
- ```
692
- test/
693
- ├── init.go ← Shared DB init — DO NOT modify
694
- ├── _bin/local.yaml ← Test DB config — DO NOT modify
695
- └── {feature}_test.go ← Add new test files here
696
- ```
697
-
698
- `init.go` starts the DB connection before any test runs. Tests that bypass this package lose the connection.
699
-
700
- **New test file boilerplate:**
701
-
702
- ```go
703
- package test
704
-
705
- import (
706
- "context"
707
- "testing"
708
- )
709
-
710
- func TestXxx_场景描述(t *testing.T) {
711
- ctx := context.Background()
712
-
713
- // arrange — hardcode inline, no external files
714
- id, err := SomeModule.Create(ctx, 1, 99.9)
715
- if err != nil {
716
- t.Fatalf("Create failed: %v", err)
717
- }
718
-
719
- // act
720
- result, err := SomeModule.GetByID(ctx, id)
721
-
722
- // assert
723
- if err != nil {
724
- t.Fatalf("GetByID failed: %v", err)
725
- }
726
- if result == nil {
727
- t.Fatal("expected result, got nil")
728
- }
729
- }
730
- ```
731
-
732
- **Rules:**
733
- - Functions under test must be **exported** (capitalized) — `package test` cannot access unexported symbols.
734
- - Use standard `testing` only (testify is available but prefer stdlib assertions).
735
- - Use `t.Fatalf` / `t.Errorf` for assertions.
736
- - Hardcode test data inline; do not read external files.
737
-
738
- **Run commands:**
739
-
740
- ```bash
741
- go test ./test/... -v # all tests
742
- go test ./test/... -run TestXxx -v # single test
743
- ```
744
-
745
- ## Validate and Deliver
746
- Run `go build ./... && go test ./test/... -v` after every change.
747
- If tests cannot run, explain the exact blocker and provide a minimal verification plan.
748
- Deliver results in concise engineering format:
749
- - Change summary.
750
- - Key file paths.
751
- - Test status and suggestions.
752
- - Unknowns plus minimal assumptions.
753
-
754
- ## Provide Database Recommendation
755
- When asked to choose storage, recommend based on:
756
- - Existing dependencies and `dx` compatibility.
757
- - Read/write pattern and query complexity.
758
- - Transaction consistency requirements.
759
- - Operational cost and team familiarity.
760
-
761
- Present recommendation with clear rationale and trade-offs.
762
-
763
- ## Resources
764
- - `~/.codex/skills/gorig-backend/references/onboarding-files.md`: Required code-reading checklist for gorig and news.
765
- - `~/.codex/skills/gorig-backend/assets/api-doc-template.md`: API documentation template.
766
- - `~/.codex/skills/gorig-backend/assets/module-readme-template.md`: Module README template.