dbmanager

package
v1.0.19 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 3, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

Database Connection Manager (dbmanager)

A comprehensive database connection manager for Go that provides centralized management of multiple named database connections with support for PostgreSQL, SQLite, MSSQL, and MongoDB.

Features

  • Multiple Named Connections: Manage multiple database connections with names like primary, analytics, cache-db
  • Multi-Database Support: PostgreSQL, SQLite, Microsoft SQL Server, and MongoDB
  • Multi-ORM Access: Each SQL connection provides access through:
    • Bun ORM - Modern, lightweight ORM
    • GORM - Popular Go ORM
    • Native - Standard library *sql.DB
    • All three share the same underlying connection pool
  • Configuration-Driven: YAML configuration with Viper integration
  • Production-Ready Features:
    • Automatic health checks and reconnection
    • Prometheus metrics
    • Connection pooling with configurable limits
    • Retry logic with exponential backoff
    • Graceful shutdown
    • OpenTelemetry tracing support

Installation

go get github.com/bitechdev/ResolveSpec/pkg/dbmanager

Quick Start

1. Configuration

Create a configuration file (e.g., config.yaml):

dbmanager:
  default_connection: "primary"

  # Global connection pool defaults
  max_open_conns: 25
  max_idle_conns: 5
  conn_max_lifetime: 30m
  conn_max_idle_time: 5m

  # Retry configuration
  retry_attempts: 3
  retry_delay: 1s
  retry_max_delay: 10s

  # Health checks
  health_check_interval: 30s
  enable_auto_reconnect: true

  connections:
    # Primary PostgreSQL connection
    primary:
      type: postgres
      host: localhost
      port: 5432
      user: myuser
      password: mypassword
      database: myapp
      sslmode: disable
      default_orm: bun
      enable_metrics: true
      enable_tracing: true
      enable_logging: true

    # Read replica for analytics
    analytics:
      type: postgres
      dsn: "postgres://readonly:pass@analytics:5432/analytics"
      default_orm: bun
      enable_metrics: true

    # SQLite cache
    cache-db:
      type: sqlite
      filepath: /var/lib/app/cache.db
      max_open_conns: 1

    # MongoDB for documents
    documents:
      type: mongodb
      host: localhost
      port: 27017
      database: documents
      user: mongouser
      password: mongopass
      auth_source: admin
      enable_metrics: true
2. Initialize Manager
package main

import (
    "context"
    "log"

    "github.com/bitechdev/ResolveSpec/pkg/config"
    "github.com/bitechdev/ResolveSpec/pkg/dbmanager"
)

func main() {
    // Load configuration
    cfgMgr := config.NewManager()
    if err := cfgMgr.Load(); err != nil {
        log.Fatal(err)
    }
    cfg, _ := cfgMgr.GetConfig()

    // Create database manager
    mgr, err := dbmanager.NewManager(cfg.DBManager)
    if err != nil {
        log.Fatal(err)
    }
    defer mgr.Close()

    // Connect all databases
    ctx := context.Background()
    if err := mgr.Connect(ctx); err != nil {
        log.Fatal(err)
    }

    // Your application code here...
}
3. Use Database Connections
Get Default Database
// Get the default database (as configured common.Database interface)
db, err := mgr.GetDefaultDatabase()
if err != nil {
    log.Fatal(err)
}

// Use it with any query
var users []User
err = db.NewSelect().
    Model(&users).
    Where("active = ?", true).
    Scan(ctx, &users)
Get Named Connection with Specific ORM
// Get primary connection
primary, err := mgr.Get("primary")
if err != nil {
    log.Fatal(err)
}

// Use with Bun
bunDB, err := primary.Bun()
if err != nil {
    log.Fatal(err)
}
err = bunDB.NewSelect().Model(&users).Scan(ctx)

// Use with GORM (same underlying connection!)
gormDB, err := primary.GORM()
if err != nil {
    log.Fatal(err)
}
gormDB.Where("active = ?", true).Find(&users)

// Use native *sql.DB
nativeDB, err := primary.Native()
if err != nil {
    log.Fatal(err)
}
rows, err := nativeDB.QueryContext(ctx, "SELECT * FROM users WHERE active = $1", true)
Use MongoDB
// Get MongoDB connection
docs, err := mgr.Get("documents")
if err != nil {
    log.Fatal(err)
}

mongoClient, err := docs.MongoDB()
if err != nil {
    log.Fatal(err)
}

collection := mongoClient.Database("documents").Collection("articles")
// Use MongoDB driver...
Change Default Database
// Switch to analytics database as default
err := mgr.SetDefaultDatabase("analytics")
if err != nil {
    log.Fatal(err)
}

// Now GetDefaultDatabase() returns the analytics connection
db, _ := mgr.GetDefaultDatabase()

Configuration Reference

Manager Configuration
Field Type Default Description
default_connection string "" Name of the default connection
connections map {} Map of connection name to ConnectionConfig
max_open_conns int 25 Global default for max open connections
max_idle_conns int 5 Global default for max idle connections
conn_max_lifetime duration 30m Global default for connection max lifetime
conn_max_idle_time duration 5m Global default for connection max idle time
retry_attempts int 3 Number of connection retry attempts
retry_delay duration 1s Initial retry delay
retry_max_delay duration 10s Maximum retry delay
health_check_interval duration 30s Interval between health checks
enable_auto_reconnect bool true Auto-reconnect on health check failure
Connection Configuration
Field Type Description
name string Unique connection name
type string Database type: postgres, sqlite, mssql, mongodb
dsn string Complete connection string (overrides individual params)
host string Database host
port int Database port
user string Username
password string Password
database string Database name
sslmode string SSL mode (postgres/mssql): disable, require, etc.
schema string Default schema (postgres/mssql)
filepath string File path (sqlite only)
auth_source string Auth source (mongodb)
replica_set string Replica set name (mongodb)
read_preference string Read preference (mongodb): primary, secondary, etc.
max_open_conns int Override global max open connections
max_idle_conns int Override global max idle connections
conn_max_lifetime duration Override global connection max lifetime
conn_max_idle_time duration Override global connection max idle time
connect_timeout duration Connection timeout (default: 10s)
query_timeout duration Query timeout (default: 30s)
enable_tracing bool Enable OpenTelemetry tracing
enable_metrics bool Enable Prometheus metrics
enable_logging bool Enable connection logging
default_orm string Default ORM for Database(): bun, gorm, native
tags map[string]string Custom tags for filtering/organization

Advanced Usage

Health Checks
// Manual health check
if err := mgr.HealthCheck(ctx); err != nil {
    log.Printf("Health check failed: %v", err)
}

// Per-connection health check
primary, _ := mgr.Get("primary")
if err := primary.HealthCheck(ctx); err != nil {
    log.Printf("Primary connection unhealthy: %v", err)

    // Manual reconnect
    if err := primary.Reconnect(ctx); err != nil {
        log.Printf("Reconnection failed: %v", err)
    }
}
Connection Statistics
// Get overall statistics
stats := mgr.Stats()
fmt.Printf("Total connections: %d\n", stats.TotalConnections)
fmt.Printf("Healthy: %d, Unhealthy: %d\n", stats.HealthyCount, stats.UnhealthyCount)

// Per-connection stats
for name, connStats := range stats.ConnectionStats {
    fmt.Printf("%s: %d open, %d in use, %d idle\n",
        name,
        connStats.OpenConnections,
        connStats.InUse,
        connStats.Idle)
}

// Individual connection stats
primary, _ := mgr.Get("primary")
stats := primary.Stats()
fmt.Printf("Wait count: %d, Wait duration: %v\n",
    stats.WaitCount,
    stats.WaitDuration)
Prometheus Metrics

The package automatically exports Prometheus metrics:

  • dbmanager_connections_total - Total configured connections by type
  • dbmanager_connection_status - Connection health status (1=healthy, 0=unhealthy)
  • dbmanager_connection_pool_size - Connection pool statistics by state
  • dbmanager_connection_wait_count - Times connections waited for availability
  • dbmanager_connection_wait_duration_seconds - Total wait duration
  • dbmanager_health_check_duration_seconds - Health check execution time
  • dbmanager_reconnect_attempts_total - Reconnection attempts and results
  • dbmanager_connection_lifetime_closed_total - Connections closed due to max lifetime
  • dbmanager_connection_idle_closed_total - Connections closed due to max idle time

Metrics are automatically updated during health checks. To manually publish metrics:

if mgr, ok := mgr.(*connectionManager); ok {
    mgr.PublishMetrics()
}

Architecture

Single Connection Pool, Multiple ORMs

A key design principle is that Bun, GORM, and Native all wrap the same underlying *sql.DB connection pool:

┌─────────────────────────────────────┐
│        SQL Connection               │
├─────────────────────────────────────┤
│  ┌─────────┐  ┌──────┐  ┌────────┐ │
│  │   Bun   │  │ GORM │  │ Native │ │
│  └────┬────┘  └───┬──┘  └───┬────┘ │
│       │           │         │      │
│       └───────────┴─────────┘      │
│              *sql.DB                │
│         (single pool)               │
└─────────────────────────────────────┘

Benefits:

  • No connection duplication
  • Consistent pool limits across all ORMs
  • Unified connection statistics
  • Lower resource usage
Provider Pattern

Each database type has a dedicated provider:

  • PostgresProvider - Uses pgx driver
  • SQLiteProvider - Uses glebarez/sqlite (pure Go)
  • MSSQLProvider - Uses go-mssqldb
  • MongoProvider - Uses official mongo-driver

Providers handle:

  • Connection establishment with retry logic
  • Health checking
  • Connection statistics
  • Connection cleanup

Best Practices

  1. Use Named Connections: Be explicit about which database you're accessing

    primary, _ := mgr.Get("primary")    // Good
    db, _ := mgr.GetDefaultDatabase()   // Risky if default changes
    
  2. Configure Connection Pools: Tune based on your workload

    connections:
      primary:
        max_open_conns: 100  # High traffic API
        max_idle_conns: 25
      analytics:
        max_open_conns: 10   # Background analytics
        max_idle_conns: 2
    
  3. Enable Health Checks: Catch connection issues early

    health_check_interval: 30s
    enable_auto_reconnect: true
    
  4. Use Appropriate ORM: Choose based on your needs

    • Bun: Modern, fast, type-safe - recommended for new code
    • GORM: Mature, feature-rich - good for existing GORM code
    • Native: Maximum control - use for performance-critical queries
  5. Monitor Metrics: Watch connection pool utilization

    • If wait_count is high, increase max_open_conns
    • If idle is always high, decrease max_idle_conns

Troubleshooting

Connection Failures

If connections fail to establish:

  1. Check configuration:

    # Test connection manually
    psql -h localhost -U myuser -d myapp
    
  2. Enable logging:

    connections:
      primary:
        enable_logging: true
    
  3. Check retry attempts:

    retry_attempts: 5  # Increase retries
    retry_max_delay: 30s
    
Pool Exhaustion

If you see "too many connections" errors:

  1. Increase pool size:

    max_open_conns: 50  # Increase from default 25
    
  2. Reduce connection lifetime:

    conn_max_lifetime: 15m  # Recycle faster
    
  3. Monitor wait stats:

    stats := primary.Stats()
    if stats.WaitCount > 1000 {
        log.Warn("High connection wait count")
    }
    
MongoDB vs SQL Confusion

MongoDB connections don't support SQL ORMs:

docs, _ := mgr.Get("documents")

// ✓ Correct
mongoClient, _ := docs.MongoDB()

// ✗ Error: ErrNotSQLDatabase
bunDB, err := docs.Bun()  // Won't work!

SQL connections don't support MongoDB:

primary, _ := mgr.Get("primary")

// ✓ Correct
bunDB, _ := primary.Bun()

// ✗ Error: ErrNotMongoDB
mongoClient, err := primary.MongoDB()  // Won't work!

Migration Guide

From Raw database/sql

Before:

db, err := sql.Open("postgres", dsn)
defer db.Close()

rows, err := db.Query("SELECT * FROM users")

After:

mgr, _ := dbmanager.NewManager(cfg.DBManager)
mgr.Connect(ctx)
defer mgr.Close()

primary, _ := mgr.Get("primary")
nativeDB, _ := primary.Native()

rows, err := nativeDB.Query("SELECT * FROM users")
From Direct Bun/GORM

Before:

sqldb, _ := sql.Open("pgx", dsn)
bunDB := bun.NewDB(sqldb, pgdialect.New())

