hitrix

package module
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 9, 2021 License: MIT Imports: 33 Imported by: 0

README

codecov Go Report Card MIT license

Hitrix

Hitrix is a web framework written in Go (Golang) and support Graphql and REST api. Hitrix is based on top of Gqlgen and Gin Framework and it's high performance and easy to use

Built-in features:
  • It supports all features of Gqlgen and Gin Framework
  • Integrated with ORM
  • Follows Dependency injection pattern
  • Provides many DI services that makes your live easier. You can read more about them here
  • Provides Dev panel where you can monitor and manage your application(monitoring, error log, db alters redis status and so on)

Installation

go get -u github.com/coretrix/hitrix

Quick start

  1. Run next command into your project's main folder and the graph structure will be created
go run github.com/99designs/gqlgen init
  1. Create cmd folder into your project and file called main.go

Put the next code into the file:

package main

import (
	"github.com/coretrix/hitrix"
	"github.com/gin-gonic/gin"
	
	"your-project/graph" //path you your graph
	"your-project/graph/generated" //path you your graph generated folder
)

func main() {
	s, deferFunc := hitrix.New(
		"app-name", "your secret",
	).Build()
    defer deferFunc()
	s.RunServer(9999, generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}),  func(ginEngine *gin.Engine) {
		//here you can register all your middlewares
	})
}

You are able register DI services in your main.go file in that way:

package main

import (
	"github.com/coretrix/hitrix"
	"github.com/coretrix/hitrix/service/registry"
	"your-project/entity"
	"your-project/graph"
	"your-project/graph/generated"
	"github.com/coretrix/hitrix/pkg/middleware"
	"github.com/gin-gonic/gin"
)

func main() {
	s, deferFunc := hitrix.New(
		"app-name", "your secret",
	).RegisterDIService(
		registry.ServiceProviderErrorLogger(), //register redis error logger
		registry.ServiceProviderConfigDirectory("../config"), //register config service. As param you should point to the folder of your config file
		registry.ServiceDefinitionOrmRegistry(entity.Init), //register our ORM and pass function where we set some configurations 
		registry.ServiceDefinitionOrmEngine(), //register our ORM engine for background processes
		registry.ServiceDefinitionOrmEngineForContext(), //register our ORM engine per context used in foreground processes
		registry.ServiceProviderJWT(), //register JWT DI service
		registry.ServiceProviderPassword(), //register pasword DI service
	).Build()
    defer deferFunc()

	s.RunServer(9999, generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}),  func(ginEngine *gin.Engine) {
		middleware.Cors(ginEngine)
	})
}

Now I will explain the main.go file line by line

  1. We create New instance of Hitrix and pass app name and a secret that is used from our security services

  2. We register some DI services

    2.1. Global DI service for error logger. It will be used for error handler as well in case of panic If you register SlackApi error logger also it will send messages to slack channel

    2.2. Global DI service that loads config file

    2.3. Global DI service that initialize our ORM registry

    2.4. Global DI ORM engine used in background processes

    2.5. Request DI ORM engine used in foreground processes

    2.6. Global DI JWT service used by dev panel

    2.7. Global DI Password service used by dev-panel

  3. We run the server on port 9999, pass graphql resolver and as third param we pass all middlewares we need.
    As you can see in our example we register only Cors middleware

Register Dev Panel

If you want to use our dev panel and to be able to manage alters, error log, redis monitoring, redis stream and so on you should execute next steps:

Create AdminUserEntity
package entity

import (
	"github.com/latolukasz/orm"
)

type AdminUserEntity struct {
	orm.ORM   `orm:"table=admin_users;redisCache"`
	ID        uint64
	Email     string `orm:"unique=Email"`
	Password  string

	UserEmailIndex *orm.CachedQuery `queryOne:":Email = ?"`
}

func (e *AdminUserEntity) GetUsername() string {
	return e.Email
}

func (e *AdminUserEntity) GetPassword() string {
	return e.Password
}

