fix: route issues
This commit is contained in:
@@ -7,10 +7,12 @@ import (
|
|||||||
|
|
||||||
"github.com/iancoleman/strcase"
|
"github.com/iancoleman/strcase"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
"go.ipao.vip/atomctl/v2/pkg/utils/gomod"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RenderBuildOpts struct {
|
type RenderBuildOpts struct {
|
||||||
PackageName string
|
PackageName string
|
||||||
|
ModuleName string
|
||||||
ProjectPackage string
|
ProjectPackage string
|
||||||
Routes []RouteDefinition
|
Routes []RouteDefinition
|
||||||
}
|
}
|
||||||
@@ -19,6 +21,7 @@ func buildRenderData(opts RenderBuildOpts) (RenderData, error) {
|
|||||||
rd := RenderData{
|
rd := RenderData{
|
||||||
PackageName: opts.PackageName,
|
PackageName: opts.PackageName,
|
||||||
ProjectPackage: opts.ProjectPackage,
|
ProjectPackage: opts.ProjectPackage,
|
||||||
|
ModuleName: gomod.GetModuleName(),
|
||||||
Imports: []string{},
|
Imports: []string{},
|
||||||
Controllers: []string{},
|
Controllers: []string{},
|
||||||
Routes: make(map[string][]Router),
|
Routes: make(map[string][]Router),
|
||||||
@@ -147,7 +150,6 @@ func buildParamToken(item ParamDefinition) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func scalarSuffix(t string) string {
|
func scalarSuffix(t string) string {
|
||||||
switch t {
|
switch t {
|
||||||
case "string", "int", "int8", "int16", "int32", "int64",
|
case "string", "int", "int8", "int16", "int32", "int64",
|
||||||
|
|||||||
9
pkg/ast/route/manual.go.tpl
Normal file
9
pkg/ast/route/manual.go.tpl
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package {{.PackageName}}
|
||||||
|
|
||||||
|
func (r *Routes) Path() string {
|
||||||
|
return "/{{.PackageName}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Routes) Middlewares() []any{
|
||||||
|
return []any{}
|
||||||
|
}
|
||||||
@@ -11,8 +11,12 @@ import (
|
|||||||
//go:embed router.go.tpl
|
//go:embed router.go.tpl
|
||||||
var routeTpl string
|
var routeTpl string
|
||||||
|
|
||||||
|
//go:embed manual.go.tpl
|
||||||
|
var routeManualTpl string
|
||||||
|
|
||||||
type RenderData struct {
|
type RenderData struct {
|
||||||
PackageName string
|
PackageName string
|
||||||
|
ModuleName string
|
||||||
ProjectPackage string
|
ProjectPackage string
|
||||||
Imports []string
|
Imports []string
|
||||||
Controllers []string
|
Controllers []string
|
||||||
@@ -31,9 +35,11 @@ type Router struct {
|
|||||||
|
|
||||||
func Render(path string, routes []RouteDefinition) error {
|
func Render(path string, routes []RouteDefinition) error {
|
||||||
routePath := filepath.Join(path, "routes.gen.go")
|
routePath := filepath.Join(path, "routes.gen.go")
|
||||||
|
routeManualPath := filepath.Join(path, "routes.manual.go")
|
||||||
|
|
||||||
data, err := buildRenderData(RenderBuildOpts{
|
data, err := buildRenderData(RenderBuildOpts{
|
||||||
PackageName: filepath.Base(path),
|
PackageName: filepath.Base(path),
|
||||||
|
ModuleName: gomod.GetModuleName(),
|
||||||
ProjectPackage: gomod.GetModuleName(),
|
ProjectPackage: gomod.GetModuleName(),
|
||||||
Routes: routes,
|
Routes: routes,
|
||||||
})
|
})
|
||||||
@@ -49,5 +55,15 @@ func Render(path string, routes []RouteDefinition) error {
|
|||||||
if err := os.WriteFile(routePath, out, 0o644); err != nil {
|
if err := os.WriteFile(routePath, out, 0o644); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// if routes.manual.go not exists then create it
|
||||||
|
if _, err := os.Stat(routeManualPath); os.IsNotExist(err) {
|
||||||
|
manualOut, err := renderManualTemplate(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(routeManualPath, manualOut, 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type TemplateInfo struct {
|
|||||||
// RouteRenderer implements TemplateRenderer for route generation
|
// RouteRenderer implements TemplateRenderer for route generation
|
||||||
type RouteRenderer struct {
|
type RouteRenderer struct {
|
||||||
template *template.Template
|
template *template.Template
|
||||||
|
manualTemplate *template.Template
|
||||||
info TemplateInfo
|
info TemplateInfo
|
||||||
logger *log.Entry
|
logger *log.Entry
|
||||||
}
|
}
|
||||||
@@ -54,6 +55,10 @@ func NewRouteRenderer() *RouteRenderer {
|
|||||||
renderer.logger.WithError(err).Error("Failed to initialize template")
|
renderer.logger.WithError(err).Error("Failed to initialize template")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if err := renderer.initializeManualTemplate(); err != nil {
|
||||||
|
renderer.logger.WithError(err).Error("Failed to initialize manual template")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
renderer.info.Size = len(routeTpl)
|
renderer.info.Size = len(routeTpl)
|
||||||
renderer.logger.WithFields(log.Fields{
|
renderer.logger.WithFields(log.Fields{
|
||||||
@@ -64,6 +69,22 @@ func NewRouteRenderer() *RouteRenderer {
|
|||||||
return renderer
|
return renderer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RouteRenderer) initializeManualTemplate() error {
|
||||||
|
// Create template with sprig functions and custom options
|
||||||
|
tmpl := template.New(r.info.Name + "manual").
|
||||||
|
Funcs(sprig.FuncMap()).
|
||||||
|
Option("missingkey=error")
|
||||||
|
|
||||||
|
// Parse the template
|
||||||
|
parsedTmpl, err := tmpl.Parse(routeManualTpl)
|
||||||
|
if err != nil {
|
||||||
|
return WrapError(err, "failed to parse route template")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.manualTemplate = parsedTmpl
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// initializeTemplate sets up the template with proper functions and options
|
// initializeTemplate sets up the template with proper functions and options
|
||||||
func (r *RouteRenderer) initializeTemplate() error {
|
func (r *RouteRenderer) initializeTemplate() error {
|
||||||
// Create template with sprig functions and custom options
|
// Create template with sprig functions and custom options
|
||||||
@@ -81,6 +102,41 @@ func (r *RouteRenderer) initializeTemplate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render renders the template with the provided data
|
||||||
|
func (r *RouteRenderer) RenderManual(data RenderData) ([]byte, error) {
|
||||||
|
// Validate input data
|
||||||
|
if err := r.validateRenderData(data); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create buffer for rendering
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.Grow(estimatedBufferSize(data)) // Pre-allocate buffer for better performance
|
||||||
|
|
||||||
|
// Execute template with error handling
|
||||||
|
if err := r.manualTemplate.Execute(&buf, data); err != nil {
|
||||||
|
r.logger.WithError(err).WithFields(log.Fields{
|
||||||
|
"package_name": data.PackageName,
|
||||||
|
"routes_count": len(data.Routes),
|
||||||
|
}).Error("Template execution failed")
|
||||||
|
return nil, WrapError(err, "template execution failed for package: %s", data.PackageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rendered content
|
||||||
|
result := buf.Bytes()
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, NewRouteError(ErrTemplateFailed, "rendered content is empty for package: %s", data.PackageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.logger.WithFields(log.Fields{
|
||||||
|
"package_name": data.PackageName,
|
||||||
|
"routes_count": len(data.Routes),
|
||||||
|
"content_length": len(result),
|
||||||
|
}).Debug("Template rendered successfully")
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Render renders the template with the provided data
|
// Render renders the template with the provided data
|
||||||
func (r *RouteRenderer) Render(data RenderData) ([]byte, error) {
|
func (r *RouteRenderer) Render(data RenderData) ([]byte, error) {
|
||||||
// Validate input data
|
// Validate input data
|
||||||
@@ -152,10 +208,20 @@ func (r *RouteRenderer) validateRenderData(data RenderData) error {
|
|||||||
|
|
||||||
for i, route := range routes {
|
for i, route := range routes {
|
||||||
if route.Method == "" {
|
if route.Method == "" {
|
||||||
return NewRouteError(ErrInvalidInput, "route method cannot be empty for controller %s, route %d", controllerName, i)
|
return NewRouteError(
|
||||||
|
ErrInvalidInput,
|
||||||
|
"route method cannot be empty for controller %s, route %d",
|
||||||
|
controllerName,
|
||||||
|
i,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if route.Route == "" {
|
if route.Route == "" {
|
||||||
return NewRouteError(ErrInvalidInput, "route path cannot be empty for controller %s, route %d", controllerName, i)
|
return NewRouteError(
|
||||||
|
ErrInvalidInput,
|
||||||
|
"route path cannot be empty for controller %s, route %d",
|
||||||
|
controllerName,
|
||||||
|
i,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,3 +255,11 @@ func renderTemplate(data RenderData) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return renderer.Render(data)
|
return renderer.Render(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func renderManualTemplate(data RenderData) ([]byte, error) {
|
||||||
|
renderer := NewRouteRenderer()
|
||||||
|
if renderer == nil {
|
||||||
|
return nil, NewRouteError(ErrTemplateFailed, "failed to create route renderer")
|
||||||
|
}
|
||||||
|
return renderer.RenderManual(data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
{{.}}
|
{{.}}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
"{{.ModuleName}}/app/middlewares"
|
||||||
|
|
||||||
. "go.ipao.vip/atom/fen"
|
. "go.ipao.vip/atom/fen"
|
||||||
_ "go.ipao.vip/atom"
|
_ "go.ipao.vip/atom"
|
||||||
_ "go.ipao.vip/atom/contracts"
|
_ "go.ipao.vip/atom/contracts"
|
||||||
@@ -23,6 +25,8 @@ import (
|
|||||||
// @provider contracts.HttpRoute atom.GroupRoutes
|
// @provider contracts.HttpRoute atom.GroupRoutes
|
||||||
type Routes struct {
|
type Routes struct {
|
||||||
log *log.Entry `inject:"false"`
|
log *log.Entry `inject:"false"`
|
||||||
|
middlewares *middlewares.Middlewares
|
||||||
|
|
||||||
{{- if .Controllers }}
|
{{- if .Controllers }}
|
||||||
// Controller instances
|
// Controller instances
|
||||||
{{- range .Controllers }}
|
{{- range .Controllers }}
|
||||||
@@ -54,7 +58,7 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
{{- range $value }}
|
{{- range $value }}
|
||||||
{{- if .Route }}
|
{{- if .Route }}
|
||||||
r.log.Debugf("Registering route: {{.Method}} {{.Route}} -> {{.Controller}}.{{.Action}}")
|
r.log.Debugf("Registering route: {{.Method}} {{.Route}} -> {{.Controller}}.{{.Action}}")
|
||||||
router.{{.Method}}("{{.Route}}", {{.Func}}(
|
router.{{.Method}}("{{.Route}}"[len(r.Path()):], {{.Func}}(
|
||||||
r.{{.Controller}}.{{.Action}},
|
r.{{.Controller}}.{{.Action}},
|
||||||
{{- if .Params }}
|
{{- if .Params }}
|
||||||
{{- range .Params }}
|
{{- range .Params }}
|
||||||
|
|||||||
207
templates/project/llm.txt.raw
Normal file
207
templates/project/llm.txt.raw
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# Backend Dev Rules (HTTP API + Model)
|
||||||
|
|
||||||
|
This file condenses `backend/docs/dev/http_api.md` + `backend/docs/dev/model.md` into a checklist/rule format for LLMs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0) Golden rules (DO / DO NOT)
|
||||||
|
|
||||||
|
- DO follow existing module layout under `backend/app/http/<module>/`.
|
||||||
|
- DO keep controller methods thin: parse/bind → call `services.*` → return result/error.
|
||||||
|
- DO regenerate code after changes (routes/docs/models).
|
||||||
|
- DO NOT manually edit generated files:
|
||||||
|
- `backend/app/http/**/routes.gen.go`
|
||||||
|
- `backend/app/http/**/provider.gen.go`
|
||||||
|
- `backend/docs/docs.go`
|
||||||
|
- DO keep Swagger annotations consistent with actual Fiber route paths (including `:param`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Add a new HTTP API endpoint
|
||||||
|
|
||||||
|
### 1.1 Where code lives
|
||||||
|
|
||||||
|
- Controllers: `backend/app/http/<module>/*.go`
|
||||||
|
- Example module: `backend/app/http/super/tenant.go`, `backend/app/http/super/user.go`
|
||||||
|
- DTOs: `backend/app/http/<module>/dto/*`
|
||||||
|
- Routes (generated): `backend/app/http/<module>/routes.gen.go`
|
||||||
|
- Swagger output (generated): `backend/docs/swagger.yaml`, `backend/docs/swagger.json`, `backend/docs/docs.go`
|
||||||
|
|
||||||
|
### 1.2 Controller method signatures
|
||||||
|
|
||||||
|
- “Return data” endpoints: return `(<T>, error)`
|
||||||
|
- Example: `(*requests.Pager, error)` for paginated list
|
||||||
|
- “No data” endpoints: return `error`
|
||||||
|
|
||||||
|
### 1.3 Swagger annotations (minimum set)
|
||||||
|
|
||||||
|
Place above the handler function:
|
||||||
|
|
||||||
|
- `@Summary`
|
||||||
|
- `@Tags`
|
||||||
|
- `@Accept json`
|
||||||
|
- `@Produce json`
|
||||||
|
- `@Param` (query/path/body as needed)
|
||||||
|
- `@Success` for 200 responses
|
||||||
|
- `@Router <path> [get|post|patch|delete|put]`
|
||||||
|
- `@Bind` for parameters (see below)
|
||||||
|
|
||||||
|
Common `@Success` patterns:
|
||||||
|
|
||||||
|
- Paginated list: `requests.Pager{items=dto.Item}`
|
||||||
|
- Single object: `dto.Item`
|
||||||
|
- Array: `{array} dto.Item`
|
||||||
|
|
||||||
|
### 1.4 Parameter binding (@Bind)
|
||||||
|
|
||||||
|
Format:
|
||||||
|
|
||||||
|
`@Bind <paramName> <position> [key(<key>)] [model(<field>|<type>[:<field>])]`
|
||||||
|
|
||||||
|
Positions:
|
||||||
|
|
||||||
|
- `path`, `query`, `body`, `header`, `cookie`, `local`, `file`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `paramName` MUST match function parameter name (case-sensitive).
|
||||||
|
- Default key name is `paramName` ; override via `key(...)`.
|
||||||
|
- Scalar types: `string/int/int32/int64/float32/float64/bool`.
|
||||||
|
- Pointer types are supported (framework will handle deref for most positions).
|
||||||
|
|
||||||
|
#### Model binding (path-only)
|
||||||
|
|
||||||
|
Used to bind a model instance from a path value:
|
||||||
|
|
||||||
|
- `model(id)` (recommended)
|
||||||
|
- `model(id:int)` / `model(code:string)`
|
||||||
|
- `model(pkg.Type:field)` or `model(pkg.Type)` (default field is `id`)
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
|
||||||
|
- Generated binder queries by field and returns first row as the parameter value.
|
||||||
|
- Auto-imports field helper for query building.
|
||||||
|
|
||||||
|
### 1.5 Generate routes + providers + swagger docs
|
||||||
|
|
||||||
|
Run from `backend/`:
|
||||||
|
|
||||||
|
- Generate routes: `atomctl gen route`
|
||||||
|
- Generate providers: `atomctl gen provider`
|
||||||
|
- Generate swagger docs: `atomctl swag init`
|
||||||
|
|
||||||
|
### 1.6 Local verify
|
||||||
|
|
||||||
|
- Build/run: `make run`
|
||||||
|
- Use REST client examples: `backend/test/[module]/[controller].http` (extend it for new endpoints)
|
||||||
|
|
||||||
|
### 1.7 Testing
|
||||||
|
|
||||||
|
- Prefer existing test style under `backend/tests/e2e`.
|
||||||
|
- Run: `make test`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Add / update a DB model
|
||||||
|
|
||||||
|
Models live in:
|
||||||
|
|
||||||
|
- `backend/database/models/*` (generated model code + optional manual extensions)
|
||||||
|
|
||||||
|
### 2.1 Migration → model generation workflow
|
||||||
|
|
||||||
|
1) Create migration:
|
||||||
|
|
||||||
|
- `atomctl migrate create alter_table` or `atomctl migrate create create_table`
|
||||||
|
|
||||||
|
2) Edit migration:
|
||||||
|
|
||||||
|
- No explicit `BEGIN/COMMIT` needed (framework handles).
|
||||||
|
- Table name should be plural (e.g. `tenants`).
|
||||||
|
|
||||||
|
3) Apply migration:
|
||||||
|
|
||||||
|
- `atomctl migrate up`
|
||||||
|
|
||||||
|
4) Map complex field types (JSON/ARRAY/UUID/…) via transform file:
|
||||||
|
|
||||||
|
- `backend/database/.transform.yaml` → `field_type.<table>`
|
||||||
|
|
||||||
|
5) Generate models:
|
||||||
|
|
||||||
|
- `atomctl gen model`
|
||||||
|
|
||||||
|
### 2.2 Enum strategy
|
||||||
|
|
||||||
|
- DO NOT use native DB ENUM.
|
||||||
|
- Define enums in Go under `backend/pkg/consts/<table>.go`, example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// swagger:enum UserStatus
|
||||||
|
// ENUM(pending_verify, verified, banned, )
|
||||||
|
type UserStatus string
|
||||||
|
```
|
||||||
|
|
||||||
|
- Generate enum code: `atomctl gen enum`
|
||||||
|
|
||||||
|
### 2.3 Supported field types (`gen/types/`)
|
||||||
|
|
||||||
|
`backend/database/.transform.yaml` typically imports `go.ipao.vip/gen` so you can use `types.*` in `field_type`.
|
||||||
|
|
||||||
|
Common types:
|
||||||
|
|
||||||
|
- JSON: `types.JSON`, `types.JSONMap`, `types.JSONType[T]`, `types.JSONSlice[T]`
|
||||||
|
- Array: `types.Array[T]`
|
||||||
|
- UUID: `types.UUID`, `types.BinUUID`
|
||||||
|
- Date/Time: `types.Date`, `types.Time`
|
||||||
|
- Money/XML/URL/Binary: `types.Money`, `types.XML`, `types.URL`, `types.HexBytes`
|
||||||
|
- Bit string: `types.BitString`
|
||||||
|
- Network: `types.Inet`, `types.CIDR`, `types.MACAddr`
|
||||||
|
- Ranges: `types.Int4Range`, `types.Int8Range`, `types.NumRange`, `types.TsRange`, `types.TstzRange`, `types.DateRange`
|
||||||
|
- Geometry: `types.Point`, `types.Polygon`, `types.Box`, `types.Circle`, `types.Path`
|
||||||
|
- Fulltext: `types.TSQuery`, `types.TSVector`
|
||||||
|
- Nullable: `types.Null[T]` and aliases (requires DB NULL)
|
||||||
|
|
||||||
|
Reference:
|
||||||
|
|
||||||
|
- Detailed examples: `gen/types/README.md`
|
||||||
|
|
||||||
|
### 2.4 Relationships (GORM-aligned) via `.transform.yaml`
|
||||||
|
|
||||||
|
Define in `field_relate.<table>.<FieldName>`:
|
||||||
|
|
||||||
|
- `relation`: `belongs_to` | `has_one` | `has_many` | `many_to_many`
|
||||||
|
- `table`: target table
|
||||||
|
- `pivot`: join table (many_to_many only)
|
||||||
|
- `foreign_key`, `references`
|
||||||
|
- `join_foreign_key`, `join_references` (many_to_many only)
|
||||||
|
- `json`: JSON field name in API outputs
|
||||||
|
|
||||||
|
Generator will convert snake_case columns to Go struct field names (e.g. `class_id` → `ClassID`).
|
||||||
|
|
||||||
|
### 2.5 Extending generated models
|
||||||
|
|
||||||
|
- Add manual methods/hooks by creating `backend/database/models/<table>.go`.
|
||||||
|
- Keep generated files untouched ; put custom logic only in your own file(s).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Service layer injection (when adding services)
|
||||||
|
|
||||||
|
- Services are in `backend/app/services`.
|
||||||
|
- After creating/updating a service provider, regenerate wiring:
|
||||||
|
- `atomctl gen service`
|
||||||
|
- `atomctl gen provider`
|
||||||
|
- Service call conventions:
|
||||||
|
- **Service-to-service (inside `services` package)**: call directly as `CamelCaseServiceStructName.Method()` (no `services.` prefix).
|
||||||
|
- **From outside (controllers/handlers/etc.)**: call via the package entrypoint `services.CamelCaseServiceStructName.Method()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Quick command summary (run in `backend/`)
|
||||||
|
|
||||||
|
- `make run` / `make build` / `make test`
|
||||||
|
- `atomctl gen route` / `atomctl gen provider` / `atomctl swag init`
|
||||||
|
- `atomctl migrate create ...` / `atomctl migrate up`
|
||||||
|
- `atomctl gen model` / `atomctl gen enum` / `atomctl gen service`
|
||||||
|
- `make init` (full refresh)
|
||||||
Reference in New Issue
Block a user