Skip to content

axen-software/forge

Repository files navigation

Forge

Forge is a comprehensive Go utility library for building production-ready backend applications on Google Cloud Platform. It provides battle-tested abstractions for databases, logging, storage, event handling, authorization, and more.

Installation

go get github.com/axen-software/forge

Requirements

  • Go 1.21+
  • GCP Project (for cloud services)

Modules Overview

Module Description
db/sql PostgreSQL connection with pgxpool
db/nosql MongoDB client wrapper
logger Zap-based structured logging
glog GCP Cloud Logging integration
bigquery GCP BigQuery client
storage GCS file storage
firebase Firebase Admin SDK
ctasks GCP Cloud Tasks
event CloudEvents-based event bus
casbin RBAC/ABAC authorization
setting Environment configuration
validator Struct validation

PostgreSQL Database

import (
    "github.com/axen-software/forge/db"
    "github.com/axen-software/forge/db/sql"
)

func main() {
    // Create configuration
    cfg := &db.Config{
        Type:     db.SQL,
        Driver:   sql.Postgres,
        Host:     "localhost",
        Port:     "5432",
        User:     "postgres",
        Password: "password",
        Database: "mydb",
        SSLMode:  "disable",
        Debug:    true,
    }

    // Or use DSN directly
    cfg := &db.Config{
        Type:   db.SQL,
        Driver: sql.Postgres,
        DSN:    "postgres://user:pass@localhost:5432/mydb?sslmode=disable",
    }

    // Create connection
    database, err := sql.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer database.Close()

    // Use with standard database/sql
    rows, err := database.DB().Query("SELECT * FROM users")

    // Or use the pgxpool directly for better performance
    pool := database.Pool()

    // Check if debug mode for Ent ORM
    if database.IsDebug() {
        client = client.Debug()
    }
}

MongoDB

import (
    "github.com/axen-software/forge/db"
    "github.com/axen-software/forge/db/nosql"
)

func main() {
    cfg := &db.Config{
        Type:     db.NoSQL,
        Host:     "localhost",
        Port:     "27017",
        User:     "admin",
        Password: "password",
        Database: "mydb",
    }

    // Or use DSN
    cfg := &db.Config{
        Type: db.NoSQL,
        DSN:  "mongodb://admin:password@localhost:27017/mydb",
    }

    database, err := nosql.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer database.Close(ctx)

    // Get default database
    db := database.GetDefaultDatabase()

    // Query collection
    collection := db.Collection("users")
    cursor, err := collection.Find(ctx, bson.M{})
}

Logger

Zap-based structured logging with environment-aware formatting.

import (
    "github.com/axen-software/forge/logger"
    zaplogger "github.com/axen-software/forge/logger/zap"
    "github.com/axen-software/forge/setting"
)

func main() {
    cfg := &logger.Config{
        LogLevel:      "info", // debug, info, warn, error
        CallerEnabled: true,
    }

    log := zaplogger.NewZapLogger(cfg, setting.Production)
    log.WithName("my-service")

    // Basic logging
    log.Info("Server started")
    log.Infof("Listening on port %d", 8080)
    log.Error("Something went wrong")

    // Structured logging with fields
    log.Infow("User logged in", logger.Fields{
        "user_id":  "123",
        "ip":       "192.168.1.1",
        "duration": "50ms",
    })

    // Error logging
    log.Err("Database error", err)

    // Flush before shutdown
    log.Sync()
}

Global Logger

import "github.com/axen-software/forge/logger/l"

// Use anywhere without initialization
l.Info("Quick log message")
l.Errorf("Error: %v", err)

GCP Cloud Logging

Direct integration with GCP Cloud Logging for production environments.

import "github.com/axen-software/forge/glog"

func main() {
    logger, err := glog.New(ctx, &glog.Config{
        ProjectID: "my-gcp-project",
        LogName:   "my-service",
        DefaultLabels: map[string]string{
            "env":     "production",
            "version": "1.0.0",
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    defer logger.Close()

    // Simple logging
    logger.LogInfo(ctx, "Application started")
    logger.LogWarning(ctx, "High memory usage detected")
    logger.LogError(ctx, "Failed to process request")

    // With custom labels
    logger.LogInfo(ctx, "User action", map[string]string{
        "user_id": "123",
        "action":  "login",
    })

    // Structured payload
    logger.Log(ctx, glog.Entry{
        Severity: glog.Info,
        Payload: map[string]any{
            "event":   "order_created",
            "orderId": "ORD-123",
            "amount":  99.99,
        },
    })

    // With distributed tracing
    logger.LogWithTrace(ctx, glog.Entry{
        Severity: glog.Info,
        Payload:  "Processing request",
    }, traceID, spanID)

    // HTTP request logging
    logger.Log(ctx, glog.Entry{
        Severity: glog.Info,
        Payload:  "Request completed",
        HTTPRequest: &glog.HTTPRequest{
            Request:  req,
            Status:   200,
            Latency:  50 * time.Millisecond,
            RemoteIP: "192.168.1.1",
        },
    })
}

BigQuery

import (
    "github.com/axen-software/forge/bigquery"
    bq "cloud.google.com/go/bigquery"
)

func main() {
    client, err := bigquery.New(ctx, &bigquery.Config{
        ProjectID:      "my-gcp-project",
        DefaultDataset: "analytics",
        Location:       "US",
        QueryTimeout:   60 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Execute query
    result, err := client.Query(ctx, `
        SELECT user_id, COUNT(*) as event_count 
        FROM events 
        GROUP BY user_id 
        LIMIT 100
    `)
    for _, row := range result.Rows {
        fmt.Printf("User: %v, Events: %v\n", row["user_id"], row["event_count"])
    }

    // Parameterized query (prevents SQL injection)
    result, err := client.QueryWithParams(ctx, 
        "SELECT * FROM users WHERE email = @email",
        []bq.QueryParameter{
            {Name: "email", Value: "user@example.com"},
        },
    )

    // Dry run for cost estimation
    dryRun, err := client.DryRun(ctx, "SELECT * FROM large_table")
    fmt.Printf("Query will process: %d bytes\n", dryRun.TotalBytesProcessed)

    // Streaming insert
    type Event struct {
        UserID    string    `bigquery:"user_id"`
        EventType string    `bigquery:"event_type"`
        Timestamp time.Time `bigquery:"timestamp"`
    }
    
    event := Event{
        UserID:    "user-123",
        EventType: "page_view",
        Timestamp: time.Now(),
    }
    err = client.Insert(ctx, "analytics", "events", event)

    // Batch insert
    events := []any{event1, event2, event3}
    err = client.InsertBatch(ctx, "analytics", "events", events)

    // Table management
    exists, _ := client.TableExists(ctx, "analytics", "events")
    if !exists {
        schema := bq.Schema{
            {Name: "user_id", Type: bq.StringFieldType},
            {Name: "event_type", Type: bq.StringFieldType},
            {Name: "timestamp", Type: bq.TimestampFieldType},
        }
        client.CreateTable(ctx, "analytics", "events", schema,
            bigquery.WithTableDescription("User events table"),
            bigquery.WithTimePartitioning("timestamp", 90*24*time.Hour),
        )
    }
}

Cloud Storage

import (
    "github.com/axen-software/forge/storage"
    "github.com/axen-software/forge/storage/config"
)

func main() {
    client, err := storage.NewStorage(ctx, &config.Config{
        Enable:        true,
        Provider:      "gcs",
        ProjectID:     "my-gcp-project",
        PublicBucket:  "my-public-bucket",
        PrivateBucket: "my-private-bucket",
    })
    if err != nil {
        log.Fatal(err)
    }

    // Upload file
    data := []byte("Hello, World!")
    err = client.UploadFromData(ctx, "path/to/file.txt", data, false) // false = public bucket

    // Download file
    content, err := client.GetFileData(ctx, "path/to/file.txt", false)

    // Get file URL
    url, err := client.GetFileUrl(ctx, "path/to/file.txt", false)

    // Delete file
    err = client.DeleteFile(ctx, "path/to/file.txt", false)

    // Copy/Move files
    client.CopyFile(ctx, "src/file.txt", "dst/file.txt", false, true, "", "") // public to private
    client.MoveFile(ctx, "old/path.txt", "new/path.txt", false, false, "", "")

    // Generate presigned URLs
    urlHandler, _ := storage.NewStorageURLHandler(ctx, cfg)
    uploadURL, _ := urlHandler.GeneratePutPresignedURLPrivateBucket("uploads", "file.pdf", 15*time.Minute)
    downloadURL, _ := urlHandler.GenerateGetPresignedURLPrivateBucket("files", "document.pdf", 1*time.Hour)
}

Firebase

import "github.com/axen-software/forge/firebase"

func main() {
    cfg := &firebase.Config{
        ProjectID: "my-firebase-project",
    }

    // Firestore
    firestore, err := firebase.NewFirestoreClient(ctx, cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer firestore.Close()

    // Use embedded firestore.Client
    doc, err := firestore.Collection("users").Doc("user-123").Get(ctx)

    // Firebase Auth
    auth, err := firebase.NewAuthClient(ctx, cfg)
    user, err := auth.GetUser(ctx, "user-uid")

    // Realtime Database
    database, err := firebase.NewDatabaseClient(ctx, cfg)
}

Cloud Tasks

import "github.com/axen-software/forge/ctasks"

func main() {
    client, err := ctasks.New(ctx, &ctasks.Config{
        ProjectID:    "my-gcp-project",
        Location:     "us-central1",
        DefaultQueue: "default-queue",
        DefaultUrl:   "https://my-service.run.app/tasks",
    })
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close(ctx)

    // Create a task with payload
    payload := []byte(`{"user_id": "123", "action": "send_email"}`)
    headers := map[string]string{"Content-Type": "application/json"}
    
    task, err := client.CreateTask(ctx, 
        "email-queue",
        "https://my-service.run.app/send-email",
        headers,
        payload,
        time.Now().Add(5*time.Minute), // schedule time
    )

    // Create task with options (more flexible)
    task, err := client.CreateTaskWithOptions(ctx, "email-queue",
        ctasks.WithHttpRequest(
            ctasks.POST,
            "https://my-service.run.app/send-email",
            headers,
            payload,
        ),
        ctasks.WithScheduleTime(time.Now().Add(1*time.Hour)),
        ctasks.WithAuth("service-account@project.iam.gserviceaccount.com", "https://my-service.run.app"),
        ctasks.WithAutoRevive(), // for tasks scheduled >28 days
    )

    // Delete task
    err = client.DeleteTask(ctx, "email-queue", taskID)

    // Use middleware for Cloud Tasks trigger endpoints
    e := echo.New()
    e.POST("/tasks", handler, client.TriggerHandlerMiddleware)
}

Event Bus

CloudEvents-based event publishing and subscribing.

import (
    "github.com/axen-software/forge/event"
    "github.com/axen-software/forge/event/eventbus"
    cloudevents "github.com/cloudevents/sdk-go/v2"
)

// Define event handler
type UserCreatedHandler struct{}

func (h *UserCreatedHandler) EventType() string {
    return "user.created"
}

func (h *UserCreatedHandler) Handle(ctx context.Context, e cloudevents.Event) error {
    var userData map[string]any
    e.DataAs(&userData)
    fmt.Printf("User created: %v\n", userData)
    return nil
}

func main() {
    bus := eventbus.NewEventBus()

    // Register handlers
    bus.RegisterHandler(&UserCreatedHandler{})

    // Start subscribing
    bus.Subscribe(ctx)

    // Publish event
    e := cloudevents.NewEvent()
    e.SetType("user.created")
    e.SetSource("my-service")
    e.SetData(cloudevents.ApplicationJSON, map[string]any{
        "user_id": "123",
        "email":   "user@example.com",
    })

    bus.Publish(ctx, e)
}

Pub/Sub Publisher

import "github.com/axen-software/forge/event"

func main() {
    publisher, err := event.NewPublisher(ctx, &event.Config{
        ProjectID: "my-gcp-project", 
        TopicID:   "events",
    })
    if err != nil {
        log.Fatal(err)
    }

    e := cloudevents.NewEvent()
    e.SetType("order.created")
    e.SetData(cloudevents.ApplicationJSON, orderData)

    if err := publisher.Publish(ctx, e); err != nil {
        log.Error("Failed to publish event", err)
    }
}

Authorization (Casbin)

RBAC/ABAC authorization with database-backed policies.

import "github.com/axen-software/forge/casbin"

func main() {
    // Create enforcer with SQL adapter
    enforcer, err := casbin.NewEnforcer(
        "path/to/model.conf",  // Casbin model file
        db.DB(),               // *sql.DB connection
        "postgres",            // driver name
        "casbin_policies",     // policy table name
        logger,
    )
    if err != nil {
        log.Fatal(err)
    }

    // Verify access
    hasAccess, err := enforcer.VerifyAccess(ctx, "user:123", "/api/users", "GET")
    if !hasAccess {
        return echo.ErrForbidden
    }

    // Add policy (using embedded Casbin enforcer)
    enforcer.AddPolicy("user:123", "/api/admin", "GET")
    enforcer.SavePolicy()

    // Health check
    if err := enforcer.Health(); err != nil {
        log.Error("Policy store unhealthy")
    }
}

Environment

import "github.com/axen-software/forge/setting"

func main() {
    env := setting.Production // or setting.Development, setting.Staging, setting.Local

    if env.IsProduction() {
        // Production-specific config
    }

    if env.IsLocal() || env.IsDevelopment() {
        // Enable debug features
    }

    name := env.GetEnvironmentName() // "production"
}

Validation

import "github.com/axen-software/forge/validator"

type CreateUserRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Name     string `json:"name" validate:"required,min=2,max=100"`
    Age      int    `json:"age" validate:"gte=0,lte=150"`
    Password string `json:"password" validate:"required,min=8"`
}

func main() {
    req := CreateUserRequest{
        Email:    "invalid-email",
        Name:     "A",
        Age:      200,
        Password: "short",
    }

    if err := validator.Validate(req); err != nil {
        // Returns validation errors
        fmt.Println(err)
    }
}

Configuration Pattern

All modules follow a consistent configuration pattern using mapstructure and env tags:

type Config struct {
    Enable    bool   `mapstructure:"enable" env:"SERVICE_ENABLE"`
    ProjectID string `mapstructure:"projectId" env:"SERVICE_PROJECT_ID"`
    // ...
}

Load with Viper:

import "github.com/spf13/viper"

var cfg Config
viper.Unmarshal(&cfg)

Or with environment variables:

import "github.com/caarlos0/env/v11"

var cfg Config
env.Parse(&cfg)

License

MIT License - see LICENSE for details.

About

utility functions and packages

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages