Databases
Postgres
bash
go get github.com/jackc/pgconn
go get github.com/jackc/pgx/v4
go get github.com/jackc/pgx/v4/stdlib
go
// models.go
package data
import (
"context"
"database/sql"
"errors"
"log"
"time"
"golang.org/x/crypto/bcrypt"
)
const dbTimeout = time.Second * 3
var db *sql.DB
type PostgresRepository struct {
Conn *sql.DB
}
func NewPostgresRepository(conn *sql.DB) *PostgresRepository {
db = conn
return &PostgresRepository{
Conn: conn,
}
}
type User struct {
ID int `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Password string `json:"-"`
Active int `json:"active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// GetAll returns a slice of all users, sorted by last name
func (u *PostgresRepository) GetAll() ([]*User, error) {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at
from users order by last_name`
rows, err := u.Conn.QueryContext(ctx, query)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*User
for rows.Next() {
var user User
err := rows.Scan(
&user.ID,
&user.Email,
&user.FirstName,
&user.LastName,
&user.Password,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
log.Println("Error scanning", err)
return nil, err
}
users = append(users, &user)
}
return users, nil
}
// GetByEmail returns one user by email
func (u *PostgresRepository) GetByEmail(email string) (*User, error) {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where email = $1`
var user User
row := db.QueryRowContext(ctx, query, email)
err := row.Scan(
&user.ID,
&user.Email,
&user.FirstName,
&user.LastName,
&user.Password,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, err
}
return &user, nil
}
// GetOne returns one user by id
func (u *PostgresRepository) GetOne(id int) (*User, error) {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
query := `select id, email, first_name, last_name, password, user_active, created_at, updated_at from users where id = $1`
var user User
row := db.QueryRowContext(ctx, query, id)
err := row.Scan(
&user.ID,
&user.Email,
&user.FirstName,
&user.LastName,
&user.Password,
&user.Active,
&user.CreatedAt,
&user.UpdatedAt,
)
if err != nil {
return nil, err
}
return &user, nil
}
// Update updates one user in the database, using the information
// stored in the receiver u
func (u *PostgresRepository) Update(user User) error {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
stmt := `update users set
email = $1,
first_name = $2,
last_name = $3,
user_active = $4,
updated_at = $5
where id = $6
`
_, err := db.ExecContext(ctx, stmt,
user.Email,
user.FirstName,
user.LastName,
user.Active,
time.Now(),
user.ID,
)
if err != nil {
return err
}
return nil
}
// DeleteByID deletes one user from the database, by ID
func (u *PostgresRepository) DeleteByID(id int) error {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
stmt := `delete from users where id = $1`
_, err := db.ExecContext(ctx, stmt, id)
if err != nil {
return err
}
return nil
}
// Insert inserts a new user into the database, and returns the ID of the newly inserted row
func (u *PostgresRepository) Insert(user User) (int, error) {
ctx, cancel := context.WithTimeout(context.Background(), dbTimeout)
defer cancel()
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), 12)
if err != nil {
return 0, err
}
var newID int
stmt := `insert into users (email, first_name, last_name, password, user_active, created_at, updated_at)
values ($1, $2, $3, $4, $5, $6, $7) returning id`
err = db.QueryRowContext(ctx, stmt,
user.Email,
user.FirstName,
user.LastName,
hashedPassword,
user.Active,
time.Now(),
time.Now(),
).Scan(&newID)
if err != nil {
return 0, err
}
return newID, nil
}
go
// main.go, instantiate connection and repo
package main
import (
"authentication/data"
_ "github.com/jackc/pgconn"
_ "github.com/jackc/pgx/v4"
_ "github.com/jackc/pgx/v4/stdlib"
)
func connectToDB() *sql.DB {
dsn := os.Getenv("DSN")
for {
conn, err := openDB(dsn)
if err != nil {
log.Println("DB not ready yet...")
counts++
} else {
log.Println("Connected to DB!")
return conn
}
if counts > 10 {
log.Println(err)
return nil
}
log.Println("Backing off for 2 seconds...")
time.Sleep(time.Second * 2)
continue
}
}
func (app *Config) setupRepo(conn *sql.DB) {
db := data.NewPostgresRepository(conn)
app.Repo = db
}
Testing
It is best to use a repository pattern when dealing with databases, because it allows you to implement a mock repo that implements all the same functions as the real repo with mock return values.
go
// repository.go
package data
type Repository interface {
GetAll() ([]*User, error)
GetByEmail(email string) (*User, error)
GetOne(id int) (*User, error)
Insert(user User) (int, error)
Update(user User) error
DeleteByID(id int) error
ResetPassword(password string, user User) error
PasswordMatches(password string, user User) (bool, error)
}
go
package data
import (
"database/sql"
"time"
)
type PostgresTestRepository struct {
Conn *sql.DB
}
func NewPostgresTestRepository(conn *sql.DB) *PostgresTestRepository {
return &PostgresTestRepository{
Conn: conn,
}
}
// Implement all methods of Repository interface
func (repo *PostgresTestRepository) GetAll() ([]*User, error) {
users := []*User{}
return users, nil
}
go
// setup_test.go
package main
import (
"authentication/data"
"os"
"testing"
)
var testApp Config
func TestMain(m *testing.M) {
repo := data.NewPostgresTestRepository(nil)
testApp.Repo = repo
os.Exit(m.Run())
}
Mongo
bash
go get go.mongodb.org/mongo-driver/mongo
go get go.mongodb.org/mongo-driver/mongo/options
go
// models.go
package data
import (
"context"
"log"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
var client *mongo.Client
func New(mongo *mongo.Client) Models {
client = mongo
return Models{
LogEntry: LogEntry{},
}
}
type Models struct {
LogEntry LogEntry
}
type LogEntry struct {
ID string `bson:"_id,omitempty" json:"id,omitempty"`
Name string `bson:"name" json:"name"`
Data string `bson:"data" json:"data"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
func (l *LogEntry) Insert(entry LogEntry) error {
collection := client.Database("logs").Collection("logs")
_, err := collection.InsertOne(context.TODO(), LogEntry{
Name: entry.Name,
Data: entry.Data,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
})
if err != nil {
log.Println("Error insertint into logs: ", err)
return err
}
return nil
}
func (l *LogEntry) All() ([]*LogEntry, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
collection := client.Database("logs").Collection("logs")
opts := options.Find()
opts.SetSort(bson.D{{"created_at", -1}})
cursor, err := collection.Find(context.TODO(), bson.D{}, opts)
if err != nil {
log.Println("Finding all docs error: ", err)
return nil, err
}
defer cursor.Close(ctx)
var logs []*LogEntry
for cursor.Next(ctx) {
var item LogEntry
err := cursor.Decode(&item)
if err != nil {
log.Println("Error decoding log into slice: ", err)
return nil, err
} else {
logs = append(logs, &item)
}
}
return logs, nil
}
func (l *LogEntry) GetOne(id string) (*LogEntry, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
collection := client.Database("logs").Collection("logs")
docId, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, err
}
var entry LogEntry
err = collection.FindOne(ctx, bson.M{"_id": docId}).Decode(&entry)
if err != nil {
return nil, err
}
return &entry, nil
}
func (l *LogEntry) DropCollections() error {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
collection := client.Database("logs").Collection("logs")
if err := collection.Drop(ctx); err != nil {
return err
}
return nil
}
func (l *LogEntry) Update() (*mongo.UpdateResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
collection := client.Database("logs").Collection("logs")
docId, err := primitive.ObjectIDFromHex(l.ID)
if err != nil {
return nil, err
}
result, err := collection.UpdateOne(
ctx,
bson.M{"_id": docId},
bson.D{
{"$set", bson.D{
{"name", l.Name},
{"data", l.Data},
{"updated_at", time.Now()},
}},
},
)
if err != nil {
return nil, err
}
return result, err
}
go
// main.go
package main
import (
"context"
"fmt"
"log"
"logger/data"
"net"
"net/http"
"net/rpc"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
const (
mongoURL = "mongodb://mongo:27017"
)
var client *mongo.Client
type Config struct {
Models data.Models
}
func main() {
// connect to mongo
mongoClient, err := connectToMongo()
if err != nil {
log.Panic(err)
}
client = mongoClient
// create a context to disconnect
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// close connection
defer func() {
if err = client.Disconnect(ctx); err != nil {
panic(err)
}
}()
app := Config{
Models: data.New(client),
}
}
func connectToMongo() (*mongo.Client, error) {
// create connection options
clientOptions := options.Client().ApplyURI(mongoURL)
clientOptions.SetAuth(options.Credential{
Username: "admin",
Password: "password",
})
c, err := mongo.Connect(context.TODO(), clientOptions)
if err != nil {
log.Println("Error connecting: %s", err)
return nil, err
}
return c, nil
}