Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
This repository was archived by the owner on Apr 19, 2024. It is now read-only.
/kagoPublic archive

KaGo

License

NotificationsYou must be signed in to change notification settings

kamalshkeir/kago

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation



Buy Me A Coffee

KaGo Web Framework (not maintained anymore)

If you like Kago you will love Korm, this package will not be supported soon

Korm or Kmux or Ksbus are more composable, faster, and more powerful :

  • Kmux is faster using combination of safemap and radix tree , also it does not have param routing conflicts, you can have/test/user and/test/:table, if exact route match for the first , the request handled only by the first , otherwise if something like/test/something ->/test/:table
  • Kmux also can generate swagger docs from you code or from comments
  • Ksbus and Korm are perfect combination, allowing you to sync your servers and clients python and javascript all togethers

KaGo is a Django like web framework, that encourages clean and rapid development.

You can start a new project using 2 lines of code, run a high performance server with CRUD Admin Dashboard, interactive shell, easy and performant ORM, AutoSSL and lots more...

List of packages extracted from kago in order to make it more composable :

Quick List of latest features :

  • NEW : HandleMany To Many relationship
  • NEW : orm.AutoMigrate generate query to file instead of executing it directly, you can execute it later using go run main.go shell -> migrate
  • NEW : orm.AddTrigger and orm.DropTrigger,examples
  • NEW :Admin Search and OrderBy
  • NEW :Auto SSL Letsencrypt Certificates and keep them up to date (auto renew 1 month before expire)
  • BareBone Mode Router Only
  • ORM handle coakroachdb in addition to sqlite, postgres, mysql and mariadb (check Performance at the bottom of this readme)
  • Watcher/Auto-Reloader
  • orm.AutoMigrate will handle all your migrations from a struct model, if you remove a field from the migrated struct, you will be prompted to do the migration, it can handle foreign_keys,many to many, checks, indexes,...
  • Fully editableCRUD Admin Dashboard (assets folder)
  • RealtimeLogs at/logs running with flaggo run main.go --logs
  • ConvenientRouter that handle params with regex type checking, Websockets and SSE protocols also
  • Interactive Shellgo run main.go shell
  • Maybe the easiestORM and SQL builder using go generics (working with sqlite, mysql,postgres and coakroach)
  • OpenApidocs at/docs running with flaggo run main.go --docs with dedicated package docs to help you manipulate docs.json file
  • AES encryption for authentication sessions using package encryptor
  • Argon2 hashing using package hash
  • EnvLoader the fastest package envloader to load directly env to struct
  • Internal Eventbus using go routines and channels (very powerful), use cases are endless
  • Monitoring Prometheus/grafana/metrics running with flaggo run main.go --monitoring
  • Profiler golang official debug pprof/debug/pprof/(heap|profile\trace) running with flaggo run main.go --profiler
  • Embed your application static and template files
  • Ready to use Progressive Web App Support (pure js, without workbox)

Join ourdiscussions here


Installation

get the last version
go get -u github.com/kamalshkeir/kago@v1.2.7

Drivers moved outside this package to not get them all in your go.mod file

go get github.com/kamalshkeir/sqlitedrivergo get github.com/kamalshkeir/pgdrivergo get github.com/kamalshkeir/mysqldriver

Quick start

