This post extends GoApp-Http-Service-Ping-Pong

Intro

In this post we will go through the process of extending the http go app we made on the previous part with DB CRUD operations.

What we will see:

  • Add new endpoints
  • Use Postgres DB to save data
  • Use CRUD operations to modify data

Your workspace callback function from part 1 should look like this


ls .
go.mod  go.sum  main.go

and without further ado…

Start Postgres

We will use docker to start a Postgresql service instance

If you do not have docker-compose installed, please run:


mkdir -vp local/bin
curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o ./local/bin/docker-compose
chmod +x ./local/bin/docker-compose
echo 'export PATH="${PWD}/local/bin:${PATH}"' > .env
source .env

Ensure that docker-compose has been installed successfully


command -v docker-compose

The above command should print the absolute path to the docker-compose “${PWD}/local/bin” If you close the terminal, to access again docker-compose, go to the rootDir and issue:


source .env

The yaml file below defines a postgresql service and a postgresql web helper service If you are short of resources, you can remove lines 12 - 20 since they are optional.

Save the yaml below as docker-compose.yml in the rootDir.

version: "3.1"
services:
  pgsql:
    container_name: db
    environment:
      POSTGRES_USER: webapp_user
      POSTGRES_PASSWORD: webapp_password
      POSTGRES_DB: webapp_db
    image: postgres
    ports:
      - "5432:5432"

  pgweb:
    container_name: pgweb
    depends_on:
      - pgsql
    environment:
      VIRTUAL_PORT: 8081
    image: sosedoff/pgweb
    ports:
      - "8081:8081"

Start Postgres

docker-compose -f docker-compose.yml up -d

# Creating network "workdir_default" with the default driver

If you used the pgweb service from above, you can test that all is good by navigating from your browser to http://localhost:8081 and enter

  • Host: db
  • Username: webapp_user
  • Password: webapp_password
  • Database: webapp_db
  • SSL Mode: disable

If you did not use the pgweb service, then run the command below and ensure DB has started successfully:


docker ps -a

#	CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
#	...
#	357063d16648        sosedoff/pgweb      "/usr/bin/pgweb --bi…"   4 minutes ago       Up 4 minutes        0.0.0.0:8081->8081/tcp   pgweb
#	...

Next we are going to create the DB Factory function and the CRUD methods

DB Factory

For our DB we will create a new directory in order to keep things organized


mkdir -p repository

In the repository directory create a new file db.go and paste the following code

package repository

import (
	"fmt"

	"github.com/google/uuid"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

// ConnectionInfo for storing the db connection info
type ConnectionInfo struct {
	Username string
	Password string
	Database string
	Hostname string
	Port     string
}

// DBSchema to define the DB Schema Structure
type DBSchema struct {
	gorm.Model
	CID    uuid.UUID `gorm:"unique;not null"`
	Client string    `gorm:"unique;not null" json:"client_id"`
	Count  int       `gorm:"not null" json:"count"`
}

// DBService to define a new service for the DB
type DBService struct {
	Service *gorm.DB
}

// NewDBFactory for creating a new DBService
func NewDBFactory(connectionInfo ConnectionInfo, s *DBSchema) (*DBService, error) {
	dbUri := fmt.Sprintf(
		"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
		connectionInfo.Hostname,
		connectionInfo.Username,
		connectionInfo.Password,
		connectionInfo.Database,
		connectionInfo.Port,
	)
	db, err := gorm.Open(postgres.Open(dbUri), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})

	if err != nil {
		return nil, err
	}

	db.AutoMigrate(&DBSchema{})

	// Anonymous client
	err = db.Where("c_id = ?", "00000000-0000-0000-0000-000000000000").First(&DBSchema{}).Error
	if err != nil {
		if err.Error() == "record not found" {
			anonymous := DBSchema{
				Client: "Anonymous",
				Count:  0,
			}
			_ = db.Create(&anonymous)
		} else {
			return nil, err
		}
	}

	dbService := &DBService{
		Service: db,
	}

	return dbService, nil
}