var users []User
bunDB.NewSelect().Model(&users).Scan(ctx)

After:

mgr, _ := dbmanager.NewManager(cfg.DBManager)
mgr.Connect(ctx)

primary, _ := mgr.Get("primary")
bunDB, _ := primary.Bun()

var users []User
bunDB.NewSelect().Model(&users).Scan(ctx)

License

Same as the parent project.

Contributing

Contributions are welcome! Please submit issues and pull requests to the main repository.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrConnectionNotFound is returned when a connection with the given name doesn't exist
	ErrConnectionNotFound = errors.New("connection not found")

	// ErrInvalidConfiguration is returned when the configuration is invalid
	ErrInvalidConfiguration = errors.New("invalid configuration")

	// ErrConnectionClosed is returned when attempting to use a closed connection
	ErrConnectionClosed = errors.New("connection is closed")

	// ErrNotSQLDatabase is returned when attempting SQL operations on a non-SQL database
	ErrNotSQLDatabase = errors.New("not a SQL database")

	// ErrNotMongoDB is returned when attempting MongoDB operations on a non-MongoDB connection
	ErrNotMongoDB = errors.New("not a MongoDB connection")

	// ErrUnsupportedDatabase is returned when the database type is not supported
	ErrUnsupportedDatabase = errors.New("unsupported database type")

	// ErrNoDefaultConnection is returned when no default connection is configured
	ErrNoDefaultConnection = errors.New("no default connection configured")

	// ErrAlreadyConnected is returned when attempting to connect an already connected connection
	ErrAlreadyConnected = errors.New("already connected")
)

Common errors

Functions

func RecordReconnectAttempt

func RecordReconnectAttempt(name string, dbType DatabaseType, success bool)

RecordReconnectAttempt records a reconnection attempt

func ResetInstance added in v1.0.7

func ResetInstance()

ResetInstance resets the singleton instance (primarily for testing purposes). WARNING: This should only be used in tests. Calling this in production code while the manager is in use can lead to undefined behavior.

func SetupManager added in v1.0.7

func SetupManager(cfg ManagerConfig) error

SetupManager initializes the singleton database manager with the provided configuration. This function must be called before GetInstance(). Returns an error if the manager is already initialized or if configuration is invalid.

Types

type ConfigurationError

type ConfigurationError struct {
	Field string
	Err   error
}

ConfigurationError wraps configuration-related errors

func NewConfigurationError

func NewConfigurationError(field string, err error) *ConfigurationError

NewConfigurationError creates a new ConfigurationError

func (*ConfigurationError) Error

func (e *ConfigurationError) Error() string

func (*ConfigurationError) Unwrap

func (e *ConfigurationError) Unwrap() error

type Connection

type Connection interface {
	// Metadata
	Name() string
	Type() DatabaseType

	// ORM Access (SQL databases only)
	Bun() (*bun.DB, error)
	GORM() (*gorm.DB, error)
	Native() (*sql.DB, error)

	// Common Database interface (for SQL databases)
	Database() (common.Database, error)

	// MongoDB Access (MongoDB only)
	MongoDB() (*mongo.Client, error)

	// Lifecycle
	Connect(ctx context.Context) error
	Close() error
	HealthCheck(ctx context.Context) error
	Reconnect(ctx context.Context) error

	// Stats
	Stats() *ConnectionStats
}

Connection represents a single named database connection

type ConnectionConfig

