gorig-cli 1.0.23 → 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 -189
  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 -38
  69. package/templates/skills/codex/gorig-backend/SKILL.md +0 -189
  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 -38
@@ -0,0 +1,332 @@
1
+ # Persistent CRUD Module
2
+
3
+ Use this reference when the user asks to add a business CRUD module, persistent resource, management capability, or storage-backed service.
4
+
5
+ CRUD is not inherently HTTP. Treat persistence and business behavior as the core capability. Add HTTP routes/controllers only when the user asks for an HTTP API, when the existing project exposes similar modules through HTTP, or when the delivery contract explicitly requires route-level smoke verification.
6
+
7
+ ## Storage Selection
8
+
9
+ The verified persistent CRUD workflow currently supports only:
10
+
11
+ - MySQL through `domainx/dx`
12
+ - MongoDB through `domainx/dx`
13
+
14
+ Before implementation:
15
+
16
+ 1. If the user explicitly specifies MySQL, use MySQL.
17
+ 2. If the user explicitly specifies MongoDB or Mongo, use MongoDB.
18
+ 3. If the user specifies another persistence backend such as PostgreSQL, SQLite, Redis, files, or an external API, state that the verified persistent CRUD workflow supports only MySQL and MongoDB, then ask whether to use one of those two or proceed as custom unverified work.
19
+ 4. If the user does not specify storage, inspect the target project first:
20
+ - Reuse the existing database kind when the project has clear existing models/configuration for only one of MySQL or MongoDB.
21
+ - If both exist, ask which one this module should use.
22
+ - If neither is clear, ask the user to choose MySQL or MongoDB before coding.
23
+
24
+ Do not silently default to MySQL or MongoDB when the project context does not make the choice obvious.
25
+
26
+ ## Configuration Discovery and Setup
27
+
28
+ Persistent CRUD requires a matching database connection. Before writing code:
29
+
30
+ 1. Inspect `_bin/local.yaml`, `_bin/dev.yaml`, `_bin/prod.yaml`, test config under `test/_bin/`, and existing model `DConfig()` values.
31
+ 2. Determine whether the selected database key already exists:
32
+ - MySQL connection names are configured under `Mysql.<name>`, and models return that same `<name>` from `DConfig()`.
33
+ - MongoDB connection names are configured under `mongo.<name>`, and models return that same `<name>` from `DConfig()`.
34
+ 3. If configuration is missing, ask the user whether to:
35
+ - add non-secret skeleton keys to the environment YAML files, or
36
+ - provide values through environment variables.
37
+ 4. Never invent or commit real credentials. Prefer environment variables for passwords and private connection strings.
38
+
39
+ When the user explicitly runs or approves `gorig-cli create <module> --crud --db ...`, the generator adds an idempotent non-secret connection skeleton to `_bin/local.yaml`, `_bin/dev.yaml`, `_bin/prod.yaml`, and `test/_bin/local.yaml`. Existing connection blocks with the same name are preserved.
40
+
41
+ Gorig uses Viper with prefix `GORIG` and `.` -> `_` key replacement. For example:
42
+
43
+ - `Mysql.Main.Write.Host` can be overridden with `GORIG_MYSQL_MAIN_WRITE_HOST`
44
+ - `Mysql.Main.Write.Pass` can be overridden with `GORIG_MYSQL_MAIN_WRITE_PASS`
45
+ - `mongo.main.uri` can be overridden with `GORIG_MONGO_MAIN_URI`
46
+ - `mongo.main.auth.password` can be overridden with `GORIG_MONGO_MAIN_AUTH_PASSWORD`
47
+
48
+ ### MySQL skeleton
49
+
50
+ Add this shape when the selected backend is MySQL and the project has no existing MySQL config. Keep secrets empty unless the user explicitly provides development credentials.
51
+
52
+ ```yaml
53
+ Mysql:
54
+ Main:
55
+ GormInit: 1
56
+ IsOpenReadDb: 0
57
+ Write:
58
+ Host: 127.0.0.1
59
+ Port: 3306
60
+ DataBase: demo_api
61
+ User: root
62
+ Pass: ""
63
+ Charset: utf8mb4
64
+ SetConnMaxLifetime: 60
65
+ SetMaxIdleConns: 10
66
+ SetMaxOpenConns: 100
67
+ ```
68
+
69
+ Required model config:
70
+
71
+ ```go
72
+ func (*D) DConfig() (domainx.ConType, string, string) {
73
+ return domainx.Mysql, "Main", Table
74
+ }
75
+ ```
76
+
77
+ ### MongoDB skeleton
78
+
79
+ Add this shape when the selected backend is MongoDB and the project has no existing Mongo config. Keep credentials empty unless the user explicitly provides development credentials.
80
+
81
+ ```yaml
82
+ mongo:
83
+ main:
84
+ uri: mongodb://127.0.0.1:27017
85
+ db:
86
+ name: demo_api
87
+ auth:
88
+ need: false
89
+ source: admin
90
+ user: ""
91
+ password: ""
92
+ retry:
93
+ writes: true
94
+ reads: true
95
+ pool:
96
+ max: 10
97
+ min: 1
98
+ conn:
99
+ idle:
100
+ time:
101
+ max: 10
102
+ ```
103
+
104
+ Required model config:
105
+
106
+ ```go
107
+ func (*D) DConfig() (domainx.ConType, string, string) {
108
+ return domainx.Mongo, "main", Table
109
+ }
110
+ ```
111
+
112
+ If the user cannot provide a reachable database during implementation, generate and compile the code but report DB integration as skipped. Do not claim create/list/page/update/delete behavior is verified until the selected database is reachable.
113
+
114
+ ## Core Module Layout
115
+
116
+ For a persistent business module, generate or maintain:
117
+
118
+ ```text
119
+ domain/<module>/
120
+ ├── service.go
121
+ ├── dto.go
122
+ ├── README.md
123
+ └── model/
124
+ └── <module>.go
125
+ test/
126
+ └── <module>_test.go
127
+ ```
128
+
129
+ Use this additional HTTP adapter only when HTTP is in scope:
130
+
131
+ ```text
132
+ domain/<module>/
133
+ ├── router.go
134
+ └── controller.go
135
+ doc/
136
+ └── <module>.md
137
+ ```
138
+
139
+ Keep request/response DTOs in `dto.go`. Keep persistent data structures and `DConfig()` in `model/<module>.go`. Do not put response DTOs under `model/`.
140
+
141
+ ## Service Capability
142
+
143
+ Services must expose the business CRUD behavior independently of HTTP:
144
+
145
+ - create
146
+ - get/info by ID
147
+ - list with optional filters
148
+ - page with optional filters
149
+ - update by ID
150
+ - delete by ID
151
+
152
+ Services must:
153
+
154
+ - Accept `context.Context` and propagate it into `dx` and logs.
155
+ - Validate required fields and supported enum/filter values.
156
+ - Return `utils/errors` errors.
157
+ - Use `utils/logger` structured logs.
158
+ - Guard empty update/delete conditions.
159
+ - Treat not-found and duplicate cases explicitly.
160
+
161
+ ## Query and Pagination Safety
162
+
163
+ List and page behavior must be database-backed.
164
+
165
+ Rules:
166
+
167
+ - Use `dx.Page(page, size, lastID)` for paginated API responses. Do not call `Find()`, convert the full result to `List()`, and slice it in memory.
168
+ - Push optional filters into the `dx` query before `Find()` or `Page()`. Do not fetch all records and filter in Go.
169
+ - Push sort order into `Sort(...)`. Do not sort an unbounded in-memory slice for API responses.
170
+ - Use `Count`, `Sum`, `Exists`, and projection APIs when only aggregate or existence data is needed. Do not fetch full rows to count, sum, or check existence in Go.
171
+ - Put a conservative maximum page size in DTO validation or service validation when the project has no existing convention.
172
+ - For background jobs that must inspect many records, use restrictive matches plus `FindEach` or `AllEach`; do not load an entire table into memory.
173
+ - If a requirement cannot be expressed through `dx`, use a direct driver escape hatch with equivalent database-side filtering, pagination, and limits.
174
+
175
+ The only acceptable in-memory post-processing is bounded processing after the database already applied the main filter/page/limit, and it must not change pagination semantics.
176
+
177
+ ## Optional HTTP Adapter
178
+
179
+ When HTTP is in scope, use clear resource routes unless the existing project has a stricter convention:
180
+
181
+ | Method | Path | Purpose |
182
+ |---|---|---|
183
+ | POST | `/<module>/create` | Create a record |
184
+ | GET | `/<module>/info?id=` | Load one record |
185
+ | GET | `/<module>/list` | List records with optional filters |
186
+ | GET | `/<module>/page` | Page records with optional filters |
187
+ | POST | `/<module>/update` | Update a record |
188
+ | DELETE | `/<module>/delete?id=` | Delete a record |
189
+
190
+ Controllers must:
191
+
192
+ - `defer apix.HandlePanic(ctx)`
193
+ - Bind body/query parameters with the resolved `apix` signatures.
194
+ - Return through `apix.HandleData` with the project's established business code shape.
195
+ - Keep transport logic only; call service functions for business behavior.
196
+
197
+ ## MySQL Model Pattern
198
+
199
+ Use MySQL when the user chooses MySQL or the project clearly uses MySQL for similar modules.
200
+
201
+ ```go
202
+ package model
203
+
204
+ import "github.com/jom-io/gorig/domainx"
205
+
206
+ const Table = "order"
207
+
208
+ type D struct {
209
+ Name string `gorm:"column:name;type:varchar(128);not null;uniqueIndex:uk_name" json:"name"`
210
+ Status string `gorm:"column:status;type:varchar(32);not null;index:idx_status" json:"status"`
211
+ }
212
+
213
+ func (*D) DConfig() (domainx.ConType, string, string) {
214
+ return domainx.Mysql, "Main", Table
215
+ }
216
+ ```
217
+
218
+ Confirm the database name from existing project config or user input. Do not invent production database names.
219
+
220
+ ## MongoDB Model Pattern
221
+
222
+ Use MongoDB when the user chooses MongoDB or the project clearly uses MongoDB for similar modules.
223
+
224
+ ```go
225
+ package model
226
+
227
+ import "github.com/jom-io/gorig/domainx"
228
+
229
+ const Table = "order"
230
+
231
+ type D struct {
232
+ Name string `bson:"name" json:"name"`
233
+ Status string `bson:"status" json:"status"`
234
+ }
235
+
236
+ func (*D) DConfig() (domainx.ConType, string, string) {
237
+ return domainx.Mongo, "main", Table
238
+ }
239
+ ```
240
+
241
+ Confirm collection and database naming with existing project conventions or user input.
242
+
243
+ ## Migration and Indexes
244
+
245
+ Register indexes in `service.go`:
246
+
247
+ ```go
248
+ func init() {
249
+ domainx.AutoMigrate(
250
+ func() domainx.ConTable {
251
+ return dx.On[model.D](context.Background()).Complex()
252
+ },
253
+ domainx.CtIdx(domainx.Unique, "name"),
254
+ domainx.CtIdx(domainx.Idx, "status"),
255
+ )
256
+ }
257
+ ```
258
+
259
+ Pass bare field names to `CtIdx`; the framework handles its own field path mapping. Verify this against the resolved Gorig source before implementation.
260
+
261
+ ## Standard Service Operations
262
+
263
+ Use `domainx/dx` for baseline CRUD:
264
+
265
+ ```go
266
+ id, err := dx.On[model.D](ctx, &d).Save()
267
+ item, err := dx.On[model.D](ctx).WithID(id).Get()
268
+ list, err := dx.On[model.D](ctx).Eq("status", status).Sort("id").Find()
269
+ page, err := dx.On[model.D](ctx).Eq("status", status).Sort("id").Page(page, size, lastID)
270
+ err = dx.On[model.D](ctx).WithID(id).Updates(fields)
271
+ err = dx.On[model.D](ctx).WithID(id).Delete()
272
+ ```
273
+
274
+ Check the exact `dx` signatures in the resolved project version before coding.
275
+
276
+ For API pagination, prefer the `Page` call above. Use `Find` only for intentionally bounded lists, such as small lookup sets or internal logic with a restrictive match.
277
+
278
+ In the verified Gorig implementation, omitted/`false` trailing booleans enable the normal zero-value check, so empty optional filters are skipped. Passing `true` disables that check and forces an empty/zero condition into the query. Do not use `value == ""` as the third argument for optional filters.
279
+
280
+ ## Documentation
281
+
282
+ Always update:
283
+
284
+ - `domain/<module>/README.md`: module purpose, storage choice, indexes, service operations, tests, limitations.
285
+
286
+ When HTTP is in scope, also update:
287
+
288
+ - `doc/<module>.md`: public API contract, request/response examples, error cases.
289
+
290
+ ## Verification
291
+
292
+ Always run compile-level checks:
293
+
294
+ ```sh
295
+ go fmt ./...
296
+ go vet ./...
297
+ go build ./...
298
+ go test ./... -v
299
+ ```
300
+
301
+ The generated test layout separates validation tests from live-database tests:
302
+
303
+ - `test/<module>_test.go` runs with ordinary `go test` and does not require a database.
304
+ - `test/init_<backend>_integration_test.go` starts the selected Gorig database service.
305
+ - `test/<module>_integration_test.go` exercises the real CRUD lifecycle.
306
+ - `test/_bin/local.yaml` is the normal database configuration source for tests because the test package runs with `test/` as its working directory.
307
+
308
+ After filling the local connection values or exporting the documented environment variables, run:
309
+
310
+ ```sh
311
+ go test -tags=integration,mysql ./test/... -v
312
+ go test -tags=integration,mongo ./test/... -v
313
+ ```
314
+
315
+ Run only the command for the selected backend. If both backends are intentionally configured, run both commands separately so failures identify the affected connection.
316
+
317
+ When the selected database is available, verify effect-level service behavior:
318
+
319
+ - create success
320
+ - duplicate/invalid create
321
+ - get/info success
322
+ - get/info not found
323
+ - list filter
324
+ - page filter
325
+ - update success
326
+ - update invalid/not found
327
+ - delete success
328
+ - delete not found
329
+
330
+ When HTTP is in scope, also verify route-level smoke checks and confirm generated API docs match actual routes and response shapes.
331
+
332
+ If MySQL or MongoDB is not available, do not claim integration verification. Report the missing connection/configuration and mark DB integration as skipped.
@@ -0,0 +1,128 @@
1
+ # Basic Project Bootstrap
2
+
3
+ Use this workflow when creating a new locally runnable Gorig backend.
4
+
5
+ ## Confirm Before Creation
6
+
7
+ Confirm or infer safely:
8
+
9
+ - Project directory and Go module name.
10
+ - Gorig version: preserve a requested version; otherwise use the selected stable version and report the resolution.
11
+ - Whether an existing non-empty directory may be replaced. Never pass `--force` without explicit authorization.
12
+ - Local base port.
13
+ - Git initialization is a post-generation skill step. Ask after generation and verification; do not let the CLI initialize Git implicitly.
14
+
15
+ The basic profile must not require MySQL, MongoDB, or Redis.
16
+
17
+ ## Generate
18
+
19
+ Choose the generator before executing:
20
+
21
+ 1. When validating an unreleased sibling `gorig-cli` checkout, and `../gorig-cli/bin/cli.js` exists, use `node ../gorig-cli/bin/cli.js` so the test does not accidentally invoke an older global npm package.
22
+ 2. Otherwise use the installed `gorig-cli` command.
23
+ 3. Report which generator path and version are being used.
24
+
25
+ When `gorig-cli` is installed:
26
+
27
+ ```sh
28
+ gorig-cli init demo-api \
29
+ --module example.com/demo-api \
30
+ --gorig-version v0.0.52 \
31
+ --port 9527 \
32
+ --no-start \
33
+ --no-git
34
+ ```
35
+
36
+ Unreleased sibling-checkout validation:
37
+
38
+ ```sh
39
+ node ../gorig-cli/bin/cli.js init demo-api \
40
+ --module example.com/demo-api \
41
+ --gorig-version v0.0.52 \
42
+ --port 9527 \
43
+ --no-start \
44
+ --no-git
45
+ ```
46
+
47
+ During framework development, use a verified local checkout without downloading another version:
48
+
49
+ ```sh
50
+ gorig-cli init demo-api \
51
+ --module example.com/demo-api \
52
+ --gorig-replace ../gorig \
53
+ --port 9527 \
54
+ --no-start \
55
+ --no-git
56
+ ```
57
+
58
+ Supported automation flags:
59
+
60
+ - `--module <path>`: Go module name; defaults to the project directory name.
61
+ - `--gorig-version <version>`: release, commit, branch, or `latest`.
62
+ - `--gorig-replace <path>`: local Gorig checkout; records a Go `replace` directive.
63
+ - `--port <port>`: local port; dev and prod use the next two ports.
64
+ - `--force`: replace a non-empty destination; requires explicit authorization.
65
+ - `--start`: start after generation; the default is not to start.
66
+ - `--no-start`: explicit non-interactive behavior.
67
+ - `--no-git`: skip `git init`.
68
+
69
+ ## Expected Structure
70
+
71
+ ```text
72
+ demo-api/
73
+ ├── _bin/
74
+ │ ├── local.yaml
75
+ │ ├── dev.yaml
76
+ │ └── prod.yaml
77
+ ├── _cmd/main.go
78
+ ├── domain/
79
+ │ ├── init.go
80
+ │ └── hello/
81
+ │ ├── router.go
82
+ │ ├── controller.go
83
+ │ └── service.go
84
+ ├── global/config.go
85
+ ├── test/
86
+ │ ├── _bin/local.yaml
87
+ │ └── hello_test.go
88
+ ├── .gitignore
89
+ ├── README.md
90
+ ├── go.mod
91
+ └── go.sum
92
+ ```
93
+
94
+ `bootstrap.StartUp()` owns the built-in HTTP registration. The generated domain registers routes only; it must not register `httpx.Startup` a second time.
95
+
96
+ ## Verify
97
+
98
+ Run from the generated project:
99
+
100
+ ```sh
101
+ go fmt ./...
102
+ go vet ./...
103
+ go build ./...
104
+ go test ./... -v
105
+ ```
106
+
107
+ For repeatable behavior verification:
108
+
109
+ ```sh
110
+ <skill-dir>/scripts/verify-basic-project.sh . local
111
+ <skill-dir>/scripts/verify-basic-project.sh . dev
112
+ ```
113
+
114
+ The verification script builds the binary, starts the selected environment, checks `/ping` and `/hello`, sends `SIGINT`, and requires a clean graceful shutdown.
115
+
116
+ ## Optional Git Initialization
117
+
118
+ This is owned by the skill workflow, not by a new CLI command or flag.
119
+
120
+ After project generation and verification pass, ask the user whether to initialize Git and create the first commit. If the user says yes:
121
+
122
+ 1. Copy the skill asset `assets/gorig.gitignore` to the project root as `.gitignore`. For a freshly generated project, replacing the generated `.gitignore` is expected. If the project already has user edits, merge instead of overwriting.
123
+ 2. Run `git init` from the project root when `.git/` does not already exist.
124
+ 3. Run `git add .`.
125
+ 4. Run `git commit -m "chore: initialize gorig project"`.
126
+ 5. Run `git status --short` and report whether the worktree is clean.
127
+
128
+ Do not initialize Git or create a commit without explicit user confirmation. Do not commit files excluded by `assets/gorig.gitignore`, especially runtime logs, caches, local secrets, private certificates, or machine-specific override configuration.
@@ -0,0 +1,231 @@
1
+ # Scheduled Tasks
2
+
3
+ Use this reference when a task needs cron jobs, delayed jobs, one-shot jobs, task timeout, panic recovery, deduplication, or graceful cron shutdown.
4
+
5
+ Always inspect the target project's resolved Gorig source first. The examples below reflect the locally inspected Gorig `master` commits through `0af68e8`.
6
+
7
+ ## Supported APIs
8
+
9
+ Use `github.com/jom-io/gorig/cronx`.
10
+
11
+ ```go
12
+ cronx.AddCronTask("0 */5 * * * *", func(ctx context.Context) {
13
+ logger.Info(ctx, "scheduled sync")
14
+ }, 30*time.Second)
15
+
16
+ cronx.AddDelayTask(10*time.Second, func(ctx context.Context) {
17
+ logger.Info(ctx, "delayed sync")
18
+ }, 30*time.Second)
19
+
20
+ cronx.AddOnceTask(time.Now().Add(time.Hour), func(ctx context.Context) {
21
+ logger.Info(ctx, "one-shot sync")
22
+ }, 30*time.Second)
23
+ ```
24
+
25
+ Supported in the inspected baseline:
26
+
27
+ | API | Status | Notes |
28
+ |---|---|---|
29
+ | `AddCronTask` | verified | Accepts cron specs with seconds, including `@every` specs. Supports optional timeout context. |
30
+ | `AddDelayTask` | verified | Runs once after a delay. Internally schedules a one-shot task and starts cron. |
31
+ | `AddOnceTask` | verified | Runs once around the requested time and removes itself after execution. |
32
+ | `AddTask` | deprecated | No context support; do not use for new work. |
33
+ | `AddEveryTask` | verified | Convenience wrapper for `AddCronTask("@every <interval>")` in local Gorig commit `92c28b5` and later. |
34
+ | `RegisterPersistTask` | source-verified | Registers a named Redis-backed persistent task handler. Requires Gorig commit `0af68e8` or later. |
35
+ | `AddPersistDelayTask` | source-verified | Stores a JSON payload in Redis and runs once after a delay. Runtime verification requires Redis. |
36
+ | `AddPersistOnceTask` | source-verified | Stores a JSON payload in Redis and runs once at the requested time. Runtime verification requires Redis. |
37
+
38
+ ## Registration and Lifecycle
39
+
40
+ `cronx` registers the `CRON` service from package init. In ordinary applications, importing the package and calling `bootstrap.StartUp()` is enough:
41
+
42
+ ```go
43
+ package order
44
+
45
+ import (
46
+ "context"
47
+ "time"
48
+
49
+ "github.com/jom-io/gorig/cronx"
50
+ "github.com/jom-io/gorig/utils/logger"
51
+ "go.uber.org/zap"
52
+ )
53
+
54
+ func init() {
55
+ cronx.AddCronTask("0 */10 * * * *", syncExpiredOrders, 2*time.Minute)
56
+ }
57
+
58
+ func syncExpiredOrders(ctx context.Context) {
59
+ logger.Info(ctx, "expired order sync started")
60
+ count, err := cancelExpiredOrders(ctx)
61
+ if err != nil {
62
+ logger.Error(ctx, "expired order sync failed", zap.Error(err))
63
+ return
64
+ }
65
+ logger.Info(ctx, "expired order sync finished", zap.Int64("count", count))
66
+ }
67
+ ```
68
+
69
+ Manual lifecycle is useful only in tests or custom hosts:
70
+
71
+ ```go
72
+ _ = cronx.Startup("CRON", "")
73
+ // ...
74
+ _ = cronx.Shutdown("CRON", context.Background())
75
+ ```
76
+
77
+ Do not depend on startup ordering relative to unrelated services unless the resolved framework version provides explicit ordering.
78
+
79
+ ## Interval Tasks
80
+
81
+ Use `AddEveryTask` for simple fixed intervals when the target Gorig version includes commit `92c28b5` or an equivalent fix. Older inspected versions recursively locked the task mutex and must not use this helper.
82
+
83
+ ```go
84
+ func init() {
85
+ cronx.AddEveryTask(5*time.Minute, refreshCacheWarmup, 30*time.Second)
86
+ }
87
+
88
+ func refreshCacheWarmup(ctx context.Context) {
89
+ logger.Info(ctx, "cache warmup started")
90
+ if err := warmupHotKeys(ctx); err != nil {
91
+ logger.Error(ctx, "cache warmup failed", zap.Error(err))
92
+ }
93
+ }
94
+ ```
95
+
96
+ Rules:
97
+
98
+ - Reject intervals at or below one millisecond; the framework logs and skips them.
99
+ - Use `AddCronTask` directly when the schedule needs wall-clock alignment.
100
+ - Verify `AddEveryTask` returns promptly and the task executes at least once in the target version.
101
+ - Keep the same timeout and cancellation rules as `AddCronTask`.
102
+
103
+ ## Timeout and Cancellation
104
+
105
+ When a timeout is supplied, the wrapper creates a context with that timeout. Long tasks must observe `ctx.Done()`:
106
+
107
+ ```go
108
+ cronx.AddCronTask("@every 30s", func(ctx context.Context) {
109
+ select {
110
+ case <-ctx.Done():
111
+ logger.Warn(ctx, "job canceled before completion", zap.Error(ctx.Err()))
112
+ return
113
+ case item := <-work:
114
+ process(ctx, item)
115
+ }
116
+ }, 5*time.Second)
117
+ ```
118
+
119
+ The framework logs timeout expiration. It does not forcibly stop goroutines that ignore the context, so every long-running job must check cancellation.
120
+
121
+ ## Persistent Delay Tasks
122
+
123
+ Use persistent delay tasks only when delayed work must survive process restarts and Redis is available. The inspected implementation stores task metadata and JSON payloads in Redis under the `gorig:cronx:persist:*` namespace, claims due tasks with Redis scripts, and keeps completed or failed task records for 24 hours.
124
+
125
+ Persistent task APIs require Gorig commit `0af68e8` or a later equivalent implementation.
126
+
127
+ ```go
128
+ type OrderDelayPayload struct {
129
+ OrderID int64 `json:"order_id"`
130
+ }
131
+
132
+ func (OrderDelayPayload) PersistPayload() {}
133
+
134
+ func handleOrderDelay(ctx context.Context, payload OrderDelayPayload) error {
135
+ logger.Info(ctx, "delayed order task", zap.Int64("order_id", payload.OrderID))
136
+ return processDelayedOrder(ctx, payload.OrderID)
137
+ }
138
+
139
+ func init() {
140
+ if err := cronx.RegisterPersistTask(handleOrderDelay); err != nil {
141
+ logger.Error(nil, "register persistent cron task failed", zap.Error(err))
142
+ }
143
+ }
144
+
145
+ func scheduleOrderDelay(orderID int64) (string, error) {
146
+ return cronx.AddPersistDelayTask(
147
+ 5*time.Minute,
148
+ handleOrderDelay,
149
+ OrderDelayPayload{OrderID: orderID},
150
+ 30*time.Second,
151
+ )
152
+ }
153
+ ```
154
+
155
+ For absolute run times:
156
+
157
+ ```go
158
+ taskID, err := cronx.AddPersistOnceTask(
159
+ time.Now().Add(time.Hour),
160
+ handleOrderDelay,
161
+ OrderDelayPayload{OrderID: orderID},
162
+ 30*time.Second,
163
+ )
164
+ ```
165
+
166
+ Rules:
167
+
168
+ - Register every persistent handler during application startup with `RegisterPersistTask`.
169
+ - Handlers must be named functions. Closures and anonymous functions are rejected because Redis stores only the handler name.
170
+ - Payload types must implement `PersistPayload` and must be JSON serializable. Keep JSON tags stable across deployments.
171
+ - Redis must be configured and reachable before scheduling persistent tasks.
172
+ - Use ordinary `AddDelayTask` or `AddOnceTask` when restart persistence is not required.
173
+ - Treat task handlers as at-least-once work. Make handlers idempotent because a task can be recovered from the processing set after a worker lease expires.
174
+ - Use task IDs returned by `AddPersistDelayTask` or `AddPersistOnceTask` for logs and operational troubleshooting.
175
+
176
+ Redis configuration follows the same keys used by cache and Redis messaging:
177
+
178
+ ```yaml
179
+ redis:
180
+ addr: 127.0.0.1:6379
181
+ password: ""
182
+ db: 0
183
+ ```
184
+
185
+ Environment overrides:
186
+
187
+ - `GORIG_REDIS_ADDR`
188
+ - `GORIG_REDIS_PASSWORD`
189
+ - `GORIG_REDIS_DB`
190
+
191
+ Do not commit real Redis passwords or shared production addresses in examples or fixtures.
192
+
193
+ ## Panic Recovery
194
+
195
+ `AddCronTask`, `AddDelayTask`, and `AddOnceTask` wrap handlers with panic recovery. A panic is logged and sent through the configured DingTalk panic notifier. Treat recovery as a safety net, not control flow.
196
+
197
+ Rules:
198
+
199
+ - Keep job handlers idempotent.
200
+ - Log useful identifiers before risky work.
201
+ - Avoid panics for ordinary business failures; return/log structured errors instead.
202
+ - Never use `fmt.Println` for cron diagnostics.
203
+
204
+ ## Deduplication
205
+
206
+ Duplicate detection uses the pair of schedule spec and function name. Register a stable named function when deduplication matters:
207
+
208
+ ```go
209
+ func init() {
210
+ cronx.AddCronTask("@every 1m", syncInventory)
211
+ cronx.AddCronTask("@every 1m", syncInventory) // ignored as duplicate
212
+ }
213
+
214
+ func syncInventory(ctx context.Context) {}
215
+ ```
216
+
217
+ Anonymous closures with different generated names may not deduplicate the way a reader expects.
218
+
219
+ ## Verification Checklist
220
+
221
+ - Compile against the target project's resolved Gorig version.
222
+ - Run a short deterministic job and assert execution count.
223
+ - Register the same named function twice and verify duplicate registration does not double executions.
224
+ - Verify timeout by asserting the handler observes `ctx.Done()`.
225
+ - Verify panic recovery does not terminate the test process.
226
+ - Verify delay or once jobs run exactly once.
227
+ - Call shutdown and assert no verification process is left running.
228
+ - Verify `AddEveryTask` returns promptly, does not deadlock during registration, and executes on the expected interval.
229
+ - Verify persistent task registration rejects anonymous handlers.
230
+ - Verify persistent task scheduling rejects unregistered handlers and non-JSON payloads.
231
+ - Verify persistent delay/once runtime behavior only with disposable or explicitly supplied Redis: schedule a task, start or restart the worker, assert the payload is handled, and clean up `gorig:cronx:persist:*` keys.