What we defined:

  • Lines (2 - 8): As always we import the required packages
  • Liens (11 - 17): We define the ConnectionInfo struct that we will use in main to connect to our DB
  • Lines (20 - 25): DBSchema that will be used as the schema to populate our DB Table
  • Lines (28 - 30): DBService a new service to work with our DB
  • Lines (33 - 71): NewDBFactory function that will be used to initiate a new DBService

Notice: Anonymous client record. It will be used for all clients that use the public endpoint


Notes about the Schema structure:

  • CID: ClientID is a none empty uuid that is unique
  • Client: is the client name, none empty and unique
  • Count: is the total number of the client’s requests

The AutoMigrate Method is part of gorm and handles table creation / update for us

DB CRUD Method Definitions

Here we will define our DB CRUD Methods. Each method will be responsible for doing one CRUD operation.

For a full CRUD list we need the following methods:

  • GetClient and GetClientByCID: For Reading a client
  • CreateClient: For creating a new client
  • DeleteClient: For deleting a client
  • UpdateClient: For updating a client

Method: GetClient

We start by creating GetClient GetClientByCID. In the repository package create a new file called db_crud.go and paste the following code

package repository

import (
	"errors"

	"github.com/google/uuid"
)

// GetClient DB method for getting a client record
func (s *DBService) GetClient(client string) (*DBSchema, error) {
	schema := &DBSchema{}
	err := s.Service.Where("Client = ?", client).First(&schema).Error
	return schema, err
}

// GetClient DB method for getting a client record
func (s *DBService) GetClientByCID(cid string) (*DBSchema, error) {
	schema := &DBSchema{}
	err := s.Service.Where("c_id = ?", cid).First(&schema).Error
	return schema, err
}

The GetClient Method expects the client name, defines an empty schema pointer (&DBSchema{}), searches the DB for the given client, and finally returns the schema along with the error if any

The GetClientByCID does the same that GetClient does, but it searches the records with c_id filter

Method: CreateClient

Append the following code in the db_crud.go

// CreateClient DB method for creating a new record
func (s *DBService) CreateClient(schema *DBSchema) error {
	if schema.Client == "" {
		return errors.New("DBSchema.Client can not be empty")
	}
	schema.CID = uuid.New()
	err := s.Service.Create(&schema)
	return err.Error
}

The above method expects the populated DBSchema. Note that CID is auto-generated when this method is called.

Method: UpdateClient

Append the following code in the db_crud.go

// UpdateClient DB method for updating a record
func (s *DBService) UpdateClient(client string, count int) error {
	if client == "" || count == 0 {
		return errors.New("DBSchema.Client can not be empty and DBSchema.Count can not be 0")
	}
	schema := &DBSchema{}
	err := s.Service.Where("Client = ?", client).First(&schema).Error
	if err != nil {
		return err
	}
	schema.Count = count

	err = s.Service.Save(&schema).Error
	return err
}

The above method first checks the DB if the client exists. In case it exists, then proceeds by updating the client

Method: DeleteClient

// DeleteClient DB method for deleting a record
func (s *DBService) DeleteClient(client string) error {
	if client == "" {
		return errors.New("DBSchema.Client can not be empty")
	}
	schema := &DBSchema{}
	err := s.Service.Where("Client = ?", client).First(&schema).Error
	if err != nil {
		return err
	}

	// We do not allow the deletion of the anonymous user
	if schema.CID.String() == "00000000-0000-0000-0000-000000000000" {
		return errors.New("Anonymous user is a system user.")
	}

	err = s.Service.Delete(&schema).Error
	return err
}

The above methods also checks if the records exists and proceeds with deleting it accordingly

The total code in db_crud.go should look as the code below

package repository

import (
	"errors"

	"github.com/google/uuid"
)

// GetClient DB method for getting a client record
func (s *DBService) GetClient(client string) (*DBSchema, error) {
	schema := &DBSchema{}
	err := s.Service.Where("Client = ?", client).First(&schema).Error
	return schema, err
}

// GetClient DB method for getting a client record
func (s *DBService) GetClientByCID(cid string) (*DBSchema, error) {
	schema := &DBSchema{}
	err := s.Service.Where("c_id = ?", cid).First(&schema).Error
	return schema, err
}

// CreateClient DB method for creating a new record
func (s *DBService) CreateClient(schema *DBSchema) error {
	if schema.Client == "" {
		return errors.New("DBSchema.Client can not be empty")
	}
	schema.CID = uuid.New()
	err := s.Service.Create(&schema)
	return err.Error
}

// UpdateClient DB method for updating a record
func (s *DBService) UpdateClient(client string, count int) error {
	if client == "" || count == 0 {
		return errors.New("DBSchema.Client can not be empty and DBSchema.Count can not be 0")
	}
	schema := &DBSchema{}
	err := s.Service.Where("Client = ?", client).First(&schema).Error
	if err != nil {
		return err
	}
	schema.Count = count

	err = s.Service.Save(&schema).Error
	return err
}

// DeleteClient DB method for deleting a record
func (s *DBService) DeleteClient(client string) error {
	if client == "" {
		return errors.New("DBSchema.Client can not be empty")
	}
	schema := &DBSchema{}
	err := s.Service.Where("Client = ?", client).First(&schema).Error
	if err != nil {
		return err
	}

	// We do not allow the deletion of the anonymous user
	if schema.CID.String() == "00000000-0000-0000-0000-000000000000" {
		return errors.New("Anonymous user is a system user.")
	}

	err = s.Service.Delete(&schema).Error
	return err
}

Router DB Integration

Ok so we have a db factory ready, we have the crud methods ready. Now all we need is to integrate our db client with our router service.

Open router/router.go and replace the whole content with the code below

package router

import (
	"errors"

	"github.com/gorilla/mux"
	"github.com/sirupsen/logrus"
	"github.com/ulfox/callback/repository"
)

// Service for creating a new router with logging
type Service struct {
	Router *mux.Router
	Name   string
	logger logrus.FieldLogger
	db     *repository.DBService
}

// UpdateRoutes method updates main route with our Handle Functions
func (s *Service) UpdateRoutes() {
	s.Router.HandleFunc("/api/v1/ping", s.Callback).Methods("GET")
	s.Router.HandleFunc("/api/v1/ping/{secret:[a-z0-9-]+}", s.CallbackRegistered).Methods("GET")
	s.Router.HandleFunc("/api/v1/register/{client:[a-z0-9-\\_]+}", s.Register).Methods("POST")
	s.Router.HandleFunc("/api/v1/delete/{secret:[a-z0-9-]+}", s.DeRegister).Methods("POST")
}

// NewRouter factory for creating a Service router
func NewRouter(name string, db *repository.DBService, logger logrus.FieldLogger) (*Service, error) {
	if name == "" {
		return nil, errors.New("Name not allowed to be empty")
	}

	return &Service{
		Router: mux.NewRouter().StrictSlash(true),
		Name:   name,
		logger: logger,
		db:     db,
	}, nil
}

The changes here are:

  • Included repository "github.com/ulfox/callback/repository". Do not forget to change the name with your name if you changed it in mod init
  • Lines (15): Service: DB *gorm.DB
  • Lines (19 - 24): UpdateRoutes includes 3 new endpoint handlers. See below
  • NewRouter: db *gorm.DB at line 27 and db: db, at line 36

We will create the handler functions in the Registration Endpoint Section. For now, let us have a look at the endpoints that have been registered in UpdateRoutes

Endpoint /api/v1/ping/{secret:[a-z0-9-]+}:

  • This endpoint will handle ping requests with a secret
  • Paramater allowed Any combination of Alphanumeric and "-"

Endpoint /api/v1/register/{client:[a-z0-9-\_]+}:

  • This endpoint will handle new client registration
  • Parameter allowed Any combination of Alphanumeric, "-" and "_"

Endpoint /api/v1/delete/{secret:[a-z0-9-]+}:

  • This endpoint will handler client delete requests
  • Parameter allowed Any combination of Alphanumeric and "-"

We are ready to define some endpoints

Registration Endpoint

The registration endpoint will be used by clients for signup. After a signup the client’s will continue to do ping-pongs but now authorized.

Open the endpoints.go file under router directory and add in the import section the following line

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/gorilla/mux"
	"github.com/sirupsen/logrus"
	"github.com/ulfox/callback/repository"
)

Changes:

  • Imported strings that we use for some string manipulation in the functions that will follow
  • Imported mux for quering parameters in the functions that will follow
  • Imported repository in order to have access to the schema struct that is needed to create new clients

The above import is needed in order for our functions to create new clients

In endpoints.go replace the Callback method with the code below

// Callback method for implementing a simple ping pong response
func (s *Service) Callback(w http.ResponseWriter, r *http.Request) {
	responseInfo := ResponseInfo{
		RemoteAddr: r.RemoteAddr,
		URL:        r.URL.String(),
		Handler:    "Callback",
	}

	anonymous, err := s.db.GetClient("Anonymous")
	if err != nil {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 500, w)
		return
	}

	anonymous.Count = anonymous.Count + 1
	err = s.db.UpdateClient("Anonymous", anonymous.Count)
	if err != nil {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 500, w)
		return
	}

	responseInfo.Client = "Anonymous"
	responseInfo.Count = anonymous.Count
	responseInfo.Message = "pong"
	s.requestHandler(responseInfo, 200, w)
}

The changes are marked above and are:

  • Lines (8 - 13) Read the anonymous client from the DB
  • Lines (15 - 21) Increment the count of the anonymous user by 1 and write the new Count in the DB
  • Lines (24 - 25) Include Anonymous and Count in the response payload

We will now create the Register method which will be used to handle new client registrations. Open the endpoints.go file and append the following line of code at the end

// Register method for registering a new client
func (s *Service) Register(w http.ResponseWriter, r *http.Request) {
	responseInfo := ResponseInfo{
		RemoteAddr: r.RemoteAddr,
		URL:        r.URL.String(),
		Handler:    "Register",
	}

	vars := mux.Vars(r)
	val, ok := vars["client"]
	if !ok {
		responseInfo.Error = "Client registration name can not be empty"
		s.requestHandler(responseInfo, 400, w)
		return
	}

	// We do not allow the creation of Anonymous user
	if strings.ToLower(val) == "anonymous" {
		responseInfo.Error = "Anonymous user is a system user."
		s.requestHandler(responseInfo, 400, w)
		return
	}

	user := repository.DBSchema{
		Client: val,
		Count:  0,
	}
	_, err := s.db.GetClient(val)
	responseInfo.Client = val
	if err == nil {
		responseInfo.Error = "User already exists"
		s.requestHandler(responseInfo, 400, w)
		return
	} else if err != nil && err.Error() != "record not found" {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 400, w)
		return
	}

	err = s.db.CreateClient(&user)
	if err != nil {
		if strings.HasPrefix(err.Error(), "ERROR: duplicate key value violates unique constraint") {
			responseInfo.Error = "Username is not available"
		} else {
			responseInfo.Error = err.Error()
		}

		s.requestHandler(responseInfo, 400, w)
		return
	}
	responseInfo.SecretKey = user.CID.String()

	s.requestHandler(responseInfo, 200, w)
}

In the above code we:

  • Lines (2 - 6): Create a new response payload
  • Lines (8 - 14): Check if the client has included a registration name in the payload
  • Lines (16 - 30): Check if the client exists. If it does, response with an error, otherwise continue
  • Lines (32 - 38): Create a new client with the requested name. Export the generated client secret and send it back to the client

Next we create the DeRegister endpoint. Open again the endpoints.go and append at the end the following code

