refactor(API): Relspect integration
Some checks failed
CI / Test (1.23) (push) Failing after -22m46s
CI / Test (1.22) (push) Failing after -22m32s
CI / Build (push) Failing after -23m30s
CI / Lint (push) Failing after -23m12s

This commit is contained in:
Hein
2026-02-05 13:39:43 +02:00
parent 71f26c214f
commit f9773bd07f
33 changed files with 7512 additions and 58 deletions

557
tooldoc/BUN_ORM.md Normal file
View File

@@ -0,0 +1,557 @@
# BUN ORM Integration Guide
## Overview
BUN is a fast and lightweight SQL-first ORM for Go. For WhatsHooked Phase 2, we use BUN with PostgreSQL and SQLite, integrated with ResolveSpec for REST API generation.
Official Documentation: https://bun.uptrace.dev/
## Installation
```bash
go get github.com/uptrace/bun
go get github.com/uptrace/bun/driver/pgdriver # PostgreSQL
go get github.com/uptrace/bun/driver/sqliteshim # SQLite
go get github.com/uptrace/bun/dialect/pgdialect
go get github.com/uptrace/bun/dialect/sqlitedialect
```
## Setup
### Database Connection
```go
import (
"database/sql"
"github.com/uptrace/bun"
"github.com/uptrace/bun/driver/pgdriver"
"github.com/uptrace/bun/dialect/pgdialect"
)
// PostgreSQL
func NewPostgresDB(dsn string) *bun.DB {
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
db := bun.NewDB(sqldb, pgdialect.New())
return db
}
// SQLite
import (
"github.com/uptrace/bun/driver/sqliteshim"
"github.com/uptrace/bun/dialect/sqlitedialect"
)
func NewSQLiteDB(path string) *bun.DB {
sqldb, _ := sql.Open(sqliteshim.ShimName, path)
db := bun.NewDB(sqldb, sqlitedialect.New())
return db
}
```
### Model Definition
BUN models use struct tags:
```go
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
ID string `bun:"id,pk,type:varchar(36)"`
Username string `bun:"username,unique,notnull"`
Email string `bun:"email,unique,notnull"`
Password string `bun:"password,notnull"`
Role string `bun:"role,notnull,default:'user'"`
Active bool `bun:"active,notnull,default:true"`
CreatedAt time.Time `bun:"created_at,notnull,default:now()"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:now()"`
DeletedAt bun.NullTime `bun:"deleted_at,soft_delete"`
// Relationships
APIKeys []*APIKey `bun:"rel:has-many,join:id=user_id"`
}
```
## CRUD Operations
### Create
```go
user := &User{
ID: uuid.New().String(),
Username: "john",
Email: "john@example.com",
Password: hashedPassword,
Role: "user",
Active: true,
}
_, err := db.NewInsert().
Model(user).
Exec(ctx)
```
### Read
```go
// Single record
user := new(User)
err := db.NewSelect().
Model(user).
Where("id = ?", userID).
Scan(ctx)
// Multiple records
var users []User
err := db.NewSelect().
Model(&users).
Where("active = ?", true).
Order("created_at DESC").
Limit(10).
Scan(ctx)
```
### Update
```go
// Update specific fields
_, err := db.NewUpdate().
Model(&user).
Column("username", "email").
Where("id = ?", user.ID).
Exec(ctx)
// Update all fields
_, err := db.NewUpdate().
Model(&user).
WherePK().
Exec(ctx)
```
### Delete
```go
// Soft delete (if model has soft_delete tag)
_, err := db.NewDelete().
Model(&user).
Where("id = ?", userID).
Exec(ctx)
// Hard delete
_, err := db.NewDelete().
Model(&user).
Where("id = ?", userID).
ForceDelete().
Exec(ctx)
```
## Relationships
### Has-Many
```go
type User struct {
bun.BaseModel `bun:"table:users"`
ID string `bun:"id,pk"`
APIKeys []*APIKey `bun:"rel:has-many,join:id=user_id"`
}
type APIKey struct {
bun.BaseModel `bun:"table:api_keys"`
ID string `bun:"id,pk"`
UserID string `bun:"user_id,notnull"`
User *User `bun:"rel:belongs-to,join:user_id=id"`
}
// Load with relations
var users []User
err := db.NewSelect().
Model(&users).
Relation("APIKeys").
Scan(ctx)
```
### Belongs-To
```go
var apiKey APIKey
err := db.NewSelect().
Model(&apiKey).
Relation("User").
Where("api_key.id = ?", keyID).
Scan(ctx)
```
### Many-to-Many
```go
type User struct {
ID string `bun:"id,pk"`
Roles []*Role `bun:"m2m:user_roles,join:User=Role"`
}
type Role struct {
ID string `bun:"id,pk"`
Users []*User `bun:"m2m:user_roles,join:Role=User"`
}
type UserRole struct {
UserID string `bun:"user_id,pk"`
RoleID string `bun:"role_id,pk"`
User *User `bun:"rel:belongs-to,join:user_id=id"`
Role *Role `bun:"rel:belongs-to,join:role_id=id"`
}
```
## Queries
### Filtering
```go
// WHERE clauses
err := db.NewSelect().
Model(&users).
Where("role = ?", "admin").
Where("active = ?", true).
Scan(ctx)
// OR conditions
err := db.NewSelect().
Model(&users).
WhereGroup(" OR ", func(q *bun.SelectQuery) *bun.SelectQuery {
return q.
Where("role = ?", "admin").
Where("role = ?", "moderator")
}).
Scan(ctx)
// IN clause
err := db.NewSelect().
Model(&users).
Where("role IN (?)", bun.In([]string{"admin", "moderator"})).
Scan(ctx)
// LIKE
err := db.NewSelect().
Model(&users).
Where("username LIKE ?", "john%").
Scan(ctx)
```
### Sorting
```go
err := db.NewSelect().
Model(&users).
Order("created_at DESC").
Order("username ASC").
Scan(ctx)
```
### Pagination
```go
// Offset/Limit
err := db.NewSelect().
Model(&users).
Limit(20).
Offset(40).
Scan(ctx)
// Count
count, err := db.NewSelect().
Model((*User)(nil)).
Where("active = ?", true).
Count(ctx)
```
### Aggregations
```go
// COUNT
count, err := db.NewSelect().
Model((*User)(nil)).
Count(ctx)
// GROUP BY
type Result struct {
Role string `bun:"role"`
Count int `bun:"count"`
}
var results []Result
err := db.NewSelect().
Model((*User)(nil)).
Column("role").
ColumnExpr("COUNT(*) as count").
Group("role").
Scan(ctx, &results)
```
## Transactions
```go
err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
// Create user
_, err := tx.NewInsert().
Model(&user).
Exec(ctx)
if err != nil {
return err
}
// Create API key
_, err = tx.NewInsert().
Model(&apiKey).
Exec(ctx)
if err != nil {
return err
}
return nil
})
```
## Migrations
### Create Table
```go
_, err := db.NewCreateTable().
Model((*User)(nil)).
IfNotExists().
Exec(ctx)
```
### Add Column
```go
_, err := db.NewAddColumn().
Model((*User)(nil)).
ColumnExpr("phone VARCHAR(50)").
Exec(ctx)
```
### Drop Table
```go
_, err := db.NewDropTable().
Model((*User)(nil)).
IfExists().
Exec(ctx)
```
## Hooks
### Before/After Hooks
```go
var _ bun.BeforeAppendModelHook = (*User)(nil)
func (u *User) BeforeAppendModel(ctx context.Context, query bun.Query) error {
switch query.(type) {
case *bun.InsertQuery:
u.CreatedAt = time.Now()
case *bun.UpdateQuery:
u.UpdatedAt = time.Now()
}
return nil
}
```
## ResolveSpec Integration
### Setup with BUN
```go
import (
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
"git.warky.dev/wdevs/whatshooked/pkg/models"
)
// Create ResolveSpec handler with BUN
handler := restheadspec.NewHandlerWithBun(db)
// Setup routes (models are automatically discovered)
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler, nil)
```
### Custom Queries
```go
// Use BUN directly for complex queries
var results []struct {
UserID string
HookCount int
}
err := db.NewSelect().
Model((*models.Hook)(nil)).
Column("user_id").
ColumnExpr("COUNT(*) as hook_count").
Group("user_id").
Scan(ctx, &results)
```
## Performance Tips
### 1. Use Column Selection
```go
// Don't load unnecessary columns
err := db.NewSelect().
Model(&users).
Column("id", "username", "email").
Scan(ctx)
```
### 2. Batch Operations
```go
// Insert multiple records
_, err := db.NewInsert().
Model(&users).
Exec(ctx)
```
### 3. Use Indexes
```dbml
indexes {
(user_id) [name: 'idx_hooks_user_id']
(created_at) [name: 'idx_hooks_created_at']
}
```
### 4. Connection Pooling
```go
sqldb.SetMaxOpenConns(25)
sqldb.SetMaxIdleConns(25)
sqldb.SetConnMaxLifetime(5 * time.Minute)
```
### 5. Prepared Statements
BUN automatically uses prepared statements for better performance.
## Common Patterns
### Repository Pattern
```go
type UserRepository struct {
db *bun.DB
}
func NewUserRepository(db *bun.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) GetByID(ctx context.Context, id string) (*models.User, error) {
user := new(models.User)
err := r.db.NewSelect().
Model(user).
Where("id = ?", id).
Scan(ctx)
return user, err
}
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*models.User, error) {
user := new(models.User)
err := r.db.NewSelect().
Model(user).
Where("username = ?", username).
Scan(ctx)
return user, err
}
```
### Soft Deletes
```go
type User struct {
bun.BaseModel `bun:"table:users"`
ID string `bun:"id,pk"`
DeletedAt bun.NullTime `bun:"deleted_at,soft_delete"`
}
// Automatically filters out soft-deleted records
var users []User
err := db.NewSelect().
Model(&users).
Scan(ctx)
// Include soft-deleted records
err := db.NewSelect().
Model(&users).
WhereAllWithDeleted().
Scan(ctx)
```
### Multi-Tenancy
```go
// Filter by user_id for all queries
err := db.NewSelect().
Model(&hooks).
Where("user_id = ?", currentUserID).
Scan(ctx)
```
## Error Handling
```go
import "github.com/uptrace/bun/driver/pgdriver"
err := db.NewSelect().Model(&user).Where("id = ?", id).Scan(ctx)
switch {
case errors.Is(err, sql.ErrNoRows):
// Not found
case err != nil:
var pgErr pgdriver.Error
if errors.As(err, &pgErr) {
// PostgreSQL specific error
if pgErr.IntegrityViolation() {
// Handle constraint violation
}
}
}
```
## Testing
```go
import (
"github.com/uptrace/bun"
"github.com/uptrace/bun/dbfixture"
)
func TestUserRepository(t *testing.T) {
// Use in-memory SQLite for tests
db := NewSQLiteDB(":memory:")
defer db.Close()
// Create tables
ctx := context.Background()
_, err := db.NewCreateTable().
Model((*models.User)(nil)).
Exec(ctx)
require.NoError(t, err)
// Test repository
repo := NewUserRepository(db)
user := &models.User{...}
err = repo.Create(ctx, user)
require.NoError(t, err)
}
```
## References
- BUN Documentation: https://bun.uptrace.dev/
- BUN GitHub: https://github.com/uptrace/bun
- PostgreSQL Driver: https://bun.uptrace.dev/postgres/
- SQLite Driver: https://bun.uptrace.dev/sqlite/
- ResolveSpec: https://github.com/bitechdev/ResolveSpec

259
tooldoc/CODE_GUIDELINES.md Normal file
View File

@@ -0,0 +1,259 @@
# Code Guidelines for WhatsHooked
## General Principles
- Write clean, idiomatic Go code following standard conventions
- Use meaningful variable and function names
- Keep functions small and focused on a single responsibility
- Document exported functions, types, and packages
- Handle errors explicitly, never ignore them
- Use context.Context for cancellation and timeout handling
## Project Structure
```
whatshooked/
├── cmd/ # Command-line entry points
│ ├── server/ # HTTP server command
│ └── cli/ # CLI tools
├── pkg/ # Public packages
│ ├── auth/ # Authentication & authorization
│ ├── api/ # API endpoints
│ ├── cache/ # Caching layer
│ ├── config/ # Configuration management
│ ├── events/ # Event system
│ ├── eventlogger/ # Event logging
│ ├── handlers/ # HTTP handlers
│ ├── hooks/ # Webhook management
│ ├── logging/ # Structured logging
│ ├── storage/ # Database storage layer
│ ├── utils/ # Utility functions
│ ├── webserver/ # Web server with ResolveSpec
│ ├── whatsapp/ # WhatsApp integration
│ └── whatshooked/ # Core application logic
└── tooldoc/ # Tool & library documentation
```
## Naming Conventions
### Packages
- Use lowercase, single-word package names
- Use descriptive names that reflect the package's purpose
- Avoid generic names like `util` or `common` (use `utils` with specific subdirectories if needed)
### Files
- Use snake_case for file names: `user_service.go`, `auth_middleware.go`
- Test files: `user_service_test.go`
- Keep related functionality in the same file
### Variables & Functions
- Use camelCase for private: `userService`, `handleRequest`
- Use PascalCase for exported: `UserService`, `HandleRequest`
- Use descriptive names: prefer `userRepository` over `ur`
- Boolean variables should be prefixed: `isValid`, `hasPermission`, `canAccess`
### Constants
- Use PascalCase for exported: `DefaultTimeout`
- Use camelCase for private: `defaultTimeout`
- Group related constants together
### Interfaces
- Name interfaces with -er suffix when appropriate: `Reader`, `Writer`, `Handler`
- Use descriptive names: `UserRepository`, `AuthService`
## Error Handling
```go
// Wrap errors with context
if err != nil {
return fmt.Errorf("failed to load user: %w", err)
}
// Check specific errors
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
// Custom error types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
```
## Logging
Use zerolog for structured logging:
```go
import "github.com/rs/zerolog/log"
log.Info().
Str("user_id", userID).
Msg("User logged in")
log.Error().
Err(err).
Str("operation", "database_query").
Msg("Failed to query database")
```
## Context Usage
Always pass context as the first parameter:
```go
func ProcessRequest(ctx context.Context, userID string) error {
// Check for cancellation
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Pass context to downstream calls
user, err := userRepo.GetByID(ctx, userID)
if err != nil {
return err
}
return nil
}
```
## Testing
- Write table-driven tests
- Use descriptive test names: `TestUserService_Create_WithValidData_Success`
- Mock external dependencies
- Aim for high coverage of business logic
- Use t.Run for subtests
```go
func TestUserService_Create(t *testing.T) {
tests := []struct {
name string
input *User
wantErr bool
}{
{
name: "valid user",
input: &User{Name: "John", Email: "john@example.com"},
wantErr: false,
},
{
name: "missing email",
input: &User{Name: "John"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
service := NewUserService()
err := service.Create(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
```
## Database Operations
- Use transactions for multiple related operations
- Always use prepared statements
- Handle NULL values properly
- Use meaningful struct tags
```go
type User struct {
ID string `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
## ResolveSpec Integration
When using ResolveSpec:
- Register all models in the registry
- Use schema.table format: "public.users", "core.accounts"
- Implement hooks for auth and validation
- Use lifecycle hooks for audit logging
## API Design
- Use RESTful conventions
- Return appropriate HTTP status codes
- Include meaningful error messages
- Version your APIs: `/api/v1/users`
- Document all endpoints
## Security
- Never log sensitive data (passwords, tokens, etc.)
- Validate all input
- Use parameterized queries
- Implement rate limiting
- Use HTTPS in production
- Sanitize user input
- Implement proper CORS policies
## Dependencies
- Keep dependencies minimal
- Pin versions in go.mod
- Regularly update dependencies
- Document why each dependency is needed
## Documentation
- Document all exported functions, types, and packages
- Use godoc format
- Include examples in documentation
- Keep README.md up to date
- Document configuration options
## Comments
```go
// Package auth provides authentication and authorization functionality.
package auth
// User represents a system user.
type User struct {
ID string
}
// Authenticate verifies user credentials and returns a token.
// Returns ErrInvalidCredentials if authentication fails.
func Authenticate(ctx context.Context, username, password string) (string, error) {
// Implementation
}
```
## Performance
- Use connection pools for databases
- Implement caching where appropriate
- Avoid N+1 queries
- Use batch operations when possible
- Profile critical paths
- Set appropriate timeouts
## Git Workflow
- Write clear commit messages
- Keep commits atomic and focused
- Use conventional commits format
- Create feature branches
- Run tests before committing
- Review your own changes before pushing

316
tooldoc/ORANGURU.md Normal file
View File

@@ -0,0 +1,316 @@
# Oranguru Integration Guide
## Overview
Oranguru is a React component library that provides enhanced Mantine-based components with advanced features and state management capabilities. For WhatsHooked, we'll use it to build data grids and forms for the admin interface.
## Installation
```bash
npm install @warkypublic/oranguru
```
### Peer Dependencies
```bash
npm install react zustand @mantine/core @mantine/hooks @warkypublic/artemis-kit @warkypublic/zustandsyncstore use-sync-external-store
```
## Core Concepts
### Enhanced Context Menus
Oranguru provides better menu positioning and visibility control than standard Mantine menus.
### Custom Rendering
Support for custom menu item renderers and complete menu rendering.
### State Management
Uses Zustand for component state management with sync capabilities.
## Basic Setup
```tsx
import { MantineBetterMenusProvider } from '@warkypublic/oranguru';
import { MantineProvider } from '@mantine/core';
function App() {
return (
<MantineProvider>
<MantineBetterMenusProvider>
{/* Your app content */}
</MantineBetterMenusProvider>
</MantineProvider>
);
}
```
## Using Context Menus
```tsx
import { useMantineBetterMenus } from '@warkypublic/oranguru';
function DataGrid() {
const { show, hide } = useMantineBetterMenus();
const handleContextMenu = (e: React.MouseEvent, record: any) => {
e.preventDefault();
show('record-menu', {
x: e.clientX,
y: e.clientY,
items: [
{
label: 'Edit',
onClick: () => handleEdit(record)
},
{
label: 'Delete',
onClick: () => handleDelete(record)
},
{
isDivider: true
},
{
label: 'View Details',
onClick: () => handleViewDetails(record)
}
]
});
};
return (
<table>
{records.map(record => (
<tr key={record.id} onContextMenu={(e) => handleContextMenu(e, record)}>
<td>{record.name}</td>
</tr>
))}
</table>
);
}
```
## Async Actions
```tsx
const asyncMenuItem = {
label: 'Sync Data',
onClickAsync: async () => {
await fetch('/api/sync', { method: 'POST' });
// Shows loading state automatically
}
};
```
## Custom Menu Items
```tsx
const customItem = {
renderer: ({ loading }: any) => (
<div style={{ padding: '8px 12px' }}>
{loading ? (
<Loader size="xs" />
) : (
<Group>
<IconCheck size={16} />
<Text>Custom Action</Text>
</Group>
)}
</div>
),
onClickAsync: async () => {
await performAction();
}
};
```
## Integration with Data Grids
While Oranguru doesn't provide a built-in data grid component yet, it works excellently with Mantine's DataTable or custom table implementations:
```tsx
import { useMantineBetterMenus } from '@warkypublic/oranguru';
import { DataTable } from '@mantine/datatable';
function UserGrid() {
const { show } = useMantineBetterMenus();
const [users, setUsers] = useState([]);
const columns = [
{ accessor: 'name', title: 'Name' },
{ accessor: 'email', title: 'Email' },
{ accessor: 'status', title: 'Status' }
];
const handleRowContextMenu = (e: React.MouseEvent, user: User) => {
e.preventDefault();
show('user-menu', {
x: e.clientX,
y: e.clientY,
items: [
{
label: 'Edit User',
onClick: () => navigate(`/users/${user.id}/edit`)
},
{
label: 'Deactivate',
onClickAsync: async () => {
await fetch(`/api/users/${user.id}/deactivate`, { method: 'POST' });
await refreshUsers();
}
},
{
isDivider: true
},
{
label: 'Delete',
onClick: () => handleDelete(user.id)
}
]
});
};
return (
<DataTable
columns={columns}
records={users}
onRowContextMenu={({ event, record }) => handleRowContextMenu(event, record)}
/>
);
}
```
## Form Integration
For forms, use Mantine's form components with Oranguru's menu system for enhanced UX:
```tsx
import { useForm } from '@mantine/form';
import { TextInput, Select, Button } from '@mantine/core';
function UserForm() {
const form = useForm({
initialValues: {
name: '',
email: '',
role: ''
},
validate: {
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
name: (value) => (value.length < 2 ? 'Name too short' : null)
}
});
const handleSubmit = async (values: typeof form.values) => {
try {
await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
});
notifications.show({ message: 'User created successfully' });
} catch (error) {
notifications.show({
message: 'Failed to create user',
color: 'red'
});
}
};
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
label="Name"
placeholder="John Doe"
{...form.getInputProps('name')}
/>
<TextInput
label="Email"
placeholder="john@example.com"
{...form.getInputProps('email')}
/>
<Select
label="Role"
data={['Admin', 'User', 'Viewer']}
{...form.getInputProps('role')}
/>
<Button type="submit">Create User</Button>
</form>
);
}
```
## Best Practices
1. **Provider Placement**: Place MantineBetterMenusProvider at the root of your app
2. **Menu IDs**: Use descriptive, unique IDs for each menu type
3. **Context Menus**: Always prevent default on right-click events
4. **Loading States**: Use onClickAsync for async operations to get automatic loading states
5. **Custom Renderers**: Use custom renderers for complex menu items with icons, badges, etc.
6. **Portal Rendering**: Oranguru uses portals for proper z-index handling
7. **State Management**: Leverage Zustand store for complex menu state
8. **Accessibility**: Ensure keyboard navigation works with context menus
## Common Patterns
### Multi-Select Context Menu
```tsx
const handleBulkContextMenu = (e: React.MouseEvent, selectedIds: string[]) => {
e.preventDefault();
show('bulk-menu', {
x: e.clientX,
y: e.clientY,
items: [
{
label: `Delete ${selectedIds.length} items`,
onClickAsync: async () => {
await bulkDelete(selectedIds);
}
},
{
label: 'Export selection',
onClick: () => exportItems(selectedIds)
}
]
});
};
```
### Conditional Menu Items
```tsx
const menuItems = [
{
label: 'Edit',
onClick: () => handleEdit(record)
},
canDelete(record) && {
label: 'Delete',
onClick: () => handleDelete(record)
},
{
isDivider: true
},
isAdmin && {
label: 'Admin Actions',
onClick: () => showAdminActions(record)
}
].filter(Boolean); // Remove falsy items
```
### Nested Menus (Future Feature)
While not yet supported, Oranguru is designed to support nested menus in future versions.
## Integration with WhatsHooked
For WhatsHooked's admin interface:
1. **User Management Grid**: Use DataTable with context menus for user actions
2. **Hook Configuration**: Form with validation and async submission
3. **WhatsApp Account Management**: Grid with QR code display and pairing actions
4. **API Key Management**: Grid with copy-to-clipboard and revoke actions
5. **Event Logs**: Read-only grid with filtering and export
## References
- Official Repository: https://git.warky.dev/wdevs/oranguru
- Mantine Documentation: https://mantine.dev
- Mantine DataTable: https://icflorescu.github.io/mantine-datatable/

View File

@@ -0,0 +1,504 @@
# React + Mantine + TanStack Start Integration Guide
## Overview
For WhatsHooked's admin interface, we'll use:
- **React 19**: Modern React with hooks and suspense
- **Mantine**: Component library for UI
- **TanStack Start**: Full-stack React framework with server-side rendering
- **Oranguru**: Enhanced Mantine components for grids and forms
## Project Structure
```
frontend/
├── app/
│ ├── routes/
│ │ ├── __root.tsx # Root layout
│ │ ├── index.tsx # Dashboard
│ │ ├── login.tsx # Login page
│ │ ├── users/
│ │ │ ├── index.tsx # User list
│ │ │ ├── new.tsx # Create user
│ │ │ └── $id/
│ │ │ ├── index.tsx # User details
│ │ │ └── edit.tsx # Edit user
│ │ ├── hooks/
│ │ │ ├── index.tsx # Hook list
│ │ │ └── ...
│ │ └── accounts/
│ │ ├── index.tsx # WhatsApp accounts
│ │ └── ...
│ ├── components/
│ │ ├── Layout.tsx
│ │ ├── Navbar.tsx
│ │ ├── UserGrid.tsx
│ │ └── ...
│ ├── lib/
│ │ ├── api.ts # API client
│ │ ├── auth.ts # Auth utilities
│ │ └── types.ts # TypeScript types
│ └── styles/
│ └── global.css
├── public/
│ └── assets/
├── package.json
├── tsconfig.json
└── vite.config.ts
```
## Installation
```bash
npm create @tanstack/start@latest
cd whatshooked-admin
npm install @mantine/core @mantine/hooks @mantine/notifications @mantine/form @mantine/datatable
npm install @warkypublic/oranguru
npm install @tanstack/react-query axios
npm install -D @types/react @types/react-dom
```
## Basic Setup
### app/routes/__root.tsx
```tsx
import { createRootRoute, Outlet } from '@tanstack/react-router';
import { MantineProvider } from '@mantine/core';
import { MantineBetterMenusProvider } from '@warkypublic/oranguru';
import { Notifications } from '@mantine/notifications';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
export const Route = createRootRoute({
component: () => (
<QueryClientProvider client={queryClient}>
<MantineProvider>
<MantineBetterMenusProvider>
<Notifications />
<Outlet />
</MantineBetterMenusProvider>
</MantineProvider>
</QueryClientProvider>
),
});
```
### app/lib/api.ts
```typescript
import axios from 'axios';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8825/api/v1',
headers: {
'Content-Type': 'application/json',
},
});
// Add auth token to requests
api.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle auth errors
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default api;
// API methods
export const authApi = {
login: (username: string, password: string) =>
api.post('/auth/login', { username, password }),
logout: () =>
api.post('/auth/logout'),
getProfile: () =>
api.get('/auth/profile'),
};
export const usersApi = {
list: (params?: any) =>
api.get('/users', { params }),
get: (id: string) =>
api.get(`/users/${id}`),
create: (data: any) =>
api.post('/users', data),
update: (id: string, data: any) =>
api.put(`/users/${id}`, data),
delete: (id: string) =>
api.delete(`/users/${id}`),
};
export const hooksApi = {
list: () =>
api.get('/hooks'),
create: (data: any) =>
api.post('/hooks', data),
update: (id: string, data: any) =>
api.put(`/hooks/${id}`, data),
delete: (id: string) =>
api.delete(`/hooks/${id}`),
};
export const accountsApi = {
list: () =>
api.get('/accounts'),
pair: (accountId: string) =>
api.post(`/accounts/${accountId}/pair`),
disconnect: (accountId: string) =>
api.post(`/accounts/${accountId}/disconnect`),
getQRCode: (accountId: string) =>
api.get(`/accounts/${accountId}/qr`, { responseType: 'blob' }),
};
```
## Authentication
### app/routes/login.tsx
```tsx
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useForm } from '@mantine/form';
import { TextInput, PasswordInput, Button, Paper, Title, Container } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { authApi } from '../lib/api';
export const Route = createFileRoute('/login')({
component: LoginPage,
});
function LoginPage() {
const navigate = useNavigate();
const form = useForm({
initialValues: {
username: '',
password: '',
},
validate: {
username: (value) => (value.length < 3 ? 'Username too short' : null),
password: (value) => (value.length < 6 ? 'Password too short' : null),
},
});
const handleSubmit = async (values: typeof form.values) => {
try {
const response = await authApi.login(values.username, values.password);
localStorage.setItem('auth_token', response.data.token);
notifications.show({
title: 'Success',
message: 'Logged in successfully',
color: 'green',
});
navigate({ to: '/' });
} catch (error) {
notifications.show({
title: 'Error',
message: 'Invalid credentials',
color: 'red',
});
}
};
return (
<Container size={420} my={40}>
<Title ta="center">WhatsHooked Admin</Title>
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
label="Username"
placeholder="admin"
required
{...form.getInputProps('username')}
/>
<PasswordInput
label="Password"
placeholder="Your password"
required
mt="md"
{...form.getInputProps('password')}
/>
<Button fullWidth mt="xl" type="submit">
Sign in
</Button>
</form>
</Paper>
</Container>
);
}
```
## Data Grid with Oranguru
### app/routes/users/index.tsx
```tsx
import { createFileRoute } from '@tanstack/react-router';
import { useQuery } from '@tanstack/react-query';
import { DataTable } from '@mantine/datatable';
import { useMantineBetterMenus } from '@warkypublic/oranguru';
import { Button, Group, Text } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { usersApi } from '../../lib/api';
export const Route = createFileRoute('/users/')({
component: UsersPage,
});
function UsersPage() {
const { show } = useMantineBetterMenus();
const { data: users, isLoading, refetch } = useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await usersApi.list();
return response.data;
},
});
const handleContextMenu = (e: React.MouseEvent, user: any) => {
e.preventDefault();
show('user-menu', {
x: e.clientX,
y: e.clientY,
items: [
{
label: 'Edit',
onClick: () => navigate({ to: `/users/${user.id}/edit` }),
},
{
label: 'View Details',
onClick: () => navigate({ to: `/users/${user.id}` }),
},
{
isDivider: true,
},
{
label: 'Delete',
onClickAsync: async () => {
await usersApi.delete(user.id);
notifications.show({
message: 'User deleted successfully',
color: 'green',
});
refetch();
},
},
],
});
};
const columns = [
{ accessor: 'name', title: 'Name' },
{ accessor: 'email', title: 'Email' },
{ accessor: 'role', title: 'Role' },
{
accessor: 'actions',
title: '',
render: (user: any) => (
<Group gap="xs">
<Button size="xs" onClick={() => navigate({ to: `/users/${user.id}/edit` })}>
Edit
</Button>
</Group>
),
},
];
return (
<div>
<Group justify="space-between" mb="md">
<Text size="xl" fw={700}>Users</Text>
<Button onClick={() => navigate({ to: '/users/new' })}>
Create User
</Button>
</Group>
<DataTable
columns={columns}
records={users || []}
fetching={isLoading}
onRowContextMenu={({ event, record }) => handleContextMenu(event, record)}
/>
</div>
);
}
```
## Forms
### app/routes/users/new.tsx
```tsx
import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { useForm } from '@mantine/form';
import { TextInput, Select, Button, Paper } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { usersApi } from '../../lib/api';
export const Route = createFileRoute('/users/new')({
component: NewUserPage,
});
function NewUserPage() {
const navigate = useNavigate();
const form = useForm({
initialValues: {
name: '',
email: '',
password: '',
role: 'user',
},
validate: {
name: (value) => (value.length < 2 ? 'Name too short' : null),
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
password: (value) => (value.length < 6 ? 'Password too short' : null),
},
});
const handleSubmit = async (values: typeof form.values) => {
try {
await usersApi.create(values);
notifications.show({
message: 'User created successfully',
color: 'green',
});
navigate({ to: '/users' });
} catch (error) {
notifications.show({
message: 'Failed to create user',
color: 'red',
});
}
};
return (
<Paper p="md">
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
label="Name"
placeholder="John Doe"
required
{...form.getInputProps('name')}
/>
<TextInput
label="Email"
placeholder="john@example.com"
required
mt="md"
{...form.getInputProps('email')}
/>
<PasswordInput
label="Password"
placeholder="Password"
required
mt="md"
{...form.getInputProps('password')}
/>
<Select
label="Role"
data={['admin', 'user', 'viewer']}
required
mt="md"
{...form.getInputProps('role')}
/>
<Group justify="flex-end" mt="xl">
<Button variant="subtle" onClick={() => navigate({ to: '/users' })}>
Cancel
</Button>
<Button type="submit">Create</Button>
</Group>
</form>
</Paper>
);
}
```
## Layout with Navigation
### app/components/Layout.tsx
```tsx
import { AppShell, NavLink, Group, Title } from '@mantine/core';
import { IconUsers, IconWebhook, IconBrandWhatsapp, IconKey } from '@tabler/icons-react';
import { Link, useLocation } from '@tanstack/react-router';
export function Layout({ children }: { children: React.ReactNode }) {
const location = useLocation();
const navItems = [
{ icon: IconUsers, label: 'Users', to: '/users' },
{ icon: IconWebhook, label: 'Hooks', to: '/hooks' },
{ icon: IconBrandWhatsapp, label: 'WhatsApp Accounts', to: '/accounts' },
{ icon: IconKey, label: 'API Keys', to: '/api-keys' },
];
return (
<AppShell
navbar={{ width: 250, breakpoint: 'sm' }}
padding="md"
>
<AppShell.Navbar p="md">
<Title order={3} mb="md">WhatsHooked</Title>
{navItems.map((item) => (
<NavLink
key={item.to}
component={Link}
to={item.to}
label={item.label}
leftSection={<item.icon size={20} />}
active={location.pathname.startsWith(item.to)}
/>
))}
</AppShell.Navbar>
<AppShell.Main>{children}</AppShell.Main>
</AppShell>
);
}
```
## Best Practices
1. **Code Splitting**: Use lazy loading for routes
2. **Error Boundaries**: Wrap components in error boundaries
3. **Loading States**: Show loading indicators with Suspense
4. **Optimistic Updates**: Update UI before API response
5. **Form Validation**: Use Mantine form with validation
6. **Type Safety**: Use TypeScript for all API calls
7. **Query Invalidation**: Refetch data after mutations
8. **Auth Protection**: Protect routes with auth guards
## References
- TanStack Start: https://tanstack.com/start
- Mantine: https://mantine.dev
- TanStack Query: https://tanstack.com/query
- Oranguru: See ORANGURU.md

295
tooldoc/RELSPECGO.md Normal file
View File

@@ -0,0 +1,295 @@
# relspecgo - DBML to BUN Model Generator
## Overview
relspecgo is a code generator that converts DBML (Database Markup Language) schema files into BUN ORM models for Go. It automates the creation of model structs with proper BUN tags, relationships, and indexes.
Repository: https://git.warky.dev/wdevs/relspecgo
## Installation
```bash
go install git.warky.dev/wdevs/relspecgo@latest
```
Or via Makefile:
```bash
make install-relspecgo
```
## Usage
### Basic Command
```bash
relspecgo generate --input=sql/schema.dbml --output=pkg/models --orm=bun
```
### Via Makefile
```bash
make generate-models
```
This will:
1. Read `sql/schema.dbml`
2. Generate BUN models in `pkg/models/`
3. Create proper Go structs with BUN tags
## DBML Schema Format
### Table Definition
```dbml
Table users {
id varchar(36) [primary key]
username varchar(255) [unique, not null]
email varchar(255) [unique, not null]
password varchar(255) [not null]
role varchar(50) [not null, default: 'user']
active boolean [not null, default: true]
created_at timestamp [not null, default: `now()`]
updated_at timestamp [not null, default: `now()`]
deleted_at timestamp [null]
indexes {
(deleted_at) [name: 'idx_users_deleted_at']
}
}
```
### Relationships
```dbml
Table api_keys {
id varchar(36) [primary key]
user_id varchar(36) [not null, ref: > users.id]
...
}
// Explicit relationship with cascade delete
Ref: api_keys.user_id > users.id [delete: cascade]
```
### Supported Field Types
- `varchar(n)``string`
- `text``string`
- `int`, `integer``int`
- `bigint``int64`
- `boolean`, `bool``bool`
- `timestamp`, `datetime``time.Time`
- `json`, `jsonb``json.RawMessage` or custom type
### Field Attributes
- `[primary key]` → BUN primary key tag
- `[not null]` → Required field
- `[unique]` → Unique constraint
- `[default: value]` → Default value
- `[note: 'text']` → Documentation comment
- `[ref: > table.column]` → Foreign key relationship
## Generated BUN Models
### Example Output
```go
package models
import (
"time"
"github.com/uptrace/bun"
)
type User struct {
bun.BaseModel `bun:"table:users,alias:u"`
ID string `bun:"id,pk,type:varchar(36)" json:"id"`
Username string `bun:"username,unique,notnull,type:varchar(255)" json:"username"`
Email string `bun:"email,unique,notnull,type:varchar(255)" json:"email"`
Password string `bun:"password,notnull,type:varchar(255)" json:"-"`
FullName string `bun:"full_name,type:varchar(255)" json:"full_name,omitempty"`
Role string `bun:"role,notnull,default:'user',type:varchar(50)" json:"role"`
Active bool `bun:"active,notnull,default:true" json:"active"`
CreatedAt time.Time `bun:"created_at,notnull,default:now()" json:"created_at"`
UpdatedAt time.Time `bun:"updated_at,notnull,default:now()" json:"updated_at"`
DeletedAt time.Time `bun:"deleted_at,soft_delete" json:"deleted_at,omitempty"`
// Relationships
APIKeys []*APIKey `bun:"rel:has-many,join:id=user_id" json:"api_keys,omitempty"`
Hooks []*Hook `bun:"rel:has-many,join:id=user_id" json:"hooks,omitempty"`
WhatsAppAccounts []*WhatsAppAccount `bun:"rel:has-many,join:id=user_id" json:"whatsapp_accounts,omitempty"`
}
```
### BUN Tags
- `bun:"table:users,alias:u"` - Table name and alias
- `bun:"id,pk"` - Primary key
- `bun:"username,unique"` - Unique constraint
- `bun:"password,notnull"` - NOT NULL constraint
- `bun:"role,default:'user'"` - Default value
- `bun:"type:varchar(255)"` - Explicit column type
- `bun:"deleted_at,soft_delete"` - Soft delete support
- `bun:"rel:has-many,join:id=user_id"` - Has-many relationship
## Project Structure
```
sql/
├── schema.dbml # Main schema definition
├── postgres/ # PostgreSQL specific migrations
│ ├── 20240101_init.up.sql
│ └── 20240101_init.down.sql
└── sqlite/ # SQLite specific migrations
├── 20240101_init.up.sql
└── 20240101_init.down.sql
pkg/
└── models/ # Generated BUN models
├── user.go
├── api_key.go
├── hook.go
└── ...
```
## Workflow
### 1. Define Schema
Create or update `sql/schema.dbml`:
```dbml
Table products {
id int [primary key, increment]
name varchar(255) [not null]
price decimal(10,2) [not null]
created_at timestamp [not null, default: `now()`]
}
```
### 2. Generate Models
```bash
make generate-models
```
### 3. Create Migrations
```bash
make migrate-create NAME=add_products_table
```
Edit generated migration files in `sql/postgres/` and `sql/sqlite/`
### 4. Run Migrations
```bash
make migrate-up
```
### 5. Use in Code
```go
import "git.warky.dev/wdevs/whatshooked/pkg/models"
// Query with BUN
var users []models.User
err := db.NewSelect().
Model(&users).
Relation("APIKeys").
Where("active = ?", true).
Scan(ctx)
```
## Best Practices
1. **Single Source of Truth**: Keep DBML as the source of truth for schema
2. **Regenerate After Changes**: Always run `make generate-models` after DBML changes
3. **Don't Edit Generated Files**: Modify DBML instead, then regenerate
4. **Version Control**: Commit both DBML and generated models
5. **Migrations**: Create migrations for schema changes
6. **Relationships**: Define relationships in DBML for proper code generation
7. **Indexes**: Specify indexes in DBML for performance
## Common Commands
```bash
# Generate models from DBML
make generate-models
# Create new migration
make migrate-create NAME=add_users_table
# Run migrations
make migrate-up
# Rollback migrations
make migrate-down
# Install relspecgo
make install-relspecgo
```
## DBML to SQL Conversion
relspecgo can also generate SQL from DBML:
```bash
relspecgo sql --input=sql/schema.dbml --output=sql/postgres/schema.sql --dialect=postgres
relspecgo sql --input=sql/schema.dbml --output=sql/sqlite/schema.sql --dialect=sqlite
```
## Advantages
1. **Type Safety**: Generated Go structs are type-safe
2. **Consistency**: Same schema definition for all models
3. **Documentation**: DBML serves as schema documentation
4. **Validation**: Catches schema errors before runtime
5. **IDE Support**: Full IDE autocomplete and type checking
6. **Relationships**: Automatic relationship setup
7. **Migration Friendly**: Easy to track schema changes
## Integration with ResolveSpec
Generated BUN models work seamlessly with ResolveSpec:
```go
import (
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
"git.warky.dev/wdevs/whatshooked/pkg/models"
)
// Create handler with BUN
handler := restheadspec.NewHandlerWithBun(db)
// Models are automatically discovered from BUN's table names
```
## Troubleshooting
### Models Not Generated
- Check DBML syntax
- Ensure relspecgo is installed: `make install-relspecgo`
- Verify input/output paths
### Compilation Errors
- Run `go mod tidy` to update dependencies
- Check for missing imports
- Verify BUN version compatibility
### Relationship Issues
- Ensure foreign keys are properly defined in DBML
- Check `Ref:` declarations
- Verify join conditions
## References
- relspecgo: https://git.warky.dev/wdevs/relspecgo
- DBML Syntax: https://dbml.dbdiagram.io/docs/
- BUN ORM: https://bun.uptrace.dev/
- ResolveSpec: https://github.com/bitechdev/ResolveSpec

359
tooldoc/RESOLVESPEC.md Normal file
View File

@@ -0,0 +1,359 @@
# ResolveSpec Integration Guide
## Overview
ResolveSpec is a flexible REST API framework that provides GraphQL-like capabilities while maintaining REST simplicity. It offers two approaches:
1. **ResolveSpec** - Body-based API with JSON request options
2. **RestHeadSpec** - Header-based API where query options are passed via HTTP headers
For WhatsHooked, we'll use both approaches to provide maximum flexibility.
## Installation
```bash
go get github.com/bitechdev/ResolveSpec
```
## Core Concepts
### Models
Models are Go structs that represent database tables. Use GORM tags for database mapping.
```go
type User struct {
ID string `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Email string `gorm:"uniqueIndex;not null" json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
### Registry
The registry maps schema.table names to Go models.
```go
handler := resolvespec.NewHandlerWithGORM(db)
handler.Registry.RegisterModel("public.users", &User{})
handler.Registry.RegisterModel("public.hooks", &Hook{})
```
### Routing
ResolveSpec generates routes automatically for registered models:
- `/public/users` - Collection endpoints
- `/public/users/:id` - Individual resource endpoints
## ResolveSpec (Body-Based)
Request format:
```json
POST /public/users
{
"operation": "read|create|update|delete",
"data": {
// For create/update operations
},
"options": {
"columns": ["id", "name", "email"],
"filters": [
{"column": "status", "operator": "eq", "value": "active"}
],
"preload": ["hooks:id,url,events"],
"sort": ["-created_at", "+name"],
"limit": 50,
"offset": 0
}
}
```
### Setup with Gorilla Mux
```go
import (
"github.com/gorilla/mux"
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
)
func SetupResolveSpec(db *gorm.DB) *mux.Router {
handler := resolvespec.NewHandlerWithGORM(db)
// Register models
handler.Registry.RegisterModel("public.users", &User{})
handler.Registry.RegisterModel("public.hooks", &Hook{})
// Setup routes
router := mux.NewRouter()
resolvespec.SetupMuxRoutes(router, handler, nil)
return router
}
```
## RestHeadSpec (Header-Based)
Request format:
```http
GET /public/users HTTP/1.1
X-Select-Fields: id,name,email
X-FieldFilter-Status: active
X-Preload: hooks:id,url,events
X-Sort: -created_at,+name
X-Limit: 50
X-Offset: 0
X-DetailApi: true
```
### Setup with Gorilla Mux
```go
import (
"github.com/gorilla/mux"
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
)
func SetupRestHeadSpec(db *gorm.DB) *mux.Router {
handler := restheadspec.NewHandlerWithGORM(db)
// Register models
handler.Registry.RegisterModel("public.users", &User{})
handler.Registry.RegisterModel("public.hooks", &Hook{})
// Setup routes
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler, nil)
return router
}
```
## Lifecycle Hooks (RestHeadSpec)
Add hooks for authentication, validation, and audit logging:
```go
handler.OnBeforeRead(func(ctx context.Context, req *restheadspec.Request) error {
// Check permissions
userID := ctx.Value("user_id").(string)
if !canRead(userID, req.Schema, req.Entity) {
return fmt.Errorf("unauthorized")
}
return nil
})
handler.OnAfterCreate(func(ctx context.Context, req *restheadspec.Request, result interface{}) error {
// Audit log
log.Info().
Str("user_id", ctx.Value("user_id").(string)).
Str("entity", req.Entity).
Msg("Created record")
return nil
})
```
## Authentication Integration
```go
// Middleware to extract user from JWT
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := ValidateToken(token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "user_id", user.ID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Apply to routes
router.Use(AuthMiddleware)
```
## Filtering
### Field Filters (RestHeadSpec)
```http
X-FieldFilter-Status: active
X-FieldFilter-Age: 18
```
### Search Operators (RestHeadSpec)
```http
X-SearchOp-Gte-Age: 18
X-SearchOp-Like-Name: john
```
### Body Filters (ResolveSpec)
```json
{
"options": {
"filters": [
{"column": "status", "operator": "eq", "value": "active"},
{"column": "age", "operator": "gte", "value": 18},
{"column": "name", "operator": "like", "value": "john%"}
]
}
}
```
## Pagination
### Offset-Based
```http
X-Limit: 50
X-Offset: 100
```
### Cursor-Based (RestHeadSpec)
```http
X-Cursor: eyJpZCI6IjEyMyIsImNyZWF0ZWRfYXQiOiIyMDI0LTAxLTAxIn0=
X-Limit: 50
```
## Preloading Relationships
Load related entities with custom columns:
```http
X-Preload: hooks:id,url,events,posts:id,title
```
```json
{
"options": {
"preload": ["hooks:id,url,events", "posts:id,title"]
}
}
```
## Sorting
```http
X-Sort: -created_at,+name
```
Prefix with `-` for descending, `+` for ascending.
## Response Formats (RestHeadSpec)
### Simple Format (default)
```http
X-DetailApi: false
```
Returns: `[{...}, {...}]`
### Detailed Format
```http
X-DetailApi: true
```
Returns:
```json
{
"data": [{...}, {...}],
"meta": {
"total": 100,
"limit": 50,
"offset": 0,
"cursor": "..."
}
}
```
## CORS Configuration
```go
corsConfig := &common.CORSConfig{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"*"},
ExposedHeaders: []string{"X-Total-Count", "X-Cursor"},
}
restheadspec.SetupMuxRoutes(router, handler, corsConfig)
```
## Error Handling
ResolveSpec returns standard HTTP error codes:
- 200: Success
- 400: Bad Request
- 401: Unauthorized
- 404: Not Found
- 500: Internal Server Error
Error response format:
```json
{
"error": "error message",
"details": "additional context"
}
```
## Best Practices
1. **Register models before routes**: Always register all models before calling SetupMuxRoutes
2. **Use lifecycle hooks**: Implement authentication and validation in hooks
3. **Schema naming**: Use `schema.table` format consistently
4. **Transactions**: Use database transactions for multi-record operations
5. **Validation**: Validate input in OnBeforeCreate/OnBeforeUpdate hooks
6. **Audit logging**: Use OnAfter* hooks for audit trails
7. **Performance**: Use preloading instead of N+1 queries
8. **Security**: Implement row-level security in hooks
9. **Rate limiting**: Add rate limiting middleware
10. **Monitoring**: Log all operations for monitoring
## Common Patterns
### User Filtering (Multi-tenancy)
```go
handler.OnBeforeRead(func(ctx context.Context, req *restheadspec.Request) error {
userID := ctx.Value("user_id").(string)
// Add user_id filter
req.Options.Filters = append(req.Options.Filters, Filter{
Column: "user_id",
Operator: "eq",
Value: userID,
})
return nil
})
```
### Soft Deletes
```go
handler.OnBeforeDelete(func(ctx context.Context, req *restheadspec.Request) error {
// Convert to update with deleted_at
req.Operation = "update"
req.Data = map[string]interface{}{
"deleted_at": time.Now(),
}
return nil
})
```
### Validation
```go
handler.OnBeforeCreate(func(ctx context.Context, req *restheadspec.Request) error {
user := req.Data.(*User)
if user.Email == "" {
return fmt.Errorf("email is required")
}
if !isValidEmail(user.Email) {
return fmt.Errorf("invalid email format")
}
return nil
})
```
## References
- Official Docs: https://github.com/bitechdev/ResolveSpec
- ResolveSpec README: /pkg/resolvespec/README.md
- RestHeadSpec README: /pkg/restheadspec/README.md