Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to pass DB in echo.Context to handler function #2075

Closed
3 tasks done
cyberkryption opened this issue Jan 19, 2022 · 5 comments
Closed
3 tasks done

Unable to pass DB in echo.Context to handler function #2075

cyberkryption opened this issue Jan 19, 2022 · 5 comments
Labels

Comments

@cyberkryption
Copy link

cyberkryption commented Jan 19, 2022

Issue Description

Checklist

  • Dependencies installed
  • No typos
  • Searched existing issues and docs

Expected behaviour

I am new to echo and am building a rest api using swagger. I want to add db to echo.Context so that it is defined for api handlers giving them access to database.

I searched the issues and found #514 which seems to suggest that adding something like e.Context().Set("db", db) in place of the ContxtDB function in main.go might work but this failed as well.

However, I am struggling to understand why the db is undefined. Have I made a silly mistake somewhere that I just cant see?

Actual behaviour

db undefined in licence.go in api package

Steps to reproduce

See below

Working code to debug

package main

import (
	"fmt"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/swaggo/echo-swagger"
	_ "github.com/mattn/go-sqlite3"
	"licence-server/api"
	"licence-server/database"
	"licence-server/docs"
	"licence-server/middlewares"

func main() {
	db := database.Connect()
	defer db.Close()

	e := echo.New()

	// Pass database to all handlers by adding to echo context
	e.Use(middlewares.ContextDB(db))

	e.Use(middleware.Logger())

      e.POST("/api/v1/licence", api.Create_a_Licence)

Licence.go in licence-server/api.go in api package

package api

import (
	_ "github.com/jinzhu/gorm/dialects/sqlite"
	"github.com/labstack/echo/v4"
	"licence-server/model"
	"net/http"
)

< abridged>

func Create_a_Licence(c echo.Context) error {
	var CreateLicence model.Licence
	if err := c.Bind(CreateLicence); err != nil {
		return err
	}
	result := db.Create(&CreateLicence)
	return c.String(http.StatusOK, "Licence Created")
}
package middlewares

import (
	"github.com/jinzhu/gorm"
	"github.com/labstack/echo/v4"
)

// ContextDB : Inject db into echo Context
func ContextDB(db *gorm.DB) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			c.Set("db", db)
			return next(c)
		}
	}
}

Any help appreciated

@aldas
Copy link
Contributor

aldas commented Jan 19, 2022

This is example for setting DB into context with middleware:

const (
	dbContextKey = "__db"
)

type DummyDB struct{}

func (db *DummyDB) QuerySomething() (string, error) {
	return "result", nil
}

func dbMiddleware(db *DummyDB) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			c.Set(dbContextKey, db)
			return next(c)
		}
	}
}

func main() {
	e := echo.New()

	e.Use(middleware.Logger())

	db := new(DummyDB)
	e.Use(dbMiddleware(db))

	e.GET("/", func(c echo.Context) error {
		db := c.Get(dbContextKey).(*DummyDB)
		result, err := db.QuerySomething()
		if err != nil {
			return err
		}
		return c.String(http.StatusOK, result)
	})

	log.Fatal(e.Start(":8080"))
}

Or you could structure your code into "controller" that have db injected to them

type DummyDB struct{}

func (db *DummyDB) GetUsernameByID(ctx context.Context, ID int) (string, error) {
	return "adam", nil
}

func main() {
	e := echo.New()

	e.Use(middleware.Logger())

	api := e.Group("/api")

	db := new(DummyDB)
	RegisterRoutes(api, db)

	log.Fatal(e.Start(":8080"))
}

// -----------------------------------------------------
// -----------------------------------------------------
// -----------------------------------------------------
// this is located in "users" package. So you would group code by domain
// package users

type db interface { // if you use interface in this package you can easily test handler with mocked db
	GetUsernameByID(ctx context.Context, ID int) (string, error)
}

type users struct {
	db db
}

func RegisterRoutes(parent *echo.Group, db db) {
	u := users{
		db: db,
	}

	g := parent.Group("/users")

	// register all routes on controller
	g.GET("/:id/username", u.username)
}

func (ctrl *users) username(c echo.Context) error {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "invalid ID")
	}
	
	result, err := ctrl.db.GetUsernameByID(c.Request().Context(), id)
	if err != nil {
		return err
	}
	return c.String(http.StatusOK, result)
}

And test with mocked db

type mockDummyDB struct {
	mock.Mock
}

func (m *mockDummyDB) GetUsernameByID(ctx context.Context, ID int) (string, error) {
	args := m.Called(ctx, ID)
	return args.Get(0).(string), args.Error(1)
}

func TestName(t *testing.T) {
	var testCases = []struct {
		name           string
		whenID         string
		resultID       int
		resultUsername string
		resultError    error
		expect         string
		expectErr      string
	}{
		{
			name:           "ok",
			whenID:         "1",
			resultID:       1,
			resultUsername: "test",
			resultError:    nil,
			expect:         "test",
		},
		{
			name:           "nok, db failure",
			whenID:         "1",
			resultID:       1,
			resultUsername: "",
			resultError:    errors.New("db_error"),
			expect:         "",
			expectErr:      "db_error",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			e := echo.New()
			req := httptest.NewRequest(http.MethodGet, "/does-not-matter-in-this-example", nil)
			rec := httptest.NewRecorder()
			c := e.NewContext(req, rec)

			c.SetParamNames("id")
			c.SetParamValues(tc.whenID)

			mockedDB := new(mockDummyDB)
			mockedDB.On("GetUsernameByID", mock.Anything, tc.resultID).Return(tc.resultUsername, tc.resultError)

			users := users{mockedDB}
			err := users.username(c)

			assert.Equal(t, tc.expect, rec.Body.String())
			if tc.expectErr != "" {
				assert.EqualError(t, err, tc.expectErr)
			} else {
				assert.NoError(t, err)
			}
			mockedDB.AssertExpectations(t)
		})
	}
}

nb: I did not try if it really works. but you should get the idea.

@aldas aldas added the question label Jan 19, 2022
@cyberkryption
Copy link
Author

cyberkryption commented Jan 20, 2022

Can you check the middleware setup for me . Repo is at https://github.com/cyberkryption/gorm-swagger

Would it be better to use the controllers option, I also need to refactor routing at some stage.

@aldas
Copy link
Contributor

aldas commented Jan 20, 2022

controller-like structs have their advantages. You can set your services there instead of DB connection have have better layering in you application. "controller" for your transport layer, service for your domain related logic including necessary objects (repositories or plain db connection) for accessing storage layer.

For smaller application this layer may be overkill but for larger application this helps with code organization. I have applications where I have multiple different transport layers - http and messagebus (mqtt) which use same service layer code and do not know anything about storage layer as it is contained in service layer code.

@aldas
Copy link
Contributor

aldas commented Jan 23, 2022

Would it be ok to close this issue? Just pointing out that we have also https://github.com/labstack/echo/discussions

@cyberkryption
Copy link
Author

Apologies, I forgot to close it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants