1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 10:27:06 +00:00

add tutorial for the official mongodb go driver

Former-commit-id: 8353dd101c37c223bba404403f9f8fa2d042fede
This commit is contained in:
Gerasimos (Makis) Maropoulos
2019-01-28 05:36:44 +02:00
parent 680b5a0923
commit 4284739151
17 changed files with 635 additions and 5 deletions

View File

@@ -0,0 +1,2 @@
PORT=8080
DSN=mongodb://localhost:27017

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,58 @@
# Build RESTful API with the official MongoDB Go Driver and Iris
Article is coming soon, follow and stay tuned
- <https://medium.com/@kataras>
- <https://dev.to/kataras>
Read [the fully functional example](main.go).
```sh
$ go get -u github.com/mongodb/mongo-go-driver
$ go get -u github.com/joho/godotenv
```
```sh
# .env file contents
PORT=8080
DSN=mongodb://localhost:27017
```
```sh
$ go run main.go
> 2019/01/28 05:17:59 Loading environment variables from file: .env
> 2019/01/28 05:17:59 ◽ PORT=8080
> 2019/01/28 05:17:59 ◽ DSN=mongodb://localhost:27017
> Now listening on: http://localhost:8080
```
```sh
GET : http://localhost:8080/api/store/movies
POST : http://localhost:8080/api/store/movies
GET : http://localhost:8080/api/store/movies/{id}
PUT : http://localhost:8080/api/store/movies/{id}
DELETE : http://localhost:8080/api/store/movies/{id}
```
## Screens
### Add a Movie
![](0_create_movie.png)
### Update a Movie
![](1_update_movie.png)
### Get all Movies
![](2_get_all_movies.png)
### Get a Movie by its ID
![](3_get_movie.png)
### Delete a Movie by its ID
![](4_delete_movie.png)

View File

@@ -0,0 +1,101 @@
package storeapi
import (
"github.com/kataras/iris/_examples/tutorial/mongodb/httputil"
"github.com/kataras/iris/_examples/tutorial/mongodb/store"
"github.com/kataras/iris"
)
type MovieHandler struct {
service store.MovieService
}
func NewMovieHandler(service store.MovieService) *MovieHandler {
return &MovieHandler{service: service}
}
func (h *MovieHandler) GetAll(ctx iris.Context) {
movies, err := h.service.GetAll(nil)
if err != nil {
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to retrieve all movies")
return
}
if movies == nil {
// will return "null" if empty, with this "trick" we return "[]" json.
movies = make([]store.Movie, 0)
}
ctx.JSON(movies)
}
func (h *MovieHandler) Get(ctx iris.Context) {
id := ctx.Params().Get("id")
m, err := h.service.GetByID(nil, id)
if err != nil {
if err == store.ErrNotFound {
ctx.NotFound()
} else {
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to retrieve movie [%s]", id)
}
return
}
ctx.JSON(m)
}
func (h *MovieHandler) Add(ctx iris.Context) {
m := new(store.Movie)
err := ctx.ReadJSON(m)
if err != nil {
httputil.FailJSON(ctx, iris.StatusBadRequest, err, "Malformed request payload")
return
}
err = h.service.Create(nil, m)
if err != nil {
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to create a movie")
return
}
ctx.StatusCode(iris.StatusCreated)
ctx.JSON(m)
}
func (h *MovieHandler) Update(ctx iris.Context) {
id := ctx.Params().Get("id")
var m store.Movie
err := ctx.ReadJSON(&m)
if err != nil {
httputil.FailJSON(ctx, iris.StatusBadRequest, err, "Malformed request payload")
return
}
err = h.service.Update(nil, id, m)
if err != nil {
if err == store.ErrNotFound {
ctx.NotFound()
return
}
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to update movie [%s]", id)
return
}
}
func (h *MovieHandler) Delete(ctx iris.Context) {
id := ctx.Params().Get("id")
err := h.service.Delete(nil, id)
if err != nil {
if err == store.ErrNotFound {
ctx.NotFound()
return
}
httputil.InternalServerErrorJSON(ctx, err, "Server was unable to delete movie [%s]", id)
return
}
}

71
_examples/tutorial/mongodb/env/env.go vendored Normal file
View File

