{"id":34914506,"url":"https://github.com/ilxqx/vef-framework-go","last_synced_at":"2026-01-27T09:19:16.887Z","repository":{"id":315250581,"uuid":"1058580611","full_name":"ilxqx/vef-framework-go","owner":"ilxqx","description":"A comprehensive, enterprise-grade web framework for Go that accelerates the development of   scalable, maintainable applications","archived":false,"fork":false,"pushed_at":"2026-01-26T09:22:34.000Z","size":2254,"stargazers_count":8,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-26T23:41:08.187Z","etag":null,"topics":["api","backend","backend-development","crud","database","fiber","framework","go","golang","middleware","orm","validation","web-development","web-framework"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ilxqx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"security/cached_role_permission_loader.go","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-17T09:23:12.000Z","updated_at":"2026-01-26T09:11:37.000Z","dependencies_parsed_at":"2025-10-04T18:20:51.445Z","dependency_job_id":"7a56fa57-141d-463f-91bd-6384a6bde449","html_url":"https://github.com/ilxqx/vef-framework-go","commit_stats":null,"previous_names":["ilxqx/vef-framework-go"],"tags_count":60,"template":false,"template_full_name":null,"purl":"pkg:github/ilxqx/vef-framework-go","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilxqx%2Fvef-framework-go","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilxqx%2Fvef-framework-go/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilxqx%2Fvef-framework-go/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilxqx%2Fvef-framework-go/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ilxqx","download_url":"https://codeload.github.com/ilxqx/vef-framework-go/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ilxqx%2Fvef-framework-go/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28810476,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-27T07:41:26.337Z","status":"ssl_error","status_checked_at":"2026-01-27T07:41:08.776Z","response_time":168,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","backend","backend-development","crud","database","fiber","framework","go","golang","middleware","orm","validation","web-development","web-framework"],"created_at":"2025-12-26T12:02:22.142Z","updated_at":"2026-01-27T09:19:16.873Z","avatar_url":"https://github.com/ilxqx.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# VEF Framework Go\n\n📖 [English](./README.md) | [简体中文](./README.zh-CN.md)\n\n[![GitHub Release](https://img.shields.io/github/v/release/ilxqx/vef-framework-go)](https://github.com/ilxqx/vef-framework-go/releases)\n[![Build Status](https://img.shields.io/github/actions/workflow/status/ilxqx/vef-framework-go/test.yml?branch=main)](https://github.com/ilxqx/vef-framework-go/actions/workflows/test.yml)\n[![Coverage](https://img.shields.io/codecov/c/github/ilxqx/vef-framework-go)](https://codecov.io/gh/ilxqx/vef-framework-go)\n[![Go Reference](https://pkg.go.dev/badge/github.com/ilxqx/vef-framework-go.svg)](https://pkg.go.dev/github.com/ilxqx/vef-framework-go)\n[![Go Report Card](https://goreportcard.com/badge/github.com/ilxqx/vef-framework-go)](https://goreportcard.com/report/github.com/ilxqx/vef-framework-go)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ilxqx/vef-framework-go)\n[![License](https://img.shields.io/github/license/ilxqx/vef-framework-go)](https://github.com/ilxqx/vef-framework-go/blob/main/LICENSE)\n\nA modern Go web development framework built on Uber FX dependency injection and Fiber, designed for rapid enterprise application development with opinionated conventions and comprehensive built-in features.\n\n## ⚠️ Development Status \u0026 Stability Notice\n\n\u003e **Important**: VEF Framework Go is under active development and has not yet reached a stable 1.0 release. While the framework is currently in a functionally stable state, breaking changes may occur as we refine best practices and improve conventions. We strive to minimize disruption, but architectural improvements sometimes require non-backward-compatible updates. Please exercise caution when using this framework in production environments and be prepared to handle migration efforts for major version updates.\n\n## Features\n\n- **RPC + REST Api Routing** - RPC requests via `POST /api`, REST requests via standard HTTP methods under `/api/\u003cresource\u003e`\n- **Generic CRUD Apis** - Pre-built type-safe CRUD operations with minimal boilerplate\n- **Type-Safe ORM** - Bun-based ORM with fluent query builder and automatic audit tracking\n- **Multi-Strategy Authentication** - Jwt, OpenApi signature, and password authentication out of the box\n- **Modular Design** - Uber FX dependency injection with pluggable modules\n- **Built-in Features** - Cache, event bus, cron scheduler, object storage, data validation, i18n\n- **RBAC \u0026 Data Permissions** - Row-level security with customizable data scopes\n\n## Quick Start\n\n### Installation\n\n```bash\ngo get github.com/ilxqx/vef-framework-go\n```\n\n**Requirements:** Go 1.25.0 or higher\n\n**Troubleshooting:** If you encounter ambiguous import errors with `google.golang.org/genproto` during `go mod tidy`, run:\n\n```bash\ngo get google.golang.org/genproto@latest\ngo mod tidy\n```\n\n### Minimal Example\n\nCreate `main.go`:\n\n```go\npackage main\n\nimport \"github.com/ilxqx/vef-framework-go\"\n\nfunc main() {\n    vef.Run()\n}\n```\n\nCreate `configs/application.toml`:\n\n```toml\n[vef.app]\nname = \"my-app\"\nport = 8080\n\n[vef.datasource]\ntype = \"postgres\"\nhost = \"localhost\"\nport = 5432\nuser = \"postgres\"\npassword = \"password\"\ndatabase = \"mydb\"\nschema = \"public\"\n```\n\nRun the application:\n\n```bash\ngo run main.go\n```\n\nYour Api server is now running at `http://localhost:8080`.\n\n## Project Structure\n\n### Recommended Module Organization\n\nVEF Framework applications follow a modular architecture pattern where business domains are organized into self-contained modules. This pattern is demonstrated in production applications and provides clear separation of concerns.\n\n**Directory Structure:**\n\n```\nmy-app/\n├── cmd/\n│   └── server/\n│       └── main.go           # Application entry - composes all modules\n├── configs/\n│   └── application.toml       # Configuration file\n└── internal/\n    ├── auth/                  # Authentication providers\n    │   ├── module.go          # Auth module definition\n    │   ├── user_loader.go     # UserLoader implementation\n    │   └── user_info_loader.go\n    ├── sys/                   # System/admin features\n    │   ├── models/            # Data models\n    │   ├── payloads/          # API parameters\n    │   ├── resources/         # API resources\n    │   ├── schemas/           # Generated from models (via vef-cli)\n    │   └── module.go          # System module definition\n    ├── [domain]/              # Business domains (e.g., order, inventory)\n    │   ├── models/\n    │   ├── payloads/\n    │   ├── resources/\n    │   ├── schemas/\n    │   └── module.go\n    ├── vef/                   # VEF framework integrations\n    │   ├── module.go\n    │   ├── build_info.go      # Generated build metadata\n    │   ├── *_subscriber.go    # Event subscribers\n    │   └── *_loader.go        # Data loaders\n    └── web/                   # SPA frontend integration (optional)\n        ├── dist/              # Static assets\n        └── module.go\n```\n\n### Module Composition\n\nEach module exports a `vef.Module()` that encapsulates its dependencies and resources. The main.go composes these modules in dependency order:\n\n```go\npackage main\n\nimport (\n    \"github.com/ilxqx/vef-framework-go\"\n    \"my-app/internal/auth\"\n    \"my-app/internal/sys\"\n    ivef \"my-app/internal/vef\"\n    \"my-app/internal/web\"\n)\n\nfunc main() {\n    vef.Run(\n        ivef.Module,     // Framework integrations (your app's vef module)\n        web.Module,      // SPA serving (optional)\n        auth.Module,     // Authentication providers\n        sys.Module,      // System resources\n        // Add your business domain modules here\n    )\n}\n```\n\n**Module Definition Example:**\n\n```go\n// internal/sys/module.go\npackage sys\n\nimport (\n    \"github.com/ilxqx/vef-framework-go\"\n    \"my-app/internal/sys/resources\"\n)\n\nvar Module = vef.Module(\n    \"app:sys\",\n    vef.ProvideApiResource(resources.NewUserResource),\n    vef.ProvideApiResource(resources.NewRoleResource),\n    // Register other resources and services\n)\n```\n\n**Benefits of this pattern:**\n- **Clear boundaries**: Each module owns its models, APIs, and business logic\n- **Testability**: Modules can be tested independently\n- **Scalability**: Easy to add new domains without affecting existing code\n- **Maintainability**: Changes are localized to specific modules\n\n## Architecture\n\n### RPC and REST Routing\n\nVEF supports two routing strategies that can be used side by side:\n\n- **RPC**: Single endpoint `POST /api` with a unified request/response format (example below)\n- **REST**: Standard HTTP verbs under `/api/\u003cresource\u003e` (default base path). External apps can still authenticate with OpenApi signatures on these endpoints.\n\n**RPC Request Format:**\n\n```json\n{\n  \"resource\": \"sys/user\",\n  \"action\": \"find_page\",\n  \"version\": \"v1\",\n  \"params\": {\n    \"keyword\": \"john\"\n  },\n  \"meta\": {\n    \"page\": 1,\n    \"size\": 20\n  }\n}\n```\n\n**RPC Response Format:**\n\n```json\n{\n  \"code\": 0,\n  \"message\": \"Success\",\n  \"data\": {\n    \"page\": 1,\n    \"size\": 20,\n    \"total\": 100,\n    \"items\": [...]\n  }\n}\n```\n\n**REST Example (same base path):**\n\n```\nGET /api/sys/user/page?page=1\u0026size=20\u0026keyword=john\n```\n\nParams vs Meta:\n- `params` carries business data (e.g., search filters, create/update fields). Define your structs embedding `api.P`.\n- `meta` carries request-level options (e.g., pagination for `find_page`, export/import format). Define your structs embedding `api.M` (e.g., `page.Pageable`).\n  - For REST, `params` can come from path/query/body and `meta` can be provided via `X-Meta-*` headers.\n\n### Dependency Injection\n\nVEF leverages Uber FX for dependency injection. Register components using helper functions:\n\n```go\nvef.Run(\n    vef.ProvideApiResource(NewUserResource),\n    vef.Provide(NewUserService),\n)\n```\n\n## Defining Models\n\nAll models should embed `orm.Model` for automatic audit field management:\n\n```go\npackage models\n\nimport (\n    \"github.com/ilxqx/vef-framework-go/null\"\n    \"github.com/ilxqx/vef-framework-go/orm\"\n)\n\ntype User struct {\n    orm.BaseModel `bun:\"table:sys_user,alias:su\"`\n    orm.Model     \n    \n    Username string      `json:\"username\" validate:\"required,alphanum,max=32\" label:\"Username\"`\n    Email    null.String `json:\"email\" validate:\"omitempty,email,max=64\" label:\"Email\"`\n    IsActive bool        `json:\"isActive\"`\n}\n```\n\n**Field Tags:**\n\n- `bun` - Bun ORM configuration (table name, column mapping, relations)\n- `json` - JSON serialization name\n- `validate` - Validation rules ([go-playground/validator](https://github.com/go-playground/validator))\n- `label` - Human-readable field name for error messages\n\n**Audit Fields** (automatically maintained by `orm.Model`):\n\n- `id` - Primary key (20-character XID in base32 encoding)\n- `created_at`, `created_by` - Creation timestamp and user ID\n- `created_by_name` - Creator name (scan-only, not stored in database)\n- `updated_at`, `updated_by` - Last update timestamp and user ID\n- `updated_by_name` - Updater name (scan-only, not stored in database)\n\nNote: Database columns use snake_case (e.g., `created_at`), while JSON fields use camelCase (e.g., `createdAt`) as shown in the model tags.\n\n**Null Types:** Use `null.String`, `null.Int`, `null.Bool`, etc. for nullable fields.\n\n### Field Types for Boolean Columns\n\nChoosing the right type depends on your target database and whether you need tri‑state (NULL) semantics.\n\nKey guidance:\n- Prefer `bool` in most cases. Modern mainstream databases natively support boolean types, and plain `bool` maps well.\n- Use `sql.Bool` when you need to store booleans as numeric types (e.g., tinyint/smallint with 0/1) for databases that lack native boolean or when you explicitly require numeric storage for compatibility.\n- Use `null.Bool` when you need tri‑state: NULL, false, true. It serializes to database values as NULL/1/0.\n\nDecision guide:\n\n| Use Case | Preferred Type | Database Column |\n|----------|----------------|-----------------|\n| Non‑nullable boolean on DBs with native boolean | `bool` | boolean/true native type |\n| Nullable boolean (tri‑state) | `null.Bool` | boolean or numeric (often smallint/tinyint) |\n| Target DB without native boolean or require numeric 0/1 storage | `sql.Bool` (non‑null) / `null.Bool` (nullable) | smallint/tinyint with 0/1 |\n| Go‑only/computed (not stored) | `bool` with `bun:\"-\"` | N/A |\n\nType details and examples:\n\n1) Plain `bool` — recommended for native boolean columns\n```go\ntype User struct {\n    orm.Model\n    // Database: boolean (native), NOT NULL as needed\n    IsActive bool `json:\"isActive\"` // bun tag usually not required when using native boolean\n}\n```\n\n2) `sql.Bool` — numeric 0/1 storage for compatibility\n```go\nimport \"github.com/ilxqx/vef-framework-go/sql\"\n\ntype User struct {\n    orm.Model\n    // Database: numeric boolean (0/1), for DBs without native boolean or enforced numeric schema\n    IsActive sql.Bool `json:\"isActive\" bun:\"type:smallint,notnull,default:0\"`\n    IsLocked sql.Bool `json:\"isLocked\" bun:\"type:smallint,notnull,default:0\"`\n}\n```\nWhen you don’t have to support non‑boolean databases, prefer plain `bool` for simplicity.\n\n3) `null.Bool` — tri‑state (NULL/false/true)\n```go\nimport \"github.com/ilxqx/vef-framework-go/null\"\n\ntype User struct {\n    orm.Model\n    // Database: allows NULL; stored as NULL/0/1 (use numeric column for maximum compatibility)\n    IsVerified null.Bool `json:\"isVerified\" bun:\"type:smallint\"`\n}\n```\nThree‑state logic:\n- `null.Bool{Valid: false}` → NULL in database\n- `null.Bool{Valid: true, Bool: false}` → 0/false\n- `null.Bool{Valid: true, Bool: true}` → 1/true\n\n4) Go‑only fields (not stored)\n```go\ntype User struct {\n    orm.Model\n    Username string `json:\"username\"`\n\n    // Computed field — not stored in database\n    HasPermissions bool `json:\"hasPermissions\" bun:\"-\"`\n}\n```\n\nCommon patterns:\n```go\n// Native boolean DBs (recommended)\ntype UserNative struct {\n    orm.Model\n    IsActive bool      `json:\"isActive\"`\n    IsLocked bool      `json:\"isLocked\"`\n    IsEmailVerified null.Bool `json:\"isEmailVerified\"` // use NULL when needed\n}\n\n// Numeric storage for compatibility\ntype UserNumeric struct {\n    orm.Model\n    IsActive sql.Bool       `json:\"isActive\" bun:\"type:smallint,notnull,default:0\"`\n    IsLocked sql.Bool       `json:\"isLocked\" bun:\"type:smallint,notnull,default:0\"`\n    IsEmailVerified null.Bool `json:\"isEmailVerified\" bun:\"type:smallint\"`\n}\n```\n\n## Building CRUD Apis\n\n### Resource Naming Best Practices\n\nWhen defining API resources, follow a consistent naming convention to avoid conflicts and make API ownership clear.\n\n**Recommended Pattern: `{app}/{domain}/{entity}`**\n\nThis three-level namespace pattern is used in production applications and provides several benefits:\n\n```go\n// Good examples with application namespace (RPC)\napi.NewRPCResource(\"smp/sys/user\")           // System user resource\napi.NewRPCResource(\"smp/md/organization\")    // Master data organization\napi.NewRPCResource(\"erp/order/item\")         // Clear domain separation\n\n// Acceptable for single-app projects\napi.NewRPCResource(\"sys/user\")               // No app namespace\n\n// Avoid - too generic, risks conflicts\napi.NewRPCResource(\"user\")                   // ❌ No namespace\n```\n\n**Note:** RPC resources use `snake_case` segments. For REST resources, use `api.NewRESTResource` and `kebab-case` segments (e.g., `sys/data-dict`).\n\n**Benefits of Application Namespacing:**\n\n- **Conflict Prevention**: Avoids API resource collisions in shared deployments or when merging codebases\n- **Clear Ownership**: Immediately identifies which application owns the resource\n- **Modularity**: Supports multiple applications or microservices using the same framework\n- **Migration Safety**: Easy to identify and migrate resources when restructuring\n\n**Framework Reserved Namespaces:**\n\nThe following resource namespaces are reserved for system APIs and must not be used in custom API definitions:\n\n- `security/auth` - Authentication APIs\n- `sys/storage` - Storage APIs\n- `sys/monitor` - Monitoring APIs\n\nUsing these reserved names will cause application startup failures due to duplicate API definitions.\n\n### Step 1: Define Parameter Structures\n\n**Search Parameters:**\n\n```go\npackage payloads\n\nimport \"github.com/ilxqx/vef-framework-go/api\"\n\ntype UserSearch struct {\n    api.P\n    Keyword string `json:\"keyword\" search:\"contains,column=username|email\"`\n    IsActive *bool `json:\"isActive\" search:\"eq\"`\n}\n```\n\n**Create/Update Parameters:**\n\n```go\ntype UserParams struct {\n    api.P\n    ID       string      `json:\"id\"` // Required for updates\n\n    Username string      `json:\"username\" validate:\"required,alphanum,max=32\" label:\"Username\"`\n    Email    null.String `json:\"email\" validate:\"omitempty,email,max=64\" label:\"Email\"`\n    IsActive bool        `json:\"isActive\"`\n}\n```\n\n**Separate Create and Update Parameters:**\n\nWhen Create and Update operations have different validation requirements, use struct embedding to share common fields while allowing operation-specific validation:\n\n```go\n// Shared fields\ntype UserParams struct {\n    api.P\n    ID       string\n    Username string      `json:\"username\" validate:\"required,alphanum,max=32\" label:\"Username\"`\n    Email    null.String `json:\"email\" validate:\"omitempty,email,max=64\" label:\"Email\"`\n    IsActive bool        `json:\"isActive\"`\n}\n\n// Create requires password\ntype UserCreateParams struct {\n    UserParams      `json:\",inline\"`\n    Password        string `json:\"password\" validate:\"required,min=6,max=16\" label:\"Password\"`\n    PasswordConfirm string `json:\"passwordConfirm\" validate:\"required,eqfield=Password\" label:\"Confirm Password\"`\n}\n\n// Update has optional password\ntype UserUpdateParams struct {\n    UserParams      `json:\",inline\"`\n    Password        null.String `json:\"password\" validate:\"omitempty,min=6,max=16\" label:\"Password\"`\n    PasswordConfirm null.String `json:\"passwordConfirm\" validate:\"omitempty,eqfield=Password\" label:\"Confirm Password\"`\n}\n```\n\nThen use the specific params in your resource:\n\n```go\nCreate: apis.NewCreate[models.User, payloads.UserCreateParams](),\nUpdate: apis.NewUpdate[models.User, payloads.UserUpdateParams](),\n```\n\n**Benefits:**\n- **Type-safe validation**: Different rules for Create vs Update (required vs optional password)\n- **Clear contracts**: API requirements are explicit in code\n- **Better error messages**: Validation errors match the operation's actual requirements\n- **Code reuse**: Common fields are defined once and embedded\n\n### Step 2: Create Api Resource\n\n\u003e **⚠️ IMPORTANT: Reserved System API Namespaces**\n\u003e\n\u003e The framework reserves the following resource namespaces for system APIs. **DO NOT** use these resource names in your custom API definitions, as they will conflict with built-in framework functionality and cause application startup failures:\n\u003e\n\u003e - `security/auth` - Authentication APIs (login, logout, refresh, get_user_info)\n\u003e - `sys/storage` - Storage APIs (upload, get_presigned_url, delete_temp, stat, list)\n\u003e - `sys/monitor` - Monitoring APIs (get_overview, get_cpu, get_memory, get_disk, etc.)\n\u003e\n\u003e The framework automatically detects duplicate API definitions and will fail to start if conflicts are found. Use custom resource namespaces like `app/`, `custom/`, or your own domain-specific prefixes to avoid conflicts.\n\n```go\npackage resources\n\nimport (\n    \"github.com/ilxqx/vef-framework-go/api\"\n    \"github.com/ilxqx/vef-framework-go/apis\"\n)\n\ntype UserResource struct {\n    api.Resource\n    apis.FindAll[models.User, payloads.UserSearch]\n    apis.FindPage[models.User, payloads.UserSearch]\n    apis.Create[models.User, payloads.UserParams]\n    apis.Update[models.User, payloads.UserParams]\n    apis.Delete[models.User]\n}\n\nfunc NewUserResource() api.Resource {\n    return \u0026UserResource{\n        Resource: api.NewRPCResource(\"smp/sys/user\"),  // ✓ Use app/domain/entity to avoid conflicts\n        FindAll: apis.NewFindAll[models.User, payloads.UserSearch](),\n        FindPage: apis.NewFindPage[models.User, payloads.UserSearch](),\n        Create: apis.NewCreate[models.User, payloads.UserParams](),\n        Update: apis.NewUpdate[models.User, payloads.UserParams](),\n        Delete: apis.NewDelete[models.User](),\n    }\n}\n```\n\n### Step 3: Register Resource\n\n```go\nfunc main() {\n    vef.Run(\n        vef.ProvideApiResource(resources.NewUserResource),\n    )\n}\n```\n\n### Pre-built Apis\n\n| Api | Description | Action |\n|-----|-------------|--------|\n| FindOne | Find single record | find_one |\n| FindAll | Find all records | find_all |\n| FindPage | Paginated query | find_page |\n| Create | Create record | create |\n| Update | Update record | update |\n| Delete | Delete record | delete |\n| CreateMany | Batch create | create_many |\n| UpdateMany | Batch update | update_many |\n| DeleteMany | Batch delete | delete_many |\n| FindTree | Hierarchical query | find_tree |\n| FindOptions | Options list (label/value) | find_options |\n| FindTreeOptions | Tree options | find_tree_options |\n| Import | Import from Excel/CSV | import |\n| Export | Export to Excel/CSV | export |\n\n**Note:** The actions above are **RPC** action names. For **REST** resources, actions are expressed as HTTP methods and sub-paths (e.g., `GET /`, `GET /page`, `POST /`, `PUT /:id`).\n\n### Api Builder Methods\n\nConfigure Api behavior with fluent builder methods:\n\n```go\nCreate: apis.NewCreate[User, UserParams]().\n    Action(\"create_user\").             // Custom action name\n    Public().                          // No authentication required\n    PermToken(\"sys.user.create\").      // Permission token\n    EnableAudit().                     // Enable audit logging\n    Timeout(10 * time.Second).         // Request timeout\n    RateLimit(10, 1*time.Minute).      // 10 requests per minute\n```\n\n**Note:** FindApi types (FindOne, FindAll, FindPage, FindTree, FindOptions, FindTreeOptions, Export) have additional configuration methods. See [FindApi Configuration Methods](#findapi-configuration-methods) for details.\n\n### FindApi Configuration Methods\n\nAll FindApi types (FindOne, FindAll, FindPage, FindTree, FindOptions, FindTreeOptions, Export) support a unified query configuration system using fluent methods. These methods allow you to customize query behavior, add conditions, configure sorting, and process results.\n\n#### Common Configuration Methods\n\n| Method | Description | Default QueryPart | Applicable APIs |\n|--------|-------------|-------------------|-----------------|\n| `WithProcessor` | Set post-processing function for query results | N/A | All FindApi |\n| `WithOptions` | Add multiple FindApiOptions | N/A | All FindApi |\n| `WithSelect` | Add column to SELECT clause | QueryRoot | All FindApi |\n| `WithSelectAs` | Add column with alias to SELECT clause | QueryRoot | All FindApi |\n| `WithDefaultSort` | Set default sorting specifications | QueryRoot | All FindApi |\n| `WithCondition` | Add WHERE condition using ConditionBuilder | QueryRoot | All FindApi |\n| `WithRelation` | Add relation join | QueryRoot | All FindApi |\n| `WithAuditUserNames` | Fetch audit user names (created_by_name, updated_by_name) | QueryRoot | All FindApi |\n| `WithQueryApplier` | Add custom query applier function | QueryRoot | All FindApi |\n| `DisableDataPerm` | Disable data permission filtering | N/A | All FindApi |\n\n**WithProcessor Example:**\n\nThe `Processor` function is executed after the database query completes but before returning results to the client. This allows you to transform, enrich, or filter the query results.\n\nCommon use cases:\n- **Data masking**: Hide sensitive information (passwords, tokens)\n- **Computed fields**: Add calculated values based on existing data\n- **Nested structure transformation**: Convert flat data to hierarchical structures\n- **Aggregation**: Compute statistics or summaries\n\n```go\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithProcessor(func(users []User, search UserSearch, ctx fiber.Ctx) any {\n        // Data masking\n        for i := range users {\n            users[i].Password = \"***\"\n            users[i].ApiToken = \"\"\n        }\n        return users\n    }),\n\n// Example: Adding computed fields in paged results (processor receives items slice)\nFindPage: apis.NewFindPage[Order, OrderSearch]().\n    WithProcessor(func(items []Order, search OrderSearch, ctx fiber.Ctx) any {\n        for i := range items {\n            // Calculate total amount\n            items[i].TotalAmount = items[i].Quantity * items[i].UnitPrice\n        }\n        return items\n    }),\n\n// Example: Nested structure transformation\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithProcessor(func(users []User, search UserSearch, ctx fiber.Ctx) any {\n        // Group users by department\n        type DepartmentUsers struct {\n            DepartmentName string `json:\"departmentName\"`\n            Users          []User `json:\"users\"`\n        }\n        \n        grouped := make(map[string]*DepartmentUsers)\n        for _, user := range users {\n            if _, exists := grouped[user.DepartmentId]; !exists {\n                grouped[user.DepartmentId] = \u0026DepartmentUsers{\n                    DepartmentName: user.DepartmentName,\n                    Users:          []User{},\n                }\n            }\n            grouped[user.DepartmentId].Users = append(grouped[user.DepartmentId].Users, user)\n        }\n        \n        result := make([]DepartmentUsers, 0, len(grouped))\n        for _, dept := range grouped {\n            result = append(result, *dept)\n        }\n        return result\n    }),\n```\n\n**WithSelect / WithSelectAs Example:**\n\n```go\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithSelect(\"username\").\n    WithSelectAs(\"email_address\", \"email\"),\n```\n\n**WithDefaultSort Example:**\n\n```go\nFindPage: apis.NewFindPage[User, UserSearch]().\n    WithDefaultSort(\u0026sort.OrderSpec{\n        Column:    \"created_at\",\n        Direction: sort.OrderDesc,\n    }),\n\n// Production pattern: Use schema-generated column names for type safety\nimport \"my-app/internal/sys/schemas\"\n\nFindPage: apis.NewFindPage[User, UserSearch]().\n    WithDefaultSort(\u0026sort.OrderSpec{\n        Column:    schemas.User.CreatedAt(true), // Type-safe column with table prefix\n        Direction: sort.OrderDesc,\n    }),\n\n// For tree structures, use sort_order field\nFindTree: apis.NewFindTree[Menu, MenuSearch](buildMenuTree).\n    WithDefaultSort(\u0026sort.OrderSpec{\n        Column:    schemas.Menu.SortOrder(true),\n        Direction: sort.OrderAsc,\n    }),\n```\n\nPass empty arguments to disable default sorting:\n\n```go\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithDefaultSort(), // Disable default sorting\n```\n\n**WithCondition Example:**\n\n```go\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithCondition(func(cb orm.ConditionBuilder) {\n        cb.Equals(\"is_deleted\", false)\n        cb.Equals(\"is_active\", true)\n    }),\n```\n\n**WithRelation Example:**\n\n```go\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithRelation(\u0026orm.RelationSpec{\n        // Join the Profile model; foreign/referenced keys are auto-resolved\n        Model: (*Profile)(nil),\n        // Optional: customize alias/columns\n        // Alias: \"p\",\n        SelectedColumns: []orm.ColumnInfo{\n            {Name: \"name\", AutoAlias: true},\n            {Name: \"email\", AutoAlias: true},\n        },\n    }),\n```\n\n**WithAuditUserNames Example:**\n\n```go\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithAuditUserNames(\u0026User{}), // Uses \"name\" column by default\n\n// Or specify custom column name\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithAuditUserNames(\u0026User{}, \"username\"),\n\n// Production pattern: Use package-level model instance\n// In models package: var UserModel = \u0026User{}\nFindPage: apis.NewFindPage[User, UserSearch]().\n    WithAuditUserNames(models.UserModel), // Recommended for consistency\n```\n\n**WithQueryApplier Example:**\n\n```go\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithQueryApplier(func(query orm.SelectQuery, search UserSearch, ctx fiber.Ctx) error {\n        // Custom query logic\n        if search.IncludeInactive {\n            query.Where(func(cb orm.ConditionBuilder) {\n                cb.Or(\n                    cb.Equals(\"is_active\", true),\n                    cb.Equals(\"is_active\", false),\n                )\n            })\n        }\n        return nil\n    }),\n```\n\n**DisableDataPerm Example:**\n\n```go\nFindAll: apis.NewFindAll[User, UserSearch]().\n    DisableDataPerm(), // Must be called before API registration\n```\n\n**Important:** `DisableDataPerm()` must be called before the API is registered (before the `Setup` method is executed). It should be chained immediately after `NewFindXxx()`. By default, data permission filtering is enabled and automatically applied during `Setup`.\n\n#### QueryPart System\n\nThe `parts` parameter in configuration methods specifies which part(s) of the query the option applies to. This is particularly important for tree APIs that use recursive CTEs (Common Table Expressions).\n\n| QueryPart | Description | Use Case |\n|-----------|-------------|----------|\n| `QueryRoot` | Outer/root query | Sorting, limiting, final filtering |\n| `QueryBase` | Base query (in CTE) | Initial conditions, starting nodes |\n| `QueryRecursive` | Recursive query (in CTE) | Recursive traversal configuration |\n| `QueryAll` | All query parts | Column selection, relations |\n\n**Default Behavior:**\n\n- `WithSelect`, `WithSelectAs`, `WithRelation`: Default to `QueryRoot` (applies to the main/root query)\n- `WithCondition`, `WithQueryApplier`, `WithDefaultSort`: Default to `QueryRoot` (applies to root query only)\n\n**Normal Query Example:**\n\n```go\nFindAll: apis.NewFindAll[User, UserSearch]().\n    WithSelect(\"username\").              // Applies to QueryRoot (main query)\n    WithCondition(func(cb orm.ConditionBuilder) {\n        cb.Equals(\"is_active\", true)     // Applies to QueryRoot (main query)\n    }),\n```\n\n**Tree Query Example:**\n\n```go\nFindTree: apis.NewFindTree[Category, CategorySearch](buildTree).\n    // Select columns for both base and recursive queries\n    WithSelect(\"sort\", apis.QueryBase, apis.QueryRecursive).\n    \n    // Filter only starting nodes\n    WithCondition(func(cb orm.ConditionBuilder) {\n        cb.IsNull(\"parent_id\")           // Only applies to QueryBase\n    }, apis.QueryBase).\n    \n    // Add condition to recursive traversal\n    WithCondition(func(cb orm.ConditionBuilder) {\n        cb.Equals(\"is_active\", true)     // Applies to QueryRecursive\n    }, apis.QueryRecursive),\n```\n\n#### Tree Query Configuration\n\n`FindTree` and `FindTreeOptions` use recursive CTEs (Common Table Expressions) to query hierarchical data. Understanding how QueryPart applies to different parts of the recursive query is essential for proper configuration.\n\n**Recursive CTE Structure:**\n\n```sql\nWITH RECURSIVE tree AS (\n    -- QueryBase: Initial query for root nodes\n    SELECT * FROM categories WHERE parent_id IS NULL\n    \n    UNION ALL\n    \n    -- QueryRecursive: Recursive query joining with CTE\n    SELECT c.* FROM categories c\n    INNER JOIN tree t ON c.parent_id = t.id\n)\n-- QueryRoot: Final SELECT from CTE\nSELECT * FROM tree ORDER BY sort\n```\n\n**QueryPart Behavior in Tree Queries:**\n\n- `WithSelect` / `WithSelectAs`: Default to `QueryBase` and `QueryRecursive` (columns must be consistent in both parts of UNION)\n- `WithCondition` / `WithQueryApplier`: Default to `QueryBase` only (filter starting nodes)\n- `WithRelation`: Default to `QueryBase` and `QueryRecursive` (joins needed in both parts)\n- `WithDefaultSort`: Applies to `QueryRoot` (sort final results)\n\n**Complete Tree Query Example:**\n\n```go\nFindTree: apis.NewFindTree[Category, CategorySearch](\n    func(categories []Category) []Category {\n        // Build tree structure from flat list\n        return buildCategoryTree(categories)\n    },\n).\n    // Add custom columns to both base and recursive queries\n    WithSelect(\"sort\", apis.QueryBase, apis.QueryRecursive).\n    WithSelect(\"icon\", apis.QueryBase, apis.QueryRecursive).\n    \n    // Filter starting nodes (only active root categories)\n    WithCondition(func(cb orm.ConditionBuilder) {\n        cb.Equals(\"is_active\", true)\n        cb.IsNull(\"parent_id\")\n    }, apis.QueryBase).\n    \n    // Add relation to both queries\n    WithRelation(\u0026orm.RelationSpec{\n        Model: (*Metadata)(nil),\n        SelectedColumns: []orm.ColumnInfo{\n            {Name: \"icon\", AutoAlias: true},\n            {Name: \"sort_order\", Alias: \"sortOrder\"},\n        },\n    }, apis.QueryBase, apis.QueryRecursive).\n    \n    // Fetch audit user names\n    WithAuditUserNames(\u0026User{}).\n    \n    // Sort final results\n    WithDefaultSort(\u0026sort.OrderSpec{\n        Column:    \"sort\",\n        Direction: sort.OrderAsc,\n    }),\n```\n\n**FindTreeOptions Configuration:**\n\n`FindTreeOptions` follows the same configuration pattern as `FindTree`:\n\n```go\nFindTreeOptions: apis.NewFindTreeOptions[Category, CategorySearch]().\n    WithDefaultColumnMapping(\u0026apis.DataOptionColumnMapping{\n        LabelColumn: \"name\",\n        ValueColumn: \"id\",\n    }).\n    WithIdColumn(\"id\").\n    WithParentIdColumn(\"parent_id\").\n    WithCondition(func(cb orm.ConditionBuilder) {\n        cb.Equals(\"is_active\", true)\n    }, apis.QueryBase),\n```\n\n#### API-Specific Configuration Methods\n\n**FindPage:**\n\n```go\nFindPage: apis.NewFindPage[User, UserSearch]().\n    WithDefaultPageSize(20), // Set default page size (used when request doesn't specify or is invalid)\n```\n\n**FindOptions:**\n\n```go\nFindOptions: apis.NewFindOptions[User, UserSearch]().\n    WithDefaultColumnMapping(\u0026apis.DataOptionColumnMapping{\n        LabelColumn:       \"name\",        // Column for option label (default: \"name\")\n        ValueColumn:       \"id\",          // Column for option value (default: \"id\")\n        DescriptionColumn: \"description\", // Optional description column\n    }),\n\n// Advanced: Include additional metadata in options\nFindOptions: apis.NewFindOptions[Menu, MenuSearch]().\n    WithDefaultColumnMapping(\u0026apis.DataOptionColumnMapping{\n        LabelColumn:       \"name\",\n        ValueColumn:       \"id\",\n        DescriptionColumn: \"remark\",\n        MetaColumns: []string{\n            \"type\",                  // Menu type (D=Directory, M=Menu, B=Button)\n            \"icon\",                  // Icon identifier\n            \"sort_order AS sortOrder\", // Display order with alias\n        },\n    }),\n```\n\n**FindTree:**\n\nFor hierarchical data structures, use `FindTree` with the `treebuilder` package to convert flat database results into nested tree structures:\n\n```go\nimport \"github.com/ilxqx/vef-framework-go/treebuilder\"\n\nFindTree: apis.NewFindTree[models.Organization, payloads.OrganizationSearch](\n    buildOrganizationTree,\n).\n    WithIDColumn(\"id\").              // ID column name (default: \"id\")\n    WithParentIDColumn(\"parent_id\"). // Parent ID column name (default: \"parent_id\")\n    WithDefaultSort(\u0026sort.OrderSpec{\n        Column:    \"sort_order\",\n        Direction: sort.OrderAsc,\n    })\n\nfunc buildOrganizationTree(flatModels []models.Organization) []models.Organization {\n    return treebuilder.Build(\n        flatModels,\n        treebuilder.Adapter[models.Organization]{\n            GetID:       func(m models.Organization) string { return m.ID },\n            GetParentID: func(m models.Organization) string { return m.ParentID.ValueOrZero() },\n            SetChildren: func(m *models.Organization, children []models.Organization) {\n                m.Children = children\n            },\n        },\n    )\n}\n```\n\n**Model Requirements:**\n\nYour model must have:\n- A parent ID field (typically `null.String` to support root nodes)\n- A children field (slice of same model type, marked with `bun:\"-\"` since it's computed)\n\n```go\ntype Organization struct {\n    orm.Model\n    Name     string          `json:\"name\"`\n    ParentID null.String     `json:\"parentID\" bun:\"type:varchar(20)\"` // NULL for root nodes\n    Children []Organization  `json:\"children\" bun:\"-\"`                // Computed, not in DB\n}\n```\n\nThe `treebuilder.Build` function handles the conversion from flat list to hierarchical structure, properly nesting children under their parents.\n\n**FindTreeOptions:**\n\nCombines both options and tree configuration to return hierarchical option lists:\n\n```go\nFindTreeOptions: apis.NewFindTreeOptions[models.Organization, payloads.OrganizationSearch]().\n    WithDefaultColumnMapping(\u0026apis.DataOptionColumnMapping{\n        LabelColumn: \"name\",\n        ValueColumn: \"id\",\n    }).\n    WithIDColumn(\"id\").\n    WithParentIDColumn(\"parent_id\").\n    WithDefaultSort(\u0026sort.OrderSpec{\n        Column:    \"sort_order\",\n        Direction: sort.OrderAsc,\n    })\n```\n\nThe tree options API automatically uses the internal tree builder to convert flat results into nested option structures, perfect for cascading selectors or hierarchical menus.\n\n**Export:**\n\n```go\nExport: apis.NewExport[User, UserSearch]().\n    WithDefaultFormat(\"excel\").                   // Default export format: \"excel\" or \"csv\"\n    WithExcelOptions(\u0026excel.ExportOptions{        // Excel-specific options\n        SheetName: \"Users\",\n    }).\n    WithCsvOptions(\u0026csv.ExportOptions{            // CSV-specific options\n        Delimiter: ',',\n    }).\n    WithPreExport(func(users []User, search UserSearch, ctx fiber.Ctx, db orm.DB) error {\n        // Modify data before export (e.g., data masking)\n        for i := range users {\n            users[i].Password = \"***\"\n        }\n        return nil\n    }).\n    WithFilenameBuilder(func(search UserSearch, ctx fiber.Ctx) string {\n        // Generate dynamic filename\n        return fmt.Sprintf(\"users_%s\", time.Now().Format(\"20060102\"))\n    }),\n```\n\n### Pre/Post Hooks\n\nAdd custom business logic before/after CRUD operations:\n\n```go\nCreate: apis.NewCreate[User, UserParams]().\n    WithPreCreate(func(model *User, params *UserParams, ctx fiber.Ctx, db orm.DB) error {\n        // Hash password before creating user\n        hashed, err := bcrypt.GenerateFromPassword([]byte(params.Password), bcrypt.DefaultCost)\n        if err != nil {\n            return err\n        }\n        model.Password = string(hashed)\n        return nil\n    }).\n    WithPostCreate(func(model *User, params *UserParams, ctx fiber.Ctx, tx orm.DB) error {\n        // Send welcome email after user creation (within transaction)\n        return sendWelcomeEmail(model.Email)\n    }),\n```\n\nAvailable hooks:\n\n**Single Record Operations:**\n\n- `WithPreCreate`, `WithPostCreate` - Before/after creation (`WithPostCreate` runs in transaction)\n- `WithPreUpdate`, `WithPostUpdate` - Before/after update (receives both old and new model, `WithPostUpdate` runs in transaction)\n- `WithPreDelete`, `WithPostDelete` - Before/after deletion (`WithPostDelete` runs in transaction)\n\n**Batch Operations:**\n\n- `WithPreCreateMany`, `WithPostCreateMany` - Before/after batch creation (`WithPostCreateMany` runs in transaction)\n- `WithPreUpdateMany`, `WithPostUpdateMany` - Before/after batch update (receives old and new model arrays, `WithPostUpdateMany` runs in transaction)\n- `WithPreDeleteMany`, `WithPostDeleteMany` - Before/after batch deletion (`WithPostDeleteMany` runs in transaction)\n\n**Import/Export Operations:**\n\n- `WithPreImport`, `WithPostImport` - Before/after import (`WithPreImport` for validation, `WithPostImport` runs in transaction)\n- `WithPreExport` - Before export (for data formatting)\n\n**Production Patterns:**\n\n```go\n// System user protection - Prevent deletion of critical system users\nDelete: apis.NewDelete[User]().\n    WithPreDelete(func(model *User, ctx fiber.Ctx, db orm.DB) error {\n        // Protect system-internal users from deletion\n        switch model.Username {\n        case \"system\", \"anonymous\", \"cron\":\n            return result.Err(\"Cannot delete system internal user\")\n        }\n        return nil\n    }),\n\n// Conditional password hashing - Only hash if password is being changed\nUpdate: apis.NewUpdate[User, UserUpdateParams]().\n    WithPreUpdate(func(oldModel *User, newModel *User, params *UserUpdateParams, ctx fiber.Ctx, db orm.DB) error {\n        // Only hash password if it's being updated\n        if params.Password.Valid \u0026\u0026 params.Password.String != \"\" {\n            hashed, err := bcrypt.GenerateFromPassword([]byte(params.Password.String), bcrypt.DefaultCost)\n            if err != nil {\n                return err\n            }\n            newModel.Password = string(hashed)\n        } else {\n            // Preserve existing password\n            newModel.Password = oldModel.Password\n        }\n        return nil\n    }),\n\n// Business validation - Validate business rules before operation\nCreate: apis.NewCreate[Order, OrderParams]().\n    WithPreCreate(func(model *Order, params *OrderParams, ctx fiber.Ctx, db orm.DB) error {\n        // Validate order total matches item totals\n        if model.TotalAmount \u003c= 0 {\n            return result.Err(\"Order total must be greater than zero\")\n        }\n\n        // Check inventory availability\n        if !checkInventoryAvailable(model.Items) {\n            return result.Err(\"Insufficient inventory for one or more items\")\n        }\n\n        return nil\n    }),\n```\n\n### Custom Handlers\n\n#### Mixing Generated and Custom APIs\n\nYou can combine pre-built CRUD APIs with custom actions using `api.WithOperations()`. This allows you to extend resources with domain-specific operations while maintaining the framework's conventions. For **RPC** resources, the handler is resolved by mapping `action` (snake_case) to a PascalCase method on the resource (e.g., `find_role_permissions` → `FindRolePermissions`). For **REST** resources, `OperationSpec.Handler` is required.\n\n```go\npackage resources\n\nimport (\n    \"github.com/ilxqx/vef-framework-go/api\"\n    \"github.com/ilxqx/vef-framework-go/apis\"\n)\n\ntype RoleResource struct {\n    api.Resource\n    apis.FindPage[models.Role, payloads.RoleSearch]\n    apis.Create[models.Role, payloads.RoleParams]\n    apis.Update[models.Role, payloads.RoleParams]\n    apis.Delete[models.Role]\n}\n\nfunc NewRoleResource() api.Resource {\n    return \u0026RoleResource{\n        Resource: api.NewRPCResource(\n            \"app/sys/role\",\n            api.WithOperations(\n                api.OperationSpec{\n                    Action: \"find_role_permissions\",\n                },\n                api.OperationSpec{\n                    Action:      \"save_role_permissions\",\n                    EnableAudit: true, // Enable audit logging for this action\n                },\n            ),\n        ),\n        FindPage: apis.NewFindPage[models.Role, payloads.RoleSearch](),\n        Create:   apis.NewCreate[models.Role, payloads.RoleParams](),\n        Update:   apis.NewUpdate[models.Role, payloads.RoleParams](),\n        Delete:   apis.NewDelete[models.Role](),\n    }\n}\n\n// Custom handler method for find_role_permissions action\nfunc (r *RoleResource) FindRolePermissions(\n    ctx fiber.Ctx,\n    db orm.DB,\n    params payloads.RolePermissionQuery,\n) error {\n    // Custom business logic\n    // ...\n    return result.Ok(permissions).Response(ctx)\n}\n\n// Custom handler method for save_role_permissions action\nfunc (r *RoleResource) SaveRolePermissions(\n    ctx fiber.Ctx,\n    db orm.DB,\n    params payloads.RolePermissionParams,\n) error {\n    // Transaction-based custom logic\n    return db.RunInTx(ctx.Context(), func(txCtx context.Context, tx orm.DB) error {\n        // Save permissions in transaction\n        // ...\n        return nil\n    })\n}\n```\n\n**Key Points:**\n\n- **Method Naming**: Handler method names must be in PascalCase matching the snake_case action name (e.g., `find_role_permissions` → `FindRolePermissions`)\n- **API Spec Configuration**: Each custom action can have its own configuration (permissions, audit, rate limiting)\n- **Injection Rules**: Custom handler methods follow the same parameter injection rules as generated handlers\n- **Mixed APIs**: You can freely mix generated CRUD APIs with custom actions in the same resource\n\n#### REST Resource Example (Explicit Handlers)\n\nREST operations require an explicit handler in `OperationSpec.Handler`. You can provide a method name or a function.\n\n```go\ntype RoleRestResource struct {\n    api.Resource\n}\n\nfunc NewRoleRestResource() api.Resource {\n    return \u0026RoleRestResource{\n        Resource: api.NewRESTResource(\n            \"sys/role\",\n            api.WithOperations(\n                api.OperationSpec{\n                    Action:  \"get /:id\",\n                    Handler: \"GetRole\",\n                },\n                api.OperationSpec{\n                    Action:  \"post /\",\n                    Handler: \"CreateRole\",\n                },\n            ),\n        ),\n    }\n}\n\nfunc (r *RoleRestResource) GetRole(ctx fiber.Ctx, db orm.DB, params payloads.RoleGetParams) error {\n    // ...\n    return result.Ok(role).Response(ctx)\n}\n\nfunc (r *RoleRestResource) CreateRole(ctx fiber.Ctx, db orm.DB, params payloads.RoleParams) error {\n    // ...\n    return result.Ok(role).Response(ctx)\n}\n```\n\n#### Simple Custom Handlers\n\nAdd custom endpoints by defining methods on your resource:\n\n```go\nfunc (r *UserResource) ResetPassword(\n    ctx fiber.Ctx,\n    db orm.DB,\n    logger log.Logger,\n    principal *security.Principal,\n    params ResetPasswordParams,\n) error {\n    logger.Infof(\"User %s resetting password\", principal.Id)\n    \n    // Custom business logic\n    var user models.User\n    if err := db.NewSelect().\n        Model(\u0026user).\n        Where(func(cb orm.ConditionBuilder) {\n            cb.Equals(\"id\", principal.Id)\n        }).\n        Scan(ctx.Context()); err != nil {\n        return err\n    }\n    \n    // Update password\n    // ...\n    \n    return result.Ok().Response(ctx)\n}\n```\n\n**Injectable Parameters:**\n\n- `fiber.Ctx` - HTTP context\n- `orm.DB` - Database connection\n- `log.Logger` - Logger instance\n- `mold.Transformer` - Data transformer\n- `*security.Principal` - Current authenticated user\n- `page.Pageable` - Pagination parameters\n- Custom structs embedding `api.P`\n- Custom structs embedding `api.M` (request metadata)\n- Resource struct fields (direct fields, `api:\"in\"` tagged fields, or embedded structs)\n\n**Example of Resource Field Injection:**\n\n```go\ntype UserResource struct {\n    api.Resource\n    userService *UserService  // Resource field\n}\n\nfunc NewUserResource(userService *UserService) api.Resource {\n    return \u0026UserResource{\n        Resource: api.NewRPCResource(\"sys/user\"),\n        userService: userService,\n    }\n}\n\n// Handler can inject userService directly\nfunc (r *UserResource) SendNotification(\n    ctx fiber.Ctx,\n    service *UserService,  // Injected from r.userService\n    params NotificationParams,\n) error {\n    return service.SendEmail(params.Email, params.Message)\n}\n```\n\n**Why use parameter injection instead of `r.userService` directly?**\n\nIf your service implements the `log.LoggerConfigurable[T]` interface, the framework will automatically call the `WithLogger` method when injecting the service, providing a request-scoped logger. This allows each request to have its own logging context with request ID and other contextual information.\n\n```go\ntype UserService struct {\n    logger log.Logger\n}\n\n// Implement log.LoggerConfigurable[*UserService] interface\nfunc (s *UserService) WithLogger(logger log.Logger) *UserService {\n    return \u0026UserService{logger: logger}\n}\n\nfunc (s *UserService) SendEmail(email, message string) error {\n    s.logger.Infof(\"Sending email to %s\", email)  // Request-scoped logger\n    // ...\n}\n```\n\n## Database Operations\n\n### Query Builder\n\n```go\nvar users []models.User\nerr := db.NewSelect().\n    Model(\u0026users).\n    Where(func(cb orm.ConditionBuilder) {\n        cb.Equals(\"is_active\", true)\n        cb.GreaterThan(\"age\", 18)\n        cb.Contains(\"username\", keyword)\n    }).\n    Relation(\"Profile\").\n    OrderByDesc(\"created_at\").\n    Limit(10).\n    Scan(ctx)\n```\n\n### Condition Builder Methods\n\nBuild type-safe query conditions:\n\n- `Equals(column, value)` - Equal to\n- `NotEquals(column, value)` - Not equal to\n- `GreaterThan(column, value)` - Greater than\n- `GreaterThanOrEquals(column, value)` - Greater than or equal\n- `LessThan(column, value)` - Less than\n- `LessThanOrEquals(column, value)` - Less than or equal\n- `Contains(column, value)` - LIKE %value%\n- `StartsWith(column, value)` - LIKE value%\n- `EndsWith(column, value)` - LIKE %value\n- `In(column, values)` - IN clause\n- `Between(column, min, max)` - BETWEEN clause\n- `IsNull(column)` - IS NULL\n- `IsNotNull(column)` - IS NOT NULL\n- `Or(conditions...)` - OR multiple conditions\n\n### Search Tags\n\nAutomatically apply query conditions using `search` tags:\n\n```go\ntype UserSearch struct {\n    api.P\n    Username string `search:\"eq\"`                                    // username = ?\n    Email    string `search:\"contains\"`                              // email LIKE ?\n    Age      int    `search:\"gte\"`                                   // age \u003e= ?\n    Status   string `search:\"in\"`                                    // status IN (?)\n    Keyword  string `search:\"contains,column=username|email|name\"`   // Search multiple columns\n}\n```\n\n**Supported Operators:**\n\n**Comparison Operators:**\n\n| Tag | SQL Operator | Description |\n|-----|--------------|-------------|\n| `eq` | = | Equal |\n| `neq` | != | Not equal |\n| `gt` | \u003e | Greater than |\n| `gte` | \u003e= | Greater than or equal |\n| `lt` | \u003c | Less than |\n| `lte` | \u003c= | Less than or equal |\n\n**Range Operators:**\n\n| Tag | SQL Operator | Description |\n|-----|--------------|-------------|\n| `between` | BETWEEN | Between range |\n| `notBetween` | NOT BETWEEN | Not between range |\n\n**Collection Operators:**\n\n| Tag | SQL Operator | Description |\n|-----|--------------|-------------|\n| `in` | IN | In list |\n| `notIn` | NOT IN | Not in list |\n\n**Null Check Operators:**\n\n| Tag | SQL Operator | Description |\n|-----|--------------|-------------|\n| `isNull` | IS NULL | Is null |\n| `isNotNull` | IS NOT NULL | Is not null |\n\n**String Matching (Case Sensitive):**\n\n| Tag | SQL Operator | Description |\n|-----|--------------|-------------|\n| `contains` | LIKE %?% | Contains |\n| `notContains` | NOT LIKE %?% | Does not contain |\n| `startsWith` | LIKE ?% | Starts with |\n| `notStartsWith` | NOT LIKE ?% | Does not start with |\n| `endsWith` | LIKE %? | Ends with |\n| `notEndsWith` | NOT LIKE %? | Does not end with |\n\n**String Matching (Case Insensitive):**\n\n| Tag | SQL Operator | Description |\n|-----|--------------|-------------|\n| `iContains` | ILIKE %?% | Contains (case insensitive) |\n| `iNotContains` | NOT ILIKE %?% | Does not contain (case insensitive) |\n| `iStartsWith` | ILIKE ?% | Starts with (case insensitive) |\n| `iNotStartsWith` | NOT ILIKE ?% | Does not start with (case insensitive) |\n| `iEndsWith` | ILIKE %? | Ends with (case insensitive) |\n| `iNotEndsWith` | NOT ILIKE %? | Does not end with (case insensitive) |\n\n### Transactions\n\nExecute multiple operations in a transaction:\n\n```go\nerr := db.RunInTx(ctx.Context(), func(txCtx context.Context, tx orm.DB) error {\n    // Insert user\n    _, err := tx.NewInsert().Model(\u0026user).Exec(txCtx)\n    if err != nil {\n        return err // Auto-rollback\n    }\n\n    // Update related records\n    _, err = tx.NewUpdate().Model(\u0026profile).WherePK().Exec(txCtx)\n    return err // Auto-commit on nil, rollback on error\n})\n```\n\n## Authentication \u0026 Authorization\n\n### Authentication Methods\n\nVEF supports multiple authentication strategies:\n\n1. **Jwt Authentication** (default) - Bearer token or query parameter `?__accessToken=xxx`\n2. **OpenApi Signature** - For external applications using HMAC signature\n3. **Password Authentication** - Username/password login\n\n### Implementing User Loader\n\nImplement `security.UserLoader` to integrate with your user system:\n\n```go\npackage services\n\nimport (\n    \"context\"\n    \"github.com/ilxqx/vef-framework-go/orm\"\n    \"github.com/ilxqx/vef-framework-go/security\"\n)\n\ntype MyUserLoader struct {\n    db orm.DB\n}\n\nfunc (l *MyUserLoader) LoadByUsername(ctx context.Context, username string) (*security.Principal, string, error) {\n    var user models.User\n    if err := l.db.NewSelect().\n        Model(\u0026user).\n        Where(func(cb orm.ConditionBuilder) {\n            cb.Equals(\"username\", username)\n        }).\n        Scan(ctx); err != nil {\n        return nil, \"\", err\n    }\n\n    principal := \u0026security.Principal{\n        Type: security.PrincipalTypeUser,\n        Id:   user.Id,\n        Name: user.Name,\n        Roles: []string{\"user\"}, // Load from database\n    }\n\n    return principal, user.Password, nil // Return hashed password\n}\n\nfunc (l *MyUserLoader) LoadById(ctx context.Context, id string) (*security.Principal, error) {\n    // Similar implementation\n}\n\nfunc NewMyUserLoader(db orm.DB) *MyUserLoader {\n    return \u0026MyUserLoader{db: db}\n}\n\n// Register in main.go\nfunc main() {\n    vef.Run(\n        vef.Provide(NewMyUserLoader),\n    )\n}\n```\n\n### Permission Control\n\nSet permission tokens on Apis:\n\n```go\nCreate: apis.NewCreate[User, UserParams]().\n    PermToken(\"sys.user.create\"),\n```\n\n#### Using Built-in RBAC Implementation (Recommended)\n\nThe framework provides a built-in Role-Based Access Control (RBAC) implementation. You only need to implement the `security.RolePermissionsLoader` interface:\n\n```go\npackage services\n\nimport (\n    \"context\"\n    \"github.com/ilxqx/vef-framework-go/orm\"\n    \"github.com/ilxqx/vef-framework-go/security\"\n)\n\ntype MyRolePermissionsLoader struct {\n    db orm.DB\n}\n\n// LoadPermissions loads all permissions for the given role\n// Returns map[permission token]data scope\nfunc (l *MyRolePermissionsLoader) LoadPermissions(ctx context.Context, role string) (map[string]security.DataScope, error) {\n    // Load role permissions from database\n    var permissions []RolePermission\n    if err := l.db.NewSelect().\n        Model(\u0026permissions).\n        Where(func(cb orm.ConditionBuilder) {\n            cb.Equals(\"role_code\", role)\n        }).\n        Scan(ctx); err != nil {\n        return nil, err\n    }\n    \n    // Build mapping of permission tokens to data scopes\n    result := make(map[string]security.DataScope)\n    for _, perm := range permissions {\n        // Create corresponding DataScope instance based on scope type\n        var dataScope security.DataScope\n        switch perm.DataScopeType {\n        case \"all\":\n            dataScope = security.NewAllDataScope()\n        case \"self\":\n            dataScope = security.NewSelfDataScope(\"\")\n        case \"dept\":\n            dataScope = NewDepartmentDataScope() // Custom implementation\n        // ... more custom data scopes\n        }\n        \n        result[perm.PermissionToken] = dataScope\n    }\n    \n    return result, nil\n}\n\nfunc NewMyRolePermissionsLoader(db orm.DB) security.RolePermissionsLoader {\n    return \u0026MyRolePermissionsLoader{db: db}\n}\n\n// Register in main.go\nfunc main() {\n    vef.Run(\n        vef.Provide(NewMyRolePermissionsLoader),\n    )\n}\n```\n\n**Note:** The framework will automatically use your `RolePermissionsLoader` implementation to initialize the built-in RBAC permission checker and data permission resolver.\n\n#### Fully Custom Permission Control\n\nIf you need to implement completely custom permission control logic (non-RBAC), you can implement the `security.PermissionChecker` interface and replace the framework's implementation:\n\n```go\ntype MyCustomPermissionChecker struct {\n    // Custom fields\n}\n\nfunc (c *MyCustomPermissionChecker) HasPermission(ctx context.Context, principal *security.Principal, permToken string) (bool, error) {\n    // Custom permission check logic\n    // ...\n    return true, nil\n}\n\nfunc NewMyCustomPermissionChecker() security.PermissionChecker {\n    return \u0026MyCustomPermissionChecker{}\n}\n\n// Replace framework implementation in main.go\nfunc main() {\n    vef.Run(\n        vef.Provide(NewMyCustomPermissionChecker),\n        vef.Replace(vef.Annotate(\n            NewMyCustomPermissionChecker,\n            vef.As(new(security.PermissionChecker)),\n        )),\n    )\n}\n```\n\n### Data Permissions\n\nData permissions implement row-level data access control, restricting users to specific data scopes.\n\n#### Built-in Data Scopes\n\nThe framework provides two built-in data scope implementations:\n\n1. **AllDataScope** - Unrestricted access to all data (typically for administrators)\n2. **SelfDataScope** - Access only to self-created data\n\n```go\nimport \"github.com/ilxqx/vef-framework-go/security\"\n\n// All data\nallScope := security.NewAllDataScope()\n\n// Only self-created data (defaults to created_by column)\nselfScope := security.NewSelfDataScope(\"\")\n\n// Custom creator column name\nselfScope := security.NewSelfDataScope(\"creator_id\")\n```\n\n#### Using Built-in RBAC Data Permissions (Recommended)\n\nThe framework's RBAC implementation automatically handles data permissions. Simply return the data scope for each permission token in `RolePermissionsLoader.LoadPermissions`:\n\n```go\nfunc (l *MyRolePermissionsLoader) LoadPermissions(ctx context.Context, role string) (map[string]security.DataScope, error) {\n    result := make(map[string]security.DataScope)\n    \n    // Assign different data scopes to different permissions\n    result[\"sys.user.view\"] = security.NewAllDataScope()      // View all users\n    result[\"sys.user.edit\"] = security.NewSelfDataScope(\"\")    // Edit only self-created users\n    \n    return result, nil\n}\n```\n\n**Data Scope Priority:** When a user has multiple roles with different data scopes for the same permission token, the framework selects the scope with the highest priority. Built-in priority constants:\n\n- `security.PrioritySelf` (10) - Self-created data only\n- `security.PriorityDepartment` (20) - Department data\n- `security.PriorityDepartmentAndSub` (30) - Department and sub-department data\n- `security.PriorityOrganization` (40) - Organization data\n- `security.PriorityOrganizationAndSub` (50) - Organization and sub-organization data\n- `security.PriorityCustom` (60) - Custom data scope\n- `security.PriorityAll` (10000) - All data\n\n#### Custom Data Scopes\n\nImplement the `security.DataScope` interface to create custom data access scopes:\n\n```go\npackage scopes\n\nimport (\n    \"github.com/ilxqx/vef-framework-go/orm\"\n    \"github.com/ilxqx/vef-framework-go/security\"\n)\n\ntype DepartmentDataScope struct{}\n\nfunc NewDepartmentDataScope() security.DataScope {\n    return \u0026DepartmentDataScope{}\n}\n\nfunc (s *DepartmentDataScope) Key() string {\n    return \"department\"\n}\n\nfunc (s *DepartmentDataScope) Priority() int {\n    return security.PriorityDepartment // Use framework-defined priority\n}\n\nfunc (s *DepartmentDataScope) Supports(principal *security.Principal, table *orm.Table) bool {\n    // Check if table has department_id column\n    field, _ := table.Field(\"department_id\")\n    return field != nil\n}\n\nfunc (s *DepartmentDataScope) Apply(principal *security.Principal, query orm.SelectQuery) error {\n    // Get user's department ID from principal.Details\n    type UserDetails struct {\n        DepartmentID string `json:\"departmentId\"`\n    }\n    \n    details, ok := principal.Details.(UserDetails)\n    if !ok {\n        return nil // If no department info, don't apply filter\n    }\n    \n    // Apply filtering condition\n    query.Where(func(cb orm.ConditionBuilder) {\n        cb.Equals(\"department_id\", details.DepartmentID)\n    })\n    \n    return nil\n}\n```\n\nThen use the custom data scope in your `RolePermissionsLoader`:\n\n```go\nfunc (l *MyRolePermissionsLoader) LoadPermissions(ctx context.Context, role string) (map[string]security.DataScope, error) {\n    result := make(map[string]security.DataScope)\n    \n    result[\"sys.user.view\"] = NewDepartmentDataScope() // View only department users\n    \n    return result, nil\n}\n```\n\n#### Fully Custom Data Permission Resolution\n\nIf you need to implement completely custom data permission resolution logic (non-RBAC), you can implement the `security.DataPermissionResolver` interface and replace the framework's implementation:\n\n```go\ntype MyCustomDataPermResolver struct {\n    // Custom fields\n}\n\nfunc (r *MyCustomDataPermResolver) ResolveDataScope(ctx context.Context, principal *security.Principal, permToken string) (security.DataScope, error) {\n    // Custom data permission resolution logic\n    // ...\n    return security.NewAllDataScope(), nil\n}\n\nfunc NewMyCustomDataPermResolver() security.DataPermissionResolver {\n    return \u0026MyCustomDataPermResolver{}\n}\n\n// Replace framework implementation in main.go\nfunc main() {\n    vef.Run(\n        vef.Provide(NewMyCustomDataPermResolver),\n        vef.Replace(vef.Annotate(\n            NewMyCustomDataPermResolver,\n            vef.As(new(security.DataPermissionResolver)),\n        )),\n    )\n}\n```\n\n## Configuration\n\n### Configuration File\n\nPlace `application.toml` in `./configs/` or `./` directory, or specify via `VEF_CONFIG_PATH` environment variable.\n\n**Complete Configuration Example:**\n\n```toml\n[vef.app]\nname = \"my-app\"          # Application name\nport = 8080              # HTTP port\nbody_limit = \"10MB\"      # Request body size limit\n\n[vef.datasource]\ntype = \"postgres\"        # Database type: postgres, mysql, sqlite\nhost = \"localhost\"\nport = 5432\nuser = \"postgres\"\npassword = \"password\"\ndatabase = \"mydb\"\nschema = \"public\"        # PostgreSQL schema\n# path = \"./data.db\"    # SQLite database file path\n\n[vef.security]\ntoken_expires = \"2h\"     # Jwt token expiration time\n\n[vef.storage]\nprovider = \"minio\"       # Storage provider: memory, filesystem, minio (default: memory)\n\n[vef.storage.minio]\nendpoint = \"localhost:9000\"\naccess_key = \"minioadmin\"\nsecret_key = \"minioadmin\"\nuse_ssl = false\nregion = \"us-east-1\"\nbucket = \"mybucket\"\n\n[vef.storage.filesystem]\nroot = \"./storage\"       # Used when provider = \"filesystem\"\n\n[vef.redis]\nhost = \"localhost\"\nport = 6379\nuser = \"\"                # Optional\npassword = \"\"            # Optional\ndatabase = 0             # 0-15\nnetwork = \"tcp\"          # tcp or unix\n\n[vef.cors]\nenabled = true\nallow_origins = [\"*\"]\n```\n\n### Environment Variables\n\nOverride configuration with environment variables:\n\n- `VEF_CONFIG_PATH` - Configuration file path\n- `VEF_LOG_LEVEL` - Log level (debug, info, warn, error)\n- `VEF_NODE_ID` - XID node identifier for ID generation\n- `VEF_I18N_LANGUAGE` - Language (en, zh-CN)\n\n## Advanced Features\n\n### Cache\n\nUse in-memory or Redis cache:\n\n```go\nimport (\n    \"github.com/ilxqx/vef-framework-go/cache\"\n    \"time\"\n)\n\n// In-memory cache\nmemCache := cache.NewMemory[models.User](\n    cache.WithMemMaxSize(1000),\n    cache.WithMemDefaultTtl(5 * time.Minute),\n)\n\n// Redis cache\nredisCache := cache.NewRedis[models.User](\n    redisClient,\n    \"users\",\n    cache.WithRdsDefaultTtl(10 * time.Minute),\n)\n\n// Usage\nuser, err := memCache.GetOrLoad(ctx, \"user:123\", func(ctx context.Context) (models.User, error) {\n    // Fallback loader when cache miss\n    return loadUserFromDB(ctx, \"123\")\n})\n```\n\n### Event Bus\n\nPublish and subscribe to events:\n\n```go\nimport \"github.com/ilxqx/vef-framework-go/event\"\n\n// Publishing events\nfunc (r *UserResource) CreateUser(ctx fiber.Ctx, bus event.Bus, ...) error {\n    // Create user logic\n    \n    bus.Publish(event.NewBaseEvent(\n        \"user.created\",\n        event.WithSource(\"user-service\"),\n        event.WithMeta(\"userID\", user.Id),\n    ))\n    \n    return result.Ok().Response(ctx)\n}\n\n// Subscribing to events\nfunc main() {\n    vef.Run(\n        vef.Invoke(func(bus event.Bus, logger log.Logger) {\n            unsubscribe := bus.Subscribe(\"user.created\", func(ctx context.Context, e event.Event) {\n                // Handle event\n                logger.Infof(\"User created: %s\", e.Meta()[\"userID\"])\n            })\n            \n            // Optionally unsubscribe later\n            _ = unsubscribe\n        }),\n    )\n}\n```\n\n### Lifecycle Hooks\n\nThe framework provides lifecycle management through `vef.Lifecycle`, allowing you to register hooks that execute during application startup and shutdown. This is essential for proper resource cleanup, particularly for event subscribers.\n\n#### Event Subscriber Cleanup\n\nWhen registering event subscribers, you should clean up subscriptions on shutdown to prevent resource leaks:\n\n```go\nimport (\n    \"github.com/ilxqx/vef-framework-go\"\n    \"github.com/ilxqx/vef-framework-go/event\"\n    \"github.com/ilxqx/vef-framework-go/orm\"\n)\n\nvar Module = vef.Module(\n    \"app:vef\",\n    vef.Invoke(\n        func(lc vef.Lifecycle, db orm.DB, subscriber event.Subscriber) {\n            // Create and register audit event subscriber\n            auditSub := NewAuditEventSubscriber(db, subscriber)\n\n            // Register cleanup hook\n            lc.Append(vef.StopHook(func() {\n                auditSub.Unsubscribe()  // Cleanup on shutdown\n            }))\n\n            // Create and register login event subscriber\n            loginSub := NewLoginEventSubscriber(db, subscriber)\n\n            // Register cleanup hook\n            lc.Append(vef.StopHook(func() {\n                loginSub.Unsubscribe()  // Cleanup on shutdown\n            }))\n        },\n    ),\n)\n```\n\n**Key Patterns:**\n\n1. **Store unsubscribe function**: Event subscriber constructors should return an `UnsubscribeFunc` when they call `bus.Subscribe()`\n2. **Register stop hooks**: Use `lc.Append(vef.StopHook(...))` to register cleanup functions\n3. **Call unsubscribe in hooks**: Invoke the stored `Unsubscribe()` function during shutdown\n\n**Example Event Subscriber Implementation:**\n\n```go\ntype AuditEventSubscriber struct {\n    db           orm.DB\n    unsubscribe  event.UnsubscribeFunc\n}\n\nfunc NewAuditEventSubscriber(db orm.DB, subscriber event.Subscriber) *AuditEventSubscriber {\n    sub := \u0026AuditEventSubscriber{db: db}\n\n    // Subscribe and store unsubscribe function\n    sub.unsubscribe = subscriber.Subscribe(\"*.created\", sub.handleAuditEvent)\n\n    return sub\n}\n\nfunc (s *AuditEventSubscriber) handleAuditEvent(ctx context.Context, e event.Event) {\n    // Handle audit logging\n}\n\nfunc (s *AuditEventSubscriber) Unsubscribe() {\n    if s.unsubscribe != nil {\n        s.unsubscribe()\n    }\n}\n```\n\nThis pattern ensures graceful shutdown without resource leaks or orphaned subscriptions.\n\n### Context Helpers\n\nThe `contextx` package provides utility functions to access request-scoped resources when dependency injection is not available. These helpers are useful in custom handlers, hooks, or other scenarios where you need to access framework-provided resources from the Fiber context.\n\n```go\nimport \"github.com/ilxqx/vef-framework-go/contextx\"\n\nfunc (r *RoleResource) CustomMethod(ctx fiber.Ctx) error {\n    // Get request-scoped database (with operator pre-configured)\n    db := contextx.DB(ctx)\n\n    // Get current authenticated user\n    principal := contextx.Principal(ctx)\n\n    // Get request-scoped logger (includes request ID)\n    logger := contextx.Logger(ctx)\n\n    // Use the resources\n    logger.Infof(\"User %s performing custom operation\", principal.Id)\n\n    var model models.SomeModel\n    if err := db.NewSelect().Model(\u0026model).Scan(ctx.Context()); err != nil {\n        return err\n    }\n\n    return result.Ok(model).Response(ctx)\n}\n```\n\n**Available Helpers:**\n\n- **`contextx.DB(ctx)`** - Returns request-scoped `orm.DB` with audit fields (like `operator`) pre-configured\n- **`contextx.Principal(ctx)`** - Returns current `*security.Principal` (authenticated user or anonymous)\n- **`contextx.Logger(ctx)`** - Returns request-scoped `log.Logger` with request ID for correlation\n- **`contextx.DataPermApplier(ctx)`** - Returns request-scoped `security.DataPermissionApplier` used by the data permission middleware\n\n**When to Use:**\n\n- **Use contextx helpers**: In custom handlers where you cannot use parameter injection, or in utility functions that only receive `fiber.Ctx`\n- **Prefer parameter injection**: When defining API handler methods, let the framework inject dependencies directly as parameters for better testability and clarity\n\n**Example - Using Both Patterns:**\n\n```go\n// Prefer this: Parameter injection in handler\nfunc (r *UserResource) UpdateProfile(\n    ctx fiber.Ctx,\n    db orm.DB,           // Injected by framework\n    logger log.Logger,   // Injected by framework\n    params ProfileParams,\n) error {\n    logger.Infof(\"Updating profile\")\n    // ...\n}\n\n// Use contextx when injection not available\nfunc helperFunction(ctx fiber.Ctx) error {\n    db := contextx.DB(ctx)       // Extract from context\n    logger := contextx.Logger(ctx)\n    logger.Infof(\"Helper function\")\n    // ...\n}\n```\n\n### Cron Scheduler\n\nThe framework provides cron job scheduling based on [gocron](https://github.com/go-co-op/gocron).\n\n#### Basic Usage\n\nInject `cron.Scheduler` via DI and create jobs:\n\n```go\nimport (\n    \"context\"\n    \"time\"\n    \"github.com/ilxqx/vef-framework-go/cron\"\n)\n\nfunc main() {\n    vef.Run(\n        vef.Invoke(func(scheduler cron.Scheduler) {\n            // Cron expression job (5-field format)\n            scheduler.NewJob(\n                cron.NewCronJob(\n                    \"0 0 * * *\",  // Expression: daily at midnight\n                    false,         // withSeconds: use 5-field format\n                    cron.WithName(\"daily-cleanup\"),\n                    cron.WithTags(\"maintenance\"),\n                    cron.WithTask(func(ctx context.Context) {\n                        // Task logic\n                    }),\n                ),\n            )\n            \n            // Fixed interval job\n            scheduler.NewJob(\n                cron.NewDurationJob(\n                    5*time.Minute,\n                    cron.WithName(\"health-check\"),\n                    cron.WithTask(func() {\n                        // Every 5 minutes\n                    }),\n                ),\n            )\n        }),\n    )\n}\n```\n\n#### Job Types\n\nThe framework supports multiple job scheduling strategies:\n\n**1. Cron Expression Jobs**\n\n```go\n// 5-field format: minute hour day month weekday\nscheduler.NewJob(\n    cron.NewCronJob(\n        \"30 * * * *\",  // Every hour at minute 30\n        false,          // No seconds field\n        cron.WithName(\"hourly-report\"),\n        cron.WithTask(func() {\n            // Generate report\n        }),\n    ),\n)\n\n// 6-field format: second minute hour day month weekday\nscheduler.NewJob(\n    cron.NewCronJob(\n        \"0 30 * * * *\",  // Every hour at minute 30, second 0\n        true,             // With seconds field\n        cron.WithName(\"precise-task\"),\n        cron.WithTask(func() {\n            // Precise timing task\n        }),\n    ),\n)\n```\n\n**2. Fixed Interval Jobs**\n\n```go\nscheduler.NewJob(\n    cron.NewDurationJob(\n        10*time.Second,\n        cron.WithName(\"metrics-collector\"),\n        cron.WithTask(func() {\n            // Collect metrics every 10 seconds\n        }),\n    ),\n)\n```\n\n**3. Random Interval Jobs**\n\n```go\nscheduler.NewJob(\n    cron.NewDurationRandomJob(\n        1*time.Minute,  // Minimum interval\n        5*time.Minute,  // Maximum interval\n        cron.WithName(\"random-check\"),\n        cron.WithTask(func() {\n            // Execute at random intervals between 1-5 minutes\n        }),\n    ),\n)\n```\n\n**4. One-Time Jobs**\n\n```go\n// Execute immediately\nscheduler.NewJob(\n    cron.NewOneTimeJob(\n        []time.Time{},  // Empty slice means immediate execution\n        cron.WithName(\"init-task\"),\n        cron.WithTask(func() {\n            // Initialization task\n        }),\n    ),\n)\n\n// Execute at specific time\nscheduler.NewJob(\n    cron.NewOneTimeJob(\n        []time.Time{time.Now().Add(1 * time.Hour)},\n        cron.WithName(\"delayed-task\"),\n        cron.WithTask(func() {\n            // Execute after 1 hour\n        }),\n    ),\n)\n\n// Execute at multiple specific times\nscheduler.NewJob(\n    cron.NewOneTimeJob(\n        []time.Time{\n            time.Date(2024, 12, 31, 23, 59, 0, 0, time.Local),\n            time.Date(2025, 1, 1, 0, 0, 0, 0, time.Local),\n        },\n        cron.WithName(\"new-year-task\"),\n        cron.WithTask(func() {\n            // Execute at specific times\n        }),\n    ),\n)\n```\n\n#### Job Configuration Options\n\n```go\nscheduler.NewJob(\n    cron.NewDurationJob(\n        1*time.Hour,\n        // Job name (required)\n        cron.WithName(\"backup-task\"),\n        \n        // Tags (for grouping and bulk operations)\n        cron.WithTags(\"backup\", \"critical\"),\n        \n        // Task handler function (required)\n        cron.WithTask(func(ctx context.Context) {\n            // If the function accepts context.Context, the framework auto-injects it\n            // Supports graceful shutdown and timeout control\n        }),\n        \n        // Allow concurrent execution (default is singleton mode)\n        cron.WithConcurrent(),\n        \n        // Set start time\n        cron.WithStartAt(time.Now().Add(10 * time.Minute)),\n        \n        // Start immediately\n        cron.WithStartImmediately(),\n        \n        // Set stop time\n        cron.WithStopAt(time.Now().Add(24 * time.Hour)),\n        \n        // Limit number of runs\n        cron.WithLimitedRuns(100),\n        \n        // Custom context\n        cron.WithContext(context.Background()),\n    ),\n)\n```\n\n#### Job Management\n\n```go\nvef.Invoke(func(scheduler cron.Scheduler) {\n    // Create job\n    job, _ := scheduler.NewJob(\n        cron.NewDurationJob(\n            1*time.Minute,\n            cron.WithName(\"my-task\"),\n            cron.WithTags(\"tag1\", \"tag2\"),\n            cron.WithTask(func() {}),\n        ),\n    )\n    \n    // Get all jobs\n    allJobs := scheduler.Jobs()\n    \n    // Remove jobs by tags\n    scheduler.RemoveByTags(\"tag1\", \"tag2\")\n    \n    // Remove job by ID\n    scheduler.RemoveJob(job.Id())\n    \n    // Update job definition\n    scheduler.Update(job.Id(), cron.NewDurationJob(\n        2*time.Minute,\n        cron.WithName(\"my-task-updated\"),\n        cron.WithTask(func() {}),\n    ))\n    \n    // Run job immediately (doesn't affect schedule)\n    job.RunNow()\n    \n    // Get next run time\n    nextRun, _ := job.NextRun()\n    \n    // Get last run time\n    lastRun, _ := job.LastRun()\n    \n    // Stop all jobs\n    scheduler.StopJobs()\n})\n```\n\n### File Storage\n\nThe framework provides built-in file storage functionality with support for MinIO, filesystem, and in-memory storage.\n\n#### Built-in Storage Resource\n\nThe framework automatically registers the `sys/storage` resource with the following Api endpoints:\n\n| Action | Description |\n|--------|-------------|\n| `upload` | Upload file (auto-generates unique filename) |\n| `get_presigned_url` | Get presigned URL (for direct access or upload) |\n| `delete_temp` | Delete temporary file (only keys under `temp/`) |\n| `stat` | Get file metadata |\n| `list` | List files |\n\n**Upload Example:**\n\n```bash\n# Using built-in upload Api\ncurl -X POST http://localhost:8080/api \\\n  -H \"Authorization: Bearer \u003ctoken\u003e\" \\\n  -F \"resource=sys/storage\" \\\n  -F \"action=upload\" \\\n  -F \"version=v1\" \\\n  -F \"params[file]=@/path/to/file.jpg\" \\\n  -F \"params[contentType]=image/jpeg\" \\\n  -F \"params[metadata][key1]=value1\"\n```\n\n**Upload Response:**\n\n```json\n{\n  \"code\": 0,\n  \"message\": \"Success\",\n  \"data\": {\n    \"key\": \"temp/2025/01/15/550e8400-e29b-41d4-a716-446655440000.jpg\",\n    \"size\": 1024000,\n    \"contentType\": \"image/jpeg\",\n    \"etag\": \"\\\"d41d8cd98f00b204e9800998ecf8427e\\\"\",\n    \"lastModified\": \"2025-01-15T10:30:00Z\",\n    \"metadata\": {\n      \"Original-Filename\": \"file.jpg\",\n      \"key1\": \"value1\"\n    }\n  }\n}\n```\n\n#### File Key Conventions\n\nThe framework uses the following naming convention for uploaded files:\n\n- **Temporary files**: `temp/YYYY/MM/DD/{uuid}{extension}`\n  - Example: `temp/2025/01/15/550e8400-e29b-41d4-a716-446655440000.jpg`\n  - Original filename is preserved in `Original-Filename` metadata\n\n- **Permanent files**: Promote temporary files via `PromoteObject`\n  - Removes `temp/` prefix from the path\n  - Example: `temp/2025/01/15/xxx.jpg` → `2025/01/15/xxx.jpg`\n\n#### Custom File Upload\n\nInject `storage.Service` in custom resources for file uploads:\n\n```go\nimport (\n    \"mime/multipart\"\n\n    \"github.com/gofiber/fiber/v3\"\n    \"github.com/ilxqx/vef-framework-go/api\"\n    \"github.com/ilxqx/vef-framework-go/result\"\n    \"github.com/ilxqx/vef-framework-go/storage\"\n)\n\n// Define upload parameter struct\ntype UploadAvatarParams struct {\n    api.P\n\n    File *multipart.FileHeader `json:\"file\"`\n}\n\nfunc (r *UserResource) UploadAvatar(\n    ctx fiber.Ctx,\n    service storage.Service,\n    params UploadAvatarParams,\n) error {\n    // Check if file exists\n    if params.File == nil {\n        return result.Err(\"File is required\")\n    }\n\n    // Open uploaded file\n    reader, err := params.File.Open()\n    if err != nil {\n        return err\n    }\n    defer reader.Close()\n\n    // Custom file path\n    info, err := service.PutObject(ctx.Context(), storage.PutObjectOptions{\n        Key:         \"avatars/\" + params.File.Filename,\n        Reader:      reader,\n        Size:        params.File.Size,\n        ContentType: params.File.Header.Get(\"Content-Type\"),\n        Metadata: map[string]string{\n            \"userID\": \"12345\",\n        },\n    })\n    if err != nil {\n        return err\n    }\n    \n    return result.Ok(info).Response(ctx)\n}\n```\n\n#### Promoting Temporary Files\n\nUse `PromoteObject` to convert temporary uploads to permanent files:\n\n```go\n// After business logic confirms, promote temporary file\ninfo, err := provider.PromoteObject(ctx.Context(), \"temp/2025/01/15/xxx.jpg\")\n// info.Key becomes: \"2025/01/15/xxx.jpg\"\n```\n\n#### Storage Configuration\n\nSet `vef.storage.provider` to `minio`, `filesystem`, or `memory` (default) and configure the matching section in `application.toml`:\n\n```toml\n[vef.storage]\nprovider = \"minio\"  # options: minio, filesystem, memory\n\n[vef.storage.minio]\nendpoint = \"localhost:9000\"\naccess_key = \"minioadmin\"\nsecret_key = \"minioadmin\"\nuse_ssl = false\nregion = \"us-east-1\"\nbucket = \"mybucket\"\n\n[vef.storage.filesystem]\nroot = \"./storage\"       # Base directory when provider = \"filesystem\"\n```\n\n### Data Validation\n\nUse [go-playground/validator](https://github.com/go-playground/validator) tags:\n\n```go\ntype UserParams struct {\n    Username string `validate:\"required,alphanum,min=3,max=32\" label:\"Username\"`\n    Email    string `validate:\"required,email\" label:\"Email\"`\n    Age      int    `validate:\"min=18,max=120\" label:\"Age\"`\n    Website  string `validate:\"omitempty,url\" label:\"Website\"`\n    Password string `validate:\"required,min=8,containsany=!@#$%^\u0026*\" label:\"Password\"`\n}\n```\n\n**Common Rules:**\n\n| Rule | Description |\n|------|-------------|\n| `required` | Required field |\n| `omitempty` | Optional field (skip validation if empty) |\n| `min` | Minimum value (number) or minimum length (string) |\n| `max` | Maximum value (number) or maximum length (string) |\n| `len` | Exact length |\n| `eq` | Equal to |\n| `ne` | Not equal to |\n| `gt` | Greater than |\n| `gte` | Greater than or equal to |\n| `lt` | Less than |\n| `lte` | Less than or equal to |\n| `alpha` | Alphabetic characters only |\n| `alphanum` | Alphanumeric characters |\n| `ascii` | ASCII characters |\n| `numeric` | Numeric string |\n| `email` | Email address |\n| `url` | URL |\n| `uuid` | UUID format |\n| `ip` | IP address |\n| `json` | JSON format |\n| `contains` | Contains substring |\n| `startswith` | Starts with string |\n| `endswith` | Ends with string |\n\n### CLI Tools\n\nVEF Framework provides the `vef-cli` command-line tool for code generation and project scaffolding tasks.\n\n#### Generate Build Info\n\nThe `generate-build-info` command creates a build_info.go file with app version, commit hash, and build timestamp:\n\n```bash\ngo run github.com/ilxqx/vef-framework-go/cmd/vef-cli@latest generate-build-info -o internal/vef/build_info.go -p vef\n```\n\n**Options:**\n- `-o, --output` - Output file path (default: `build_info.go`)\n- `-p, --package` - Package name (default: current directory name)\n\n**Usage in go:generate:**\n\n```go\n//go:generate go run github.com/ilxqx/vef-framework-go/cmd/vef-cli@latest generate-build-info -o internal/vef/build_info.go -p vef\n```\n\nThe generated file provides a `BuildInfo` variable compatible with the monitor module:\n\n```go\npackage vef\n\nimport \"github.com/ilxqx/vef-framework-go/monitor\"\n\n// BuildInfo is a pointer to build metadata used by the monitor module.\nvar BuildInfo = \u0026monitor.BuildInfo{\n    AppVersion: \"v1.0.0\",               // From git tags (or \"dev\")\n    BuildTime:  \"2025-01-15T10:30:00Z\", // Build timestamp\n    GitCommit:  \"abc123...\",            // Git commit SHA\n}\n```\n\n**Generated Fields:**\n- **Version**: Extracted from git tags (e.g., `v1.0.0`). Falls back to `\"dev\"` if no tags exist.\n- **Commit**: Full git commit SHA from current HEAD.\n- **BuildTime**: UTC timestamp when the file was generated.\n\n#### Generate Model Schema\n\nThe `generate-model-schema` command generates type-safe field accessor functions for your models:\n\n```bash\ngo run github.com/ilxqx/vef-framework-go/cmd/vef-cli@latest generate-model-schema -i ./models -o ./schemas -p schemas\n```\n\n**Options:**\n- `-i, --input` - Input directory containing model files (required)\n- `-o, --output` - Output directory for generated schema files (required)\n- `-p, --package` - Package name for generated files (required)\n\n**Usage in go:generate:**\n\n```go\n//go:generate go run github.com/ilxqx/vef-framework-go/cmd/vef-cli@latest generate-model-schema -i ./models -o ./schemas -p schemas\n```\n\nThe generated schema provides type-safe field accessors:\n\n```go\npackage schemas\n\nvar User = struct {\n    ID        func(withTablePrefix ...bool) string\n    Username  func(withTablePrefix ...bool) string\n    Email     func(withTablePrefix ...bool) string\n    CreatedAt func(withTablePrefix ...bool) string\n    // ... other fields\n}{\n    ID:        field(\"id\", \"su\"),\n    Username:  field(\"username\", \"su\"),\n    Email:     field(\"email\", \"su\"),\n    CreatedAt: field(\"created_at\", \"su\"),\n}\n```\n\n**Usage in queries:**\n\n```go\nimport \"my-app/internal/sys/schemas\"\n\n// Type-safe column references\ndb.NewSelect().\n    Model(\u0026users).\n    Where(func(cb orm.ConditionBuilder) {\n        cb.Equals(schemas.User.Username(), \"admin\")\n        cb.IsNotNull(schemas.User.Email())\n    }).\n    OrderBy(schemas.User.CreatedAt(true) + \" DESC\"). // With table prefix\n    Scan(ctx)\n```\n\n**Benefits:**\n- **Type safety**: Catch typos at compile time\n- **IDE autocomplete**: Field names are discoverable\n- **Refactoring support**: Renaming fields updates all references\n- **Table prefix handling**: Optionally include table alias in column names\n\nFor AI-assisted development guidelines, see `cmd/CMD_DEV_GUIDELINES.md`.\n\n## Best Practices\n\n### Project Structure\n\n```txt\nmy-app/\n├── cmd/\n│   └── main.go                 # Application entry point\n├── configs/\n│   └── application.toml        # Configuration file\n├── internal/\n│   ├── models/                 # Data models\n│   │   ├── user.go\n│   │   └── order.go\n│   ├── payloads/               # Api parameters\n│   │   ├── user.go\n│   │   └── order.go\n│   ├── resources/              # Api resources\n│   │   ├── user.go\n│   │   └── order.go\n│   └── services/               # Business services\n│       ├── user_service.go\n│       └── email_service.go\n└── go.mod\n```\n\n### Naming Conventions\n\n- **Models:** Singular PascalCase (e.g., `User`, `Order`)\n- **Resources:** Lowercase with slashes (e.g., `sys/user`, `shop/order`, `auth/user_role`)\n- **Parameters:** `XxxParams` (Create/Update), `XxxSearch` (Query)\n- **Actions:** Lowercase snake_case (e.g., `find_page`, `create_user`)\n\n### Error Handling\n\nUse framework's Result type for consistent error responses:\n\n```go\nimport \"github.com/ilxqx/vef-framework-go/result\"\n\n// Success\nreturn result.Ok(data).Response(ctx)\n\n// Error\nreturn result.Err(\"Operation failed\")\nreturn result.Err(\"Invalid parameters\", result.WithCode(result.ErrCodeBadRequest))\nreturn result.Errf(\"User %s not found\", username)\n```\n\n### Logging\n\nInject logger and use:\n\n```go\nfunc (r *UserResource) Handler(\n    ctx fiber.Ctx,\n    logger log.Logger,\n) error {\n    logger.Infof(\"Processing request from %s\", ctx.IP())\n    logger.Warnf(\"Unusual activity detected\")\n    logger.Errorf(\"Operation failed: %v\", err)\n    \n    return nil\n}\n```\n\n## Documentation \u0026 Resources\n\n- [Fiber Web Framework](https://gofiber.io/) - Underlying HTTP framework\n- [Bun ORM](https://bun.uptrace.dev/) - Database ORM\n- [Go Playground Validator](https://github.com/go-playground/validator) - Data validation\n- [Uber FX](https://uber-go.github.io/fx/) - Dependency injection\n\n## License\n\nThis project is licensed under the [Apache License 2.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filxqx%2Fvef-framework-go","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Filxqx%2Fvef-framework-go","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Filxqx%2Fvef-framework-go/lists"}