Router + Admin + Database + Shell
package mainimport ("github.com/kamalshkeir/kago""github.com/kamalshkeir/sqlitedriver""github.com/kamalshkeir/mysqldriver""github.com/kamalshkeir/pgdriver")funcmain() {sqlitedriver.Use()// use one of this driversmysqldriver.Use()pgdriver.Use()app:=kago.New()// router + admin + database + shellapp.Run()}

BareBone Router (no assets cloned)

package mainimport ("github.com/kamalshkeir/kago")funcmain() {app:=kago.BareBone()// router only, bot need for sql driversapp.Run()}

1- running 'go run main.go' the first time, will clone assets folder if run with kago.New and will not if run using kago.BareBone

go run main.go

2- create super user

go run main.go shell    -> createsuperuser

3- Run the server:

# run localy:go run main.go# default: -h localhost -p 9313

That's it, you can visit /admin


Run HTTPS letsencrypt in production using env vars and tags

if you have already certificates
go run main.go -h example.com -p 443 --cert cerkey.pem --key privkey.pem
Autocerts (certs generated and renewed automatically) :
go run main.go -h example.com -p 443# this will generate 2 certificates from letsencrypt example.com and www.example.com to directory ./certs
to add more domains, you can use tag 'domains':
go run main.go -h example.com -p 443 -domains a.example.com, b.example.com,...
# most important tags and env vars that you can override:HOST         -h            DEFAULT:"localhost"PORT         -p   DEFAULT:"9313"DOMAINS      -domains   DEFAULT:""CERT      -cert         DEFAULT:""KEY      -key          DEFAULT:""PROFILER     -profiler     DEFAULT:falseDOCS         -docs         DEFAULT:falseLOGS         -logs         DEFAULT:falseMONITORING   -monitoring   DEFAULT:false

Watcher or Auto-reloader

install

go install github.com/kamalshkeir/kago/cmd/katcher

or get the binary from releases

Then you can run:

katcher --root${PWD} (will watch all files at root)katcher --root${PWD} --watch assets/templates,assets/static (will watch only'${PWD}/assets/templates' and'${PWD}/assets/static' folders)

Generated Admin Dashboard

an easy way to create your own theme, is to modify files inside assets , upload the assets folder into a repo and set these 2 values:
settings.REPO_USER="YOUR_REPO_USER" // default kamalshkeirsettings.REPO_NAME="YOUR_REPO_NAME" // default kago-assets
you can easily override any handler of any url by creating a new one with the same method and path.
for example, to override the handler at GET /admin/login:
app.GET("/admin/login",func(c*kamux.Context) {...})// this will add another handler, and remove the old one completely// so it is very safe to do it this way also

Admin + PWA default handlers

// all these handlers can be overridenr.GET("/mon/ping",func(c*kamux.Context) {c.Status(200).Text("pong")})r.GET("/offline",OfflineView)r.GET("/manifest.webmanifest",ManifestView)r.GET("/sw.js",ServiceWorkerView)r.GET("/robots.txt",RobotsTxtView)r.GET("/admin",kamux.Admin(IndexView))r.GET("/admin/login",kamux.Auth(LoginView))r.POST("/admin/login",kamux.Auth(LoginPOSTView))r.GET("/admin/logout",LogoutView)r.POST("/admin/delete/row",kamux.Admin(DeleteRowPost))r.POST("/admin/update/row",kamux.Admin(UpdateRowPost))r.POST("/admin/create/row",kamux.Admin(CreateModelView))r.POST("/admin/drop/table",kamux.Admin(DropTablePost))r.GET("/admin/table/model:str",kamux.Admin(AllModelsGet))r.POST("/admin/table/model:str",kamux.Admin(AllModelsPost))r.GET("/admin/get/model:str/id:int",kamux.Admin(SingleModelGet))r.GET("/admin/export/table:str",kamux.Admin(ExportView))r.POST("/admin/import",kamux.Admin(ImportView))r.GET("/logs",kamux.Admin(LogsGetView))r.SSE("/sse/logs",kamux.Admin(LogsSSEView))// Example : how to override a handler// add it before kago.New()admin.LoginView=func(c*kamux.Context) {...}...// Example : how to override a handler middlewarekamux.Auth=func(handler kamux.Handler) kamux.Handler {// handlerFunc...}// Example : how to override a Global middlewarekamux.GZIP=func(handler http.Handler) http.Handler {// Handler...}

Admin Search


Routing

Using GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, HandlerFunc

package mainimport ("github.com/kamalshkeir/kago""github.com/kamalshkeir/kago/core/kamux")funcmain() {// Creates a KaGo routerapp:=kago.New()// You can add GLOBAL middlewares easily  (GZIP,CSRF,LIMITER,RECOVERY)app.UseMiddlewares(kamux.GZIP)// this is an adapter for go std http handlerfunc, example of getting param param1 of type int (regex validated)app.HandlerFunc("*","/test/param1:int/*",func(w http.ResponseWriter,r*http.Request) {params,ok:=kamux.ParamsHandleFunc(r)ifok {...}w.Write([]byte("ok"))})app.HandlerFunc("GET","/",func(w http.ResponseWriter,r*http.Request) {// using net/http handlerFuncw.Write([]byte("hello world"))})// handle any method kamux Handlerapp.Handle("*","/any/:test",func(c*kamux.Context) {logger.Success(c.Params["test"])c.Json(kamux.M{"message":"message here",})})app.Handle("GET","/any2/:test",func(c*kamux.Context) {logger.Success(c.Params)c.Json(kamux.M{"message":"message here",})})// handle GET /any/hello and /any/worldapp.GET("/any/(hello|world)",func(c*kamux.Context) {c.Text("hello world")})// middlewares for single handler (Auth,Admin,BasicAuth)// Auth check if user is authenticated and pass to c.Html '.User' and '.Request' accessible in all templatesapp.GET("/",kamux.Auth(IndexHandler))app.POST("/somePost",posting)app.PUT("/somePut",putting)app.PATCH("/somePatch",patching)app.DELETE("/someDelete",deleting)app.HEAD("/someDelete",head)app.OPTIONS("/someDelete",options)app.Run()}varIndexHandler=func(c*kamux.Context) {ifparam1,ok:=c.Params["param1"];ok {c.Json(kamux.M{// Status default to 200, so no need to add it"param1":param1,})// send json}else {c.Status(400).Json(kamux.M{"error":"message",})// send json with status code 400}}

Websockets + Server Sent Events

After this one, you will stop being afraid to try websockets everywhere

funcmain() {app:=kago.New()// no need to upgrade the request , all you need to worry about is// inside this handler, you can enjoy realtime communicationapp.WS("/ws/test",func(c*kamux.WsContext) {rand:=utils.GenerateRandomString(5)c.AddClient(rand)// add connection to broadcast list// listen for messages coming from 1 userfor {// receive JsonmapStringAny,err:=c.ReceiveJson()// receive Textstr,err:=c.ReceiveText()iferr!=nil {// on error you can remove client from broadcastList and break the loopc.RemoveRequester(rand)break}// send Json to current usererr=c.Json(kamux.M{"Hello":"World",})// send Text to current usererr=c.Text("any data string")// broadcast to all connected usersc.Broadcast(kamux.M{"you can send":"struct insetead of maps here",})// broadcast to all connected users except current user, the one who send the last messagec.BroadcastExceptCaller(map[string]any{"you can send":"struct insetead of maps here",})}})app.Run()}

Server Sent Events

funcmain() {app:=kago.New()// will be hitted every 1-2 sec, you can check anything if change and send data on the fly using c.StreamResponseapp.SSE("/sse/logs",func(c*kamux.Context) {lenStream:=len(logger.StreamLogs)iflenStream>0 {lastOne:=lenStream-1err:=c.StreamResponse(logger.StreamLogs[lastOne])iferr==nil{logger.StreamLogs=[]string{}}}})app.Run()}

Parameters (path + query)

funcmain() {app:=kago.New()// Query string parameters are parsed using the existing underlying request object// request url : /?page=3app.GET("/",func(c*kamux.Context) {page:=c.QueryParam("page")ifpage!="" {c.Json(kamux.M{"page":page,})}else {c.Status(404)}})// This handler will match /anyString but will not match /// accepted param Type: string,slug,int,float and validated on the go, before it hit the handlerapp.POST("/param1:slug",func(c*kamux.Context) {ifparam1,ok:=c.Params["param1"];ok {c.Json(kamux.M{"param1":param1,})}else {c.Status(404).Text("Not Found")}})// OR// param1 can be ascii, no symboleapp.PATCH("/test/:param1",func(c*kamux.Context) {ifparam1,ok:=c.Params["param1"];ok {c.Json(kamux.M{"param1":param1,})}else {c.Status(404).Text("Not Found")}})// param1 can be ascii, no symbole -> /test/anything will workapp.POST("/test/param1:str",func(c*kamux.Context)//  /test/anything will not work, should be int, validated with regex -> /100 will workapp.POST("/test/param1:int",func(c*kamux.Context)//  2 digit float -> /test/3.45 will workapp.POST("/test/param1:float",func(c*kamux.Context)// slug param should match abcd-efgh, no spacingapp.POST("/test/param1:slug",func(c*kamux.Context) app.Run()}

Context Http

There is also WsContext seen above
funcmain() {app:=kago.New()// Query string parameters are parsed using the existing underlying request object// request url : /?page=3app.GET("/",func(c*kamux.Context) {page:=c.QueryParam("page")ifpage!="" {c.Status(200).Json(map[string]any{"page":page,})}else {c.SetStatus(404)}})// This handler will match /anyString but will not match /// accepted param Type: string,slug,int,float and validated on the go, before it hit the handlerapp.POST("/param1:slug",func(c*kamux.Context) {ifparam1,ok:=c.Params["param1"];ok {c.Json(map[string]any{"param1":param1,})}else {c.Status(404).Text("Not Found")}})// and many morec.AddHeader(key,valuestring)// append a header if key existc.SetHeader(key,valuestring)// replace a header if key existc.writeHeader(statusCodeint)// set status code like c.writeHeader(statusCode int)c.SetStatus(statusCodeint)// set status code like c.writeHeader(statusCode int)c.IsAuthenticated()bool// return true if valid user authenticatedc.User()models.User// get User from request if middlewares.Auth or middlewares.Admin usedc.Status(200).Json(bodyany)c.Status(200).JsonIndent(bodyany)c.Status(200).Html(template_namestring,datamap[string]any)c.Status(301).Redirect(pathstring)// redirect to pathc.BodyJson()map[string]any// get request body as mapc.BodyText()string// get request body as stringc.StreamResponse(responsestring)error//SSEc.ServeFile("application/json; charset=utf-8","./test.json")c.ServeEmbededFile(content_typestring,embed_file []byte)c.ParseMultipartForm(size...int64) (formDataurl.Values,formFilesmap[string][]*multipart.FileHeader)// return form data and form filesc.UploadFile(received_filename,folder_outstring,acceptedFormats...string) (string,[]byte,error)// UploadFile upload received_filename into folder_out and return url,fileByte,errorc.UploadFiles(received_filenames []string,folder_outstring,acceptedFormats...string) ([]string,[][]byte,error)// UploadFilse handle also if it's the same name but multiple files or multiple names multiple filesc.DeleteFile(pathstring)errorc.Download(data_bytes []byte,asFilenamestring)c.EnableTranslations()// EnableTranslations get user ip, then location country using nmap, so don't use it if u don't have it install, and then it parse csv file to find the language spoken in this country, to finaly set cookie 'lang' to 'en' or 'fr'...c.GetUserIP()string// get user ipapp.Run()}

Multipart/Urlencoded Form

funcmain() {app:=kago.New()app.POST("/ajax",func(c*kamux.Context) {// get json body to map[string]anyrequestData:=c.BodyJson()ifemail,ok:=requestData["email"];ok {...        }iferr!=nil {c.Status(400).Json(kamux.M{"error":"User doesn not Exist",    })        }})app.Run()}

Upload file

funcmain() {app:=kago.New()// kamux.MultipartSize = 10<<20 memory limit set to 10Mbapp.POST("/upload",func(c*kamux.Context) {// UploadFileFromFormData upload received_filename into folder_out and return url,fileByte,error//c.UploadFile(received_filename,folder_out string, acceptedFormats ...string) (string,[]byte,error)pathToFile,dataBytes,err:=c.UploadFile("filename_from_form","images","png","jpg")// you can save pathToFile in db from herec.Status(200).Text(file.Filename+" uploaded")})app.Run()}

Cookies

funcmain() {app:=kago.New()kamux.COOKIE_EXPIRE=time.Now().Add(7*24*time.Hour)app.POST("/ajax",func(c*kamux.Context) {// get json body to map[string]anyrequestData:=c.BodyJson()ifemail,ok:=requestData["email"];ok {...        }// set cookiec.SetCookie(key,valuestring)c.GetCookie(keystring)c.DeleteCookie(keystring)})app.Run()}

HTML functions maps

// To add one, automatically loaded into your htmlapp.NewFuncMap(funcNamestring,functionany)/* FUNC MAPS */varfunctions= template.FuncMap{"contains":func(strstring,substrings...string)bool"startWith":func(strstring,substrings...string)bool"finishWith":func(strstring,substrings...string)bool"generateUUID":func() template.HTML"add":func(aint,bint)int"safe":func(strstring) template.HTML"timeFormat":func (tany)string"truncate":func(strany,sizeint)any"csrf_token":func (r*http.Request) template.HTML// generate input with id csrf_token"date":func(tany)string// dd Month yyyy"slug":func(strstring)string"translateFromLang":func (translation,languagestring)any"translateFromRequest":func (translationstring,request*http.Request)any }

Add Custom Static And Templates Folders

you can build all your static and templates files into the binary by simply embeding folder using app.Embed
app.Embed(staticDir*embed.FS,templateDir*embed.FS)app.ServeLocalDir(dirPath,webPathstring)app.ServeEmbededDir(pathLocalDirstring,embededembed.FS,webPathstring)app.AddLocalTemplates(pathToDirstring)errorapp.AddEmbededTemplates(template_embedembed.FS,rootDirstring)error// examples:// initial static and templates//settings.STATIC_DIR="static" --> to serve at /static (optional)//settings.TEMPLATE_DIR="templates" --> templates inside this folder are used with c.Htmlapp.GET("/test/id:int",func(c*kamux.Context) {id,ok:=c.Params["id"]if!ok {...return}c.Html("index.html",kamux.M{"id":id,})})// you can add static and templates foldersapp.ServeLocalDir("/path/to/static","static")// serve local /path/to/static folder at /static/*app.AddLocalTemplates("/path/to/templates1")// templates inside this folder are used with c.Htmlapp.AddLocalTemplates("/path/to/templates2")// you can serve also add embeded templates and static foldersrouter.ServeEmbededDir("/path/to/static",Static,"static")// serve embeded assets/static folder at endpoint /static/*router.AddEmbededTemplates(Templates,"/path/to/templates")

Middlewares

Global server middlewares

funcmain() {app:=New()app.UseMiddlewares(kamux.GZIP,kamux.CSRF,kamux.LIMITER,kamux.RECOVERY,)// GZIP nothing to do , just add it// LOGS /!\no need to add it using app.UseMiddlewares, instead you have a flag --logs that enable /logs// when logs middleware used, you will have a colored log for requests and also all logs from logger library displayed in the terminal and at /logs enabled for admin only// add the middleware like above and enjoy SSE logs in your browser not persisting if you ask// LIMITER// if enabled , requester are blocked 5 minutes if make more then 50 request/s , you can change these values:kamux.LIMITER_TOKENS=50kamux.LIMITER_TIMEOUT=5*time.Minute// RECOVERY// will recover any error and log it, you can see it in console and also at /logs if LOGS middleware enabled// CORS// this is how to use CORS, it's applied globaly , but defined by the handler, all methods except GET of courseapp.AllowOrigines(origines...string)// allow origines global, can be "*" to allow allapp.POST(patternstring,handlerkamux.Handler,allowed_origines...string)app.POST("/users/post",func(c*kamux.Context) {// allow origine for domain.com and domain2.com and same origin},"domain.com","domain2.com")// CSRF// CSRF middleware get csrf_token from header, middleware will put it in cookies// this is a helper javascript function you can use to get cookie csrf and the set it globaly for all your requestfunctiongetCookie(name) {varcookieValue=null;if (document.cookie&&document.cookie!= '') {varcookies=document.cookie.split(';');for (vari=0;i< cookies.length;i++) {varcookie=cookies[i].trim();// Does this cookie string begin with the name we want?if (cookie.substring(0,name.length+1)== (name+'=')) {cookieValue=decodeURIComponent(cookie.substring(name.length+1));break;}}}returncookieValue;}letcsrftoken=getCookie("csrf_token");// or a you have also a template function called csrf_token// you can use it in templates to render a hidden input of csrf_token<formid="form1">{{csrf_token .request }}</form>// you can get it from the input from js and send it in headers// middleware csrf will try to find this header name:COOKIE NAME: 'csrf_token'HEADER NAME: 'X-CSRF-Token'app.Run()}

Handler middlewares

// USAGE:r.GET("/admin",kamux.Admin(IndexView))// will check from session cookie if user.is_admin is truer.GET("/admin/login",kamux.Auth(LoginView))// will get session from cookies decrypt it and validate itr.GET("/test",kamux.BasicAuth(LoginView,"username","password"))

ORM

i waited go1.18 and generics to make this package orm to keep performance at it's best with convenient way to query your data, even from multiple databases

queries are cached using powerfull eventbus style that empty cache when changes in database may corrupt your data, so use it until you have a problem with it

to disable it :
orm.UseCache=false

Let's start by ways to add new database:

orm.NewDatabaseFromDSN(dbType,dbNamestring,dbDSN...string) (error)orm.NewDatabaseFromConnection(dbType,dbNamestring,conn*sql.DB) (error)orm.GetConnection(dbName...string)// GetConnection return default connection for orm.DefaultDatabase (if dbName not given or "" or "default") else it return the specified oneorm.UseForAdmin(dbNamestring)// UseForAdmin use specific database in admin panel if manymatched dborm.ManyToMany(table1,table2string,dbName...string)error// create m2m_table1_table2 table relations

Utility database queries:

orm.GetAllTables(dbName...string) []string// if dbName not given, .env used instead to default the db to get all tables in the given dborm.GetAllColumnsTypes(tablestring,dbName...string)map[string]string// clear i thinkorm.CreateUser(email,passwordstring,isAdminint,dbName...string)error// password will be hashed using argon2

Migrations

using the shell, you can migrate a .sql filego run main.go shell

OR

you can migrate from a struct usingAuto Migrate

when kago app executed, all models registered using AutoMigrate will be synchronized with the database so if you add a field to you struct or add a column to your table, you will have a prompt proposing migration

execute AutoMigrate and don't think about it, it will handle all synchronisations between your project structs types like in the example Bookmark below

If you need to change a tag, remove the field, restart, put the new one with the new tag, restart again, that's it


Available Tags by struct field type andExamples :


#String Field:

Without parameter                With parameter                           
*  text (create column as TEXT not VARCHAR)*  notnull*  unique*   iunique // insensitive unique*  index, +index, index+ (INDEX ascending)*  index-, -index (INDEX descending)*  default (DEFAULT '')
* default:'any' (DEFAULT 'any')*mindex:...* uindex:username,Iemail // CREATE UNIQUE INDEX ON users (username,LOWER(email)) // email is lower because of 'I' meaning Insensitive for email* fk:...* size:50  (VARCHAR(50))* check:...

Int, Uint, Int64, Uint64 Fields:

Without parameter                
*   -   (To Ignore a field)*   autoinc, pk  (PRIMARY KEY)*   notnull      (NOT NULL)*  index, +index, index+ (CREATE INDEX ON COLUMN)*  index-, -index(CREATE INDEX DESC ON COLUMN)     *   unique  (CREATE UNIQUE INDEX ON COLUMN) *   default (DEFAULT 0)
With parameter                           
Available 'on_delete' and 'on_update' options: cascade,(donothing,noaction),(setnull,null),(setdefault,default)*   fk:{table}.{column}:{on_delete}:{on_update} *   check: len(to_check) > 10 ; check: is_used=true (You can chain checks or keep it in the same CHECK separated by AND)*   mindex: first_name, last_name (CREATE MULTI INDEX ON COLUMN + first_name + last_name)*   uindex: first_name, last_name (CREATE MULTI UNIQUE INDEX ON COLUMN + first_name + last_name) *   default:5 (DEFAULT 5)

Bool : bool is INTEGER NOT NULL checked between 0 and 1 (in order to be consistent accross sql dialects)

Without parameter                With parameter                           
*  index, +index, index+ (CREATE INDEX ON COLUMN)*  index-, -index(CREATE INDEX DESC ON COLUMN)  *   default (DEFAULT 0)
*   default:1 (DEFAULT 1)*   mindex:...*   fk:...

time.Time :

Without parameter                With parameter
*  index, +index, index+ (CREATE INDEX ON COLUMN)*  index-, -index(CREATE INDEX DESC ON COLUMN)  *   now (NOT NULL and defaulted to current timestamp)*   update (NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)
*   fk:...*   check:...

Float64 :

Without parameter                With parameter                           
*   notnull*  index, +index, index+ (CREATE INDEX ON COLUMN)*  index-, -index(CREATE INDEX DESC ON COLUMN)  *   unique*   default
*   default:...*   fk:...*   mindex:...*   uindex:...*   check:...

AutoMigrate Usage

orm.AutoMigrate[Tcomparable](tableNamestring,dbName...string)error//Examples:// this is the actual user model used initialytypeUserstruct {Idint`json:"id,omitempty" orm:"pk"`Uuidstring`json:"uuid,omitempty" orm:"size:40;iunique"`Emailstring`json:"email,omitempty" orm:"size:50;iunique"`Passwordstring`json:"password,omitempty" orm:"size:150"`IsAdminbool`json:"is_admin,omitempty" orm:"default:false"`Imagestring`json:"image,omitempty" orm:"size:100;default:''"`CreatedAt time.Time`json:"created_at,omitempty" orm:"now"`}typeBookmarkstruct {Iduint`orm:"pk"`UserIdint`orm:"fk:users.id:cascade:setnull"`// options cascade,(donothing,noaction),(setnull,null),(setdefault,default)IsDoneboolToCheckstring`orm:"size:50; notnull; check: len(to_check) > 2 AND len(to_check) < 10; check: is_done=true"`// column type will be VARCHAR(50)Contentstring`orm:"text"`// column type will be TEXT, and will have Rich Text Editor in admin panelUpdatedAt time.Time`orm:"update"`// will update when model updated, handled by triggers for sqlite, coakroach and postgres, and builtin mysqlCreatedAt time.Time`orm:"now"`// now is default to current timestamp and of type TEXT for sqlite}// TO DEBUG , or to see queries executed on migrations, set orm.DEBUG=true// To migrate/connect/sync with database:err:= orm.AutoMigrate[Bookmark]("bookmarks")// after this command if you remove a field from struct model, you will have a prompt wit suggestions to resolve the problem// will produce:CREATETABLEIFNOTEXISTSbookmarks (idINTEGERNOTNULLPRIMARYKEYAUTOINCREMENT,user_idINTEGER,is_usedINTEGERNOTNULLCHECK (is_usedIN (0,1)  ),to_checkVARCHAR(50)UNIQUENOTNULLCHECK (length(to_check)>10  ),contentTEXT,created_atTEXTNOTNULLDEFAULTCURRENT_TIMESTAMP,FOREIGNKEY (user_id)REFERENCESusers(id)ONDELETECASCADE);
And in the admin panel, you can see that 'bookmarks' table created, with a rich text editor for the column 'content'
this is because all columns with field type TEXT will be rendered as a rich text editor automatically

ORM Triggers

funcAddTrigger(onTable,col,bf_af_UpdateInsertDeletestring,ofColumn,stmtstring,forEachRowbool,whenEachRowstring,dbName...string)funcDropTrigger(onField,tableNamestring)//example:orm.AddTrigger("users","email","AFTER UPDATE","email","UPDATE users SET uuid='test' where users.id=NEW.id",false,"")// this trigger will update uuid to 'test' whenever email is updated

Queries and Sql Builder (handle multiple database)

to query, insert, update and delete using structs:

orm.Model[Tcomparable]()*Builder[T]// starterorm.BuilderS[Tcomparable]()*Builder[T]// starter(b*Builder[T]).Database(dbNamestring)*Builder[T](b*Builder[T]).Insert(model*T) (int,error)(b*Builder[T]).Set(querystring,args...any) (int,error)(b*Builder[T]).Delete() (int,error)// finisher(b*Builder[T]).Drop() (int,error)// finisher(b*Builder[T]).Select(columns...string)*Builder[T](b*Builder[T]).Where(querystring,args...any)*Builder[T](b*Builder[T]).Query(querystring,args...any)*Builder[T](b*Builder[T]).Limit(limitint)*Builder[T](b*Builder[T]).Context(ctxcontext.Context)*Builder[T](b*Builder[T]).Page(pageNumberint)*Builder[T](b*Builder[T]).OrderBy(fields...string)*Builder[T](b*Builder[T]).Debug()*Builder[T]// print the query statement(b*Builder[T]).All() ([]T,error)// finisher(b*Builder[T]).One() (T,error)// finisher(b*Builder[T])AddRelated(relatedTablestring,whereRelatedTablestring,whereRelatedArgs...any) (int,error)//MANY TO MANY use it after orm.ManyToMany(...)(b*Builder[T])DeleteRelated(relatedTablestring,whereRelatedTablestring,whereRelatedArgs...any) (int,error)//MANY TO MANY use it after orm.ManyToMany(...)(b*Builder[T])GetRelated(relatedTablestring,destany)error//MANY TO MANY use it after orm.ManyToMany(...)(b*Builder[T])JoinRelated(relatedTablestring,destany)error//MANY TO MANY use it after orm.ManyToMany(...)

Examples

// then you can query your data as models.User data// you have 2 finisher : All and One// Select and Paginationorm.Model[models.User]().Select("email","uuid").OrderBy("-id").Limit(PAGINATION_PER).Page(1).All()// INSERTuuid,_:=orm.GenerateUUID()hashedPass,_:=hash.GenerateHash("password")orm.Model[models.User]().Insert(&models.User{Uuid:uuid,Email:"test@example.com",Password:hashedPass,IsAdmin:false,Image:"",CreatedAt:time.Now(),})//if using more than one dborm.Database[models.User]("dbNameHere").Where("id = ? AND email = ?",1,"test@example.com").All()// whereorm.Model[models.User]().Where("id = ? AND email = ?",1,"test@example.com").One()// deleteorm.Model[models.User]().Where("id = ? AND email = ?",1,"test@example.com").Delete()// drop tableorm.Model[models.User]().Drop()// updateorm.Model[models.User]().Where("id = ?",1).Set("email = ?","new@example.com")

to query, insert, update and delete using map[string]any:

orm.Exec(dbName,querystring,args...any)errororm.Query(dbName,querystring,args...)  ([]map[string]any,error)orm.Table(tableNamestring)*BuilderM// starterorm.BuilderMap(tableNamestring)*BuilderM// starter(b*BuilderM).Database(dbNamestring)*BuilderM(b*BuilderM).Select(columns...string)*BuilderM(b*BuilderM).Where(querystring,args...any)*BuilderM(b*BuilderM).Query(querystring,args...any)*BuilderM(b*BuilderM).Limit(limitint)*BuilderM(b*BuilderM).Page(pageNumberint)*BuilderM(b*BuilderM).OrderBy(fields...string)*BuilderM(b*BuilderM).Context(ctxcontext.Context)*BuilderM(b*BuilderM).Debug()*BuilderM(b*BuilderM).All() ([]map[string]any,error)(b*BuilderM).One() (map[string]any,error)(b*BuilderM).Insert(fields_comma_separatedstring,fields_values []any) (int,error)(b*BuilderM).Set(querystring,args...any) (int,error)(b*BuilderM).Delete() (int,error)(b*BuilderM).Drop() (int,error)(b*BuilderM)AddRelated(relatedTablestring,whereRelatedTablestring,whereRelatedArgs...any) (int,error)//MANY TO MANY use it after orm.ManyToMany(...)(b*BuilderM)GetRelated(relatedTablestring,dest*[]map[string]any)error//MANY TO MANY use it after orm.ManyToMany(...)(b*BuilderM)JoinRelated(relatedTablestring,dest*[]map[string]any)error//MANY TO MANY use it after orm.ManyToMany(...)(b*BuilderM)DeleteRelated(relatedTablestring,whereRelatedTablestring,whereRelatedArgs...any) (int,error)//MANY TO MANY use it after orm.ManyToMany(...)

Examples

// for using maps , no need to link any model of course// then you can query your data as models.User data// you have 2 finisher : All and One for queries// Select and Paginationorm.Table("users").Select("email","uuid").OrderBy("-id").Limit(PAGINATION_PER).Page(1).All()// INSERTuuid,_:=orm.GenerateUUID()hashedPass,_:=hash.GenerateHash("password")orm.Table("users").Insert("uuid,email,password,is_admin,created_at",uuid,"email@example.com",hashedPass,false,time.Now())//if using more than one dborm.Database("dbNameHere").Where("id = ? AND email = ?",1,"test@example.com").All()// whereorm.Table("users").Where("id = ? AND email = ?",1,"test@example.com").One()// deleteorm.Table("users").Where("id = ? AND email = ?",1,"test@example.com").Delete()// drop tableorm.Table("users").Drop()// updateorm.Table("users").Where("id = ?",1).Set("email = ?","new@example.com")

SHELL

Very useful shell to explore, no need to install extra dependecies or binary, you can run:
go run main.go shellgo run main.gohelp
AVAILABLE COMMANDS:[databases, use, tables, columns, migrate, createsuperuser, createuser, getall, get, drop, delete, clear/cls, q/quit/exit, help/commands]'databases':  list all connected databases'use':  use a specific database'tables':  list all tablesin database'columns':  list all columns of a table'migrate':  migrate initial users to env database'createsuperuser':  create a admin user'createuser':  create a regular user'getall':  get all rows given a table name'get':  get single row wher field equal_to'delete':  delete rows where field equal_to'drop':  drop a table given table name'clear/cls':  clear console

Env Loader

this minimalistic package is one of my favorite, you basicaly need to Load env variables from file and to fill a struct Config with these values

First you may need to env vars from file and set them using : envloader.Load(...files)

here is how tediously i was loading env variables:

Before:

//func (router*Router)LoadEnv(files...string) {m,err:=envloader.LoadToMap(files...)iferr!=nil {return}fork,v:=rangem {switchk {case"SECRET":settings.Secret=vcase"EMBED_STATIC":ifb,err:=strconv.ParseBool(v);!logger.CheckError(err) {settings.Config.EmbedStatic=b}case"EMBED_TEMPLATES":ifb,err:=strconv.ParseBool(v);!logger.CheckError(err) {settings.Config.EmbedTemplates=b}case"DB_TYPE":ifv=="" {v="sqlite"}settings.Config.Db.Type=vcase"DB_DSN":ifv=="" {v="db.sqlite"}settings.Config.DbDSN=vcase"DB_NAME":ifv=="" {logger.Error("DB_NAME from env file cannot be empty")os.Exit(1)}settings.Config.DbName=vcase"SMTP_EMAIL":settings.Config.SmtpEmail=vcase"SMTP_PASS":settings.Config.SmtpPass=vcase"SMTP_HOST":settings.Config.SmtpHost=vcase"SMTP_PORT":settings.Config.SmtpPort=v}}}

After:

envloader.Load(".env")// load env files and add to env vars// the command:err:=envloader.FillStruct(&Config)// fill struct with env vars

Struct to fill

typeGlobalConfigstruct {Hoststring`env:"HOST|localhost"`// DEFAULTED: if HOST not found default to localhostPortstring`env:"PORT|9313"`Embedstruct {Staticbool`env:"EMBED_STATIC|false"`Templatesbool`env:"EMBED_TEMPLATES|false"`}Dbstruct {Namestring`env:"DB_NAME|db"`Typestring`env:"DB_TYPE"`// REEQUIRED: this env var is required, you will have error if emptyDSNstring`env:"DB_DSN|"`// NOT REQUIRED: if DB_DSN not found it's not required, it's ok to stay empty}Smtpstruct {Emailstring`env:"SMTP_EMAIL|"`Passstring`env:"SMTP_PASS|"`Hoststring`env:"SMTP_HOST|"`Portstring`env:"SMTP_PORT|"`}Profilerbool`env:"PROFILER|false"`Docsbool`env:"DOCS|false"`Logsbool`env:"LOGS|false"`Monitoringbool`env:"MONITORING|false"`}

OpenApi documentation ready if enabled using flags or settings vars

// running the app using flag --docsgo run main.go --docs// go to /docs

to edit the documentation, you have a docs package

doc:=docs.New()doc.AddModel("User",docs.Model{Type:"object",RequiredFields: []string{"email","password"},Properties:map[string]docs.Property{"email":{Required:true,Type:"string",Example:"example@xyz.com",},"password":{Required:true,Type:"string",Example:"************",Format:"password",},},})doc.AddPath("/admin/login","post",docs.Path{Tags: []string{"Auth"},Summary:"login post request",OperationId:"login-post",Description:"Login Post Request",Requestbody: docs.RequestBody{Description:"email and password for login",Required:true,Content:map[string]docs.ContentType{"application/json":{Schema: docs.Schema{Ref:"#/components/schemas/User",},},},},Responses:map[string]docs.Response{"404":{Description:"NOT FOUND"},"403":{Description:"WRONG PASSWORD"},"500":{Description:"INTERNAL SERVER ERROR"},"200":{Description:"OK"},},Consumes: []string{"application/json"},Produces: []string{"application/json"},})doc.Save()

Encryption

// AES Encrypt use SECRET in .envencryptor.Encrypt(datastring) (string,error)encryptor.Decrypt(datastring) (string,error)

Hashing

// Argon2 hashinghash.GenerateHash(passwordstring) (string,error)hash.ComparePasswordToHash(password,hashstring) (bool,error)

Eventbus Internal handle any data using generics, just make sure you are using same type for the same topic

eventbus.Subscribe("any_topic",func(datamap[string]any) {...})eventbus.Subscribe("any_topic222",func(dataint) {...})eventbus.Publish("any_topic",map[string]any{"type":"update","table":b.tableName,"database":b.database,})eventbus.Publish("any_topic222",222)

LOGS

go run main.go --logswill enable:- /logs

PPROF official golang profiling tools

go run main.go --profilerwill enable:- /debug/pprof- /debug/pprof/profile- /debug/pprof/heap- /debug/pprof/trace

Grafana with Prometheus monitoring

Enable /metrics for prometheus

go run main.go --monitoring

Create file 'prometheus.yml' anywhere

scrape_configs:-job_name:api-serverscrape_interval:5sstatic_configs:  -targets:['host.docker.internal:9313']#replace host.docker.internal per localhost if you run it localy

Prometheus

docker run -d --name prometheus -v {path_to_prometheus.yml}:/etc/prometheus/prometheus.yml -p 9090:9090 prom/prometheus

Grafana

docker run -d --name grafana -p 3000:3000 grafana/grafana-enterprise
dockerexec -it grafana grafana-cli admin reset-admin-password newpass
Visithttp://localhost:3000 username: admin

That's it, you can import your favorite dashboard:

  • 10826
  • 240

Build single binary with all static and html files

//go:embed assets/staticvarStatic embed.FS//go:embed assets/templatesvarTemplates embed.FSfuncmain() {app:=New()app.UseMiddlewares(middlewares.GZIP)app.Embed(&Static,&Templates)app.Run()}

Then

go build

Some Benchmarks

////////////////////////////////////// postgres without cacheBenchmarkGetAllS-4                  1472            723428 ns/op            5271 B/op         80 allocs/opBenchmarkGetAllM-4                  1502            716418 ns/op            4912 B/op         85 allocs/opBenchmarkGetRowS-4                   826           1474674 ns/op            2288 B/op         44 allocs/opBenchmarkGetRowM-4                   848           1392919 ns/op            2216 B/op         44 allocs/opBenchmarkGetAllTables-4             1176            940142 ns/op             592 B/op         20 allocs/opBenchmarkGetAllColumnsTypes-4             417           2862546 ns/op            1456 B/op         46 allocs/op////////////////////////////////////// postgres with cacheBenchmarkGetAllS-4               2825896               427.9 ns/op           208 B/op          2 allocs/opBenchmarkGetAllM-4               6209617               188.9 ns/op            16 B/op          1 allocs/opBenchmarkGetRowS-4               2191544               528.1 ns/op           240 B/op          4 allocs/opBenchmarkGetRowM-4               3799377               305.5 ns/op            48 B/op          3 allocs/opBenchmarkGetAllTables-4         76298504                21.41 ns/op            0 B/op          0 allocs/opBenchmarkGetAllColumnsTypes-4        59004012                19.92 ns/op            0 B/op          0 allocs/op///////////////////////////////////// mysql without cacheBenchmarkGetAllS-4                  1221            865469 ns/op            7152 B/op        162 allocs/opBenchmarkGetAllM-4                  1484            843395 ns/op            8272 B/op        215 allocs/opBenchmarkGetRowS-4                   427           3539007 ns/op            2368 B/op         48 allocs/opBenchmarkGetRowM-4                   267           4481279 ns/op            2512 B/op         54 allocs/opBenchmarkGetAllTables-4              771           1700035 ns/op             832 B/op         26 allocs/opBenchmarkGetAllColumnsTypes-4             760           1537301 ns/op            1392 B/op         44 allocs/op///////////////////////////////////// mysql with cacheBenchmarkGetAllS-4               2933072               414.5 ns/op           208 B/op          2 allocs/opBenchmarkGetAllM-4               6704588               180.4 ns/op            16 B/op          1 allocs/opBenchmarkGetRowS-4               2136634               545.4 ns/op           240 B/op          4 allocs/opBenchmarkGetRowM-4               4111814               292.6 ns/op            48 B/op          3 allocs/opBenchmarkGetAllTables-4         58835394                21.52 ns/op            0 B/op          0 allocs/opBenchmarkGetAllColumnsTypes-4        59059225                19.99 ns/op            0 B/op          0 allocs/op///////////////////////////////////// sqlite without cacheBenchmarkGetAllS-4                 13664             85506 ns/op            2056 B/op         62 allocs/opBenchmarkGetAllS_GORM-4            10000            101665 ns/op            9547 B/op        155 allocs/opBenchmarkGetAllM-4                 13747             83989 ns/op            1912 B/op         61 allocs/opBenchmarkGetAllM_GORM-4            10000            107810 ns/op            8387 B/op        237 allocs/opBenchmarkGetRowS-4                 12702             91958 ns/op            2192 B/op         67 allocs/opBenchmarkGetRowM-4                 13256             89095 ns/op            2048 B/op         66 allocs/opBenchmarkGetAllTables-4            14264             83939 ns/op             672 B/op         32 allocs/opBenchmarkGetAllColumnsTypes-4           15236             79498 ns/op            1760 B/op         99 allocs/op///////////////////////////////////// sqlite with cacheBenchmarkGetAllS-4               2951642               399.5 ns/op           208 B/op          2 allocs/opBenchmarkGetAllM-4               6537204               177.2 ns/op            16 B/op          1 allocs/opBenchmarkGetRowS-4               2248524               531.4 ns/op           240 B/op          4 allocs/opBenchmarkGetRowM-4               4084453               287.9 ns/op            48 B/op          3 allocs/opBenchmarkGetAllTables-4         52592826                20.39 ns/op            0 B/op          0 allocs/opBenchmarkGetAllColumnsTypes-4        64293176                20.87 ns/op            0 B/op          0 allocs/op

Many to many example

typeClassstruct {Iduint`orm:"pk"`Namestring`orm:"size:100"`IsAvailableboolCreatedAt   time.Time`orm:"now"`}typeStudentstruct {Iduint`orm:"pk"`Namestring`orm:"size:100"`CreatedAt time.Time`orm:"now"`}typeTeacherstruct {Iduint`orm:"pk"`Namestring`orm:"size:100"`CreatedAt time.Time`orm:"now"`}// migratefuncmigrate() {err:= orm.AutoMigrate[Class]("classes")ifklog.CheckError(err) {return}err= orm.AutoMigrate[Student]("students")ifklog.CheckError(err) {return}err= orm.AutoMigrate[Teacher]("teachers")ifklog.CheckError(err) {return}}// orm.ManyToMany create relation table named m2m_table1_table2// create relationserr=orm.ManyToMany("classes","students")iflogger.CheckError(err) {return}err=orm.ManyToMany("classes","teachers")iflogger.CheckError(err) {return}// then you can use it like so to get related data// get related to map to structstd:= []Student{}err=orm.Model[Class]().Where("name = ?","Math").Select("name").OrderBy("-name").Limit(1).GetRelated("students",&std)// get related to mapstd:= []map[string]any{}err=orm.Table("classes").Where("name = ?","Math").Select("name").OrderBy("-name").Limit(1).GetRelated("students",&std)// join related to mapstd:= []map[string]any{}err=orm.Table("classes").Where("name = ?","Math").JoinRelated("students",&std)// join related to mapstd:= []Student{}err=orm.Model[Class]().Where("name = ?","Math").JoinRelated("students",&std)// to add relation_,err=orm.Model[Class]().AddRelated("students","name = ?","hisName")_,err=orm.Model[Student]().AddRelated("classes","name = ?","French")_,err=orm.Table("students").AddRelated("classes","name = ?","French")// delete relation_,err=orm.Model[Class]().Where("name = ?","Math").DeleteRelated("students","name = ?","hisName")_,err=orm.Table("classes").Where("name = ?","Math").DeleteRelated("students","name = ?","hisName")

🔗 Links

portfoliolinkedin


Licence

LicenceBSD-3


[8]ページ先頭

©2009-2025 Movatter.jp