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 and so on)
Installation
go get -u github.com/coretrix/hitrix
Quick start
- Run next command into your project's main folder and the graph structure will be created
go run github.com/99designs/gqlgen init
- 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
-
We create New instance of Hitrix and pass app name and a secret that is used from our security services
-
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
-
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
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
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(
®istry.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
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
-
Add directive @validate(rules: String!) on INPUT_FIELD_DEFINITION
into your schema.graphqls
file
-
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
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)
}