This package provides automatic schema migration (only for appending new columns, not for changing data types). This package also can be used to generate ORM.
go install github.com/fatih/gomodifytags@latest
go install github.com/kokizzu/replacer@latest
import "github.com/kokizzu/gotro/L"
func ConnectTarantool() *tarantool.Connection {
return Tt.Connect1(`host`, `3301`, `user`, `pass`)
}
// then use it like this:
tt := &Tt.Adapter{Connection: ConnectTarantool(), Reconnect: ConnectTarantool}
- create a
model/
directory inside project - create a
m[Domain]
directory inside project, for example if the domain is authentication, you might want to createmAuth
- create a
[domain]_tables.go
something like this:
package mAuth
import "github.com/kokizzu/gotro/D/Tt"
const (
TableUserss Tt.TableName = `users`
Id = `id`
Email = `email`
Password = `password`
CreatedBy = `createdBy`
CreatedAt = `createdAt`
UpdatedBy = `updatedBy`
UpdatedAt = `updatedAt`
DeletedBy = `deletedBy`
DeletedAt = `deletedAt`
IsDeleted = `isDeleted`
RestoredBy = `restoredBy`
RestoredAt = `restoredAt`
PasswordSetAt = `passwordSetAt`
SecretCode = `secretCode`
SecretCodeAt = `secretCodeAt`
VerificationSentAt = `verificationSentAt`
VerifiedAt = `verifiedAt`
LastLoginAt = `lastLoginAt`
)
const (
TableSessions Tt.TableName = `sessions`
SessionToken = `sessionToken`
UserId = `userId`
ExpiredAt = `expiredAt`
)
var TarantoolTables = map[Tt.TableName]*Tt.TableProp{
// can only adding fields on back, and must IsNullable: true
// primary key must be first field and set to Unique: fieldName
TableUserss: {
Fields: []Tt.Field{
{Id, Tt.Unsigned},
{Email, Tt.String},
{Password, Tt.String},
{CreatedAt, Tt.Integer},
{CreatedBy, Tt.Unsigned},
{UpdatedAt, Tt.Integer},
{UpdatedBy, Tt.Unsigned},
{DeletedAt, Tt.Integer},
{DeletedBy, Tt.Unsigned},
{IsDeleted, Tt.Boolean},
{RestoredAt, Tt.Integer},
{RestoredBy, Tt.Unsigned},
{PasswordSetAt, Tt.Integer},
{SecretCode, Tt.String},
{SecretCodeAt, Tt.Integer},
{VerificationSentAt, Tt.Integer},
{VerifiedAt, Tt.Integer},
{LastLoginAt, Tt.Integer},
},
AutoIncrementId: true,
Unique2: Email,
Indexes: []string{IsDeleted, SecretCode},
},
TableSessions: {
Fields: []Tt.Field{
{SessionToken, Tt.String},
{UserId, Tt.Unsigned},
{ExpiredAt, Tt.Integer},
},
Unique1: SessionToken,
},
}
func GenerateORM() {
Tt.GenerateOrm(TarantoolTables)
}
- create a
[domain]_generator_test.go
something like this:
package mAuth
import (
"testing"
)
//go:generate go test -run=XXX -bench=Benchmark_GenerateOrm
func Benchmark_GenerateOrm(b *testing.B) {
GenerateORM()
b.SkipNow()
}
- run the test to generate new ORM, that would generate
rq[Domain]/rq[Domain]__ORM.GEN.go
andwc[Domain]/wc[Domain]__ORM.GEN.go
file, you might want to create a helper script for that:
#!/usr/bin/env bash
cd ./model
cat *.go | grep '//go:generate ' | cut -d ' ' -f 2- | bash -x > /tmp/1.log
for i in ./m*; do
if [[ ! -d "$i" ]] ; then continue ; fi
echo $i
pushd .
cd "$i"
# generate ORM
go test -bench=.
for j in ./*; do
echo $j
if [[ ! -d "$j" ]] ; then continue ; fi
pushd .
cd "$j"
echo `pwd`
cat *.go | grep '//go:generate ' | cut -d ' ' -f 2- | bash -x >> /tmp/1.log
popd
done
popd
done
- in your web server engine/domain logic (one that initializes dependencies), create methods to help initialize the buffer, something like this:
type Domain struct {
Taran *Tt.Adapter
}
func NewDomain() *Domain {
d := &Domain{
Taran: &Tt.Adapter{conf.ConnectTarantool(), conf.ConnectTarantool},
}
return d
}
- last step is just call generated method to manipulate or query, something like this:
func (d *Domain) BusinessLogic1(in *BusinessLogic1_In) (out BusinessLogic1_Out) {
// do something else
user := wcAuth.NewUserMutator(d.Taran)
user.Email = in.Email
if !user.FindById() {
user.Id = id64.UID()
user.CreatedAt = in.UnixNow()
if !user.DoInsert() {
out.SetError(500, `failed to insert user record, db down?`)
return
}
}
user.SetUpdatedAt(in.UnixNow())
// do other manipulation
// use .Set* if you have to call DoUpdateBy*()
if !user.DoUpdateById() {
out.SetError(500, `failed to insert user record, db down?`)
return
}
}
// or if you only need to read
func (d *Domain) mustLogin(token string, userAgent string, out *ResponseCommon) *conf.Session {
sess := &conf.Session{}
if token == `` {
out.SetError(400, `missing session token`)
return nil
}
if !sess.Decrypt(token, userAgent) {
out.SetError(400, `invalid session token`) // if got this, possibly wrong userAgent-sessionToken pair
return nil
}
if sess.ExpiredAt <= fastime.UnixNow() {
out.SetError(400, `token expired`)
return nil
}
session := rqAuth.NewSessions(d.Taran)
session.SessionToken = token
if !session.FindBySessionToken() {
out.SetError(400, `session missing from database, wrong env?`)
return nil
}
if session.ExpiredAt <= fastime.UnixNow() {
out.SetError(403, `session expired or logged out`)
return nil
}
return sess
}
- If you need to create an extension method for the ORM, just add a new file on
rq[Domain]/anything.go
, with a new struct method from generated ORM, something like this:
package rqAuth
import (
"myProject/conf"
"github.com/kokizzu/gotro/I"
"github.com/kokizzu/gotro/L"
"github.com/kokizzu/gotro/X"
"golang.org/x/crypto/bcrypt"
)
func (s *Users) FindOffsetLimit(offset, limit uint32) (res []*Users) {
query := `
SELECT ` + s.SqlSelectAllFields() + `
FROM ` + s.SqlTableName() + `
ORDER BY ` + s.SqlId() + `
LIMIT ` + X.ToS(limit) + `
OFFSET ` + X.ToS(offset) // note: for string, use S.Z or S.XSS to prevent SQL injection
if conf.DEBUG_MODE {
L.Print(query)
}
s.Adapter.QuerySql(query, func(row []any) {
obj := &Users{}
obj.FromArray(row)
obj.CensorFields()
res = append(res, obj)
})
return
}
func (s *Users) CheckPassword(currentPassword string) bool {
hash := []byte(s.Password)
pass := []byte(currentPassword)
err := bcrypt.CompareHashAndPassword(hash, pass)
return !L.IsError(err, `bcrypt.CompareHashAndPassword`)
}
// call before outputting to client
func (s *Users) CensorFields() {
s.Password = ``
s.SecretCode = ``
}
or in wc[Domain]/anything.go
if you need to manipulate things
func (p *UsersMutator) SetEncryptPassword(password string) bool {
pass, err := bcrypt.GenerateFromPassword([]byte(password), 0)
p.SetPassword(string(pass))
return !L.IsError(err, `bcrypt.GenerateFromPassword`)
}
- to initialize automatic migration, just create
model/run_migration.go
func RunMigration() {
L.Print(`run migration..`)
tt := &Tt.Adapter{Connection: ConnectTarantool(), Reconnect: ConnectTarantool}
tt.MigrateTables(mAuth.ClickhouseTables)
// add other tarantool tables to be migrated here
}
then call it on main
func main() {
model.RunMigration()
}