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