// DeRegister method for deleting a client
func (s *Service) DeRegister(w http.ResponseWriter, r *http.Request) {
	responseInfo := ResponseInfo{
		RemoteAddr: r.RemoteAddr,
		URL:        "/api/v1/delete/***",
		Handler:    "DeRegister",
	}

	vars := mux.Vars(r)
	val, ok := vars["secret"]
	if !ok {
		responseInfo.Error = "Client secret is missing"
		s.requestHandler(responseInfo, 401, w)
		return
	}

	user, err := s.db.GetClientByCID(val)
	if err != nil {
		responseInfo.Error = "Wrong secret, unauthorized"
		s.requestHandler(responseInfo, 401, w)
		return
	}

	// We do not allow the deletion of Anonymous user
	if strings.ToLower(user.Client) == "anonymous" {
		responseInfo.Error = "Anonymous user is a system user."
		s.requestHandler(responseInfo, 400, w)
		return
	}

	err = s.db.DeleteClient(user.Client)
	if err != nil {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 500, w)
		return
	}

	responseInfo.Message = "Deleted"
	responseInfo.Client = user.Client
	s.requestHandler(responseInfo, 200, w)
}

In the above code we:

  • Lines (2 - 6): Create a new response payload
  • Lines (8 - 14): Check if the client has included his secret in the payload.
  • Lines (16 - 21): Check if the a client with that secret exists. If it does not, response with an error, otherwise continue
  • Lines (23 - 28): Delete the client from the DB. This is ireversable and the particular client id can no longer be used until it is hard deleted
  • Lines (30 - 32): Update the response payload and reply back to the client

We have one more handler function to define, and that is the CallbackRegistered method. Again, open the endpoints.go file and append at the end the following code

// CallbackRegistered method for implementing a simple ping pong response
func (s *Service) CallbackRegistered(w http.ResponseWriter, r *http.Request) {
	responseInfo := ResponseInfo{
		RemoteAddr: r.RemoteAddr,
		URL:        "/api/v1/ping/***",
		Handler:    "CallbackRegistered",
	}

	vars := mux.Vars(r)
	val, ok := vars["secret"]
	if !ok {
		responseInfo.Error = "Client secret is missing"
		s.requestHandler(responseInfo, 401, w)
		return
	}

	user, err := s.db.GetClientByCID(val)
	if err != nil {
		responseInfo.Error = "Wrong secret, unauthorized"
		s.requestHandler(responseInfo, 401, w)
		return
	}

	user.Count = user.Count + 1
	err = s.db.UpdateClient(user.Client, user.Count)
	if err != nil {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 500, w)
		return
	}

	responseInfo.Count = user.Count
	responseInfo.Message = "pong"
	responseInfo.Client = user.Client
	s.requestHandler(responseInfo, 200, w)
}
  • Lines (2 - 6): Create a new response payload
  • Lines (8 - 14): Check if the client has included his secret in the payload.
  • Lines (16 - 21): Check if the a client with that secret exists. If it does not, response with an error, otherwise continue
  • Lines (23 - 29): Increment the count by one for the client and update the client’s record in the DB
  • Lines (31 - 34): Update the response payload and reply back to the client

The full endpoints.go code is presented below


package router

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/gorilla/mux"
	"github.com/sirupsen/logrus"
	"github.com/ulfox/callback/repository"
)

// ResponseInfo for sending data back to the client
type ResponseInfo struct {
	Client     string `json:"client,omitempty"`
	SecretKey  string `json:"secret_key,omitempty"`
	Message    string `json:"message,omitempty"`
	Count      int    `json:"count,omitempty"`
	Error      string `json:"error,omitempty"`
	RemoteAddr string `json:"-"`
	URL        string `json:"-"`
	Handler    string `json:"-"`
}

// requestHandler for handling request
func (s *Service) requestHandler(responseInfo ResponseInfo, httpErr int, w http.ResponseWriter) {
	s.logger.WithFields(logrus.Fields{
		"Server":  s.Name,
		"Client":  responseInfo.RemoteAddr,
		"URL":     responseInfo.URL,
		"Address": responseInfo.RemoteAddr,
		"Handler": responseInfo.Handler,
		"Error":   responseInfo.Error,
	}).Info(responseInfo.Message)

	w.WriteHeader(httpErr)
	jsonResponse, err := json.Marshal(responseInfo)
	if err != nil {
		w.WriteHeader(400)
		fmt.Fprintf(
			w,
			err.Error(),
		)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(jsonResponse)
}

// Callback method for implementing a simple ping pong response
func (s *Service) Callback(w http.ResponseWriter, r *http.Request) {
	responseInfo := ResponseInfo{
		RemoteAddr: r.RemoteAddr,
		URL:        r.URL.String(),
		Handler:    "Callback",
	}

	anonymous, err := s.db.GetClient("Anonymous")
	if err != nil {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 500, w)
		return
	}

	anonymous.Count = anonymous.Count + 1
	err = s.db.UpdateClient("Anonymous", anonymous.Count)
	if err != nil {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 500, w)
		return
	}

	responseInfo.Client = "Anonymous"
	responseInfo.Count = anonymous.Count
	responseInfo.Message = "pong"
	s.requestHandler(responseInfo, 200, w)
}

