18F/e-QIP-prototype

View on GitHub
api/postgresql/db.go

Summary

Maintainability
A
1 hr
Test Coverage
package postgresql

import (
    "crypto/tls"
    "fmt"
    "net/url"
    "strings"
    "time"

    "github.com/go-pg/pg"

    "github.com/18F/e-QIP-prototype/api"
)

// Service to help abstract the technical implementation per driver used.
type Service struct {
    Log      api.LogService
    Env      api.Settings
    database *pg.DB
}

// DBConfig contains everything neccecary to setup a postgres db connection
type DBConfig struct {
    User     string
    Password string
    Address  string
    DBName   string
    SSLMode  string
}

// PostgresConnectURI creates a connection string, which is used by the golang sql Open() function
func PostgresConnectURI(conf DBConfig) string {
    // By user (+ password) + database + host
    uri := &url.URL{Scheme: "postgres"}
    username := conf.User

    // Check if there is a password set. If not then we need to create
    // the Userinfo structure in a different way so we don't include
    // exta colons (:).
    pw := conf.Password
    if pw == "" {
        uri.User = url.User(username)
    } else {
        uri.User = url.UserPassword(username, pw)
    }

    // The database name will be part of the URI path so it needs
    // a prefix of "/"
    database := conf.DBName
    uri.Path = fmt.Sprintf("/%s", database)

    // Host can be either "address + port" or just "address"
    host := conf.Address
    uri.Host = host

    if conf.SSLMode != "" {
        params := url.Values{}
        params.Set("sslmode", conf.SSLMode)
        uri.RawQuery = params.Encode()
    }

    return uri.String()
}

// NewPostgresService returns a configured postgres service
func NewPostgresService(config DBConfig, logger api.LogService) *Service {
    service := Service{
        Log: logger,
    }

    options := &pg.Options{
        User:     config.User,
        Password: config.Password,
        Addr:     config.Address,
        Database: config.DBName,
    }

    if config.SSLMode != "" {
        // Copied from the implementation of go-pg /sigh
        switch config.SSLMode {
        case "allow", "prefer", "require":
            options.TLSConfig = &tls.Config{InsecureSkipVerify: true}
        case "disable":
            options.TLSConfig = nil
        default:
            service.Log.Warn("Unknown PG sslmdode specified, ignoring", api.LogFields{
                "sslmode": config.SSLMode,
            })
        }
    }

    db := pg.Connect(options)

    // Add logging
    db.OnQueryProcessed(func(event *pg.QueryProcessedEvent) {
        query, err := event.FormattedQuery()
        if err == nil {
            service.Log.Debug("Executed database query", api.LogFields{
                "elapsed": time.Since(event.StartTime),
                "query":   query,
            })
        }
    })

    service.database = db

    return &service
}

// Raw executes a string of SQL.
func (service *Service) Raw(query interface{}, params ...interface{}) error {
    _, err := service.database.Exec(query, params...)
    return err
}

// Find will check if the model exists and run the additional functionality.
func (service *Service) Find(query interface{}, callback func(query interface{})) {
    if count, err := service.database.Model(query).Count(); count > 0 && err == nil {
        service.Select(query)
        callback(query)
    }
}

// FindAll instances of a type of model.
func (service *Service) FindAll(query interface{}) error {
    return explicitNotFoundError(service.database.Model(query).Select())
}

// Where is a conditional selection based on the model in the data store.
func (service *Service) Where(model interface{}, condition string, params ...interface{}) error {
    return explicitNotFoundError(service.database.Model(model).Where(condition, params...).Select())
}

// ColumnsWhere is a conditional selection based on the model in the data store only returning specific quoted columns.
func (service *Service) ColumnsWhere(model interface{}, columns []string, condition string, params ...interface{}) error {
    return explicitNotFoundError(service.database.Model(model).Column(columns...).Where(condition, params...).Select())
}

// Count return the number of rows found.
func (service *Service) Count(model interface{}, condition string, params ...interface{}) int {
    count, _ := service.database.Model(model).Where(condition, params...).Count()
    return count
}

// CountExpr return the number of rows found with an expression.
func (service *Service) CountExpr(model interface{}, expr string, retval interface{}, condition string, params ...interface{}) {
    service.database.Model(model).ColumnExpr(expr).Where(condition, params...).Select(retval)
}

// Array fills an array from the model and expression.
func (service *Service) Array(model interface{}, expr string, retval interface{}, condition string, params ...interface{}) {
    service.database.Model(model).ColumnExpr(expr).Where(condition, params...).Select(pg.Array(retval))
}

// Insert persists the new model in the data store
func (service *Service) Insert(query ...interface{}) error {
    return service.database.Insert(query...)
}

// Update persists the existing model in the data store
func (service *Service) Update(query interface{}) error {
    return service.database.Update(query)
}

// Save persists the model in the data store
func (service *Service) Save(query ...interface{}) error {
    for _, q := range query {
        err := service.Insert(q)
        if err != nil {
            // oh dear god, only try and update if it fails because of a duplicated primary key.
            if strings.HasPrefix(err.Error(), "ERROR #23505 duplicate key value violates unique constraint") && strings.HasSuffix(err.Error(), `_pkey"`) {
                err = service.Update(q)
            }
        }

        // If there were no rows found we already handle this.
        if err != nil && err != pg.ErrNoRows {
            return err
        }
    }

    return nil
}

// Delete removes the model from the data store
func (service *Service) Delete(query interface{}) error {
    return service.database.Delete(query)
}

// Select returns the model from the data store
func (service *Service) Select(query interface{}) error {
    return explicitNotFoundError(service.database.Select(query))
}

func explicitNotFoundError(err error) error {
    if err == pg.ErrNoRows {
        return api.DatabaseErrorNotFound("NOT_FOUND")
    }
    return err
}

// Close closes the DB connection
func (service *Service) Close() error {
    return service.database.Close()
}