type ConnectionConfig struct {
	// Name is the unique name of this connection
	Name string `mapstructure:"name"`

	// Type is the database type (postgres, sqlite, mssql, mongodb)
	Type DatabaseType `mapstructure:"type"`

	// DSN is the complete Data Source Name / connection string
	// If provided, this takes precedence over individual connection parameters
	DSN string `mapstructure:"dsn"`

	// Connection parameters (used if DSN is not provided)
	Host     string `mapstructure:"host"`
	Port     int    `mapstructure:"port"`
	User     string `mapstructure:"user"`
	Password string `mapstructure:"password"`
	Database string `mapstructure:"database"`

	// PostgreSQL/MSSQL specific
	SSLMode string `mapstructure:"sslmode"` // disable, require, verify-ca, verify-full
	Schema  string `mapstructure:"schema"`  // Default schema

	// SQLite specific
	FilePath string `mapstructure:"filepath"`

	// MongoDB specific
	AuthSource     string `mapstructure:"auth_source"`
	ReplicaSet     string `mapstructure:"replica_set"`
	ReadPreference string `mapstructure:"read_preference"` // primary, secondary, etc.

	// Connection pool settings (overrides global defaults)
	MaxOpenConns    *int           `mapstructure:"max_open_conns"`
	MaxIdleConns    *int           `mapstructure:"max_idle_conns"`
	ConnMaxLifetime *time.Duration `mapstructure:"conn_max_lifetime"`
	ConnMaxIdleTime *time.Duration `mapstructure:"conn_max_idle_time"`

	// Timeouts
	ConnectTimeout time.Duration `mapstructure:"connect_timeout"`
	QueryTimeout   time.Duration `mapstructure:"query_timeout"`

	// Features
	EnableTracing bool `mapstructure:"enable_tracing"`
	EnableMetrics bool `mapstructure:"enable_metrics"`
	EnableLogging bool `mapstructure:"enable_logging"`

	// DefaultORM specifies which ORM to use for the Database() method
	// Options: "bun", "gorm", "native"
	DefaultORM string `mapstructure:"default_orm"`

	// Tags for organization and filtering
	Tags map[string]string `mapstructure:"tags"`
}

ConnectionConfig defines configuration for a single database connection

func (*ConnectionConfig) ApplyDefaults

func (cc *ConnectionConfig) ApplyDefaults(global *ManagerConfig)

ApplyDefaults applies default values and global settings to the connection configuration

func (*ConnectionConfig) BuildDSN

func (cc *ConnectionConfig) BuildDSN() (string, error)

BuildDSN builds a connection string from individual parameters

func (*ConnectionConfig) GetConnMaxIdleTime

func (cc *ConnectionConfig) GetConnMaxIdleTime() *time.Duration

func (*ConnectionConfig) GetConnMaxLifetime

func (cc *ConnectionConfig) GetConnMaxLifetime() *time.Duration

func (*ConnectionConfig) GetConnectTimeout

func (cc *ConnectionConfig) GetConnectTimeout() time.Duration

func (*ConnectionConfig) GetDatabase

func (cc *ConnectionConfig) GetDatabase() string

func (*ConnectionConfig) GetEnableLogging

func (cc *ConnectionConfig) GetEnableLogging() bool

func (*ConnectionConfig) GetEnableMetrics

func (cc *ConnectionConfig) GetEnableMetrics() bool

func (*ConnectionConfig) GetFilePath

func (cc *ConnectionConfig) GetFilePath() string

func (*ConnectionConfig) GetHost

func (cc *ConnectionConfig) GetHost() string

func (*ConnectionConfig) GetMaxIdleConns

func (cc *ConnectionConfig) GetMaxIdleConns() *int

func (*ConnectionConfig) GetMaxOpenConns

func (cc *ConnectionConfig) GetMaxOpenConns() *int

func (*ConnectionConfig) GetName

func (cc *ConnectionConfig) GetName() string

Getter methods to implement providers.ConnectionConfig interface

func (*ConnectionConfig) GetPassword

func (cc *ConnectionConfig) GetPassword() string

func (*ConnectionConfig) GetPort

func (cc *ConnectionConfig) GetPort() int

func (*ConnectionConfig) GetQueryTimeout

func (cc *ConnectionConfig) GetQueryTimeout() time.Duration

func (*ConnectionConfig) GetReadPreference

func (cc *ConnectionConfig) GetReadPreference() string

func (*ConnectionConfig) GetType

func (cc *ConnectionConfig) GetType() string

func (*ConnectionConfig) GetUser

func (cc *ConnectionConfig) GetUser() string

func (*ConnectionConfig) Validate

func (cc *ConnectionConfig) Validate() error

Validate validates the connection configuration

type ConnectionError

type ConnectionError struct {
	Name      string
	Operation string
	Err       error
}

ConnectionError wraps errors that occur during connection operations

func NewConnectionError

func NewConnectionError(name, operation string, err error) *ConnectionError

NewConnectionError creates a new ConnectionError

func (*ConnectionError) Error

func (e *ConnectionError) Error() string