// Register method for registering a new client
func (s *Service) Register(w http.ResponseWriter, r *http.Request) {
	responseInfo := ResponseInfo{
		RemoteAddr: r.RemoteAddr,
		URL:        r.URL.String(),
		Handler:    "Register",
	}

	vars := mux.Vars(r)
	val, ok := vars["client"]
	if !ok {
		responseInfo.Error = "Client registration name can not be empty"
		s.requestHandler(responseInfo, 400, w)
		return
	}

	// We do not allow the creation of Anonymous user
	if strings.ToLower(val) == "anonymous" {
		responseInfo.Error = "Anonymous user is a system user."
		s.requestHandler(responseInfo, 400, w)
		return
	}

	user := repository.DBSchema{
		Client: val,
		Count:  0,
	}
	_, err := s.db.GetClient(val)
	responseInfo.Client = val
	if err == nil {
		responseInfo.Error = "User already exists"
		s.requestHandler(responseInfo, 400, w)
		return
	} else if err != nil && err.Error() != "record not found" {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 400, w)
		return
	}

	err = s.db.CreateClient(&user)
	if err != nil {
		if strings.HasPrefix(err.Error(), "ERROR: duplicate key value violates unique constraint") {
			responseInfo.Error = "Username is not available"
		} else {
			responseInfo.Error = err.Error()
		}

		s.requestHandler(responseInfo, 400, w)
		return
	}
	responseInfo.SecretKey = user.CID.String()

	s.requestHandler(responseInfo, 200, w)
}

// DeRegister method for deleting a client
func (s *Service) DeRegister(w http.ResponseWriter, r *http.Request) {
	responseInfo := ResponseInfo{
		RemoteAddr: r.RemoteAddr,
		URL:        "/api/v1/delete/***",
		Handler:    "DeRegister",
	}

	vars := mux.Vars(r)
	val, ok := vars["secret"]
	if !ok {
		responseInfo.Error = "Client secret is missing"
		s.requestHandler(responseInfo, 401, w)
		return
	}

	user, err := s.db.GetClientByCID(val)
	if err != nil {
		responseInfo.Error = "Wrong secret, unauthorized"
		s.requestHandler(responseInfo, 401, w)
		return
	}

	// We do not allow the deletion of Anonymous user
	if strings.ToLower(user.Client) == "anonymous" {
		responseInfo.Error = "Anonymous user is a system user."
		s.requestHandler(responseInfo, 400, w)
		return
	}

	err = s.db.DeleteClient(user.Client)
	if err != nil {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 500, w)
		return
	}

	responseInfo.Message = "Deleted"
	responseInfo.Client = user.Client
	s.requestHandler(responseInfo, 200, w)
}

// CallbackRegistered method for implementing a simple ping pong response
func (s *Service) CallbackRegistered(w http.ResponseWriter, r *http.Request) {
	responseInfo := ResponseInfo{
		RemoteAddr: r.RemoteAddr,
		URL:        "/api/v1/ping/***",
		Handler:    "CallbackRegistered",
	}

	vars := mux.Vars(r)
	val, ok := vars["secret"]
	if !ok {
		responseInfo.Error = "Client secret is missing"
		s.requestHandler(responseInfo, 401, w)
		return
	}

	user, err := s.db.GetClientByCID(val)
	if err != nil {
		responseInfo.Error = "Wrong secret, unauthorized"
		s.requestHandler(responseInfo, 401, w)
		return
	}

	user.Count = user.Count + 1
	err = s.db.UpdateClient(user.Client, user.Count)
	if err != nil {
		responseInfo.Error = err.Error()
		s.requestHandler(responseInfo, 500, w)
		return
	}

	responseInfo.Count = user.Count
	responseInfo.Message = "pong"
	responseInfo.Client = user.Client
	s.requestHandler(responseInfo, 200, w)
}

Now we can pass the pointer db client that is exported from the NewDBFactory to our router.

Open main.go and replace the whole content with the code below

package main

import (
	"net/http"
	"os"

	"github.com/sirupsen/logrus"
	"github.com/ulfox/callback/repository"
	"github.com/ulfox/callback/router"
)

// Global variables
var (
	logger        = logrus.New()
	SRVER  string = "Callback"
)

func die(stage, msg string) {
	logger.WithFields(logrus.Fields{
		"Component": "Main",
		"Stage":     stage,
	}).Error(msg)
	os.Exit(1)
}

func main() {
	logger.WithField("Callback", SRVER).Infof("Initiated")

	u := &repository.DBSchema{}
	connectionInfo := repository.ConnectionInfo{
		Username: "webapp_user",
		Password: "webapp_password",
		Database: "webapp_db",
		Hostname: "localhost",
		Port:     "5432",
	}

	dbService, err := repository.NewDBFactory(
		connectionInfo,
		u,
	)
	if err != nil {
		die("dbService", err.Error())
	}

	service, err := router.NewRouter(SRVER, dbService, logger)
	if err != nil {
		die("NewRouter", err.Error())
	}
	service.UpdateRoutes()

	err = http.ListenAndServe(":8080", service.Router)
	if err != nil {
		die("ListenAndServe", err.Error())
	}
}

The changes are marked above and are:

  • Included “github.com/ulfox/callback/repository” and “github.com/ulfox/callback/router”. Do not forget to change the name if you used different in mod init
  • Lines (28): We initiate an empty DBSchema struct that will be used by grom’s AutoMigrate to populate our table
  • Lines (29 - 35): ConnectionInfo
  • Lines (37 - 43): dbService initiates a new db factory
  • Lines (45): We passed our db client to NewRouter factory line 18

That is it, now we can run the app

Running the Server

We have all the code in place and we are ready to run and test our service.

Open two terminals. In the first one run the server

go run main.go
INFO[0000] Initiated                                     Callback=Callback

Anonymous Callback

In the second terminal type

curl -s localhost:8080/api/v1/ping -XGET | jq
{
   "client": "Anonymous",
   "message": "pong",
   "count": 1
}

Register a new client

curl -s localhost:8080/api/v1/register/ulfox -XPOST | jq
{
  "client": "ulfox",
  "secret_key": "29555b07-61c3-467e-9d20-ea56b4f61734"
}

Callback with the registered client

curl -s localhost:8080/api/v1/ping/29555b07-61c3-467e-9d20-ea56b4f61734 -XGET | jq
{
  "client": "ulfox",
  "message": "pong",
  "count": 1
}

Delete a Client

curl localhost:8080/api/v1/delete/29555b07-61c3-467e-9d20-ea56b4f61734 -XPOST | jq
{
  "client": "ulfox",
  "message": "Deleted"
}

Servers Output

INFO[0000] Initiated  Callback=Callback
INFO[0120] pong       Address="[::1]:49046" Client="[::1]:49046" Error= Handler=Callback Server=Callback URL=/api/v1/ping
INFO[0135] pong       Address="[::1]:49048" Client="[::1]:49048" Error= Handler=Callback Server=Callback URL=/api/v1/ping
INFO[0286]            Address="[::1]:49080" Client="[::1]:49080" Error= Handler=Register Server=Callback URL=/api/v1/register/ulfox
INFO[0362] pong       Address="[::1]:49112" Client="[::1]:49112" Error= Handler=CallbackRegistered Server=Callback URL="/api/v1/ping/***"
INFO[0407] Deleted    Address="[::1]:49118" Client="[::1]:49118" Error= Handler=DeRegister Server=Callback URL=/api/v1/delete/***

Thank you for reading.