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
- 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",
).RegisterDIGlobalService(
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.ServiceProviderJWT(), //register JWT DI service
registry.ServiceProviderPassword(), //register pasword DI service
).RegisterDIRequestService(
registry.ServiceDefinitionOrmEngineForContext(), //register our ORM engine per context used in foreground processes
).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/beeorm"
)
type AdminUserEntity struct {
beeorm.ORM `orm:"table=admin_users;redisCache"`
ID uint64
Email string `orm:"unique=Email"`
Password string
UserEmailIndex *beeorm.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/beeorm"
func Init(registry *beeorm.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",
).RegisterDIGlobalService(
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 global service
func ServiceDefinitionMyService() *ServiceDefinition {
return &ServiceDefinition{
Name: "my_service",
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.ScriptInfinity interface
func (script *TestScript) Infinity() bool {
// run script and use blocking operation in cases you run all your code in goroutines
return true
}
// 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() {
h := hitrix.New("app_name", "your secret").Build()
h.RunBackgroundProcess(func(b *hitrix.BackgroundProcessor) {
b.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",
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()
Amazon S3
This service is used for storing files into amazon s3
You can register amazon s3 service this way:
registry.ServiceDefinitionAmazonS3(map[string]uint64{"products": 1}) // 1 is the bucket ID for database counter
and you should register the entity S3BucketCounterEntity
into the ORM
Also, you should put your credentials and other configs in config/hitrix.yml
amazon_s3:
endpoint: "https://somestorage.com" # set to "" if you're using https://s3.amazonaws.com
access_key_id: ENV[S3_ACCESS_KEY_ID]
secret_access_key: ENV[S3_SECRET_ACCESS_KEY_ID]
disable_ssl: false
region: us-east-1
url_prefix: prefix
domain: domain.com
buckets: # Register your buckets here for each app mode
products: # bucket name
prod: bucket-name
local: bucket-name-local
public_urls: # Register your public urls for the GetObjectCachedURL method
product: # bucket name
prod: "https://somesite.com/{{.StorageKey}}/" # Available variables are: .Environment, .BucketName, .CounterID, and, .StorageKey
local: "http://127.0.0.1/{{.Environment}}/{{.BucketName}}/{{.StorageKey}}/{{.CounterID}}" # Will output "http://127.0.0.1/local/product/1.jpeg/1"
Uploader
This service uses TUS protocol to enable fast resumable and multi-part upload of big files.
It provides an easy interface for plug-in whatever data store and locker you want to implement.
Currently, Amazon S3 data store and Redis locker are implemented. For Amazon data store to work,
you need to register Amazon S3 service before this one, also for Redis locker to work, you need
to register orm service background before this one.
You can register Uploader service this way:
registry.ServiceDefinitionUploader(tusd.Config{...}, datastore.GetAmazonS3Store, locker.GetRedisLocker)
Hitrix also provides REST uploader controller which you can register all handler methods in your
router:
var uploaderController *hitrixController.UploaderController
uploaderGroup := ginEngine.Group("/files/")
uploaderGroup.Use(middleware.AuthorizeWithHeaderStrict())
{
uploaderGroup.POST("", uploaderController.PostFileAction)
uploaderGroup.HEAD(":id", uploaderController.HeadFile)
uploaderGroup.PATCH(":id", uploaderController.PatchFile)
uploaderGroup.GET(":id", uploaderController.GetFileAction)
uploaderGroup.DELETE(":id", uploaderController.DeleteFile)
}
Also you need bucket name in config:
uploader:
bucket: media
Stripe
Stripe payment integration
You can register Stripe service this way:
hitrixRegistry.ServiceDefinitionStripe(),
Config sample:
stripe:
key: "api_key"
webhook_secrets: # map of your webhook secrets
checkout: "key"
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})
and you should register the entity OSSBucketCounterEntity
into the ORM
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",
Namespace: model.DefaultNamespace,
}
socketRegistryService.Register <- socketHolder
go socketHolder.WritePump()
go socketHolder.ReadPump(socketRegistryService, func(rawData []byte) {
s, _ := socketRegistryService.Sockets.Load(socketHolder.ID)
dto := &DTOMessage{}
err = json.Unmarshal(rawData, dto)
if err != nil {
errorLoggerService.LogError(err)
retrun
}
//handle business logic here
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 hooks for registering new connections and for unregistering already existing connections.
You can define those handlers when you register the service based on namespace of socket.
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()
Mail Mandrill service
This service is used for sending transactional emails using Mandrill
You can register the service this way:
registry.MailMandrill()
and you should register the entity MailTrackerEntity
into the ORM
Also, you should put your credentials and other configs in your config file
mandrill:
api_key: ...
default_from_email: test@coretrix.tv
from_namme: coretrix.com
Some of the functions this service provide are:
SendTemplate(ormService *beeorm.Engine, message *TemplateMessage) error
SendTemplateAsync(ormService *beeorm.Engine, message *TemplateMessage) error
SendTemplateWithAttachments(ormService *beeorm.Engine, message *TemplateAttachmentMessage) error
SendTemplateWithAttachmentsAsync(ormService *beeorm.Engine, message *TemplateAttachmentMessage) error
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:
dependencies :
JWTService
PasswordService
ClockService
GeneratorService
SMSService
# optional , when you need to support for otp
func Authenticate(ormService *beeorm.Engine, uniqueValue string, password string, entity AuthProviderEntity) (accessToken string, refreshToken string, err error) {}
func VerifyAccessToken(ormService *beeorm.Engine, accessToken string, entity beeorm.Entity) error {}
func RefreshToken(ormService *beeorm.Engine, refreshToken string) (newAccessToken string, newRefreshToken string, err error) {}
func LogoutCurrentSession(ormService *beeorm.Engine, accessKey string){}
func LogoutAllSessions(ormService *beeorm.Engine, id uint64)
func GenerateAndSendOTP(ormService *beeorm.Engine, mobile string, country string){}
func VerifyOTP(ormService *beeorm.Engine, code string, input *GenerateOTP) error{}
func AuthenticateOTP(ormService *beeorm.Engine, phone string, entity OTPProviderEntity) (accessToken string, refreshToken string, err error){}
- The
Authenticate
function will take an uniqueValue such as Email or Mobile, 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 AuthProviderEntity
interface :
type AuthProviderEntity interface {
beeorm.Entity
GetUniqueFieldName() string
GetPassword() string
}
The example of such entity is as follows:
type UserEntity struct {
beeorm.ORM `orm:"table=users;redisCache;redisSearch=search_pool"`
ID uint64 `orm:"searchable;sortable"`
Email string `orm:"required;unique=Email;searchable"`
Password string `orm:"required"`
}
func (user *UserEntity) GetUniqueFieldName() string {
return "Email"
}
func (user *UserEntity) GetPassword() string {
return user.Password
}
- 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.
- The
RefreshToken
method will generate a new token pair for given user
- The
LogoutCurrentSession
you can logout the user current session , you need to pass it the accessKey
that is the jwt identifier jti
the exists in both access and refresh token.
- The
LogoutAllSessions
you can logout the user from all sessions , you need to pass it the id
(user id).
- The
GenerateAndSendOTP
only in otp flow, it will generate code and send it to the specified number Mobile
and also returns GenerateOTP
inside it we have Token
that it is the hashed otp credentials that needs to be sent by client when verifying.
type GenerateOTP struct {
Mobile string
ExpirationTime time.Time
Token string
}
- The
VerifyOTP
only in otp flow , will compare the code
(otp code) with the input
(otp credentials) provided by client.
- The
AuthenticateOTP
only in otp flow , will get the phone
and entity
that should implement OTPProviderEntity
and query to find the user and will login the user.careful just call this after you verified the otp code using the previous method VerifyOTP
the response is asa same as the Authenticate
.
type OTPProviderEntity interface {
beeorm.Entity
GetPhoneFieldName() string
}
- 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:
- The service can also support
OTP
if you want your service to support otp you should have support_otp
key set to true under authentication
- The service also needs redis to store its sessions so you need to identify the redis storage name in config , the key is
auth_redis
under authentication
authentication:
secret: "a-deep-dark-secret" #mandatory, secret to be used for JWT
access_token_ttl: 86400 # optional, in seconds, default to 1day
refresh_token_ttl: 31536000 #optional, in seconds, default to 1year
auth_redis: default #optional , default is the default redis
support_otp: true # if you want to support otp flow in your app
otp_ttl: 120 #optional ,set it when you want to use otp, It is the ttl of otp code , default is 60 seconds
SMS Service
This service is capable of sending simple message and otp message and also calling by different sms providers .
for now we support 3 sms providers : twilio
sinch
kavenegar
dependencies :
ClockService
and also when registering the service you need to pass it the LogEntity
that is responsible to log every action made by sms service :
type LogEntity interface {
beeorm.Entity
SetStatus(string)
SetTo(string)
SetText(string)
SetFromPrimaryGateway(string)
SetFromSecondaryGateway(string)
SetPrimaryGatewayError(string)
SetSecondaryGatewayError(string)
SetType(string)
SetSentAt(time time.Time)
}
for example :
const (
SMSTrackerTypeSMS = "sms"
SMSTrackerTypeCallout = "callout"
)
type smsTrackerTypeAll struct {
SMSTrackerTypeSMS string
SMSTrackerTypeCallout string
}
var SMSTrackerTypeAll = smsTrackerTypeAll{
SMSTrackerTypeSMS: SMSTrackerTypeSMS,
SMSTrackerTypeCallout: SMSTrackerTypeCallout,
}
type SmsTrackerEntity struct {
beeorm.ORM `orm:"table=sms_tracker"`
ID uint64
Status string
To string `orm:"varchar=15"`
Text string
FromPrimaryGateway string
FromSecondaryGateway string
PrimaryGatewayError string
SecondaryGatewayError string
Type string `orm:"enum=entity.SMSTrackerTypeAll;required"`
SentAt time.Time `orm:"time"`
}
we have 2 providers active at the same time primary
secondary
and when send via primary fails we try to send with the secondary provider.
func SendOTPSMS(*OTP) error{}
func SendOTPCallout(*OTP) error{}
func SendMessage(*Message) error{}
- The
SendOTPSMS
send otp sms by providing the otp data
type OTP struct {
OTP string
Number string
CC string
Provider *Provider
Template string
}
- The
SendOTPCallout
used to call and tell the otp code
- The
SendMessage
used to send simple message
type Message struct {
Text string
Number string
Provider *Provider
}
configs
sms:
twilio:
sid: ENV[SMS_TWILIO_SID]
token: ENV[SMS_TWILIO_TOKEN]
from_number: ENV[SMS_TWILIO_FROM_NUMBER]
authy_url: ENV[SMS_TWILIO_AUTHY_URL]
authy_api_key: ENV[SMS_TWILIO_AUTHY_API_KEY]
kavenegar:
api_key: ENV[SMS_KAVENEGAR_API_KEY]
sender: ENV[SMS_KAVENEGAR_SENDER]
sinch:
app_id: ENV[SMS_SINCH_APP_ID]
app_secret: ENV[SMS_SINCH_APP_SECRET]
msg_url: ENV[SMS_SINCH_MSG_URL]
from_number: ENV[SMS_SINCH_FROM_NUMBER]
call_url: ENV[SMS_SINCH_CALL_URL]
caller_number: ENV[SMS_SINCH_CALLER_NUMBER]
CRUD
You can register CRUD service this way:
hitrixRegistry.Crud(),
This service it gives you ability to build a query and apply different query parameters to the query that should be used in listing pages
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
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)
}