@@ -0,0 +1,71 @@
package env
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/joho/godotenv"
)
var (
// Port is the PORT environment variable or 8080 if missing.
// Used to open the tcp listener for our web server.
Port string
// DSN is the DSN environment variable or mongodb://localhost:27017 if missing.
// Used to connect to the mongodb.
DSN string
)
func parse() {
Port = getDefault("PORT", "8080")
DSN = getDefault("DSN", "mongodb://localhost:27017")
}
// Load loads environment variables that are being used across the whole app.
// Loading from file(s), i.e .env or dev.env
//
// Example of a 'dev.env':
// PORT=8080
// DSN=mongodb://localhost:27017
//
// After `Load` the callers can get an environment variable via `os.Getenv`.
func Load(envFileName string) {
if args := os.Args; len(args) > 1 && args[1] == "help" {
fmt.Fprintln(os.Stderr, "https://github.com/kataras/iris/blob/master/_examples/tutorials/mongodb/README.md")
os.Exit(-1)
}
log.Printf("Loading environment variables from file: %s\n", envFileName)
// If more than one filename passed with comma separated then load from all
// of these, a env file can be a partial too.
envFiles := strings.Split(envFileName, ",")
for i := range envFiles {
if filepath.Ext(envFiles[i]) == "" {
envFiles[i] += ".env"
}
}
if err := godotenv.Load(envFiles...); err != nil {
panic(fmt.Sprintf("error loading environment variables from [%s]: %v", envFileName, err))
}
envMap, _ := godotenv.Read(envFiles...)
for k, v := range envMap {
log.Printf("◽ %s=%s\n", k, v)
}
parse()
}
func getDefault(key string, def string) string {
value := os.Getenv(key)
if value == "" {
os.Setenv(key, def)
value = def
}
return value
}

View File

@@ -0,0 +1,130 @@
package httputil
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"runtime"
"runtime/debug"
"strings"
"time"
"github.com/kataras/iris"
)
var validStackFuncs = []func(string) bool{
func(file string) bool {
return strings.Contains(file, "/mongodb/api/")
},
}
// RuntimeCallerStack returns the app's `file:line` stacktrace
// to give more information about an error cause.
func RuntimeCallerStack() (s string) {
var pcs [10]uintptr
n := runtime.Callers(1, pcs[:])
frames := runtime.CallersFrames(pcs[:n])
for {
frame, more := frames.Next()
for _, fn := range validStackFuncs {
if fn(frame.File) {
s += fmt.Sprintf("\n\t\t\t%s:%d", frame.File, frame.Line)
}
}
if !more {
break
}
}
return s
}
// HTTPError describes an HTTP error.
type HTTPError struct {
error
Stack string `json:"-"` // the whole stacktrace.
CallerStack string `json:"-"` // the caller, file:lineNumber
When time.Time `json:"-"` // the time that the error occurred.
// ErrorCode int: maybe a collection of known error codes.
StatusCode int `json:"statusCode"`
// could be named as "reason" as well
// it's the message of the error.
Description string `json:"description"`
}
func newError(statusCode int, err error, format string, args ...interface{}) HTTPError {
if format == "" {
format = http.StatusText(statusCode)
}
desc := fmt.Sprintf(format, args...)
if err == nil {
err = errors.New(desc)
}
return HTTPError{
err,
string(debug.Stack()),
RuntimeCallerStack(),
time.Now(),
statusCode,
desc,
}
}
func (err HTTPError) writeHeaders(ctx iris.Context) {
ctx.StatusCode(err.StatusCode)
ctx.Header("X-Content-Type-Options", "nosniff")
}
// LogFailure will print out the failure to the "logger".
func LogFailure(logger io.Writer, ctx iris.Context, err HTTPError) {
timeFmt := err.When.Format("2006/01/02 15:04:05")
firstLine := fmt.Sprintf("%s %s: %s", timeFmt, http.StatusText(err.StatusCode), err.Error())
whitespace := strings.Repeat(" ", len(timeFmt)+1)
fmt.Fprintf(logger, "%s\n%sIP: %s\n%sURL: %s\n%sSource: %s\n",
firstLine, whitespace, ctx.RemoteAddr(), whitespace, ctx.FullRequestURI(), whitespace, err.CallerStack)
}
// Fail will send the status code, write the error's reason
// and return the HTTPError for further use, i.e logging, see `InternalServerError`.
func Fail(ctx iris.Context, statusCode int, err error, format string, args ...interface{}) HTTPError {
httpErr := newError(statusCode, err, format, args...)
httpErr.writeHeaders(ctx)
ctx.WriteString(httpErr.Description)
return httpErr
}
// FailJSON will send to the client the error data as JSON.
// Useful for APIs.
func FailJSON(ctx iris.Context, statusCode int, err error, format string, args ...interface{}) HTTPError {
httpErr := newError(statusCode, err, format, args...)
httpErr.writeHeaders(ctx)
ctx.JSON(httpErr)
return httpErr
}
// InternalServerError logs to the server's terminal
// and dispatches to the client the 500 Internal Server Error.
// Internal Server errors are critical, so we log them to the `os.Stderr`.
func InternalServerError(ctx iris.Context, err error, format string, args ...interface{}) {
LogFailure(os.Stderr, ctx, Fail(ctx, iris.StatusInternalServerError, err, format, args...))
}
// InternalServerErrorJSON acts exactly like `InternalServerError` but instead it sends the data as JSON.
// Useful for APIs.
func InternalServerErrorJSON(ctx iris.Context, err error, format string, args ...interface{}) {
LogFailure(os.Stderr, ctx, FailJSON(ctx, iris.StatusInternalServerError, err, format, args...))
}
// UnauthorizedJSON sends JSON format of StatusUnauthorized(401) HTTPError value.
func UnauthorizedJSON(ctx iris.Context, err error, format string, args ...interface{}) HTTPError {
return FailJSON(ctx, iris.StatusUnauthorized, err, format, args...)
}

View File

@@ -0,0 +1,81 @@
package main
// go get -u github.com/mongodb/mongo-go-driver
// go get -u github.com/joho/godotenv
import (
"context"
"flag"
"fmt"
"log"
"os"
// APIs
storeapi "github.com/kataras/iris/_examples/tutorial/mongodb/api/store"
//
"github.com/kataras/iris/_examples/tutorial/mongodb/env"
"github.com/kataras/iris/_examples/tutorial/mongodb/store"
"github.com/kataras/iris"
"github.com/mongodb/mongo-go-driver/mongo"
)
const version = "0.0.1"
func init() {
var envFileName = ".env"
flagset := flag.CommandLine
flagset.StringVar(&envFileName, "env", envFileName, "the env file which web app will use to extract its environment variables")
flag.CommandLine.Parse(os.Args[1:])
env.Load(envFileName)
}
func main() {
client, err := mongo.Connect(context.Background(), env.DSN)
if err != nil {
log.Fatal(err)
}
err = client.Ping(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
defer client.Disconnect(context.TODO())
db := client.Database("store")
var (
// Collections.
moviesCollection = db.Collection("movies")
// Services.
movieService = store.NewMovieService(moviesCollection)
)
app := iris.New()
app.Use(func(ctx iris.Context) {
ctx.Header("Server", "Iris MongoDB/"+version)
ctx.Next()
})
storeAPI := app.Party("/api/store")
{
movieHandler := storeapi.NewMovieHandler(movieService)
storeAPI.Get("/movies", movieHandler.GetAll)
storeAPI.Post("/movies", movieHandler.Add)
storeAPI.Get("/movies/{id}", movieHandler.Get)
storeAPI.Put("/movies/{id}", movieHandler.Update)
storeAPI.Delete("/movies/{id}", movieHandler.Delete)
}
// GET: http://localhost:8080/api/store/movies
// POST: http://localhost:8080/api/store/movies
// GET: http://localhost:8080/api/store/movies/{id}
// PUT: http://localhost:8080/api/store/movies/{id}
// DELETE: http://localhost:8080/api/store/movies/{id}
app.Run(iris.Addr(fmt.Sprintf(":%s", env.Port)), iris.WithOptimizations)
}

View File

@@ -0,0 +1,180 @@
package store
import (
"context"
"errors"
"github.com/mongodb/mongo-go-driver/bson"
"github.com/mongodb/mongo-go-driver/bson/primitive"
"github.com/mongodb/mongo-go-driver/mongo"
// up to you:
// "github.com/mongodb/mongo-go-driver/mongo/options"
)
type Movie struct {
ID primitive.ObjectID `json:"_id" bson:"_id"` /* you need the bson:"_id" to be able to retrieve with ID filled */
Name string `json:"name"`
Cover string `json:"cover"`
Description string `json:"description"`
}
type MovieService interface {
GetAll(ctx context.Context) ([]Movie, error)
GetByID(ctx context.Context, id string) (Movie, error)
Create(ctx context.Context, m *Movie) error
Update(ctx context.Context, id string, m Movie) error
Delete(ctx context.Context, id string) error
}
type movieService struct {
C *mongo.Collection
}
var _ MovieService = (*movieService)(nil)
func NewMovieService(collection *mongo.Collection) MovieService {
// up to you:
// indexOpts := new(options.IndexOptions)
// indexOpts.SetName("movieIndex").
// SetUnique(true).
// SetBackground(true).
// SetSparse(true)
// collection.Indexes().CreateOne(context.Background(), mongo.IndexModel{
// Keys: []string{"_id", "name"},
// Options: indexOpts,
// })
return &movieService{C: collection}
}
func (s *movieService) GetAll(ctx context.Context) ([]Movie, error) {
// Note:
// The mongodb's go-driver's docs says that you can pass `nil` to "find all" but this gives NilDocument error,
// probably it's a bug or a documentation's mistake, you have to pass `bson.D{}` instead.
cur, err := s.C.Find(ctx, bson.D{})
if err != nil {
return nil, err
}
defer cur.Close(ctx)
var results []Movie
for cur.Next(ctx) {
if err = cur.Err(); err != nil {
return nil, err
}
// elem := bson.D{}
var elem Movie
err = cur.Decode(&elem)
if err != nil {
return nil, err
}
// results = append(results, Movie{ID: elem[0].Value.(primitive.ObjectID)})
results = append(results, elem)
}
return results, nil
}
func matchID(id string) (bson.D, error) {
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, err
}
filter := bson.D{{Key: "_id", Value: objectID}}
return filter, nil
}
var ErrNotFound = errors.New("not found")
func (s *movieService) GetByID(ctx context.Context, id string) (Movie, error) {
var movie Movie
filter, err := matchID(id)
if err != nil {
return movie, err
}
err = s.C.FindOne(ctx, filter).Decode(&movie)
if err == mongo.ErrNoDocuments {
return movie, ErrNotFound
}
return movie, err
}
func (s *movieService) Create(ctx context.Context, m *Movie) error {
if m.ID.IsZero() {
m.ID = primitive.NewObjectID()
}
_, err := s.C.InsertOne(ctx, m)
if err != nil {
return err
}
// The following doesn't work if you have the `bson:"_id` on Movie.ID field,
// therefore we manually generate a new ID (look above).
// res, err := ...InsertOne
// objectID := res.InsertedID.(primitive.ObjectID)
// m.ID = objectID
return nil
}
func (s *movieService) Update(ctx context.Context, id string, m Movie) error {
filter, err := matchID(id)
if err != nil {
return err
}
// update := bson.D{
// {Key: "$set", Value: m},
// }
// ^ this will override all fields, you can do that, depending on your design. but let's check each field:
elem := bson.D{}
if m.Name != "" {
elem = append(elem, bson.E{Key: "name", Value: m.Name})
}
if m.Description != "" {
elem = append(elem, bson.E{Key: "description", Value: m.Description})
}
if m.Cover != "" {
elem = append(elem, bson.E{Key: "cover", Value: m.Cover})
}
update := bson.D{
{Key: "$set", Value: elem},
}
_, err = s.C.UpdateOne(ctx, filter, update)
if err != nil {
if err == mongo.ErrNoDocuments {
return ErrNotFound
}
return err
}
return nil
}
func (s *movieService) Delete(ctx context.Context, id string) error {
filter, err := matchID(id)
if err != nil {
return err
}
_, err = s.C.DeleteOne(ctx, filter)
if err != nil {
if err == mongo.ErrNoDocuments {
return ErrNotFound
}
return err
}
return nil
}