After that you should register it to the entity.Init function

package entity

import "github.com/latolukasz/orm"

func Init(registry *orm.Registry) {
	registry.RegisterEntity(
		&AdminUserEntity{},
	)
}

Please execute this alter into your database


create table admin_users
(
    ID       bigint unsigned auto_increment primary key,
    Email    varchar(255) null,
    Password varchar(255) null,
    constraint Email unique (Email)
) charset = utf8mb4;


After that you can make GET request to http://localhost:9999/dev/create-admin/?username=contact@coretrix.com&password=coretrix This will generate sql query that should be executed into your database to create new user for dev panel

Register dev panel when you make new instance of hitrix framework in your main.go file
s, deferFunc := hitrix.New(
		"app-name", "your secret",
	).RegisterDIService(
		registry.ServiceProviderErrorLogger(), //register redis error logger
		//...
	).
    RegisterDevPanel(&entity.AdminUserEntity{}, middleware.Router, nil). //register our dev-panel and pass the entity where we save admin users, the router and the third param is used for the redis stream pool if its used
    Build()
Defining DI services

We have two types of DI services - Global and Request services Global services are singletons created once for the whole application Request services are singletons created once per request

Calling DI services

If you want to access the registered DI services you can do in in that way:

service.DI().App() //access the app
service.DI().Config() //access config
service.DI().OrmEngine() //access global orm engine
service.DI().OrmEngineForContext() //access reqeust orm engine
service.DI().JWT() //access JWT
service.DI().Password() //access JWT
//...and so on
Register new DI service

func ServiceDefinitionMyService() *ServiceDefinition {
	return &ServiceDefinition{
		Name:   "my_service",
		Global: true,
		Build: func(ctn di.Container) (interface{}, error) {
			return &yourService{}, nil
		},
	}
}

And you have to register ServiceDefinitionMyService() in your main.go file

Now you can access this service in your code using:

import (
    "github.com/coretrix/hitrix"
)

func SomeResolver(ctx context.Context) {

    service.HasService("my_service") // return true
    
    // return error if Build function returned error
    myService, has, err := service.GetServiceSafe("my_service") 
    // will panic if Build function returns error
    myService, has := service.GetServiceOptional("my_service") 
    // will panic if service is not registered or Build function returned errors
    myService := service.GetServiceRequired("my_service") 

    // if you registered service with field "Global" set to false (request service)

    myContextService, has, err := hitrix.GetServiceForRequestSafe(ctx).Get("my_service_request")
    myContextService, has := hitrix.GetServiceForRequestOptional(ctx).Get("my_service_request") 
    myContextService := hitrix.GetServiceForRequestRequired(ctx).Get("my_service_request") 
}

It's a good practice to define one object to return all available services:

package my_package
import (
    "github.com/coretrix/hitrix"
)



func MyService() MyService {
    return service.GetServiceRequired("service_key").(*MyService)
}


Setting mode
APP_MODE environment variable

You can define hitrix mode using special environment variable "APP_MODE".

Hitrix provides by default four modes:

  • hitrix.ModeLocal - local
    • should be used on local development machine (developer laptop)
    • errors and stack trace is printed directly to system console
    • log level is set to Debug level
    • log is formatted using human friendly console text formatter
    • Gin Framework is running in GinDebug mode
  • hitrix.ModeTest - test
    • should be used when you run your application tests
  • hitrix.ModeDemo - demo
    • should be used on your demo server
  • hitrix.ModeProd - prod
    • errors and stack trace is printed only using Log
    • log level is set to Warn level
    • log is formatted using json formatter

Mode is just a string. You can define any name you want. Remember that every mode that you create follows hitrix.ModeProd rules explained above.

In code you can easly check current mode using one of these methods:

service.DI().App().Mode()
service.DI().App().IsInLocalMode()
service.DI().App().IsInProdMode()
service.DI().App().IsInMode("my_mode")
APP_CONFIG_FOLDER environment variable