func (*ConnectionError) Unwrap

func (e *ConnectionError) Unwrap() error

type ConnectionStats

type ConnectionStats struct {
	Name              string
	Type              DatabaseType
	Connected         bool
	LastHealthCheck   time.Time
	HealthCheckStatus string

	// SQL connection pool stats
	OpenConnections   int
	InUse             int
	Idle              int
	WaitCount         int64
	WaitDuration      time.Duration
	MaxIdleClosed     int64
	MaxLifetimeClosed int64
}

ConnectionStats contains statistics about a database connection

type DatabaseType

type DatabaseType string

DatabaseType represents the type of database

const (
	// DatabaseTypePostgreSQL represents PostgreSQL database
	DatabaseTypePostgreSQL DatabaseType = "postgres"

	// DatabaseTypeSQLite represents SQLite database
	DatabaseTypeSQLite DatabaseType = "sqlite"

	// DatabaseTypeMSSQL represents Microsoft SQL Server database
	DatabaseTypeMSSQL DatabaseType = "mssql"

	// DatabaseTypeMongoDB represents MongoDB database
	DatabaseTypeMongoDB DatabaseType = "mongodb"
)

type Manager

type Manager interface {
	// Connection retrieval
	Get(name string) (Connection, error)
	GetDefault() (Connection, error)
	GetAll() map[string]Connection

	// Default database management
	GetDefaultDatabase() (common.Database, error)
	SetDefaultDatabase(name string) error

	// Lifecycle
	Connect(ctx context.Context) error
	Close() error
	HealthCheck(ctx context.Context) error

	// Stats
	Stats() *ManagerStats
}

Manager manages multiple named database connections

func GetInstance added in v1.0.7

func GetInstance() (Manager, error)

GetInstance returns the singleton instance of the database manager. Returns an error if SetupManager has not been called yet.

func NewManager

func NewManager(cfg ManagerConfig) (Manager, error)

NewManager creates a new database connection manager

type ManagerConfig

type ManagerConfig struct {
	// DefaultConnection is the name of the default connection to use
	DefaultConnection string `mapstructure:"default_connection"`

	// Connections is a map of connection name to connection configuration
	Connections map[string]ConnectionConfig `mapstructure:"connections"`

	// Global connection pool defaults
	MaxOpenConns    int           `mapstructure:"max_open_conns"`
	MaxIdleConns    int           `mapstructure:"max_idle_conns"`
	ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
	ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"`

	// Retry policy
	RetryAttempts int           `mapstructure:"retry_attempts"`
	RetryDelay    time.Duration `mapstructure:"retry_delay"`
	RetryMaxDelay time.Duration `mapstructure:"retry_max_delay"`

	// Health checks
	HealthCheckInterval time.Duration `mapstructure:"health_check_interval"`
	EnableAutoReconnect bool          `mapstructure:"enable_auto_reconnect"`
}

ManagerConfig contains configuration for the database connection manager

func DefaultManagerConfig

func DefaultManagerConfig() ManagerConfig

DefaultManagerConfig returns a ManagerConfig with sensible defaults

func FromConfig

func FromConfig(cfg config.DBManagerConfig) ManagerConfig

FromConfig converts config.DBManagerConfig to internal ManagerConfig

func (*ManagerConfig) ApplyDefaults

func (c *ManagerConfig) ApplyDefaults()

ApplyDefaults applies default values to the manager configuration

func (*ManagerConfig) Validate

func (c *ManagerConfig) Validate() error

Validate validates the manager configuration

type ManagerStats

type ManagerStats struct {
	TotalConnections int
	HealthyCount     int
	UnhealthyCount   int
	ConnectionStats  map[string]*ConnectionStats
}

ManagerStats contains statistics about the connection manager

type ORMType

type ORMType string

ORMType represents the ORM to use for database operations

const (
	// ORMTypeBun represents Bun ORM
	ORMTypeBun ORMType = "bun"

	// ORMTypeGORM represents GORM
	ORMTypeGORM ORMType = "gorm"

	// ORMTypeNative represents native database/sql
	ORMTypeNative ORMType = "native"
)

type Provider

type Provider = providers.Provider

Provider is an alias to the providers.Provider interface This allows dbmanager package consumers to use Provider without importing providers

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL