Skip to content

Commit 2283f30

Browse files
author
Jon Friesen
authored
add mongo support (#1)
1 parent 31f3891 commit 2283f30

File tree

8 files changed

+327
-232
lines changed

8 files changed

+327
-232
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ require (
1010
github.com/pkg/errors v0.9.1
1111
github.com/stretchr/testify v1.6.1
1212
github.com/xo/dburl v0.0.0-20200124232849-e9ec94f52bc3
13-
istio.io/pkg v0.0.0-20200630182444-e8a83c9625a3 // indirect
13+
go.mongodb.org/mongo-driver v1.5.3
1414
)

go.sum

+95-197
Large diffs are not rendered by default.

main.go

+15-32
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import (
88
"os"
99
"strings"
1010

11+
"github.com/digitalocean-apps/sample-with-database/pkg/model"
12+
"github.com/digitalocean-apps/sample-with-database/pkg/storer"
1113
"github.com/gofrs/uuid"
1214
"github.com/gorilla/mux"
13-
"github.com/jinzhu/gorm"
1415
"github.com/pkg/errors"
15-
"github.com/xo/dburl"
1616

1717
_ "github.com/jinzhu/gorm/dialects/postgres"
1818
)
1919

2020
const (
21-
defaultPort = "80"
21+
defaultPort = "8080"
2222
defaultDatabaseURL = "postgresql://postgres:[email protected]:5432/notes/?sslmode=disable"
2323

2424
startupMessage = `                                                                                
@@ -39,34 +39,17 @@ const (
3939
`
4040
)
4141

42-
// Note represents a single note.
43-
type Note struct {
44-
gorm.Model
45-
Uuid string
46-
Body string
47-
}
48-
49-
func initialMigration(db *gorm.DB) {
50-
db.AutoMigrate(&Note{})
51-
}
52-
5342
func main() {
5443
// Parse connection config.
5544
databaseURL := os.Getenv("DATABASE_URL")
5645
if databaseURL == "" {
5746
databaseURL = defaultDatabaseURL
5847
}
59-
dbURL, err := dburl.Parse(databaseURL)
60-
requireNoError(err, "parsing DATABASE_URL")
61-
62-
// Open a DB connection.
63-
dbPassword, _ := dbURL.User.Password()
64-
dbName := strings.Trim(dbURL.Path, "/")
65-
connectionString := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s", dbURL.Hostname(), dbURL.Port(), dbURL.User.Username(), dbName, dbPassword, dbURL.Query().Get("sslmode"))
66-
db, err := gorm.Open("postgres", connectionString)
48+
caCert := os.Getenv("CA_CERT")
49+
50+
db, err := storer.NewStorer(databaseURL, caCert)
6751
requireNoError(err, "connecting to database")
6852
defer db.Close()
69-
initialMigration(db)
7053

7154
// Initialize the listening port.
7255
port := os.Getenv("PORT")
@@ -89,10 +72,10 @@ func main() {
8972
fmt.Printf("==> Server listening at %s 🚀\n", bindAddr)
9073

9174
err = http.ListenAndServe(fmt.Sprintf(":%s", port), r)
92-
requireNoError(err, "connecting to database")
75+
requireNoError(err, "starting server")
9376
}
9477

95-
func notesHandler(db *gorm.DB) func(w http.ResponseWriter, r *http.Request) {
78+
func notesHandler(db storer.Storer) func(w http.ResponseWriter, r *http.Request) {
9679
return func(w http.ResponseWriter, r *http.Request) {
9780
if r.Method == http.MethodPost {
9881
var body string
@@ -117,13 +100,14 @@ func notesHandler(db *gorm.DB) func(w http.ResponseWriter, r *http.Request) {
117100
return
118101
}
119102

120-
err = db.Create(&Note{
121-
Uuid: noteUUID.String(),
103+
err = db.Create(&model.Note{
104+
UUID: noteUUID.String(),
122105
Body: body,
123-
}).Error
106+
})
124107
if !requireNoErrorInHandler(w, err, "creating note in db") {
125108
return
126109
}
110+
127111
log.Printf("POST %s %s\n", noteUUID.String(), body)
128112

129113
w.WriteHeader(http.StatusOK)
@@ -136,7 +120,7 @@ func notesHandler(db *gorm.DB) func(w http.ResponseWriter, r *http.Request) {
136120
}
137121
}
138122

139-
func noteHandler(db *gorm.DB) func(w http.ResponseWriter, r *http.Request) {
123+
func noteHandler(db storer.Storer) func(w http.ResponseWriter, r *http.Request) {
140124
return func(w http.ResponseWriter, r *http.Request) {
141125
vars := mux.Vars(r)
142126
noteID := vars["note_id"]
@@ -147,9 +131,8 @@ func noteHandler(db *gorm.DB) func(w http.ResponseWriter, r *http.Request) {
147131
return
148132
}
149133

150-
var note Note
151-
err := db.Where("uuid = ?", noteID).Take(&note).Error
152-
if gorm.IsRecordNotFoundError(err) {
134+
note, err := db.Get(noteID)
135+
if errors.Is(err, storer.ErrNotFound) {
153136
w.WriteHeader(http.StatusNotFound)
154137
return
155138
}

main_test.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010

1111
"github.com/DATA-DOG/go-sqlmock"
12+
"github.com/digitalocean-apps/sample-with-database/pkg/storer"
1213
"github.com/gorilla/mux"
1314
"github.com/jinzhu/gorm"
1415
"github.com/stretchr/testify/assert"
@@ -71,8 +72,12 @@ func TestNotesHandler(t *testing.T) {
7172
tc.mock(mock)
7273
}
7374

75+
storerClient := &storer.PG{
76+
DB: gdb,
77+
}
78+
7479
rr := httptest.NewRecorder()
75-
handler := http.HandlerFunc(notesHandler(gdb))
80+
handler := http.HandlerFunc(notesHandler(storerClient))
7681
handler.ServeHTTP(rr, req)
7782

7883
assert.Equal(t, tc.expectedStatus, rr.Code)
@@ -141,8 +146,12 @@ func TestNoteHandler(t *testing.T) {
141146
tc.mock(mock)
142147
}
143148

149+
storerClient := &storer.PG{
150+
DB: gdb,
151+
}
152+
144153
rr := httptest.NewRecorder()
145-
handler := http.HandlerFunc(noteHandler(gdb))
154+
handler := http.HandlerFunc(noteHandler(storerClient))
146155
handler.ServeHTTP(rr, req)
147156

148157
assert.Equal(t, tc.expectedStatus, rr.Code)

pkg/model/note.go

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package model
2+
3+
import "github.com/jinzhu/gorm"
4+
5+
// Note represents a single note.
6+
type Note struct {
7+
gorm.Model
8+
UUID string
9+
Body string
10+
}

pkg/storer/interface.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package storer
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/digitalocean-apps/sample-with-database/pkg/model"
9+
)
10+
11+
var (
12+
// ErrNotFound is a standard not found err
13+
ErrNotFound = errors.New("NotFound")
14+
)
15+
16+
// Storer is an interface for persistence mediusm
17+
type Storer interface {
18+
// Returns a Note
19+
Get(id string) (*model.Note, error)
20+
// Creates a Note
21+
Create(note *model.Note) error
22+
// Close closes the DB connection
23+
Close() error
24+
}
25+
26+
// NewStorer creates a storer client
27+
func NewStorer(connection string, ca string) (Storer, error) {
28+
if strings.HasPrefix(connection, "postgres") {
29+
return NewPostgresClient(connection)
30+
}
31+
32+
if strings.HasPrefix(connection, "mongodb+srv://") {
33+
return NewMongoClient(connection, ca)
34+
}
35+
36+
return nil, (fmt.Errorf("Improper connection string format: %v", connection))
37+
}

pkg/storer/mongo.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package storer
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"crypto/x509"
7+
"fmt"
8+
9+
"github.com/digitalocean-apps/sample-with-database/pkg/model"
10+
"github.com/pkg/errors"
11+
"go.mongodb.org/mongo-driver/bson"
12+
"go.mongodb.org/mongo-driver/mongo"
13+
"go.mongodb.org/mongo-driver/mongo/options"
14+
"go.mongodb.org/mongo-driver/mongo/readpref"
15+
)
16+
17+
const (
18+
defaultDatabase = "notes"
19+
defaultCollection = "notes"
20+
)
21+
22+
// Mongo is a mongo storer implementation
23+
type Mongo struct {
24+
ctx context.Context
25+
DB *mongo.Client
26+
}
27+
28+
// NewMongoClient creates a mongo client
29+
func NewMongoClient(connection string, ca string) (*Mongo, error) {
30+
opts := options.Client()
31+
opts.ApplyURI(connection)
32+
33+
roots := x509.NewCertPool()
34+
ok := roots.AppendCertsFromPEM([]byte(ca))
35+
if !ok {
36+
return nil, fmt.Errorf("appending certs from pem")
37+
}
38+
opts.SetTLSConfig(&tls.Config{
39+
RootCAs: roots,
40+
})
41+
42+
client, err := mongo.NewClient(opts)
43+
if err != nil {
44+
return nil, errors.Wrap(err, "client creation failed")
45+
}
46+
ctx := context.Background()
47+
err = client.Connect(ctx)
48+
if err != nil {
49+
return nil, errors.Wrap(err, "connection failed")
50+
}
51+
52+
err = client.Ping(ctx, readpref.Primary())
53+
if err != nil {
54+
return nil, errors.Wrap(err, "ping failed")
55+
}
56+
57+
return &Mongo{
58+
ctx: context.Background(),
59+
DB: client,
60+
}, nil
61+
}
62+
63+
// Get gets a Note from the DB
64+
func (m *Mongo) Get(id string) (*model.Note, error) {
65+
var note model.Note
66+
col := m.DB.Database(defaultDatabase).Collection(defaultCollection)
67+
err := col.FindOne(m.ctx, bson.M{"uuid": id}).Decode(&note)
68+
if err != nil {
69+
return nil, errors.Wrap(err, "finding note")
70+
}
71+
72+
return &note, nil
73+
}
74+
75+
// Create creates a note
76+
func (m *Mongo) Create(note *model.Note) error {
77+
col := m.DB.Database(defaultDatabase).Collection(defaultCollection)
78+
_, err := col.InsertOne(m.ctx, note)
79+
if err != nil {
80+
return errors.Wrap(err, "creating note")
81+
}
82+
return nil
83+
}
84+
85+
// Close the connection
86+
func (m *Mongo) Close() error {
87+
return m.DB.Disconnect(context.Background())
88+
}

pkg/storer/postgres.go

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package storer
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/digitalocean-apps/sample-with-database/pkg/model"
8+
"github.com/jinzhu/gorm"
9+
"github.com/pkg/errors"
10+
"github.com/xo/dburl"
11+
)
12+
13+
// PG is the postgres storer implementation
14+
type PG struct {
15+
DB *gorm.DB
16+
}
17+
18+
// NewPostgresClient creates a postgres client
19+
func NewPostgresClient(connection string) (*PG, error) {
20+
dbURL, err := dburl.Parse(connection)
21+
if err != nil {
22+
return nil, errors.Wrap(err, "parsing DATABASE_URL")
23+
}
24+
25+
// Open a DB connection.
26+
dbPassword, _ := dbURL.User.Password()
27+
dbName := strings.Trim(dbURL.Path, "/")
28+
connectionString := fmt.Sprintf("host=%s port=%s user=%s dbname=%s password=%s sslmode=%s", dbURL.Hostname(), dbURL.Port(), dbURL.User.Username(), dbName, dbPassword, dbURL.Query().Get("sslmode"))
29+
db, err := gorm.Open("postgres", connectionString)
30+
if err != nil {
31+
return nil, errors.Wrap(err, "connecting to database")
32+
}
33+
34+
initialMigration(db)
35+
36+
return &PG{DB: db}, nil
37+
}
38+
39+
func initialMigration(db *gorm.DB) {
40+
db.AutoMigrate(&model.Note{})
41+
}
42+
43+
// Get gets a Note from the DB
44+
func (p *PG) Get(id string) (*model.Note, error) {
45+
var note model.Note
46+
err := p.DB.Where("uuid = ?", id).Take(&note).Error
47+
if gorm.IsRecordNotFoundError(err) {
48+
return nil, ErrNotFound
49+
}
50+
if err != nil {
51+
return nil, errors.Wrap(err, "getting note from db")
52+
}
53+
54+
return &note, nil
55+
}
56+
57+
// Create creates a note
58+
func (p *PG) Create(note *model.Note) error {
59+
err := p.DB.Create(note).Error
60+
if err != nil {
61+
return errors.Wrap(err, "creating note in db")
62+
}
63+
64+
return nil
65+
}
66+
67+
// Close the DB connection
68+
func (p *PG) Close() error {
69+
return p.DB.Close()
70+
}

0 commit comments

Comments
 (0)