diff --git a/README.md b/README.md index 721e7d2..11fd4fe 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,11 @@ go get -u github.com/dewep-online/goppy ## Plugins -|Plugin|Comment|Import| -|---|---|---| -|**debug**|profiling application (pprof) with HTTP access.|`http.WithHTTPDebug()`| -|**http**|Out of the box multi-server launch of web servers with separate routing. Grouping of routers with connection to a group of dedicated middleware.|`http.WithHTTP()`| +| Plugin |Comment| Import | +|--------------|---|-------------------------| +| **debug** |profiling application (pprof) with HTTP access.| `http.WithHTTPDebug()` | +| **http** |Out of the box multi-server launch of web servers with separate routing. Grouping of routers with connection to a group of dedicated middleware.| `http.WithHTTP()` | +| **database** |Multi connection pools with MySQL and SQLite databases (with initialization migration setup).| `database.WithMySQL()` `database.WithSQLite()` | ## Quick Start diff --git a/examples/demo0/config.yaml b/examples/demo0/config.yaml new file mode 100755 index 0000000..6ed7823 --- /dev/null +++ b/examples/demo0/config.yaml @@ -0,0 +1,43 @@ +env: dev +pid: "" +level: 4 +log: /dev/stdout + +debug: + addr: 127.0.0.1:12000 + read_timeout: 0s + write_timeout: 0s + idle_timeout: 0s + shutdown_timeout: 0s + +http: + main: + addr: 127.0.0.1:8080 + read_timeout: 0s + write_timeout: 0s + idle_timeout: 0s + shutdown_timeout: 0s + +mysql: + - name: main + host: 127.0.0.1 + port: 3306 + schema: test_database + user: test + password: test + maxidleconn: 5 + maxopenconn: 5 + maxconnttl: 50s + interpolateparams: false + timezone: UTC + txisolevel: "" + charset: utf8mb4,utf8 + timeout: 5s + readtimeout: 5s + writetimeout: 5s + +sqlite: + - name: main + file: ./sqlite.db + init_migration: + - ./migration.sql diff --git a/examples/demo0/main.go b/examples/demo0/main.go new file mode 100644 index 0000000..5f47452 --- /dev/null +++ b/examples/demo0/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "github.com/dewep-online/goppy" + "github.com/dewep-online/goppy/plugins" + "github.com/dewep-online/goppy/plugins/database" + "github.com/dewep-online/goppy/plugins/http" +) + +func main() { + + app := goppy.New() + app.WithConfig("./config.yaml") + app.Plugins( + http.WithHTTPDebug(), + http.WithHTTP(), + database.WithMySQL(), + database.WithSQLite(), + ) + app.Plugins( + plugins.Plugin{ + Inject: NewController, + Resolve: func(routes *http.RouterPool, c *Controller) { + router := routes.Main() + router.Use(http.ThrottlingMiddleware(100)) + router.Get("/users", c.Users) + + api := router.Collection("/api/v1", http.ThrottlingMiddleware(100)) + api.Get("/user/{id}", c.User) + }, + }, + ) + app.Run() + +} + +type Controller struct { + db database.MySQL +} + +func NewController(v database.MySQL) *Controller { + return &Controller{ + db: v, + } +} + +func (v *Controller) Users(ctx http.Ctx) { + data := []int64{1, 2, 3, 4} + ctx.SetBody().JSON(data) +} + +func (v *Controller) User(ctx http.Ctx) { + id, _ := ctx.Param("id").Int() + + err := v.db.Pool("main").Ping() + if err != nil { + ctx.Log().Errorf("db: %s", err.Error()) + } + + ctx.SetBody().Error(http.ErrMessage{ + HTTPCode: 400, + InternalCode: "x1000", + Message: "user not found", + Ctx: map[string]interface{}{"id": id}, + }) + + ctx.Log().Infof("user - %d", id) +} diff --git a/examples/demo0/migration.sql b/examples/demo0/migration.sql new file mode 100644 index 0000000..e0c2926 --- /dev/null +++ b/examples/demo0/migration.sql @@ -0,0 +1,6 @@ +create table `test_data` +( + `id` INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE, + `data` TEXT, + `updated_at` NUMERIC +); \ No newline at end of file diff --git a/examples/demo0/sqlite.db b/examples/demo0/sqlite.db new file mode 100644 index 0000000..6768804 Binary files /dev/null and b/examples/demo0/sqlite.db differ diff --git a/examples/demo1/config.yaml b/examples/demo1/config.yaml deleted file mode 100755 index f2e06ec..0000000 --- a/examples/demo1/config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -env: dev -pid: "" -level: 4 -log: /dev/stdout - -debug: - addr: 127.0.0.1:12000 - -http: - main: - addr: 127.0.0.1:8080 - admin: - addr: 127.0.0.1:8081 diff --git a/examples/demo1/main.go b/examples/demo1/main.go deleted file mode 100644 index 393b387..0000000 --- a/examples/demo1/main.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "github.com/dewep-online/goppy" - "github.com/dewep-online/goppy/plugins" - "github.com/dewep-online/goppy/plugins/http" -) - -func main() { - - app := goppy.New() - app.WithConfig("./config.yaml") - app.Plugins( - http.WithHTTPDebug(), - http.WithHTTP(), - ) - app.Plugins( - plugins.Plugin{ - Inject: NewController, - Resolve: func(routes *http.RouterPool, c *Controller) { - router := routes.Main() - router.Use(http.ThrottlingMiddleware(1)) - router.Get("/user/{id}", c.User) - }, - }, - plugins.Plugin{ - Inject: NewAdminController, - Resolve: func(routes *http.RouterPool, c *AdminController) { - router := routes.Get("admin") - router.Get("/admin/{id}", c.Admin) - - apiColl := router.Collection("/api/v1", http.ThrottlingMiddleware(1)) - apiColl.Get("/data", c.Admin) - }, - }, - ) - app.Run() - -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -type Controller struct{} - -func NewController() *Controller { - return &Controller{} -} - -func (v *Controller) User(ctx http.Ctx) { - id, _ := ctx.Param("id").Int() - - ctx.SetBody().Error(http.ErrMessage{ - HTTPCode: 400, - InternalCode: "x1000", - Message: "Пользователь не найден", - Ctx: map[string]interface{}{"id": id}, - }) - - ctx.Log().Infof("user - %d", id) -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -type AdminController struct{} - -func NewAdminController() *AdminController { - return &AdminController{} -} - -func (v *AdminController) Admin(ctx http.Ctx) { - ctx.SetBody().Raw([]byte(ctx.URL().String())) -} diff --git a/go.mod b/go.mod index 76cd105..e412d94 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/deweppro/go-errors v0.0.4 github.com/deweppro/go-http v1.4.2 github.com/deweppro/go-logger v1.3.0 + github.com/deweppro/go-orm v1.0.6 github.com/mailru/easyjson v0.7.7 gopkg.in/yaml.v3 v3.0.1 ) @@ -14,5 +15,7 @@ require ( require ( github.com/deweppro/go-algorithms v1.2.0 // indirect github.com/deweppro/go-chan-pool v1.1.2 // indirect + github.com/go-sql-driver/mysql v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/mattn/go-sqlite3 v1.14.13 // indirect ) diff --git a/go.sum b/go.sum index f4f478a..1969fec 100644 --- a/go.sum +++ b/go.sum @@ -13,12 +13,18 @@ github.com/deweppro/go-http v1.4.2 h1:aQiydt57SRXwiAHjOxtKhRtSbNxYHG7iqWpWL9iBQu github.com/deweppro/go-http v1.4.2/go.mod h1:zta7HINyRd9nmSRtqxw6APOTXwDPBI42G9iZQwqtQVU= github.com/deweppro/go-logger v1.3.0 h1:KN6RQmb6IoNBxQ7zx7Y1AtptHeL//FRgvQyEF5PrcsE= github.com/deweppro/go-logger v1.3.0/go.mod h1:jxBBLyHmIvJ4erGUj5qeE6ir36ztyAL1pI+9GymOHVI= +github.com/deweppro/go-orm v1.0.6 h1:HoM9SniQTI6oJ95EBEW4MX0vWhZPuVj5Qxp6l9B4oVU= +github.com/deweppro/go-orm v1.0.6/go.mod h1:JEbxJLmXMjSOtlwhuJmrJKZRLhcJcexRvGZ6hgautaw= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= +github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/plugins/database/mysql.go b/plugins/database/mysql.go new file mode 100644 index 0000000..40bdb63 --- /dev/null +++ b/plugins/database/mysql.go @@ -0,0 +1,99 @@ +package database + +import ( + "fmt" + "time" + + "github.com/dewep-online/goppy/plugins" + "github.com/deweppro/go-logger" + "github.com/deweppro/go-orm" + "github.com/deweppro/go-orm/schema" + "github.com/deweppro/go-orm/schema/mysql" +) + +//ConfigMysql mysql config model +type ConfigMysql struct { + Pool []mysql.Item `yaml:"mysql"` +} + +//List getting all configs +func (v *ConfigMysql) List() (list []schema.ItemInterface) { + for _, item := range v.Pool { + list = append(list, item) + } + return +} + +func (v *ConfigMysql) Default() { + if len(v.Pool) == 0 { + v.Pool = []mysql.Item{ + { + Name: "main", + Host: "127.0.0.1", + Port: 3306, + Schema: "test_database", + User: "test", + Password: "test", + MaxIdleConn: 5, + MaxOpenConn: 5, + MaxConnTTL: time.Second * 50, + InterpolateParams: false, + Timezone: "UTC", + TxIsolationLevel: "", + Charset: "utf8mb4,utf8", + Timeout: time.Second * 5, + ReadTimeout: time.Second * 5, + WriteTimeout: time.Second * 5, + }, + } + + } +} + +//WithMySQL launch MySQL connection pool +func WithMySQL() plugins.Plugin { + return plugins.Plugin{ + Config: &ConfigMysql{}, + Inject: func(conf *ConfigMysql, log logger.Logger) (*mysqlProvider, MySQL) { + conn := mysql.New(conf) + o := orm.NewDB(conn, orm.Plugins{Logger: log}) + return &mysqlProvider{conn: conn, conf: *conf, log: log}, o + }, + } +} + +type ( + mysqlProvider struct { + conn schema.Connector + conf ConfigMysql + log logger.Logger + } + + //MySQL connection MySQL interface + MySQL interface { + Pool(name string) orm.StmtInterface + } +) + +func (v *mysqlProvider) Up() error { + if err := v.conn.Reconnect(); err != nil { + return err + } + for _, item := range v.conf.Pool { + p, err := v.conn.Pool(item.Name) + if err != nil { + return fmt.Errorf("pool `%s`: %w", item.Name, err) + } + if err = p.Ping(); err != nil { + return fmt.Errorf("pool `%s`: %w", item.Name, err) + } + v.log.WithFields( + logger.Fields{item.Name: fmt.Sprintf("%s:%d", item.Host, item.Port)}, + ).Infof("MySQL connect") + } + return nil +} + +func (v *mysqlProvider) Down() error { + return v.conn.Close() +} diff --git a/plugins/database/sqlite.go b/plugins/database/sqlite.go new file mode 100644 index 0000000..c21be42 --- /dev/null +++ b/plugins/database/sqlite.go @@ -0,0 +1,148 @@ +package database + +import ( + "context" + "database/sql" + "fmt" + "os" + + "github.com/dewep-online/goppy/plugins" + "github.com/deweppro/go-logger" + "github.com/deweppro/go-orm" + "github.com/deweppro/go-orm/schema" + "github.com/deweppro/go-orm/schema/sqlite" +) + +type ( + ConfigSqlite struct { + Pool []Item `yaml:"sqlite"` + } + Item struct { + Name string `yaml:"name"` + File string `yaml:"file"` + InitMigration []string `yaml:"init_migration"` + } +) + +func (v *ConfigSqlite) Default() { + if len(v.Pool) == 0 { + v.Pool = []Item{ + { + Name: "main", + File: "./sqlite.db", + InitMigration: []string{ + "./migration.sql", + }, + }, + } + } +} + +//List getting all configs +func (v *ConfigSqlite) List() (list []schema.ItemInterface) { + for _, item := range v.Pool { + list = append(list, item) + } + return +} + +//GetName getting config name +func (i Item) GetName() string { return i.Name } + +//GetDSN connection params +func (i Item) GetDSN() string { return i.File } + +//Setup setting config connections params +func (i Item) Setup(_ schema.SetupInterface) {} + +//WithSQLite launch SQLite connection pool +func WithSQLite() plugins.Plugin { + return plugins.Plugin{ + Config: &ConfigSqlite{}, + Inject: func(conf *ConfigSqlite, log logger.Logger) (*sqliteProvider, SQLite) { + conn := sqlite.New(conf) + o := orm.NewDB(conn, orm.Plugins{Logger: log}) + return &sqliteProvider{conn: conn, conf: *conf, log: log}, o + }, + } +} + +type ( + sqliteProvider struct { + conn schema.Connector + conf ConfigSqlite + log logger.Logger + } + + //SQLite connection SQLite interface + SQLite interface { + Pool(name string) orm.StmtInterface + } +) + +func (v *sqliteProvider) Up() error { + if err := v.conn.Reconnect(); err != nil { + return err + } + for _, item := range v.conf.Pool { + p, err := v.conn.Pool(item.Name) + if err != nil { + return fmt.Errorf("pool `%s`: %w", item.Name, err) + } + if err = p.Ping(); err != nil { + return fmt.Errorf("pool `%s`: %w", item.Name, err) + } + if err = v.migration(p, item.InitMigration); err != nil { + return fmt.Errorf("pool `%s`: %w", item.Name, err) + } + v.log.WithFields(logger.Fields{item.Name: item.File}).Infof("SQLite connect") + } + return nil +} + +func (v *sqliteProvider) Down() error { + return v.conn.Close() +} + +const sqliteMaster = "select count(*) from `sqlite_master`;" + +func (v *sqliteProvider) migration(conn *sql.DB, mig []string) error { + ctx := context.TODO() + var count int + checkDB := func() error { + row := conn.QueryRowContext(ctx, sqliteMaster) + if err := row.Scan(&count); err != nil { + return err + } + if err := row.Err(); err != nil { + return err + } + return nil + } + + if err := checkDB(); err != nil { + return err + } + + if count == 0 { + for _, filename := range mig { + b, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("read init migration `%s`: %w", filename, err) + } + if _, err = conn.ExecContext(ctx, string(b)); err != nil { + return fmt.Errorf("exec init migration `%s`: %w", filename, err) + } + } + } + + if err := checkDB(); err != nil { + return err + } + + if count == 0 { + return fmt.Errorf("empty database") + } + + return nil +}