There are another important environment variable called APP_CONFIG_FOLDER You can set path to your config folder for your demo, prod or any other environment

Environment variables in config file

Its good practice to keep your secrets like database credentials and so on out of the repository. Our advice is to keep them like environment variables and call them into config.yaml file For example your config can looks like this:

orm:
  default:
    mysql: ENV[DEFAULT_MYSQL]
    redis: ENV[DEFAULT_REDIS]
    locker: default
    local_cache: 1000

where DEFAULT_MYSQL and DEFAULT_REDIS are env variables and our framework will automatically replace ENV[DEFAULT_MYSQL] and ENV[DEFAULT_REDIS] with the right values

If you want to enable the debug for orm you can add this tag orm_debug: true on the main level of your config

Also we check if there is .env.XXX file in main config folder where XXX is the value of the APP_MODE. If there is for example .env.local we are reading those env variables and merge them with config.yaml how we presented above

Running scripts

First You need to define script definition that implements hitrix.Script interface:


type TestScript struct {}

func (script *TestScript) Code() string {
    return "test-script"
}

func (script *TestScript) Unique() bool {
    // if true you can't run more than one script at the same time
    return false
}

func (script *TestScript) Description() string {
    return "script description"
}

func (script *TestScript) Run(ctx context.Context, exit hitrix.Exit) {
    // put logic here
	if shouldExitWithCode2 {
        exit.Error()	// you can exit script and specify exit code
    }
}

Methods above are required. Optionally you can also implement these interfaces:


// hitrix.ScriptInterval interface
func (script *TestScript) Interval() time.Duration {                                                    
    // run script every minute
    return time.Minute 
}

// hitrix.ScriptIntervalOptional interface
func (script *TestScript) IntervalActive() bool {                                                    
    // only run first day of month
    return time.Now().Day() == 1
}

// hitrix.ScriptIntermediate interface
func (script *TestScript) IsIntermediate() bool {                                                    
    // script is intermediate, for example is listening for data in chain
    return true
}

// hitrix.ScriptOptional interface
func (script *TestScript) Active() bool {                                                    
    // this script is visible only in local mode
    return DIC().App().IsInLocalMode()
}

Once you defined script you can run it using RunScript method:

package main
import "github.com/coretrix/hitrix"

func main() {
	hitrix.New("app_name", "your secret").Build().RunScript(&TestScript{})
}

You can also register script as dynamic script and run it using program flag:

package main
import "github.com/coretrix/hitrix"

func main() {
	
    hitrix.New("app_name", "your secret").RegisterDIService(
        &registry.ServiceDefinition{
            Name:   "my-script",
            Global: true,
            Script: true, // you need to set true here
            Build: func(ctn di.Container) (interface{}, error) {
                return &TestScript{}, nil
            },
        },
    ).Build()
}

You can see all available script by using special flag -list-scripts:

./app -list-scripts

To run script:

./app -run-script my-script
Built-in services
App

This service contains information about the application like MODE and so on

Config

This service provides you access to your config file. We support only YAML file When you register the service registry.ServiceProviderConfigDirectory("../config") you should provide the folder where are your config files The folder structure should looks like that

config
 - app-name
    - config.yaml
 - hitrix.yaml #optional config where you can define some settings related to built-in services like slack service
ORM Engine

Used to access ORM in background scripts. It is one instance for the whole script

You can register it in that way: registry.ServiceDefinitionOrmEngine()

ORM Engine Context

Used to access ORM in foreground scripts like API. It is one instance per every request

You can register it in that way: registry.ServiceDefinitionOrmEngineForContext()

Error Logger

Used to save unhandled errors in error log. It can be used to save custom errors as well. If you have setup Slack service you also gonna receive notifications in your slack

You can register it in that way: registry.ServiceProviderErrorLogger()

SlackAPI

Gives you ability to send slack messages using slack bot. Also it's used to send messages if you use our ErrorLogger service. The config that needs to be set in hitrix.yaml is:

slack:
    token: "your token"
    error_channel: "test" #optional, used by ErrorLogger
    dev_panel_url: "test" #optional, used by ErrorLogger

You can register it in that way: registry.ServiceDefinitionSlackAPI()

JWT

You can use that service to encode and decode JWT tokens

You can register it in that way: registry.ServiceDefinitionJWT()

Password

This service it can be used to hash and verify hashed passwords. It's use the secret provided when you make new Hitrix instance

You can register it in that way: registry.ServiceDefinitionPassword()

OSS Google

This service is used for storage files into google storage

You can register it in that way: registry.OSSGoogle(map[string]uint64{"my-bucket-name": 1})

You should pass parameter as a map that contains all buckets you need as a key and as a value you should pass id. This id should be unique

In your config folder you should put the .oss.json config file that you have from google Your config file should looks like that:

{
  "type": "...",
  "project_id": "...",
  "private_key_id": "...",
  "private_key": "...",
  "client_email": "...",
  "client_id": "...",
  "auth_uri": "...",
  "token_uri": "...",
  "auth_provider_x509_cert_url": "...",
  "client_x509_cert_url": "..."
}

The last thing you need to set in domain that gonna be used for the static files. You can setup the domain in hitrix.yaml config file like this:

oss: 
  domain: myapp.com

and the url to access your static files will looks like https://static-%s.myapp.com/%s/%s where first %s is app mode

second %s is bucket name concatenated with app mode

and last %s is the id of the file

DDOS Protection

This service contains DDOS protection features

You can register it in that way: registry.ServiceProviderDDOS()

You can protect for example login endpoint from many attempts by using method ProtectManyAttempts

API logger service

This service us used to track every api request and response. You can register it in that way: registry.APILogger(&entity.APILogEntity{}),

The methods that this service provide are:

type APILogger interface {
	LogStart(logType string, request interface{})
	LogError(message string, response interface{})
	LogSuccess(response interface{})
}

You should call LogStart before you send request to the api

You should call LogError in case api return you error

You should call LogSuccess in case api return you success

WebSocket

This service add support of websockets. It manage the connections and provide you easy way to read and write messages

You can register it in that way: registry.ServiceSocketRegistry(registerHandler, unregisterHandler func(s *socket.Socket))

To be able to handle new connections you should create your own route and create a handler for it. Your handler should looks like that:

type WebsocketController struct {
}

func (controller *WebsocketController) InitConnection(c *gin.Context) {
	ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		panic(err)
	}

	socketRegistryService, has := service.DI().SocketRegistry()
	if !has {
		panic("Socket Registry is not registered")
	}

	errorLoggerService, has := service.DI().ErrorLogger()
	if !has {
		panic("Socket Registry is not registered")
	}

	connection := &socket.Connection{Send: make(chan []byte, 256), Ws: ws}
	socketHolder := &socket.Socket{
		ErrorLogger: errorLoggerService,
		Connection:  connection,
		ID:          "unique connection hash based on userID, deviceID and timestamp",
	}

	socketRegistryService.Register <- socketHolder

	go socketHolder.WritePump()
	go socketHolder.ReadPump(socketRegistryService, func(dto *socket.DTOMessage) {
		s, _ := socketRegistryService.Sockets.Load(socketHolder.ID)
		s.(*socket.Socket).Emit(dto)
	})
}

This handler initialize the new comming connections and have 2 go routines - one for writing messages and the second one for reading messages If you want to send message you should use socketRegistryService.Emit

If you want to read comming messages you should do it in the function we are passing as second parameter of ReadPump method

If you want to select certain connection you can do it by the ID and this method s, err := socketRegistryService.Sockets.Load(ID)

Also websocket service provide you two hooks for registering new connections and for unregistering already existing connections. You can define those handlers when you register the service

Clock service

This service is used for time operations. It is better to use it everywhere instead of time.Now() because it can be mocked and you can set whatever time you want in your tests

You can register it in that way: registry.ServiceClock(),

The methods that this service provide are: Now() and NowPointer()

Authentication Service

This service is used to making the life easy by doing the whole authentication life cycle using JWT token. the methods that this service provides are as follows:

func Authenticate(email string, password string, entity EmailPasswordProviderEntity) (accessToken string, refreshToken string, err error) {}
func VerifyAccessToken(accessToken string, entity orm.Entity) error {}
func RefreshToken(refreshToken string) (newAccessToken string, newRefreshToken string, err error) {}
  1. The Authenticate function will take an email, a plain password, and generates accessToken and refreshToken. You will also need to pass your entity as third argument and it will give you the specific user entity related to provided access token The entity should implement the EmailPasswordProviderEntity interface :
       type EmailPasswordProviderEntity interface {
        orm.Entity
        GetEmailCachedIndexName() string
        GetPassword() string
       }
    
    The example of such entity is as follows:
    type AdminUserEntity struct {
        orm.ORM  `orm:"table=admin_users;redisCache;redisSearch=search"`
        ID       uint64
        Email    string `orm:"unique=Email;searchable"`
        Password string
    
        UserEmailIndex *orm.CachedQuery `queryOne:":Email = ?"`
    }
    func (e *AdminUserEntity) GetPassword() string {
        return e.Password
    }
    func (e *AdminUserEntity) GetEmailCachedIndexName() string {
        return "UserEmailIndex"
    }
    
  2. The VerifyAccessToken will get the AccessToken, process the validation and expiration, and fill the entity param with the authenticated user entity in case of successful authentication.
  3. The RefreshToken method will generate a new token pair for given user
  4. You need to have a authentication key in your config file for this service to work. secret key under authentication is mandatory but other options are optional:
authentication:
  secret: "a-deep-dark-secret" #mandatory, secret to be used for JWT
  accessTokenTTL: 86400 # optional, in seconds, default to 1day
  refreshTokenTTL: 31536000 #optional, in seconds, default to 1year
Validator

We support 2 types of validators. One of them is related to graphql and the other one is related to rest

Graphql validator

There are 2 steps that needs to be executed if you want to use this kind of validator

  1. Add directive @validate(rules: String!) on INPUT_FIELD_DEFINITION into your schema.graphqls file

  2. Call ValidateDirective into your main.go file

config := generated.Config{Resolvers: &graph.Resolver{}, Directives: generated.DirectiveRoot{Validate: hitrix.ValidateDirective()} }

s.RunServer(4001, generated.NewExecutableSchema(config), func(ginEngine *gin.Engine) {
    commonMiddleware.Cors(ginEngine)
    middleware.Router(ginEngine)
})

After that you can define the validation rules in that way:

input ApplePurchaseRequest {
  ForceEmail: Boolean!
  Name: String
  Email: String @validate(rules: "email") #for rules param you can use everything supported by https://github.com/go-playground/validator validate.Var(value, rules)
  AppleReceipt: String!
}

To handle the errors you need to call function hitrix.Validate(ctx, nil) in your resolver

func (r *mutationResolver) RegisterTransactions(ctx context.Context, applePurchaseRequest model.ApplePurchaseRequest) (*model.RegisterTransactionsResponse, error) {
    if !hitrix.Validate(ctx, nil) {
        return nil, nil
    }
    // your logic here...
}

The function hitrix.Validate(ctx, nil) as second param accept callback where you can define your custom validation related to business logic

Pre deploy

If you run your binary with argument -pre-deploy the program will check for alters and if there is no alters it will exit with code 0 but if there is an alters it will exit with code 1.

You can use this feature during the deployment process check if you need to execute the alters before you deploy it

Pagination

You can use:

package helper

type URLQueryPager struct {
	// example = ?current_page=1&page_size=25
	CurrentPage int `binding:"min=1" form:"current_page"`
	PageSize    int `binding:"min=1" form:"page_size"`
}

in your code that needs pagination like:

package mypackage

import "github.com/coretrix/hitrix/pkg/helper"

type SomeURLQuery struct {
	helper.URLQueryPager
	OtherField1 string `form:"other_field_1"`
	OtherField2 int `form:"other_field_2"`
}
Tests

Hitrix provide you test helper functions which can be used to make requests to your graphql api

In your code you can create similar function that makes new instance of your app

func createContextMyApp(t *testing.T, projectName string, resolvers graphql.ExecutableSchema) *test.Ctx {
	defaultServices := []*service.Definition{
		registry.ServiceProviderConfigDirectory("../example/config"),
		registry.ServiceDefinitionOrmRegistry(entity.Init),
		registry.ServiceDefinitionOrmEngine(),
	}

	return test.CreateContext(t,
		projectName,
		resolvers,
		func(ginEngine *gin.Engine) { middleware.Router(ginEngine) },
		defaultServices,
	)
}

After that you can call queries or mutations

func TestProcessApplePurchaseWithEmail(t *testing.T) {
	type queryRegisterTransactions struct {
		RegisterTransactionsResponse *model.RegisterTransactionsResponse `graphql:"RegisterTransactions(applePurchaseRequest: $applePurchaseRequest)"`
	}

	variables := map[string]interface{}{
		"applePurchaseRequest": model.ApplePurchaseRequest{
			ForceEmail:   false,
		},
	}

	fakeMail := &mailMock.Sender{}
	fakeMail.On("SendTemplate", "hymn@abv.bg").Return(nil)

	got := &queryRegisterTransactions{}
	projectName, resolver := tests.GetWebAPIResolver()
	ctx := tests.CreateContextWebAPI(t, projectName, resolver, &tests.IoCMocks{MailService: fakeMail})

	err := ctx.HandleMutation(got, variables)
	assert.Nil(t, err)

	//...
	fakeMail.AssertExpectations(t)
}

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func InitGin

func InitGin(server graphql.ExecutableSchema, ginInitHandler GinInitHandler) *gin.Engine

func Validate

func Validate(ctx context.Context, callback func() bool) bool

func ValidateDirective

func ValidateDirective() func(ctx context.Context, obj interface{}, next graphql.Resolver, rules string) (interface{}, error)

Types

type Exit

type Exit interface {
	Valid()
	Error()
	Custom(exitCode int)
}

type GinInitHandler

type GinInitHandler func(ginEngine *gin.Engine)

type Hitrix

type Hitrix struct {
	// contains filtered or unexported fields
}

func (*Hitrix) RunRedisSearchIndexer added in v0.1.5

func (h *Hitrix) RunRedisSearchIndexer() *Hitrix

func (*Hitrix) RunScript

func (h *Hitrix) RunScript(script Script)

func (*Hitrix) RunServer

func (h *Hitrix) RunServer(defaultPort uint, server graphql.ExecutableSchema, ginInitHandler GinInitHandler)

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

func New

func New(appName string, secret string) *Registry

func (*Registry) Build

func (r *Registry) Build() (*Hitrix, func())

func (*Registry) RegisterDIService

func (r *Registry) RegisterDIService(service ...*service.Definition) *Registry

func (*Registry) RegisterDevPanel

func (r *Registry) RegisterDevPanel(devPanelUserEntity app.DevPanelUserEntity, router func(ginEngine *gin.Engine), poolStream *string) *Registry

type Script

type Script interface {
	Description() string
	Run(ctx context.Context, exit Exit)
	Unique() bool
}

type ScriptIntermediate

type ScriptIntermediate interface {
	IsIntermediate() bool
}

type ScriptInterval

type ScriptInterval interface {
	Interval() time.Duration
}

type ScriptIntervalOptional

type ScriptIntervalOptional interface {
	IntervalActive() bool
}

type ScriptOptional

type ScriptOptional interface {
	Active() bool
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL