From 620873e499034ab6da4148f8b1cf696224316199 Mon Sep 17 00:00:00 2001 From: Yan Qing Date: Sat, 14 Mar 2020 15:22:30 +0800 Subject: [PATCH] support pagination for List API; improve code. --- CHANGELOG.md | 11 +++++ config/default.yml | 2 +- sql/schema.sql | 75 ++++++++++++++++----------------- sql/update_20200314.sql | 61 +++++++++++++++++++++++++++ src/api/app_test.go | 18 ++++++++ src/api/group.go | 25 +++++++---- src/api/group_test.go | 63 +++++++++++++++------------- src/api/label.go | 4 +- src/api/label_test.go | 44 ++++++++++---------- src/api/module.go | 4 +- src/api/module_test.go | 18 ++++---- src/api/product.go | 7 +++- src/api/product_test.go | 55 ++++++++++++------------ src/api/setting.go | 2 +- src/api/setting_test.go | 33 +++++++++------ src/api/user.go | 14 ++++--- src/api/user_test.go | 73 +++++++++++++++++--------------- src/bll/group.go | 40 +++++++++++++++--- src/bll/label.go | 22 +++++++--- src/bll/module.go | 14 ++++++- src/bll/product.go | 16 +++++-- src/bll/setting.go | 19 ++++++--- src/bll/user.go | 20 ++++++--- src/model/group.go | 78 ++++++++++++++++++++++++---------- src/model/label.go | 14 ++++++- src/model/module.go | 14 ++++++- src/model/product.go | 14 ++++++- src/model/setting.go | 14 ++++++- src/model/user.go | 47 ++++++++++++++------- src/schema/group.go | 5 ++- src/schema/label.go | 20 ++++----- src/schema/module.go | 4 +- src/schema/product.go | 2 +- src/schema/setting.go | 22 +++++----- src/schema/user.go | 30 ++++++++++---- src/service/hider.go | 77 +++++++++++----------------------- src/tpl/common.go | 50 ++++++++++++++++++---- src/tpl/group.go | 9 +++- src/tpl/label.go | 72 +++++++++++++++++++++++--------- src/tpl/module.go | 4 +- src/tpl/pagination.go | 92 +++++++++++++++++++++++++++++++++++++++++ src/tpl/product.go | 29 +++++++++++-- src/tpl/setting.go | 64 ++++++++++++++++++++++++---- 43 files changed, 909 insertions(+), 392 deletions(-) create mode 100644 sql/update_20200314.sql create mode 100644 src/tpl/pagination.go diff --git a/CHANGELOG.md b/CHANGELOG.md index b63a877..10292d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,14 @@ All notable changes to this project will be documented in this file starting from version **v1.0.0**. This project adheres to [Semantic Versioning](http://semver.org/). + +----- + +## [1.1.0] - 2020-03-14 + +**Changed:** + +- Support kind for group. +- Support pagination for List API. +- Improve SQL schemas. +- Improve code. diff --git a/config/default.yml b/config/default.yml index 11d31ba..097d30b 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1,4 +1,4 @@ -addr: ":3000" +addr: ":8080" cert_file: key_file: logger: diff --git a/sql/schema.sql b/sql/schema.sql index 07f007e..5a121fe 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -1,48 +1,49 @@ -- Keywords and Reserved Words https://dev.mysql.com/doc/refman/5.7/en/keywords.html name status channel value values group user -CREATE DATABASE IF NOT EXISTS `urbs` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +CREATE DATABASE IF NOT EXISTS `urbs` CHARACTER SET utf8mb4 COLLATE utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`urbs_user` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `active_at` bigint NOT NULL DEFAULT 0, `uid` varchar(63) NOT NULL, `labels` varchar(8190) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_uid` (`uid`), KEY `idx_user_active_at` (`active_at`) -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`urbs_group` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), `sync_at` bigint NOT NULL DEFAULT 0, `uid` varchar(63) NOT NULL, + `kind` varchar(63) NOT NULL DEFAULT '', `description` varchar(1022) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `uk_group_uid` (`uid`), - KEY `idx_group_sync_at` (`sync_at`) -) ENGINE=InnoDB; + KEY `idx_group_kind` (`kind`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`urbs_product` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `deleted_at` datetime DEFAULT NULL, - `offline_at` datetime DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `deleted_at` datetime(3) DEFAULT NULL, + `offline_at` datetime(3) DEFAULT NULL, `name` varchar(63) NOT NULL, `description` varchar(1022) NOT NULL DEFAULT '', `status` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_product_name` (`name`), KEY `idx_product_created_at` (`created_at`) -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`urbs_label` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `offline_at` datetime DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `offline_at` datetime(3) DEFAULT NULL, `product_id` bigint NOT NULL, `name` varchar(63) NOT NULL, `description` varchar(1022) NOT NULL DEFAULT '', @@ -51,26 +52,26 @@ CREATE TABLE IF NOT EXISTS `urbs`.`urbs_label` ( `status` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_label_product_id_name` (`product_id`,`name`) -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`urbs_module` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `offline_at` datetime DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `offline_at` datetime(3) DEFAULT NULL, `product_id` bigint NOT NULL, `name` varchar(63) NOT NULL, `description` varchar(1022) NOT NULL DEFAULT '', `status` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_module_product_id_name` (`product_id`,`name`) -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`urbs_setting` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `offline_at` datetime DEFAULT NULL, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + `offline_at` datetime(3) DEFAULT NULL, `module_id` bigint NOT NULL, `name` varchar(63) NOT NULL, `description` varchar(1022) NOT NULL DEFAULT '', @@ -80,57 +81,57 @@ CREATE TABLE IF NOT EXISTS `urbs`.`urbs_setting` ( `status` bigint NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `uk_setting_module_id_name` (`module_id`,`name`) -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`user_group` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `sync_at` bigint NOT NULL, `user_id` bigint NOT NULL, `group_id` bigint NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_group_user_id_group_id` (`user_id`,`group_id`), - KEY `idx_user_group_sync_at` (`sync_at`) -) ENGINE=InnoDB; + KEY `idx_user_group_group_id` (`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`user_label` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `user_id` bigint NOT NULL, `label_id` bigint NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_label_user_id_label_id` (`user_id`,`label_id`) -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`user_setting` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), `user_id` bigint NOT NULL, `setting_id` bigint NOT NULL, `value` varchar(255) NOT NULL DEFAULT '', `last_value` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_setting_user_id_setting_id` (`user_id`,`setting_id`) -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`group_label` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), `group_id` bigint NOT NULL, `label_id` bigint NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_group_label_group_id_label_id` (`group_id`,`label_id`) -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; CREATE TABLE IF NOT EXISTS `urbs`.`group_setting` ( `id` bigint NOT NULL AUTO_INCREMENT, - `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), `group_id` bigint NOT NULL, `setting_id` bigint NOT NULL, `value` varchar(255) NOT NULL DEFAULT '', `last_value` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `uk_group_setting_group_id_setting_id` (`group_id`,`setting_id`) -) ENGINE=InnoDB; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin; diff --git a/sql/update_20200314.sql b/sql/update_20200314.sql new file mode 100644 index 0000000..98d039e --- /dev/null +++ b/sql/update_20200314.sql @@ -0,0 +1,61 @@ +ALTER TABLE `urbs_user` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_user` MODIFY COLUMN `uid` varchar(63) NOT NULL COLLATE utf8mb4_bin; +ALTER TABLE `urbs_user` MODIFY COLUMN `labels` varchar(8190) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; + + COLLATE=utf8mb4_bin + +ALTER TABLE `urbs_group` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_group` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_group` MODIFY COLUMN `uid` varchar(63) NOT NULL COLLATE utf8mb4_bin; +ALTER TABLE `urbs_group` MODIFY COLUMN `description` varchar(1022) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; +ALTER TABLE `urbs_group` ADD COLUMN `kind` varchar(63) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; +ALTER TABLE `urbs_group` DROP INDEX `idx_group_sync_at`; +ALTER TABLE `urbs_group` ADD INDEX `idx_group_kind` (`kind`); + +ALTER TABLE `urbs_product` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_product` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_product` MODIFY COLUMN `deleted_at` datetime(3) DEFAULT NULL; +ALTER TABLE `urbs_product` MODIFY COLUMN `offline_at` datetime(3) DEFAULT NULL; +ALTER TABLE `urbs_product` MODIFY COLUMN `name` varchar(63) NOT NULL COLLATE utf8mb4_bin; +ALTER TABLE `urbs_product` MODIFY COLUMN `description` varchar(1022) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; + +ALTER TABLE `urbs_label` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_label` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_label` MODIFY COLUMN `offline_at` datetime(3) DEFAULT NULL; +ALTER TABLE `urbs_label` MODIFY COLUMN `name` varchar(63) NOT NULL COLLATE utf8mb4_bin; +ALTER TABLE `urbs_label` MODIFY COLUMN `description` varchar(1022) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; +ALTER TABLE `urbs_label` MODIFY COLUMN `channels` varchar(255) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; +ALTER TABLE `urbs_label` MODIFY COLUMN `clients` varchar(255) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; + +ALTER TABLE `urbs_module` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_module` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_module` MODIFY COLUMN `offline_at` datetime(3) DEFAULT NULL; +ALTER TABLE `urbs_module` MODIFY COLUMN `name` varchar(63) NOT NULL COLLATE utf8mb4_bin; +ALTER TABLE `urbs_module` MODIFY COLUMN `description` varchar(1022) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; + +ALTER TABLE `urbs_setting` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_setting` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3); +ALTER TABLE `urbs_setting` MODIFY COLUMN `offline_at` datetime(3) DEFAULT NULL; +ALTER TABLE `urbs_setting` MODIFY COLUMN `name` varchar(63) NOT NULL COLLATE utf8mb4_bin; +ALTER TABLE `urbs_setting` MODIFY COLUMN `description` varchar(1022) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; +ALTER TABLE `urbs_setting` MODIFY COLUMN `channels` varchar(255) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; +ALTER TABLE `urbs_setting` MODIFY COLUMN `clients` varchar(255) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; +ALTER TABLE `urbs_setting` MODIFY COLUMN `vals` varchar(1022) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; + +ALTER TABLE `user_group` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); +ALTER TABLE `user_group` DROP INDEX `idx_user_group_sync_at`; +ALTER TABLE `user_group` ADD INDEX `idx_user_group_group_id` (`group_id`); + +ALTER TABLE `user_label` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); + +ALTER TABLE `user_setting` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); +ALTER TABLE `user_setting` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3); +ALTER TABLE `user_setting` MODIFY COLUMN `value` varchar(255) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; +ALTER TABLE `user_setting` MODIFY COLUMN `last_value` varchar(255) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; + +ALTER TABLE `group_label` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); + +ALTER TABLE `group_setting` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); +ALTER TABLE `group_setting` MODIFY COLUMN `updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3); +ALTER TABLE `group_setting` MODIFY COLUMN `value` varchar(255) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; +ALTER TABLE `group_setting` MODIFY COLUMN `last_value` varchar(255) NOT NULL COLLATE utf8mb4_bin DEFAULT ''; diff --git a/src/api/app_test.go b/src/api/app_test.go index 1aee5d3..44f5883 100644 --- a/src/api/app_test.go +++ b/src/api/app_test.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "os" "testing" "github.com/DavidCai1993/request" @@ -37,6 +38,23 @@ func SetUpTestTools() (tt *TestTools, cleanup func()) { } } +func TestMain(m *testing.M) { + tt, cleanup := SetUpTestTools() + tt.DB.Exec("TRUNCATE TABLE urbs_user;") + tt.DB.Exec("TRUNCATE TABLE urbs_group;") + tt.DB.Exec("TRUNCATE TABLE urbs_product;") + tt.DB.Exec("TRUNCATE TABLE urbs_label;") + tt.DB.Exec("TRUNCATE TABLE urbs_module;") + tt.DB.Exec("TRUNCATE TABLE urbs_setting;") + tt.DB.Exec("TRUNCATE TABLE user_group;") + tt.DB.Exec("TRUNCATE TABLE user_label;") + tt.DB.Exec("TRUNCATE TABLE user_setting;") + tt.DB.Exec("TRUNCATE TABLE group_label;") + tt.DB.Exec("TRUNCATE TABLE group_setting;") + cleanup() + os.Exit(m.Run()) +} + func TestApp(t *testing.T) { tt, cleanup := SetUpTestTools() defer cleanup() diff --git a/src/api/group.go b/src/api/group.go index 770e47a..0b7aa68 100644 --- a/src/api/group.go +++ b/src/api/group.go @@ -15,7 +15,12 @@ type Group struct { // List .. func (a *Group) List(ctx *gear.Context) error { - res, err := a.blls.Group.List(ctx) + req := tpl.Pagination{} + if err := ctx.ParseURL(&req); err != nil { + return err + } + + res, err := a.blls.Group.List(ctx, req) if err != nil { return err } @@ -25,12 +30,12 @@ func (a *Group) List(ctx *gear.Context) error { // ListLables .. func (a *Group) ListLables(ctx *gear.Context) error { - req := tpl.LabelsURL{} + req := tpl.UIDPaginationURL{} if err := ctx.ParseURL(&req); err != nil { return err } - res, err := a.blls.Group.ListLables(ctx, req.UID, req.Product) + res, err := a.blls.Group.ListLables(ctx, req.UID, req.Pagination) if err != nil { return err } @@ -40,12 +45,12 @@ func (a *Group) ListLables(ctx *gear.Context) error { // ListMembers .. func (a *Group) ListMembers(ctx *gear.Context) error { - req := tpl.UIDURL{} + req := tpl.UIDPaginationURL{} if err := ctx.ParseURL(&req); err != nil { return err } - res, err := a.blls.Group.ListMembers(ctx, req.UID) + res, err := a.blls.Group.ListMembers(ctx, req.UID, req.Pagination) if err != nil { return err } @@ -135,8 +140,9 @@ func (a *Group) RemoveLable(ctx *gear.Context) error { return err } label := &schema.Label{} - if err := service.HIDer.PutHID(label, req.HID); err != nil { - return err + label.ID = service.HIDToID(req.HID, "label") + if label.ID == 0 { + return gear.ErrBadRequest.WithMsgf("invalid hid: %s", req.HID) } if err := a.blls.Group.RemoveLable(ctx, req.UID, label.ID); err != nil { return err @@ -157,8 +163,9 @@ func (a *Group) RemoveSetting(ctx *gear.Context) error { return err } setting := &schema.Setting{} - if err := service.HIDer.PutHID(setting, req.HID); err != nil { - return err + setting.ID = service.HIDToID(req.HID, "setting") + if setting.ID == 0 { + return gear.ErrBadRequest.WithMsgf("invalid hid: %s", req.HID) } if err := a.blls.Group.RemoveSetting(ctx, req.UID, setting.ID); err != nil { return err diff --git a/src/api/group_test.go b/src/api/group_test.go index a541150..2eeca4a 100644 --- a/src/api/group_test.go +++ b/src/api/group_test.go @@ -8,50 +8,54 @@ import ( "github.com/DavidCai1993/request" "github.com/stretchr/testify/assert" + "github.com/teambition/urbs-setting/src/schema" "github.com/teambition/urbs-setting/src/tpl" ) -func createGroup(appHost string) (string, error) { - groupUID := tpl.RandUID() - - _, err := request.Post(fmt.Sprintf("%s/v1/groups:batch", appHost)). +func createGroup(tt *TestTools) (group schema.Group, err error) { + uid := tpl.RandUID() + _, err = request.Post(fmt.Sprintf("%s/v1/groups:batch", tt.Host)). Set("Content-Type", "application/json"). Send(tpl.GroupsBody{Groups: []tpl.GroupBody{ - tpl.GroupBody{UID: groupUID, Desc: groupUID}, + tpl.GroupBody{UID: uid, Kind: "org", Desc: uid}, }}). End() - if err != nil { - return "", err + if err == nil { + err = tt.DB.Where("uid= ?", uid).First(&group).Error } - return groupUID, nil + return } -func createGroupWithUsers(appHost string, count int) (group string, users []string, err error) { - group = tpl.RandUID() - - users = make([]string, count) +func createGroupWithUsers(tt *TestTools, count int) (group schema.Group, users []schema.User, err error) { + groupUID := tpl.RandUID() + userUIDs := make([]string, count) for i := 0; i < count; i++ { - users[i] = tpl.RandUID() + userUIDs[i] = tpl.RandUID() } - _, err = request.Post(fmt.Sprintf("%s/v1/groups:batch", appHost)). + _, err = request.Post(fmt.Sprintf("%s/v1/groups:batch", tt.Host)). Set("Content-Type", "application/json"). Send(tpl.GroupsBody{Groups: []tpl.GroupBody{ - tpl.GroupBody{UID: group, Desc: group}, + tpl.GroupBody{UID: groupUID, Kind: "org", Desc: groupUID}, }}). End() - if err != nil { - return "", nil, err + if err == nil { + _, err = request.Post(fmt.Sprintf("%s/v1/groups/%s/members:batch", tt.Host, groupUID)). + Set("Content-Type", "application/json"). + Send(tpl.UsersBody{Users: userUIDs}). + End() } - _, err = request.Post(fmt.Sprintf("%s/v1/groups/%s/members:batch", appHost, group)). - Set("Content-Type", "application/json"). - Send(tpl.UsersBody{Users: users}). - End() + if err == nil { + err = tt.DB.Where("uid= ?", groupUID).First(&group).Error + } - return group, users, nil + if err == nil { + err = tt.DB.Where("uid in ( ? )", userUIDs).Find(&users).Error + } + return } func TestGroupAPIs(t *testing.T) { @@ -62,7 +66,7 @@ func TestGroupAPIs(t *testing.T) { uid2 := tpl.RandUID() user := tpl.RandUID() - users, err := createUsers(tt.Host, 5) + users, err := createUsers(tt, 5) assert.Nil(t, err) t.Run(`"POST /v1/groups:batch"`, func(t *testing.T) { @@ -72,7 +76,7 @@ func TestGroupAPIs(t *testing.T) { res, err := request.Post(fmt.Sprintf("%s/v1/groups:batch", tt.Host)). Set("Content-Type", "application/json"). Send(tpl.GroupsBody{Groups: []tpl.GroupBody{ - tpl.GroupBody{UID: uid1, Desc: "test"}, + tpl.GroupBody{UID: uid1, Kind: "org", Desc: "test"}, }}). End() assert.Nil(err) @@ -90,8 +94,8 @@ func TestGroupAPIs(t *testing.T) { res, err := request.Post(fmt.Sprintf("%s/v1/groups:batch", tt.Host)). Set("Content-Type", "application/json"). Send(tpl.GroupsBody{Groups: []tpl.GroupBody{ - tpl.GroupBody{UID: uid1, Desc: "test"}, - tpl.GroupBody{UID: uid2, Desc: "test"}, + tpl.GroupBody{UID: uid1, Kind: "org", Desc: "test"}, + tpl.GroupBody{UID: uid2, Kind: "org", Desc: "test"}, }}). End() assert.Nil(err) @@ -136,6 +140,7 @@ func TestGroupAPIs(t *testing.T) { res.JSON(&json) assert.NotNil(json.Result) assert.True(len(json.Result) > 0) + assert.Equal("org", json.Result[0].Kind) }) }) @@ -173,7 +178,7 @@ func TestGroupAPIs(t *testing.T) { res, err := request.Post(fmt.Sprintf("%s/v1/groups/%s/members:batch", tt.Host, uid1)). Set("Content-Type", "application/json"). - Send(tpl.UsersBody{Users: users}). + Send(tpl.UsersBody{Users: schema.GetUsersUID(users)}). End() assert.Nil(err) assert.Equal(200, res.StatusCode) @@ -188,7 +193,7 @@ func TestGroupAPIs(t *testing.T) { res, err := request.Post(fmt.Sprintf("%s/v1/groups/%s/members:batch", tt.Host, uid1)). Set("Content-Type", "application/json"). - Send(tpl.UsersBody{Users: append(users, user)}). + Send(tpl.UsersBody{Users: append(schema.GetUsersUID(users), user)}). End() assert.Nil(err) assert.Equal(200, res.StatusCode) @@ -211,7 +216,7 @@ func TestGroupAPIs(t *testing.T) { text, err := res.Text() assert.Nil(err) - assert.True(strings.Contains(text, users[0])) + assert.True(strings.Contains(text, users[0].UID)) assert.True(strings.Contains(text, user)) json := tpl.GroupMembersRes{} diff --git a/src/api/label.go b/src/api/label.go index fad5fd0..868c8cf 100644 --- a/src/api/label.go +++ b/src/api/label.go @@ -13,11 +13,11 @@ type Label struct { // List .. func (a *Label) List(ctx *gear.Context) error { - req := tpl.ProductURL{} + req := tpl.ProductPaginationURL{} if err := ctx.ParseURL(&req); err != nil { return err } - res, err := a.blls.Label.List(ctx, req.Product) + res, err := a.blls.Label.List(ctx, req.Product, req.Pagination) if err != nil { return err } diff --git a/src/api/label_test.go b/src/api/label_test.go index 333a664..8124ee9 100644 --- a/src/api/label_test.go +++ b/src/api/label_test.go @@ -11,27 +11,29 @@ import ( "github.com/teambition/urbs-setting/src/tpl" ) -func createLabel(appHost, productName string) (*schema.Label, error) { +func createLabel(tt *TestTools, productName string) (label schema.Label, err error) { name := tpl.RandLabel() - res, err := request.Post(fmt.Sprintf("%s/v1/products/%s/labels", appHost, productName)). + _, err = request.Post(fmt.Sprintf("%s/v1/products/%s/labels", tt.Host, productName)). Set("Content-Type", "application/json"). Send(tpl.LabelBody{Name: name, Desc: name}). End() - if err != nil { - return nil, err + var product schema.Product + if err == nil { + err = tt.DB.Where("name = ?", productName).First(&product).Error } - json := tpl.LabelRes{} - res.JSON(&json) - return &json.Result, nil + if err == nil { + err = tt.DB.Where("product_id = ? and name = ?", product.ID, name).First(&label).Error + } + return } func TestLabelAPIs(t *testing.T) { tt, cleanup := SetUpTestTools() defer cleanup() - product, err := createProduct(tt.Host) + product, err := createProduct(tt) assert.Nil(t, err) n1 := tpl.RandLabel() @@ -51,13 +53,13 @@ func TestLabelAPIs(t *testing.T) { assert.Nil(err) assert.True(strings.Contains(text, `"offline_at":null`)) - json := tpl.LabelRes{} + json := tpl.LabelInfoRes{} res.JSON(&json) assert.NotNil(json.Result) assert.Equal(n1, json.Result.Name) assert.Equal("test", json.Result.Desc) - assert.Equal("", json.Result.Channels) - assert.Equal("", json.Result.Clients) + assert.Equal([]string{}, json.Result.Channels) + assert.Equal([]string{}, json.Result.Clients) assert.True(json.Result.CreatedAt.UTC().Unix() > int64(0)) assert.True(json.Result.UpdatedAt.UTC().Unix() > int64(0)) assert.Nil(json.Result.OfflineAt) @@ -100,7 +102,7 @@ func TestLabelAPIs(t *testing.T) { assert.Nil(err) assert.True(strings.Contains(text, n1)) - json := tpl.LabelsRes{} + json := tpl.LabelsInfoRes{} res.JSON(&json) assert.NotNil(json.Result) assert.True(len(json.Result) > 0) @@ -158,7 +160,7 @@ func TestLabelAPIs(t *testing.T) { }) t.Run(`"PUT /products/:product/labels/:label+:offline"`, func(t *testing.T) { - label, err := createLabel(tt.Host, product.Name) + label, err := createLabel(tt, product.Name) assert.Nil(t, err) t.Run("should work", func(t *testing.T) { @@ -173,7 +175,7 @@ func TestLabelAPIs(t *testing.T) { res.JSON(&json) assert.True(json.Result) - l := *label + l := label assert.Nil(tt.DB.First(&l).Error) assert.NotNil(l.OfflineAt) }) @@ -191,20 +193,20 @@ func TestLabelAPIs(t *testing.T) { res.JSON(&json) assert.False(json.Result) - l := *label + l := label assert.Nil(tt.DB.First(&l).Error) assert.NotNil(l.OfflineAt) }) }) t.Run(`POST "/products/:product/labels/:label+:assign"`, func(t *testing.T) { - label, err := createLabel(tt.Host, product.Name) + label, err := createLabel(tt, product.Name) assert.Nil(t, err) - users, err := createUsers(tt.Host, 3) + users, err := createUsers(tt, 3) assert.Nil(t, err) - group, err := createGroup(tt.Host) + group, err := createGroup(tt) assert.Nil(t, err) t.Run("should work", func(t *testing.T) { @@ -213,8 +215,8 @@ func TestLabelAPIs(t *testing.T) { res, err := request.Post(fmt.Sprintf("%s/v1/products/%s/labels/%s:assign", tt.Host, product.Name, label.Name)). Set("Content-Type", "application/json"). Send(tpl.UsersGroupsBody{ - Users: users[0:2], - Groups: []string{group}, + Users: schema.GetUsersUID(users[0:2]), + Groups: []string{group.UID}, }). End() assert.Nil(err) @@ -238,7 +240,7 @@ func TestLabelAPIs(t *testing.T) { res, err := request.Post(fmt.Sprintf("%s/v1/products/%s/labels/%s:assign", tt.Host, product.Name, label.Name)). Set("Content-Type", "application/json"). Send(tpl.UsersGroupsBody{ - Users: []string{users[0], users[2]}, + Users: []string{users[0].UID, users[2].UID}, }). End() assert.Nil(err) diff --git a/src/api/module.go b/src/api/module.go index 51f35c3..5586a3d 100644 --- a/src/api/module.go +++ b/src/api/module.go @@ -13,11 +13,11 @@ type Module struct { // List .. func (a *Module) List(ctx *gear.Context) error { - req := tpl.ProductURL{} + req := tpl.ProductPaginationURL{} if err := ctx.ParseURL(&req); err != nil { return err } - res, err := a.blls.Module.List(ctx, req.Product) + res, err := a.blls.Module.List(ctx, req.Product, req.Pagination) if err != nil { return err } diff --git a/src/api/module_test.go b/src/api/module_test.go index 28f6763..c1f3c0c 100644 --- a/src/api/module_test.go +++ b/src/api/module_test.go @@ -11,27 +11,29 @@ import ( "github.com/teambition/urbs-setting/src/tpl" ) -func createModule(appHost, productName string) (*schema.Module, error) { +func createModule(tt *TestTools, productName string) (module schema.Module, err error) { name := tpl.RandName() - res, err := request.Post(fmt.Sprintf("%s/v1/products/%s/modules", appHost, productName)). + _, err = request.Post(fmt.Sprintf("%s/v1/products/%s/modules", tt.Host, productName)). Set("Content-Type", "application/json"). Send(tpl.NameDescBody{Name: name, Desc: name}). End() - if err != nil { - return nil, err + var product schema.Product + if err == nil { + err = tt.DB.Where("name = ?", productName).First(&product).Error } - json := tpl.ModuleRes{} - res.JSON(&json) - return &json.Result, nil + if err == nil { + err = tt.DB.Where("product_id = ? and name = ?", product.ID, name).First(&module).Error + } + return } func TestModuleAPIs(t *testing.T) { tt, cleanup := SetUpTestTools() defer cleanup() - product, err := createProduct(tt.Host) + product, err := createProduct(tt) assert.Nil(t, err) n1 := tpl.RandName() diff --git a/src/api/product.go b/src/api/product.go index 2f38662..4fa9006 100644 --- a/src/api/product.go +++ b/src/api/product.go @@ -13,7 +13,12 @@ type Product struct { // List .. func (a *Product) List(ctx *gear.Context) error { - res, err := a.blls.Product.List(ctx) + req := tpl.Pagination{} + if err := ctx.ParseURL(&req); err != nil { + return err + } + + res, err := a.blls.Product.List(ctx, req) if err != nil { return err } diff --git a/src/api/product_test.go b/src/api/product_test.go index 3d3430f..7ef0b44 100644 --- a/src/api/product_test.go +++ b/src/api/product_test.go @@ -12,20 +12,17 @@ import ( "github.com/teambition/urbs-setting/src/tpl" ) -func createProduct(appHost string) (*schema.Product, error) { +func createProduct(tt *TestTools) (product schema.Product, err error) { name := tpl.RandName() - res, err := request.Post(fmt.Sprintf("%s/v1/products", appHost)). + _, err = request.Post(fmt.Sprintf("%s/v1/products", tt.Host)). Set("Content-Type", "application/json"). Send(tpl.NameDescBody{Name: name, Desc: name}). End() - if err != nil { - return nil, err + if err == nil { + err = tt.DB.Where("name = ?", name).First(&product).Error } - - json := tpl.ProductRes{} - res.JSON(&json) - return &json.Result, nil + return } func TestProductAPIs(t *testing.T) { @@ -156,16 +153,16 @@ func TestProductAPIs(t *testing.T) { }) t.Run(`"PUT /products/:product+:offline"`, func(t *testing.T) { - product, err := createProduct(tt.Host) + product, err := createProduct(tt) assert.Nil(t, err) - label, err := createLabel(tt.Host, product.Name) + label, err := createLabel(tt, product.Name) assert.Nil(t, err) - module, err := createModule(tt.Host, product.Name) + module, err := createModule(tt, product.Name) assert.Nil(t, err) - setting, err := createSetting(tt.Host, product.Name, module.Name) + setting, err := createSetting(tt, product.Name, module.Name) assert.Nil(t, err) t.Run("should work", func(t *testing.T) { @@ -198,17 +195,17 @@ func TestProductAPIs(t *testing.T) { assert := assert.New(t) assert.Nil(label.OfflineAt) - l := *label + l := label assert.Nil(tt.DB.First(&l).Error) assert.NotNil(l.OfflineAt) assert.Nil(module.OfflineAt) - m := *module + m := module assert.Nil(tt.DB.First(&m).Error) assert.NotNil(m.OfflineAt) assert.Nil(setting.OfflineAt) - s := *setting + s := setting assert.Nil(tt.DB.First(&s).Error) assert.NotNil(s.OfflineAt) assert.True(true) @@ -217,29 +214,29 @@ func TestProductAPIs(t *testing.T) { t.Run("should not effect other data", func(t *testing.T) { assert := assert.New(t) - product1, err := createProduct(tt.Host) + product1, err := createProduct(tt) assert.Nil(err) - product2, err := createProduct(tt.Host) + product2, err := createProduct(tt) assert.Nil(err) - label1, err := createLabel(tt.Host, product1.Name) + label1, err := createLabel(tt, product1.Name) assert.Nil(err) - label2, err := createLabel(tt.Host, product2.Name) + label2, err := createLabel(tt, product2.Name) assert.Nil(err) - users, err := createUsers(tt.Host, 10) + users, err := createUsers(tt, 10) assert.Nil(err) - group, err := createGroup(tt.Host) + group, err := createGroup(tt) assert.Nil(err) res, err := request.Post(fmt.Sprintf("%s/v1/products/%s/labels/%s:assign", tt.Host, product1.Name, label1.Name)). Set("Content-Type", "application/json"). Send(tpl.UsersGroupsBody{ - Users: users, - Groups: []string{group}, + Users: schema.GetUsersUID(users), + Groups: []string{group.UID}, }). End() assert.Nil(err) @@ -255,8 +252,8 @@ func TestProductAPIs(t *testing.T) { res, err = request.Post(fmt.Sprintf("%s/v1/products/%s/labels/%s:assign", tt.Host, product2.Name, label2.Name)). Set("Content-Type", "application/json"). Send(tpl.UsersGroupsBody{ - Users: users, - Groups: []string{group}, + Users: schema.GetUsersUID(users), + Groups: []string{group.UID}, }). End() assert.Nil(err) @@ -275,10 +272,10 @@ func TestProductAPIs(t *testing.T) { time.Sleep(time.Second * 2) - assert.Nil(tt.DB.First(product1).Error) + assert.Nil(tt.DB.First(&product1).Error) assert.NotNil(product1.OfflineAt) - assert.Nil(tt.DB.First(label1).Error) + assert.Nil(tt.DB.First(&label1).Error) assert.NotNil(label1.OfflineAt) assert.Nil(tt.DB.Table(`user_label`).Where("label_id = ?", label1.ID).Count(&count).Error) @@ -287,10 +284,10 @@ func TestProductAPIs(t *testing.T) { assert.Nil(tt.DB.Table(`group_label`).Where("label_id = ?", label1.ID).Count(&count).Error) assert.Equal(int64(0), count) - assert.Nil(tt.DB.First(product2).Error) + assert.Nil(tt.DB.First(&product2).Error) assert.Nil(product2.OfflineAt) - assert.Nil(tt.DB.First(label2).Error) + assert.Nil(tt.DB.First(&label2).Error) assert.Nil(label2.OfflineAt) assert.Nil(tt.DB.Table(`user_label`).Where("label_id = ?", label2.ID).Count(&count).Error) diff --git a/src/api/setting.go b/src/api/setting.go index 35df177..dad2d71 100644 --- a/src/api/setting.go +++ b/src/api/setting.go @@ -17,7 +17,7 @@ func (a *Setting) List(ctx *gear.Context) error { if err := ctx.ParseURL(&req); err != nil { return err } - res, err := a.blls.Setting.List(ctx, req.Product, req.Module) + res, err := a.blls.Setting.List(ctx, req.Product, req.Module, req.Pagination) if err != nil { return err } diff --git a/src/api/setting_test.go b/src/api/setting_test.go index bcdc2cf..a030fa5 100644 --- a/src/api/setting_test.go +++ b/src/api/setting_test.go @@ -11,30 +11,37 @@ import ( "github.com/teambition/urbs-setting/src/tpl" ) -func createSetting(appHost, productName, moduleName string) (*schema.Setting, error) { +func createSetting(tt *TestTools, productName, moduleName string) (setting schema.Setting, err error) { name := tpl.RandName() - res, err := request.Post(fmt.Sprintf("%s/v1/products/%s/modules/%s/settings", appHost, productName, moduleName)). + _, err = request.Post(fmt.Sprintf("%s/v1/products/%s/modules/%s/settings", tt.Host, productName, moduleName)). Set("Content-Type", "application/json"). Send(tpl.NameDescBody{Name: name, Desc: name}). End() - if err != nil { - return nil, err + var product schema.Product + var module schema.Module + if err == nil { + err = tt.DB.Where("name = ?", productName).First(&product).Error } - json := tpl.SettingRes{} - res.JSON(&json) - return &json.Result, nil + if err == nil { + err = tt.DB.Where("product_id = ? and name = ?", product.ID, moduleName).First(&module).Error + } + + if err == nil { + err = tt.DB.Where("module_id = ? and name = ?", module.ID, name).First(&setting).Error + } + return } func TestSettingAPIs(t *testing.T) { tt, cleanup := SetUpTestTools() defer cleanup() - product, err := createProduct(tt.Host) + product, err := createProduct(tt) assert.Nil(t, err) - module, err := createModule(tt.Host, product.Name) + module, err := createModule(tt, product.Name) assert.Nil(t, err) n1 := tpl.RandName() @@ -54,14 +61,14 @@ func TestSettingAPIs(t *testing.T) { assert.Nil(err) assert.True(strings.Contains(text, `"offline_at":null`)) - json := tpl.SettingRes{} + json := tpl.SettingInfoRes{} res.JSON(&json) assert.NotNil(json.Result) assert.Equal(n1, json.Result.Name) assert.Equal("test", json.Result.Desc) - assert.Equal("", json.Result.Channels) - assert.Equal("", json.Result.Clients) - assert.Equal("", json.Result.Values) + assert.Equal([]string{}, json.Result.Channels) + assert.Equal([]string{}, json.Result.Clients) + assert.Equal([]string{}, json.Result.Values) assert.True(json.Result.CreatedAt.UTC().Unix() > int64(0)) assert.True(json.Result.UpdatedAt.UTC().Unix() > int64(0)) assert.Nil(json.Result.OfflineAt) diff --git a/src/api/user.go b/src/api/user.go index 88f88cd..bd6604d 100644 --- a/src/api/user.go +++ b/src/api/user.go @@ -34,12 +34,12 @@ func (a *User) ListLablesInCache(ctx *gear.Context) error { // ListLables .. func (a *User) ListLables(ctx *gear.Context) error { - req := tpl.LabelsURL{} + req := tpl.UIDPaginationURL{} if err := ctx.ParseURL(&req); err != nil { return err } - res, err := a.blls.User.ListLables(ctx, req.UID, req.Product) + res, err := a.blls.User.ListLables(ctx, req.UID, req.Pagination) if err != nil { return err } @@ -85,8 +85,9 @@ func (a *User) RemoveLable(ctx *gear.Context) error { return err } label := &schema.Label{} - if err := service.HIDer.PutHID(label, req.HID); err != nil { - return err + label.ID = service.HIDToID(req.HID, "label") + if label.ID == 0 { + return gear.ErrBadRequest.WithMsgf("invalid hid: %s", req.HID) } if err := a.blls.User.RemoveLable(ctx, req.UID, label.ID); err != nil { return err @@ -107,8 +108,9 @@ func (a *User) RemoveSetting(ctx *gear.Context) error { return err } setting := &schema.Setting{} - if err := service.HIDer.PutHID(setting, req.HID); err != nil { - return err + setting.ID = service.HIDToID(req.HID, "setting") + if setting.ID == 0 { + return gear.ErrBadRequest.WithMsgf("invalid hid: %s", req.HID) } if err := a.blls.User.RemoveSetting(ctx, req.UID, setting.ID); err != nil { return err diff --git a/src/api/user_test.go b/src/api/user_test.go index ad9de84..3228001 100644 --- a/src/api/user_test.go +++ b/src/api/user_test.go @@ -8,25 +8,28 @@ import ( "github.com/DavidCai1993/request" "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" + "github.com/teambition/urbs-setting/src/schema" "github.com/teambition/urbs-setting/src/service" "github.com/teambition/urbs-setting/src/tpl" ) -func createUsers(appHost string, count int) ([]string, error) { +var time2020 = time.Unix(1577836800, 0) + +func createUsers(tt *TestTools, count int) (users []schema.User, err error) { uids := make([]string, count) for i := 0; i < count; i++ { uids[i] = tpl.RandUID() } - _, err := request.Post(fmt.Sprintf("%s/v1/users:batch", appHost)). + _, err = request.Post(fmt.Sprintf("%s/v1/users:batch", tt.Host)). Set("Content-Type", "application/json"). Send(tpl.UsersBody{Users: uids}). End() - if err != nil { - return nil, err + if err == nil { + err = tt.DB.Where("uid in ( ? )", uids).Find(&users).Error } - return uids, nil + return } func cleanupUserLabels(db *gorm.DB, uid string) error { @@ -118,16 +121,16 @@ func TestUserAPIs(t *testing.T) { }) t.Run("user, group, label", func(t *testing.T) { - group, users, err := createGroupWithUsers(tt.Host, 4) + group, users, err := createGroupWithUsers(tt, 4) assert.Nil(t, err) - product, err := createProduct(tt.Host) + product, err := createProduct(tt) assert.Nil(t, err) - label, err := createLabel(tt.Host, product.Name) + label, err := createLabel(tt, product.Name) assert.Nil(t, err) - label1, err := createLabel(tt.Host, product.Name) + label1, err := createLabel(tt, product.Name) assert.Nil(t, err) t.Run(`"GET /users/:uid/labels:cache" for invalid user`, func(t *testing.T) { @@ -148,7 +151,7 @@ func TestUserAPIs(t *testing.T) { t.Run(`"GET /users/:uid/labels:cache" when no label`, func(t *testing.T) { assert := assert.New(t) - res, err := request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[0], product.Name)). + res, err := request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[0].UID, product.Name)). End() assert.Nil(err) assert.Equal(200, res.StatusCode) @@ -166,13 +169,13 @@ func TestUserAPIs(t *testing.T) { res, err := request.Post(fmt.Sprintf("%s/v1/products/%s/labels/%s:assign", tt.Host, product.Name, label.Name)). Set("Content-Type", "application/json"). Send(tpl.UsersGroupsBody{ - Users: users, + Users: schema.GetUsersUID(users), }). End() assert.Nil(err) assert.Equal(200, res.StatusCode) - res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[1], product.Name)). + res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[1].UID, product.Name)). End() assert.Nil(err) assert.Equal(200, res.StatusCode) @@ -183,8 +186,8 @@ func TestUserAPIs(t *testing.T) { assert.Equal(1, len(json.Result)) assert.True(json.Timestamp > 0) assert.Equal(label.Name, json.Result[0].Label) - assert.Equal("", json.Result[0].Clients) - assert.Equal("", json.Result[0].Channels) + assert.Equal(0, len(json.Result[0].Clients)) + assert.Equal(0, len(json.Result[0].Channels)) }) t.Run(`"GET /users/:uid/labels:cache" when group label exists`, func(t *testing.T) { @@ -194,14 +197,14 @@ func TestUserAPIs(t *testing.T) { res, err := request.Post(fmt.Sprintf("%s/v1/products/%s/labels/%s:assign", tt.Host, product.Name, label1.Name)). Set("Content-Type", "application/json"). Send(tpl.UsersGroupsBody{ - Groups: []string{group}, + Groups: []string{group.UID}, }). End() assert.Nil(err) assert.Equal(200, res.StatusCode) // users[1] lables from cache - res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[1], product.Name)). + res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[1].UID, product.Name)). End() assert.Nil(err) assert.Equal(200, res.StatusCode) @@ -212,11 +215,11 @@ func TestUserAPIs(t *testing.T) { assert.Equal(1, len(json.Result)) assert.True(json.Timestamp > 0) assert.Equal(label.Name, json.Result[0].Label) - assert.Equal("", json.Result[0].Clients) - assert.Equal("", json.Result[0].Channels) + assert.Equal(0, len(json.Result[0].Clients)) + assert.Equal(0, len(json.Result[0].Channels)) // users[2] get all lables - res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[2], product.Name)). + res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[2].UID, product.Name)). End() assert.Nil(err) assert.Equal(200, res.StatusCode) @@ -227,8 +230,8 @@ func TestUserAPIs(t *testing.T) { assert.Equal(2, len(json.Result)) assert.True(json.Timestamp > 0) assert.Equal(label1.Name, json.Result[0].Label) - assert.Equal("", json.Result[0].Clients) - assert.Equal("", json.Result[0].Channels) + assert.Equal(0, len(json.Result[0].Clients)) + assert.Equal(0, len(json.Result[0].Channels)) assert.Equal(label.Name, json.Result[1].Label) @@ -236,14 +239,14 @@ func TestUserAPIs(t *testing.T) { res, err = request.Post(fmt.Sprintf("%s/v1/products/%s/labels/%s:assign", tt.Host, product.Name, label.Name)). Set("Content-Type", "application/json"). Send(tpl.UsersGroupsBody{ - Groups: []string{group}, + Groups: []string{group.UID}, }). End() assert.Nil(err) assert.Equal(200, res.StatusCode) // users[2] get all lables - res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[3], product.Name)). + res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[3].UID, product.Name)). End() assert.Nil(err) assert.Equal(200, res.StatusCode) @@ -254,8 +257,8 @@ func TestUserAPIs(t *testing.T) { assert.Equal(2, len(json.Result)) assert.True(json.Timestamp > 0) assert.Equal(label.Name, json.Result[0].Label) - assert.Equal("", json.Result[0].Clients) - assert.Equal("", json.Result[0].Channels) + assert.Equal(0, len(json.Result[0].Clients)) + assert.Equal(0, len(json.Result[0].Channels)) assert.Equal(label1.Name, json.Result[1].Label) }) @@ -263,26 +266,30 @@ func TestUserAPIs(t *testing.T) { t.Run(`"GET /users/:uid/labels"`, func(t *testing.T) { assert := assert.New(t) - res, err := request.Get(fmt.Sprintf("%s/v1/users/%s/labels", tt.Host, users[3])). + res, err := request.Get(fmt.Sprintf("%s/v1/users/%s/labels", tt.Host, users[3].UID)). End() assert.Nil(err) assert.Equal(200, res.StatusCode) json := tpl.LabelsInfoRes{} _, err = res.JSON(&json) + assert.Nil(err) assert.Equal(1, len(json.Result)) - assert.Equal(service.HIDer.HID(label), json.Result[0].HID) + assert.Equal(1, json.TotalSize) + assert.Equal("", json.NextPageToken) + assert.Equal(service.IDToHID(label.ID, "label"), json.Result[0].HID) assert.Equal(product.Name, json.Result[0].Product) assert.Equal(label.Name, json.Result[0].Name) - assert.Equal("", json.Result[0].Clients) - assert.Equal("", json.Result[0].Channels) + assert.Equal(0, len(json.Result[0].Clients)) + assert.Equal(0, len(json.Result[0].Channels)) + assert.True(json.Result[0].CreatedAt.After(time2020)) }) t.Run(`Delete label should work`, func(t *testing.T) { assert := assert.New(t) - res, err := request.Delete(fmt.Sprintf("%s/v1/users/%s/labels/%s", tt.Host, users[3], service.HIDer.HID(label))). + res, err := request.Delete(fmt.Sprintf("%s/v1/users/%s/labels/%s", tt.Host, users[3].UID, service.IDToHID(label.ID, "label"))). End() assert.Nil(err) assert.Equal(200, res.StatusCode) @@ -292,7 +299,7 @@ func TestUserAPIs(t *testing.T) { assert.Nil(err) assert.True(json.Result) - res, err = request.Delete(fmt.Sprintf("%s/v1/groups/%s/labels/%s", tt.Host, group, service.HIDer.HID(label))). + res, err = request.Delete(fmt.Sprintf("%s/v1/groups/%s/labels/%s", tt.Host, group.UID, service.IDToHID(label.ID, "label"))). End() assert.Nil(err) assert.Equal(200, res.StatusCode) @@ -302,8 +309,8 @@ func TestUserAPIs(t *testing.T) { assert.Nil(err) assert.True(json.Result) - assert.Nil(cleanupUserLabels(tt.DB, users[3])) - res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[3], product.Name)). + assert.Nil(cleanupUserLabels(tt.DB, users[3].UID)) + res, err = request.Get(fmt.Sprintf("%s/users/%s/labels:cache?product=%s", tt.Host, users[3].UID, product.Name)). End() assert.Nil(err) assert.Equal(200, res.StatusCode) diff --git a/src/bll/group.go b/src/bll/group.go index f486d5d..ffe8f8e 100644 --- a/src/bll/group.go +++ b/src/bll/group.go @@ -14,17 +14,26 @@ type Group struct { } // List 返回群组列表,TODO:支持分页 -func (b *Group) List(ctx context.Context) (*tpl.GroupsRes, error) { - groups, err := b.ms.Group.Find(ctx) +func (b *Group) List(ctx context.Context, pg tpl.Pagination) (*tpl.GroupsRes, error) { + groups, err := b.ms.Group.Find(ctx, pg) + if err != nil { + return nil, err + } + total, err := b.ms.Group.Count(ctx) if err != nil { return nil, err } res := &tpl.GroupsRes{Result: groups} + res.TotalSize = total + if len(res.Result) > pg.PageSize { + res.NextPageToken = tpl.IDToPageToken(res.Result[pg.PageSize].ID) + res.Result = res.Result[:pg.PageSize] + } return res, nil } // ListLables ... -func (b *Group) ListLables(ctx context.Context, uid, product string) (*tpl.LabelsInfoRes, error) { +func (b *Group) ListLables(ctx context.Context, uid string, pg tpl.Pagination) (*tpl.LabelsInfoRes, error) { group, err := b.ms.Group.FindByUID(ctx, uid, "id") if err != nil { return nil, err @@ -33,16 +42,26 @@ func (b *Group) ListLables(ctx context.Context, uid, product string) (*tpl.Label return nil, gear.ErrNotFound.WithMsgf("group %s not found", uid) } - labels, err := b.ms.Group.FindLables(ctx, group.ID, product) + labels, err := b.ms.Group.FindLables(ctx, group.ID, pg) + if err != nil { + return nil, err + } + total, err := b.ms.Group.CountLabels(ctx, group.ID) if err != nil { return nil, err } + res := &tpl.LabelsInfoRes{Result: labels} + res.TotalSize = total + if len(res.Result) > pg.PageSize { + res.NextPageToken = tpl.IDToPageToken(res.Result[pg.PageSize].ID) + res.Result = res.Result[:pg.PageSize] + } return res, nil } // ListMembers ... -func (b *Group) ListMembers(ctx context.Context, uid string) (*tpl.GroupMembersRes, error) { +func (b *Group) ListMembers(ctx context.Context, uid string, pg tpl.Pagination) (*tpl.GroupMembersRes, error) { group, err := b.ms.Group.FindByUID(ctx, uid, "id") if err != nil { return nil, err @@ -51,11 +70,20 @@ func (b *Group) ListMembers(ctx context.Context, uid string) (*tpl.GroupMembersR return nil, gear.ErrNotFound.WithMsgf("group %s not found", uid) } - members, err := b.ms.Group.FindMembers(ctx, group.ID) + members, err := b.ms.Group.FindMembers(ctx, group.ID, pg) + if err != nil { + return nil, err + } + total, err := b.ms.Group.CountMembers(ctx, group.ID) if err != nil { return nil, err } res := &tpl.GroupMembersRes{Result: members} + res.TotalSize = total + if len(res.Result) > pg.PageSize { + res.NextPageToken = tpl.IDToPageToken(res.Result[pg.PageSize].ID) + res.Result = res.Result[:pg.PageSize] + } return res, nil } diff --git a/src/bll/label.go b/src/bll/label.go index 9690acb..b41cf0a 100644 --- a/src/bll/label.go +++ b/src/bll/label.go @@ -14,8 +14,8 @@ type Label struct { ms *model.Models } -// List 返回产品下的标签列表,TODO:支持分页 -func (b *Label) List(ctx context.Context, productName string) (*tpl.LabelsRes, error) { +// List 返回产品下的标签列表 +func (b *Label) List(ctx context.Context, productName string, pg tpl.Pagination) (*tpl.LabelsInfoRes, error) { product, err := b.ms.Product.FindByName(ctx, productName, "id, `deleted_at`") if err != nil { return nil, err @@ -26,17 +26,27 @@ func (b *Label) List(ctx context.Context, productName string) (*tpl.LabelsRes, e if product.DeletedAt != nil { return nil, gear.ErrNotFound.WithMsgf("product %s was deleted", productName) } - labels, err := b.ms.Label.Find(ctx, product.ID) + labels, err := b.ms.Label.Find(ctx, product.ID, pg) + if err != nil { + return nil, err + } + total, err := b.ms.Label.Count(ctx, product.ID) if err != nil { return nil, err } - res := &tpl.LabelsRes{Result: labels} + labelInfos := tpl.LabelInfosFrom(labels, productName) + res := &tpl.LabelsInfoRes{Result: labelInfos} + res.TotalSize = total + if len(res.Result) > pg.PageSize { + res.NextPageToken = tpl.IDToPageToken(res.Result[pg.PageSize].ID) + res.Result = res.Result[:pg.PageSize] + } return res, nil } // Create 创建标签 -func (b *Label) Create(ctx context.Context, productName, labelName, desc string) (*tpl.LabelRes, error) { +func (b *Label) Create(ctx context.Context, productName, labelName, desc string) (*tpl.LabelInfoRes, error) { product, err := b.ms.Product.FindByName(ctx, productName, "id, `offline_at`, `deleted_at`") if err != nil { return nil, err @@ -55,7 +65,7 @@ func (b *Label) Create(ctx context.Context, productName, labelName, desc string) if err = b.ms.Label.Create(ctx, &label); err != nil { return nil, err } - return &tpl.LabelRes{Result: label}, nil + return &tpl.LabelInfoRes{Result: tpl.LabelInfoFrom(label, productName)}, nil } // Offline 下线标签 diff --git a/src/bll/module.go b/src/bll/module.go index ff5cd89..78d2a8e 100644 --- a/src/bll/module.go +++ b/src/bll/module.go @@ -15,7 +15,7 @@ type Module struct { } // List 返回产品下的功能模块列表,TODO:支持分页 -func (b *Module) List(ctx context.Context, productName string) (*tpl.ModulesRes, error) { +func (b *Module) List(ctx context.Context, productName string, pg tpl.Pagination) (*tpl.ModulesRes, error) { product, err := b.ms.Product.FindByName(ctx, productName, "id, `deleted_at`") if err != nil { return nil, err @@ -26,12 +26,22 @@ func (b *Module) List(ctx context.Context, productName string) (*tpl.ModulesRes, if product.DeletedAt != nil { return nil, gear.ErrNotFound.WithMsgf("product %s was deleted", productName) } - modules, err := b.ms.Module.Find(ctx, product.ID) + modules, err := b.ms.Module.Find(ctx, product.ID, pg) + if err != nil { + return nil, err + } + + total, err := b.ms.Module.Count(ctx, product.ID) if err != nil { return nil, err } res := &tpl.ModulesRes{Result: modules} + res.TotalSize = total + if len(res.Result) > pg.PageSize { + res.NextPageToken = tpl.IDToPageToken(res.Result[pg.PageSize].ID) + res.Result = res.Result[:pg.PageSize] + } return res, nil } diff --git a/src/bll/product.go b/src/bll/product.go index 954d567..f3ff412 100644 --- a/src/bll/product.go +++ b/src/bll/product.go @@ -14,13 +14,23 @@ type Product struct { ms *model.Models } -// List 返回产品列表,TODO:支持分页 -func (b *Product) List(ctx context.Context) (*tpl.ProductsRes, error) { - products, err := b.ms.Product.Find(ctx) +// List 返回产品列表 +func (b *Product) List(ctx context.Context, pg tpl.Pagination) (*tpl.ProductsRes, error) { + products, err := b.ms.Product.Find(ctx, pg) if err != nil { return nil, err } + total, err := b.ms.Product.Count(ctx) + if err != nil { + return nil, err + } + res := &tpl.ProductsRes{Result: products} + res.TotalSize = total + if len(res.Result) > pg.PageSize { + res.NextPageToken = tpl.IDToPageToken(res.Result[pg.PageSize].ID) + res.Result = res.Result[:pg.PageSize] + } return res, nil } diff --git a/src/bll/setting.go b/src/bll/setting.go index ac84d70..a209817 100644 --- a/src/bll/setting.go +++ b/src/bll/setting.go @@ -15,7 +15,7 @@ type Setting struct { } // List 返回产品下的功能模块配置项列表,TODO:支持分页 -func (b *Setting) List(ctx context.Context, productName, moduleName string) (*tpl.SettingsRes, error) { +func (b *Setting) List(ctx context.Context, productName, moduleName string, pg tpl.Pagination) (*tpl.SettingsInfoRes, error) { product, err := b.ms.Product.FindByName(ctx, productName, "id, `deleted_at`") if err != nil { return nil, err @@ -35,17 +35,26 @@ func (b *Setting) List(ctx context.Context, productName, moduleName string) (*tp return nil, gear.ErrNotFound.WithMsgf("module %s not found", moduleName) } - settings, err := b.ms.Setting.Find(ctx, module.ID) + settings, err := b.ms.Setting.Find(ctx, module.ID, pg) + if err != nil { + return nil, err + } + total, err := b.ms.Setting.Count(ctx, module.ID) if err != nil { return nil, err } - res := &tpl.SettingsRes{Result: settings} + res := &tpl.SettingsInfoRes{Result: tpl.SettingInfosFrom(settings, productName, moduleName)} + res.TotalSize = total + if len(res.Result) > pg.PageSize { + res.NextPageToken = tpl.IDToPageToken(res.Result[pg.PageSize].ID) + res.Result = res.Result[:pg.PageSize] + } return res, nil } // Create 创建功能模块配置项 -func (b *Setting) Create(ctx context.Context, productName, moduleName, settingName, desc string) (*tpl.SettingRes, error) { +func (b *Setting) Create(ctx context.Context, productName, moduleName, settingName, desc string) (*tpl.SettingInfoRes, error) { product, err := b.ms.Product.FindByName(ctx, productName, "id, `offline_at`, `deleted_at`") if err != nil { return nil, err @@ -75,7 +84,7 @@ func (b *Setting) Create(ctx context.Context, productName, moduleName, settingNa if err = b.ms.Setting.Create(ctx, setting); err != nil { return nil, err } - return &tpl.SettingRes{Result: *setting}, nil + return &tpl.SettingInfoRes{Result: tpl.SettingInfoFrom(*setting, productName, moduleName)}, nil } // Offline 下线功能模块配置项 diff --git a/src/bll/user.go b/src/bll/user.go index 775ebc5..8dc333d 100644 --- a/src/bll/user.go +++ b/src/bll/user.go @@ -37,16 +37,16 @@ func (b *User) ListLablesInCache(ctx context.Context, uid, product string) (*tpl } } - labels := user.GetLabels() - if result, ok := labels[product]; ok { - res.Result = result + labels := user.GetLabels(product) + if len(labels) > 0 { + res.Result = labels res.Timestamp = user.ActiveAt } return res, nil } // ListLables ... -func (b *User) ListLables(ctx context.Context, uid, product string) (*tpl.LabelsInfoRes, error) { +func (b *User) ListLables(ctx context.Context, uid string, pg tpl.Pagination) (*tpl.LabelsInfoRes, error) { user, err := b.ms.User.FindByUID(ctx, uid, "id") if err != nil { return nil, err @@ -55,11 +55,21 @@ func (b *User) ListLables(ctx context.Context, uid, product string) (*tpl.Labels return nil, gear.ErrNotFound.WithMsgf("user %s not found", uid) } - labels, err := b.ms.User.FindLables(ctx, user.ID, product) + labels, err := b.ms.User.FindLables(ctx, user.ID, pg) if err != nil { return nil, err } + total, err := b.ms.User.CountLabels(ctx, user.ID) + if err != nil { + return nil, err + } + res := &tpl.LabelsInfoRes{Result: labels} + res.TotalSize = total + if len(res.Result) > pg.PageSize { + res.NextPageToken = tpl.IDToPageToken(res.Result[pg.PageSize].ID) + res.Result = res.Result[:pg.PageSize] + } return res, nil } diff --git a/src/model/group.go b/src/model/group.go index cbc3851..7944dd9 100644 --- a/src/model/group.go +++ b/src/model/group.go @@ -38,22 +38,36 @@ func (m *Group) FindByUID(ctx context.Context, uid string, selectStr string) (*s } // Find 根据条件查找 groups -func (m *Group) Find(ctx context.Context) ([]schema.Group, error) { +func (m *Group) Find(ctx context.Context, pg tpl.Pagination) ([]schema.Group, error) { groups := make([]schema.Group, 0) - err := m.DB.Order("`created_at`").Limit(1000).Find(&groups).Error + pageToken := pg.TokenToID() + + err := m.DB.Where("`id` >= ?", pageToken). + Order("`id`").Limit(pg.PageSize + 1).Find(&groups).Error return groups, err } -const groupLabelsSQL = "select t2.`id`, t2.`name`, t2.`description`, t2.`channels`, t2.`clients`, t3.`name` as `product` " + +// Count 计算 group 总数 +func (m *Group) Count(ctx context.Context) (int, error) { + + count := 0 + err := m.DB.Model(&schema.Group{}).Count(&count).Error + return count, err +} + +const groupLabelsSQL = "select t2.`id`, t2.`created_at`, t2.`updated_at`, t2.`offline_at`, t2.`name`, " + + "t2.`description`, t2.`status`, t2.`channels`, t2.`clients`, t3.`name` as `product` " + "from `group_label` t1, `urbs_label` t2, `urbs_product` t3 " + - "where t1.`group_id` = ? and t1.`label_id` = t2.`id` and t2.`product_id` = t3.id " + - "order by t1.`created_at` desc " + - "limit 1000" + "where t1.`group_id` = ? and t1.`id` >= ? and t1.`label_id` = t2.`id` and t2.`product_id` = t3.id " + + "order by t1.`id` asc " + + "limit ?" // FindLables 根据群组 ID 返回其 labels 数据。TODO:支持更多筛选条件和分页 -func (m *Group) FindLables(ctx context.Context, groupID int64, product string) ([]tpl.LabelInfo, error) { +func (m *Group) FindLables(ctx context.Context, groupID int64, pg tpl.Pagination) ([]tpl.LabelInfo, error) { data := []tpl.LabelInfo{} - rows, err := m.DB.Raw(groupLabelsSQL, groupID).Rows() + pageToken := pg.TokenToID() + + rows, err := m.DB.Raw(groupLabelsSQL, groupID, pageToken, pg.PageSize+1).Rows() defer rows.Close() if err != nil { @@ -61,18 +75,30 @@ func (m *Group) FindLables(ctx context.Context, groupID int64, product string) ( } for rows.Next() { - label := schema.Label{} + var clients string + var channels string labelInfo := tpl.LabelInfo{} - if err := rows.Scan(&label.ID, &labelInfo.Name, &labelInfo.Desc, &labelInfo.Channels, &labelInfo.Clients, &labelInfo.Product); err != nil { + if err := rows.Scan(&labelInfo.ID, &labelInfo.CreatedAt, &labelInfo.UpdatedAt, &labelInfo.OfflineAt, + &labelInfo.Name, &labelInfo.Desc, &labelInfo.Status, &channels, &clients, &labelInfo.Product); err != nil { return nil, err } - labelInfo.HID = service.HIDer.HID(label) + labelInfo.Channels = tpl.StringToSlice(channels) + labelInfo.Clients = tpl.StringToSlice(clients) + labelInfo.HID = service.IDToHID(labelInfo.ID, "label") data = append(data, labelInfo) } return data, nil } +// CountLabels 计算 group labels 总数 +func (m *Group) CountLabels(ctx context.Context, groupID int64) (int, error) { + + count := 0 + err := m.DB.Model(&schema.GroupLabel{}).Where("group_id = ?", groupID).Count(&count).Error + return count, err +} + // BatchAdd 批量添加群组 func (m *Group) BatchAdd(ctx context.Context, groups []tpl.GroupBody) error { if len(groups) == 0 { @@ -80,14 +106,14 @@ func (m *Group) BatchAdd(ctx context.Context, groups []tpl.GroupBody) error { } syncAt := time.Now().UTC().Unix() - stmt, err := m.DB.DB().Prepare("insert ignore into `urbs_group` (`uid`, `sync_at`, `description`) values (?, ?, ?)") + stmt, err := m.DB.DB().Prepare("insert ignore into `urbs_group` (`uid`, `kind`, `sync_at`, `description`) values (?, ?, ?, ?)") if err != nil { return err } defer stmt.Close() for _, group := range groups { - if _, err := stmt.Exec(group.UID, syncAt, group.Desc); err != nil { + if _, err := stmt.Exec(group.UID, group.Kind, syncAt, group.Desc); err != nil { return err } } @@ -107,16 +133,26 @@ func (m *Group) BatchAddMembers(ctx context.Context, group *schema.Group, users return m.DB.Exec(batchAddGroupMemberSQL, group.ID, group.SyncAt, users).Error } -const groupMembersSQL = "select t2.`uid`, t1.`created_at`, t1.`sync_at` " + +// CountMembers 计算成员总数 +func (m *Group) CountMembers(ctx context.Context, groupID int64) (int, error) { + + count := 0 + err := m.DB.Model(&schema.UserGroup{}).Where("group_id = ?", groupID).Count(&count).Error + return count, err +} + +const groupMembersSQL = "select t1.`id`, t2.`uid`, t1.`created_at`, t1.`sync_at` " + "from `user_group` t1, `urbs_user` t2 " + - "where t1.`group_id` = ? and t1.`user_id` = t2.`id` " + - "order by t1.`sync_at` desc " + - "limit 10000" + "where t1.`group_id` = ? and t1.`id` >= ? and t1.`user_id` = t2.`id` " + + "order by t1.`id` asc " + + "limit ?" // FindMembers 根据条件查找群组成员 -func (m *Group) FindMembers(ctx context.Context, groupID int64) ([]tpl.GroupMember, error) { +func (m *Group) FindMembers(ctx context.Context, groupID int64, pg tpl.Pagination) ([]tpl.GroupMember, error) { data := []tpl.GroupMember{} - rows, err := m.DB.Raw(groupMembersSQL, groupID).Rows() + pageToken := pg.TokenToID() + + rows, err := m.DB.Raw(groupMembersSQL, groupID, pageToken, pg.PageSize+1).Rows() defer rows.Close() if err != nil { @@ -125,7 +161,7 @@ func (m *Group) FindMembers(ctx context.Context, groupID int64) ([]tpl.GroupMemb for rows.Next() { member := tpl.GroupMember{} - if err := rows.Scan(&member.User, &member.CreatedAt, &member.SyncAt); err != nil { + if err := rows.Scan(&member.ID, &member.User, &member.CreatedAt, &member.SyncAt); err != nil { return nil, err } data = append(data, member) @@ -141,7 +177,7 @@ func (m *Group) RemoveMembers(ctx context.Context, groupID, userID int64, syncLt err = m.DB.Where("group_id = ? and sync_at < ?", groupID, syncLt).Delete(&schema.UserGroup{}).Error } if err == nil && userID > 0 { - err = m.DB.Where("group_id = ? and user_id = ?", groupID, userID).Delete(&schema.UserGroup{}).Error + err = m.DB.Where("user_id = ? and group_id = ?", userID, groupID).Delete(&schema.UserGroup{}).Error } return err } diff --git a/src/model/label.go b/src/model/label.go index 121a940..faad16d 100644 --- a/src/model/label.go +++ b/src/model/label.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" "github.com/teambition/urbs-setting/src/schema" + "github.com/teambition/urbs-setting/src/tpl" ) // Label ... @@ -37,12 +38,21 @@ func (m *Label) FindByName(ctx context.Context, productID int64, name, selectStr } // Find 根据条件查找 labels -func (m *Label) Find(ctx context.Context, productID int64) ([]schema.Label, error) { +func (m *Label) Find(ctx context.Context, productID int64, pg tpl.Pagination) ([]schema.Label, error) { labels := make([]schema.Label, 0) - err := m.DB.Where("`product_id` = ?", productID).Order("`status`, `created_at`").Limit(1000).Find(&labels).Error + pageToken := pg.TokenToID() + err := m.DB.Where("`product_id` = ? and `id` >= ?", productID, pageToken). + Order("`id`").Limit(pg.PageSize + 1).Find(&labels).Error return labels, err } +// Count 计算 product labels 总数 +func (m *Label) Count(ctx context.Context, productID int64) (int, error) { + count := 0 + err := m.DB.Model(&schema.Label{}).Where("product_id = ?", productID).Count(&count).Error + return count, err +} + // Create ... func (m *Label) Create(ctx context.Context, label *schema.Label) error { return m.DB.Create(label).Error diff --git a/src/model/module.go b/src/model/module.go index 9bc38ac..aaeea0d 100644 --- a/src/model/module.go +++ b/src/model/module.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" "github.com/teambition/urbs-setting/src/schema" + "github.com/teambition/urbs-setting/src/tpl" ) // Module ... @@ -36,12 +37,21 @@ func (m *Module) FindByName(ctx context.Context, productID int64, name, selectSt } // Find 根据条件查找 modules -func (m *Module) Find(ctx context.Context, productID int64) ([]schema.Module, error) { +func (m *Module) Find(ctx context.Context, productID int64, pg tpl.Pagination) ([]schema.Module, error) { modules := make([]schema.Module, 0) - err := m.DB.Where("`product_id` = ?", productID).Order("`status`, `created_at`").Limit(1000).Find(&modules).Error + pageToken := pg.TokenToID() + err := m.DB.Where("`product_id` = ? and `id` >= ?", productID, pageToken). + Order("`id`").Limit(pg.PageSize + 1).Find(&modules).Error return modules, err } +// Count 计算 product modules 总数 +func (m *Module) Count(ctx context.Context, productID int64) (int, error) { + count := 0 + err := m.DB.Model(&schema.Module{}).Where("product_id = ?", productID).Count(&count).Error + return count, err +} + // Create ... func (m *Module) Create(ctx context.Context, module *schema.Module) error { return m.DB.Create(module).Error diff --git a/src/model/product.go b/src/model/product.go index 29528ef..25e7d91 100644 --- a/src/model/product.go +++ b/src/model/product.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" "github.com/teambition/urbs-setting/src/schema" + "github.com/teambition/urbs-setting/src/tpl" ) // Product ... @@ -36,12 +37,21 @@ func (m *Product) FindByName(ctx context.Context, name, selectStr string) (*sche } // Find 根据条件查找 products -func (m *Product) Find(ctx context.Context) ([]schema.Product, error) { +func (m *Product) Find(ctx context.Context, pg tpl.Pagination) ([]schema.Product, error) { products := make([]schema.Product, 0) - err := m.DB.Where("`deleted_at` is null").Order("`status`, `created_at`").Limit(1000).Find(&products).Error + pageToken := pg.TokenToID() + err := m.DB.Where("`id` >= ? and `deleted_at` is null", pageToken). + Order("`id`").Limit(pg.PageSize + 1).Find(&products).Error return products, err } +// Count 计算 products 总数 +func (m *Product) Count(ctx context.Context) (int, error) { + count := 0 + err := m.DB.Model(&schema.Product{}).Where("deleted_at is null").Count(&count).Error + return count, err +} + // Create ... func (m *Product) Create(ctx context.Context, product *schema.Product) error { return m.DB.Create(product).Error diff --git a/src/model/setting.go b/src/model/setting.go index 2cc043b..f2c7801 100644 --- a/src/model/setting.go +++ b/src/model/setting.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" "github.com/teambition/urbs-setting/src/schema" + "github.com/teambition/urbs-setting/src/tpl" ) // Setting ... @@ -36,12 +37,21 @@ func (m *Setting) FindByName(ctx context.Context, moduleID int64, name, selectSt } // Find 根据条件查找 settings -func (m *Setting) Find(ctx context.Context, moduleID int64) ([]schema.Setting, error) { +func (m *Setting) Find(ctx context.Context, moduleID int64, pg tpl.Pagination) ([]schema.Setting, error) { settings := make([]schema.Setting, 0) - err := m.DB.Where("`module_id` = ?", moduleID).Order("`status`, `created_at`").Limit(1000).Find(&settings).Error + pageToken := pg.TokenToID() + err := m.DB.Where("`module_id` = ? and `id` >= ?", moduleID, pageToken). + Order("`id`").Limit(pg.PageSize + 1).Find(&settings).Error return settings, err } +// Count 计算 module settings 总数 +func (m *Setting) Count(ctx context.Context, moduleID int64) (int, error) { + count := 0 + err := m.DB.Model(&schema.Setting{}).Where("module_id = ?", moduleID).Count(&count).Error + return count, err +} + // Create ... func (m *Setting) Create(ctx context.Context, setting *schema.Setting) error { return m.DB.Create(setting).Error diff --git a/src/model/user.go b/src/model/user.go index c62ffaa..1619532 100644 --- a/src/model/user.go +++ b/src/model/user.go @@ -43,12 +43,12 @@ func (m *User) FindByUID(ctx context.Context, uid string, selectStr string) (*sc const userCacheLabelsSQL = "(select t2.`id`, t1.`created_at`, t2.`name`, t3.`name` as `p`, t2.`channels`, t2.`clients` " + "from `user_label` t1, `urbs_label` t2, `urbs_product` t3 " + "where t1.`user_id` = ? and t1.`label_id` = t2.`id` and t2.`product_id` = t3.`id` " + - "order by t1.`created_at` desc limit 1000) " + + "order by t1.`id` desc limit 200) " + "union all " + // 期望能基于 id distinct "(select t3.`id`, t2.`created_at`, t3.`name`, t4.`name` as `p`, t3.`channels`, t3.`clients` " + "from `user_group` t1, `group_label` t2, `urbs_label` t3, `urbs_product` t4 " + "where t1.`user_id` = ? and t1.`group_id` = t2.`group_id` and t2.`label_id` = t3.`id` and t3.`product_id` = t4.`id` " + - "order by t2.`created_at` desc limit 1000)" + + "order by t2.`id` desc limit 200)" + "order by `created_at` desc" // RefreshLabels 更新 user 上的 labels 缓存,包括通过 group 关系获得的 labels @@ -78,15 +78,19 @@ func (m *User) RefreshLabels(ctx context.Context, id int64, now int64) (*schema. for rows.Next() { var ignoreID int64 var product string + var clients string + var channels string label := schema.UserCacheLabel{} // ScanRows 扫描一行记录到 user - if err := rows.Scan(&ignoreID, &ignoreTime, &label.Label, &product, &label.Channels, &label.Clients); err != nil { + if err := rows.Scan(&ignoreID, &ignoreTime, &label.Label, &product, &channels, &clients); err != nil { return err } if _, ok := set[ignoreID]; ok { continue // 去重 } + label.Channels = tpl.StringToSlice(channels) + label.Clients = tpl.StringToSlice(clients) set[ignoreID] = struct{}{} arr, ok := data[product] if !ok { @@ -94,7 +98,7 @@ func (m *User) RefreshLabels(ctx context.Context, id int64, now int64) (*schema. } data[product] = append(arr, label) } - user.PutLabels(data) + _ = user.PutLabels(data) user.ActiveAt = time.Now().UTC().Unix() return tx.Model(&schema.User{ID: id}).Updates(map[string]interface{}{ @@ -104,16 +108,19 @@ func (m *User) RefreshLabels(ctx context.Context, id int64, now int64) (*schema. return user, err } -const userLabelsSQL = "select t2.`id`, t2.`name`, t2.`description`, t2.`channels`, t2.`clients`, t3.`name` as `product` " + +const userLabelsSQL = "select t2.`id`, t2.`created_at`, t2.`updated_at`, t2.`offline_at`, t2.`name`, " + + "t2.`description`, t2.`status`, t2.`channels`, t2.`clients`, t3.`name` as `product` " + "from `user_label` t1, `urbs_label` t2, `urbs_product` t3 " + - "where t1.`user_id` = ? and t1.`label_id` = t2.`id` and t2.`product_id` = t3.id " + - "order by t1.`created_at` desc " + - "limit 1000" + "where t1.`user_id` = ? and t1.`id` >= ? and t1.`label_id` = t2.`id` and t2.`product_id` = t3.id " + + "order by t1.`id` asc " + + "limit ?" -// FindLables 根据用户 ID 返回其 labels 数据。TODO:支持更多筛选条件和分页 -func (m *User) FindLables(ctx context.Context, userID int64, product string) ([]tpl.LabelInfo, error) { +// FindLables 根据用户 ID 返回其 labels 数据。 +func (m *User) FindLables(ctx context.Context, userID int64, pg tpl.Pagination) ([]tpl.LabelInfo, error) { data := []tpl.LabelInfo{} - rows, err := m.DB.Raw(userLabelsSQL, userID).Rows() + pageToken := pg.TokenToID() + + rows, err := m.DB.Raw(userLabelsSQL, userID, pageToken, pg.PageSize+1).Rows() defer rows.Close() if err != nil { @@ -121,18 +128,30 @@ func (m *User) FindLables(ctx context.Context, userID int64, product string) ([] } for rows.Next() { - label := schema.Label{} labelInfo := tpl.LabelInfo{} - if err := rows.Scan(&label.ID, &labelInfo.Name, &labelInfo.Desc, &labelInfo.Channels, &labelInfo.Clients, &labelInfo.Product); err != nil { + var clients string + var channels string + if err := rows.Scan(&labelInfo.ID, &labelInfo.CreatedAt, &labelInfo.UpdatedAt, &labelInfo.OfflineAt, + &labelInfo.Name, &labelInfo.Desc, &labelInfo.Status, &channels, &clients, &labelInfo.Product); err != nil { return nil, err } - labelInfo.HID = service.HIDer.HID(label) + labelInfo.Channels = tpl.StringToSlice(channels) + labelInfo.Clients = tpl.StringToSlice(clients) + labelInfo.HID = service.IDToHID(labelInfo.ID, "label") data = append(data, labelInfo) } return data, nil } +// CountLabels 计算 user labels 总数 +func (m *User) CountLabels(ctx context.Context, userID int64) (int, error) { + + count := 0 + err := m.DB.Model(&schema.UserLabel{}).Where("user_id = ?", userID).Count(&count).Error + return count, err +} + // BatchAdd 批量添加用户 // uids 经过了 `^[0-9A-Za-z._-]{3,63}$` 正则验证 func (m *User) BatchAdd(ctx context.Context, uids []string) error { diff --git a/src/schema/group.go b/src/schema/group.go index b1ce83a..bb8a7e8 100644 --- a/src/schema/group.go +++ b/src/schema/group.go @@ -8,11 +8,12 @@ import ( // Group 详见 ./sql/schema.sql table `urbs_group` // 用户群组 type Group struct { - ID int64 `gorm:"column:id" json:"id"` + ID int64 `gorm:"column:id"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` - SyncAt int64 `gorm:"column:sync_at" json:"sync_at"` // 群组成员同步时间点 + SyncAt int64 `gorm:"column:sync_at" json:"sync_at"` // 群组成员同步时间点,1970 以来的秒数 UID string `gorm:"column:uid" json:"uid"` // varchar(63),群组外部ID,表内唯一, 如 Teambition organization id + Kind string `gorm:"column:kind" json:"kind"` // varchar(63),群组外部ID,表内唯一, 如 Teambition organization id Desc string `gorm:"column:description" json:"desc"` // varchar(1022),群组描述 } diff --git a/src/schema/label.go b/src/schema/label.go index 50eeaac..0ff1b86 100644 --- a/src/schema/label.go +++ b/src/schema/label.go @@ -8,16 +8,16 @@ import ( // Label 详见 ./sql/schema.sql table `urbs_label` // 灰度标签 type Label struct { - ID int64 `gorm:"column:id" json:"id"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` - OfflineAt *time.Time `gorm:"column:offline_at" json:"offline_at"` // 计划下线时间,用于灰度管理 - ProductID int64 `gorm:"column:product_id" json:"product_id"` // 所从属的产品线 ID - Name string `gorm:"column:name" json:"name"` // varchar(63) 灰度标签名称,产品线内唯一 - Desc string `gorm:"column:description" json:"desc"` // varchar(1022) 灰度标签描述 - Channels string `gorm:"column:channels" json:"channels"` // varchar(255) 标签适用的版本通道,未配置表示都适用 - Clients string `gorm:"column:clients" json:"clients"` // varchar(255) 标签适用的客户端类型,未配置表示都适用 - Status int64 `gorm:"column:status" json:"status"` // -1 下线弃用,0 未使用,大于 0 为被使用计数 + ID int64 `gorm:"column:id"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + OfflineAt *time.Time `gorm:"column:offline_at"` // 计划下线时间,用于灰度管理 + ProductID int64 `gorm:"column:product_id"` // 所从属的产品线 ID + Name string `gorm:"column:name"` // varchar(63) 灰度标签名称,产品线内唯一 + Desc string `gorm:"column:description"` // varchar(1022) 灰度标签描述 + Channels string `gorm:"column:channels"` // varchar(255) 标签适用的版本通道,未配置表示都适用 + Clients string `gorm:"column:clients"` // varchar(255) 标签适用的客户端类型,未配置表示都适用 + Status int64 `gorm:"column:status"` // -1 下线弃用,0 未使用,大于 0 为被使用计数 } // TableName retuns table name diff --git a/src/schema/module.go b/src/schema/module.go index b3dee8c..acb9bee 100644 --- a/src/schema/module.go +++ b/src/schema/module.go @@ -8,11 +8,11 @@ import ( // Module 详见 ./sql/schema.sql table `urbs_module` // 产品线的功能模块 type Module struct { - ID int64 `gorm:"column:id" json:"id"` + ID int64 `gorm:"column:id"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` OfflineAt *time.Time `gorm:"column:offline_at" json:"offline_at"` // 计划下线时间,用于灰度管理 - ProductID int64 `gorm:"column:product_id" json:"product_id"` // 所从属的产品线 ID + ProductID int64 `gorm:"column:product_id"` // 所从属的产品线 ID Name string `gorm:"column:name" json:"name"` // varchar(63) 功能模块名称,产品线内唯一 Desc string `gorm:"column:description" json:"desc"` // varchar(1022) 功能模块描述 Status int64 `gorm:"column:status" json:"status"` // -1 下线弃用,0 未使用,大于 0 为有效配置项数 diff --git a/src/schema/product.go b/src/schema/product.go index 9497906..9692e36 100644 --- a/src/schema/product.go +++ b/src/schema/product.go @@ -8,7 +8,7 @@ import ( // Product 详见 ./sql/schema.sql table `urbs_product` // 产品线 type Product struct { - ID int64 `gorm:"column:id" json:"id"` + ID int64 `gorm:"column:id"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` DeletedAt *time.Time `gorm:"column:deleted_at" json:"deleted_at"` // 删除时间,用于灰度管理 diff --git a/src/schema/setting.go b/src/schema/setting.go index 41030c5..f511fba 100644 --- a/src/schema/setting.go +++ b/src/schema/setting.go @@ -8,17 +8,17 @@ import ( // Setting 详见 ./sql/schema.sql table `urbs_setting` // 功能模块的配置项 type Setting struct { - ID int64 `gorm:"column:id" json:"id"` - CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` - OfflineAt *time.Time `gorm:"column:offline_at" json:"offline_at"` // 计划下线时间,用于灰度管理 - ModuleID int64 `gorm:"column:module_id" json:"module_id"` // 配置项所从属的功能模块 ID - Name string `gorm:"column:name" json:"name"` // varchar(63) 配置项名称,功能模块内唯一 - Desc string `gorm:"column:description" json:"desc"` // varchar(1022) 配置项描述信息 - Channels string `gorm:"column:channels" json:"channels"` // varchar(255) 配置项适用的版本通道,未配置表示都适用 - Clients string `gorm:"column:clients" json:"clients"` // varchar(255) 配置项适用的客户端类型,未配置表示都适用 - Values string `gorm:"column:vals" json:"values"` // varchar(1022) 配置项可选值集合 - Status int64 `gorm:"column:status" json:"status"` // -1 下线弃用,0 未使用,大于 0 为被使用计数 + ID int64 `gorm:"column:id"` + CreatedAt time.Time `gorm:"column:created_at"` + UpdatedAt time.Time `gorm:"column:updated_at"` + OfflineAt *time.Time `gorm:"column:offline_at"` // 计划下线时间,用于灰度管理 + ModuleID int64 `gorm:"column:module_id"` // 配置项所从属的功能模块 ID + Name string `gorm:"column:name"` // varchar(63) 配置项名称,功能模块内唯一 + Desc string `gorm:"column:description"` // varchar(1022) 配置项描述信息 + Channels string `gorm:"column:channels"` // varchar(255) 配置项适用的版本通道,未配置表示都适用 + Clients string `gorm:"column:clients"` // varchar(255) 配置项适用的客户端类型,未配置表示都适用 + Values string `gorm:"column:vals"` // varchar(1022) 配置项可选值集合 + Status int64 `gorm:"column:status"` // -1 下线弃用,0 未使用,大于 0 为被使用计数 } // TableName retuns table name diff --git a/src/schema/user.go b/src/schema/user.go index 5b06dfc..b3c3050 100644 --- a/src/schema/user.go +++ b/src/schema/user.go @@ -11,13 +11,22 @@ import ( // 缓存用户当前全部 label,根据 active_at 和 cache_label_expire 刷新 // labels 格式:TODO type User struct { - ID int64 `gorm:"column:id" json:"id"` + ID int64 `gorm:"column:id"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` UID string `gorm:"column:uid" json:"uid"` // varchar(63),用户外部ID,表内唯一, 如 Teambition user id ActiveAt int64 `gorm:"column:active_at" json:"active_at"` // 最近活跃时间戳,1970 以来的秒数,但不及时更新 Labels string `gorm:"column:labels" json:"labels"` // varchar(8190),缓存用户当前被设置的 labels } +// GetUsersUID 返回 users 数组的 uid 数组 +func GetUsersUID(users []User) []string { + uids := make([]string, len(users)) + for i, u := range users { + uids[i] = u.UID + } + return uids +} + // TableName retuns table name func (User) TableName() string { return "urbs_user" @@ -25,22 +34,29 @@ func (User) TableName() string { // UserCacheLabel 用于在 User 数据上缓存 labels type UserCacheLabel struct { - Label string `json:"l"` - Clients string `json:"cls,omitempty"` - Channels string `json:"chs,omitempty"` + Label string `json:"l"` + Clients []string `json:"cls,omitempty"` + Channels []string `json:"chs,omitempty"` } // UserCacheLabels 用于在 User 数据上缓存 labels type UserCacheLabels map[string][]UserCacheLabel // GetLabels 从 user 上读取结构化的 labels 数据 -func (u *User) GetLabels() UserCacheLabels { +func (u *User) GetLabels(product string) []UserCacheLabel { data := make(UserCacheLabels) + labels := []UserCacheLabel{} if u.Labels == "" { - return data + return labels } + _ = json.Unmarshal([]byte(u.Labels), &data) - return data + for k, arr := range data { + if k == product { + return arr + } + } + return labels } // PutLabels 把结构化的 labels 数据转成字符串设置在 user.Labels 上 diff --git a/src/service/hider.go b/src/service/hider.go index 89fc8be..2dcd8dd 100644 --- a/src/service/hider.go +++ b/src/service/hider.go @@ -1,72 +1,41 @@ package service import ( - "errors" - "github.com/teambition/urbs-setting/src/conf" - "github.com/teambition/urbs-setting/src/schema" "github.com/teambition/urbs-setting/src/util" ) func init() { - HIDer = &hIDer{ - label: util.NewHID([]byte("label" + conf.Config.HIDKey)), - setting: util.NewHID([]byte("setting" + conf.Config.HIDKey)), - } + hIDer[""] = util.NewHID([]byte(conf.Config.HIDKey)) + hIDer["label"] = util.NewHID([]byte("label" + conf.Config.HIDKey)) + hIDer["setting"] = util.NewHID([]byte("setting" + conf.Config.HIDKey)) } // HIDer 全局 HID 转换器,目前仅支持 schema.Label, schema.setting 的 ID 转换。 -var HIDer *hIDer - -type hIDer struct { - label *util.HID - setting *util.HID -} +var hIDer = map[string]*util.HID{} -// HID 从对象的 ID 转换为 HID,如果对象无效或者 ID (int64 > 0)不合法,则返回空字符串。 -func (h *hIDer) HID(obj interface{}) string { - switch v := obj.(type) { - case *schema.Label: - return h.label.ToHex(v.ID) - case schema.Label: - return h.label.ToHex(v.ID) - case *schema.Setting: - return h.setting.ToHex(v.ID) - case schema.Setting: - return h.setting.ToHex(v.ID) - default: - return "" +// IDToHID 把 int64 的 ID 转换为 string HID,如果对象无效或者 ID (int64 > 0)不合法,则返回空字符串。 +func IDToHID(id int64, kind ...string) string { + k := "" + if len(kind) > 0 { + k = kind[0] + } + if h, ok := hIDer[k]; ok { + return h.ToHex(id) } + return "" } -// PutHID 把 HID 设置到对象上,如果 HID 字符串不合法或者对象不合法,则返回错误。 -func (h *hIDer) PutHID(obj interface{}, hid string) error { - var id int64 = -1 - switch v := obj.(type) { - case *schema.Label: - if id = h.label.ToInt64(hid); id > 0 { - v.ID = id - return nil - } - case schema.Label: - if id = h.label.ToInt64(hid); id > 0 { - v.ID = id - return nil - } - case *schema.Setting: - if id = h.setting.ToInt64(hid); id > 0 { - v.ID = id - return nil - } - case schema.Setting: - if id = h.setting.ToInt64(hid); id > 0 { - v.ID = id - return nil - } +// HIDToID 把 string 的 HID 转换为 int64 ID,如果 HID 字符串不合法或者对象不合法,则返回 0。 +func HIDToID(hid string, kind ...string) int64 { + k := "" + if len(kind) > 0 { + k = kind[0] } - - if id == 0 { - return errors.New("invalid hid") + if h, ok := hIDer[k]; ok { + if id := h.ToInt64(hid); id > 0 { + return id + } } - return errors.New("unrecognized struct") + return 0 } diff --git a/src/tpl/common.go b/src/tpl/common.go index af37fd5..a83d732 100644 --- a/src/tpl/common.go +++ b/src/tpl/common.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "regexp" + "strings" "github.com/teambition/gear" ) @@ -43,18 +44,28 @@ func RandLabel() string { return fmt.Sprintf("label-%x", b) } -// ResponseType 定义了标准的 List 接口返回数据模型 +// ErrorResponseType 定义了标准的 API 接口错误时返回数据模型 +type ErrorResponseType struct { + Error string `json:"error"` + Message string `json:"message"` +} + +// SuccessResponseType 定义了标准的 API 接口成功时返回数据模型 +type SuccessResponseType struct { + TotalSize int `json:"totalSize,omitempty"` + NextPageToken string `json:"nextPageToken"` + Result interface{} `json:"result"` +} + +// ResponseType ... type ResponseType struct { - Error string `json:"error,omitempty"` - Message string `json:"message,omitempty"` - NextPageToken string `json:"nextPageToken,omitempty"` - TotalSize uint64 `json:"totalSize,omitempty"` - Result interface{} `json:"result,omitempty"` + ErrorResponseType + SuccessResponseType } // BoolRes ... type BoolRes struct { - ResponseType + SuccessResponseType Result bool `json:"result"` } @@ -88,6 +99,23 @@ func (t *UIDHIDURL) Validate() error { return nil } +// UIDPaginationURL ... +type UIDPaginationURL struct { + Pagination + UID string `json:"uid" param:"uid"` +} + +// Validate 实现 gear.BodyTemplate。 +func (t *UIDPaginationURL) Validate() error { + if !validIDNameReg.MatchString(t.UID) { + return gear.ErrBadRequest.WithMsgf("invalid uid: %s", t.UID) + } + if err := t.Pagination.Validate(); err != nil { + return err + } + return nil +} + // NameDescBody ... type NameDescBody struct { Name string `json:"Name"` @@ -133,3 +161,11 @@ func (t *UsersGroupsBody) Validate() error { } return nil } + +// StringToSlice ... +func StringToSlice(s string) []string { + if s == "" { + return make([]string, 0) + } + return strings.Split(s, ",") +} diff --git a/src/tpl/group.go b/src/tpl/group.go index df4e3a0..a870b77 100644 --- a/src/tpl/group.go +++ b/src/tpl/group.go @@ -15,6 +15,7 @@ type GroupsBody struct { // GroupBody ... type GroupBody struct { UID string `json:"uid"` + Kind string `json:"kind"` Desc string `json:"desc"` } @@ -27,6 +28,9 @@ func (t *GroupsBody) Validate() error { if !validIDNameReg.MatchString(g.UID) { return gear.ErrBadRequest.WithMsgf("invalid group uid: %s", g.UID) } + if !validLabelReg.MatchString(g.Kind) { + return gear.ErrBadRequest.WithMsgf("invalid group kind: %s", g.Kind) + } if len(g.Desc) > 1022 { return gear.ErrBadRequest.WithMsgf("desc too long: %d", len(g.Desc)) } @@ -64,12 +68,13 @@ func (t *GroupMembersURL) Validate() error { // GroupsRes ... type GroupsRes struct { - ResponseType + SuccessResponseType Result []schema.Group `json:"result"` } // GroupMember ... type GroupMember struct { + ID int64 User string `json:"user"` CreatedAt time.Time `json:"created_at"` SyncAt int64 `json:"sync_at"` // 归属关系同步时间戳,1970 以来的秒数,应该与 group.sync_at 相等 @@ -77,6 +82,6 @@ type GroupMember struct { // GroupMembersRes ... type GroupMembersRes struct { - ResponseType + SuccessResponseType Result []GroupMember `json:"result"` } diff --git a/src/tpl/label.go b/src/tpl/label.go index 77f4150..3f0326c 100644 --- a/src/tpl/label.go +++ b/src/tpl/label.go @@ -1,12 +1,16 @@ package tpl import ( + "time" + "github.com/teambition/gear" "github.com/teambition/urbs-setting/src/schema" + "github.com/teambition/urbs-setting/src/service" ) // LabelsURL ... type LabelsURL struct { + Pagination UID string `json:"uid" param:"uid"` Product string `json:"product" query:"product"` } @@ -19,6 +23,9 @@ func (t *LabelsURL) Validate() error { if t.Product != "" && !validIDNameReg.MatchString(t.Product) { return gear.ErrBadRequest.WithMsgf("invalid product name: %s", t.Product) } + if err := t.Pagination.Validate(); err != nil { + return err + } return nil } @@ -41,35 +48,60 @@ func (t *LabelBody) Validate() error { // LabelInfo ... type LabelInfo struct { - HID string `json:"hid"` - Product string `json:"product"` - Name string `json:"name"` - Desc string `json:"desc"` - Channels string `json:"channels"` - Clients string `json:"clients"` + ID int64 + HID string `json:"hid"` + Product string `json:"product"` + Name string `json:"name"` + Desc string `json:"desc"` + Channels []string `json:"channels"` + Clients []string `json:"clients"` + Status int64 `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + OfflineAt *time.Time `json:"offline_at"` +} + +// LabelInfoFrom create a LabelInfo from schema.Label +func LabelInfoFrom(label schema.Label, product string) LabelInfo { + return LabelInfo{ + ID: label.ID, + HID: service.IDToHID(label.ID, "label"), + Product: product, + Name: label.Name, + Desc: label.Desc, + Channels: StringToSlice(label.Channels), + Clients: StringToSlice(label.Clients), + Status: label.Status, + CreatedAt: label.CreatedAt, + UpdatedAt: label.UpdatedAt, + OfflineAt: label.OfflineAt, + } +} + +// LabelInfosFrom create a slice of LabelInfo from a slice of schema.Label +func LabelInfosFrom(labels []schema.Label, product string) []LabelInfo { + res := make([]LabelInfo, len(labels)) + for i, l := range labels { + res[i] = LabelInfoFrom(l, product) + } + return res } // LabelsInfoRes ... type LabelsInfoRes struct { - ResponseType + SuccessResponseType Result []LabelInfo `json:"result"` // 空数组也保留 } +// LabelInfoRes ... +type LabelInfoRes struct { + SuccessResponseType + Result LabelInfo `json:"result"` // 空数组也保留 +} + // CacheLabelsInfoRes ... type CacheLabelsInfoRes struct { - ResponseType + SuccessResponseType Timestamp int64 `json:"timestamp"` // labels 数组生成时间 Result []schema.UserCacheLabel `json:"result"` // 空数组也保留 } - -// LabelRes ... -type LabelRes struct { - ResponseType - Result schema.Label `json:"result"` // 空数组也保留 -} - -// LabelsRes ... -type LabelsRes struct { - ResponseType - Result []schema.Label `json:"result"` // 空数组也保留 -} diff --git a/src/tpl/module.go b/src/tpl/module.go index df33655..b5bdcb8 100644 --- a/src/tpl/module.go +++ b/src/tpl/module.go @@ -6,12 +6,12 @@ import ( // ModuleRes ... type ModuleRes struct { - ResponseType + SuccessResponseType Result schema.Module `json:"result"` // 空数组也保留 } // ModulesRes ... type ModulesRes struct { - ResponseType + SuccessResponseType Result []schema.Module `json:"result"` // 空数组也保留 } diff --git a/src/tpl/pagination.go b/src/tpl/pagination.go new file mode 100644 index 0000000..636533b --- /dev/null +++ b/src/tpl/pagination.go @@ -0,0 +1,92 @@ +package tpl + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/teambition/gear" + "github.com/teambition/urbs-setting/src/service" +) + +// Pagination 分页 +type Pagination struct { + PageToken string `json:"pageToken" query:"pageToken"` + PageSize int `json:"pageSize,omitempty" query:"pageSize"` + Skip int `json:"skip,omitempty" query:"skip"` +} + +// Validate ... +func (pg *Pagination) Validate() error { + if pg.Skip < 0 { + pg.Skip = 0 + } + + if pg.PageSize > 1000 { + return gear.ErrBadRequest.WithMsgf("pageSize(%v) should not great than 1000", pg.PageSize) + } + + if pg.PageSize <= 0 { + pg.PageSize = 10 + } + + return nil +} + +// TokenToID 把 pageToken 转换为 int64 +func (pg *Pagination) TokenToID() int64 { + return PageTokenToID(pg.PageToken) +} + +// TokenToTime 把 pageToken 转换为 time +func (pg *Pagination) TokenToTime() time.Time { + return PageTokenToTime(pg.PageToken) +} + +// PageTokenToID 把 pageToken 转换为 int64 +func PageTokenToID(pageToken string) int64 { + if !strings.HasPrefix(pageToken, "hid.") { + return 0 + } + return service.HIDToID(pageToken[4:]) +} + +// IDToPageToken 把 int64 转换成 pageToken +func IDToPageToken(id int64) string { + if id <= 0 { + return "" + } + return "hid." + service.IDToHID(id) +} + +// PageTokenToTime 把 pageToken 转换为 time +func PageTokenToTime(pageToken string) time.Time { + t := time.Unix(0, 0) + if pageToken == "" { + return t + } + strs := strings.Split(pageToken, ".") + if len(strs) != 3 || strs[0] != "unix" { + return t + } + var err error + var sec int64 + var nsec int64 + if sec, err = strconv.ParseInt(strs[1], 10, 64); err != nil || sec <= 0 { + return t + } + if nsec, err = strconv.ParseInt(strs[2], 10, 64); err != nil || nsec <= 0 { + return t + } + return time.Unix(sec, nsec) +} + +// TimeToPageToken 把 time 转换成 pageToken +func TimeToPageToken(t time.Time) string { + t = t.UTC() + if t.Unix() <= 0 { + return "" + } + return fmt.Sprintf("unix.%d.%d", t.Unix(), t.Nanosecond()) +} diff --git a/src/tpl/product.go b/src/tpl/product.go index 0af7645..9ffe989 100644 --- a/src/tpl/product.go +++ b/src/tpl/product.go @@ -18,6 +18,23 @@ func (t *ProductURL) Validate() error { return nil } +// ProductPaginationURL ... +type ProductPaginationURL struct { + Pagination + Product string `json:"product" param:"product"` +} + +// Validate 实现 gear.BodyTemplate。 +func (t *ProductPaginationURL) Validate() error { + if !validIDNameReg.MatchString(t.Product) { + return gear.ErrBadRequest.WithMsgf("invalid product name: %s", t.Product) + } + if err := t.Pagination.Validate(); err != nil { + return err + } + return nil +} + // ProductLabelURL ... type ProductLabelURL struct { ProductURL @@ -37,17 +54,21 @@ func (t *ProductLabelURL) Validate() error { // ProductModuleURL ... type ProductModuleURL struct { + Pagination ProductURL Module string `json:"module" param:"module"` } // Validate 实现 gear.BodyTemplate。 func (t *ProductModuleURL) Validate() error { + if !validIDNameReg.MatchString(t.Module) { + return gear.ErrBadRequest.WithMsgf("invalid module name: %s", t.Module) + } if err := t.ProductURL.Validate(); err != nil { return err } - if !validIDNameReg.MatchString(t.Module) { - return gear.ErrBadRequest.WithMsgf("invalid module name: %s", t.Module) + if err := t.Pagination.Validate(); err != nil { + return err } return nil } @@ -71,12 +92,12 @@ func (t *ProductModuleSettingURL) Validate() error { // ProductsRes ... type ProductsRes struct { - ResponseType + SuccessResponseType Result []schema.Product `json:"result"` } // ProductRes ... type ProductRes struct { - ResponseType + SuccessResponseType Result schema.Product `json:"result"` } diff --git a/src/tpl/setting.go b/src/tpl/setting.go index 03c3381..8b2b75e 100644 --- a/src/tpl/setting.go +++ b/src/tpl/setting.go @@ -1,17 +1,65 @@ package tpl import ( + "time" + "github.com/teambition/urbs-setting/src/schema" + "github.com/teambition/urbs-setting/src/service" ) -// SettingRes ... -type SettingRes struct { - ResponseType - Result schema.Setting `json:"result"` // 空数组也保留 +// SettingInfo ... +type SettingInfo struct { + ID int64 + HID string `json:"hid"` + Product string `json:"product"` + Module string `json:"module"` + Name string `json:"name"` + Desc string `json:"desc"` + Channels []string `json:"channels"` + Clients []string `json:"clients"` + Values []string `json:"values"` + Status int64 `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + OfflineAt *time.Time `json:"offline_at"` +} + +// SettingInfoFrom create a SettingInfo from schema.Setting +func SettingInfoFrom(setting schema.Setting, product, module string) SettingInfo { + return SettingInfo{ + ID: setting.ID, + HID: service.IDToHID(setting.ID, "setting"), + Product: product, + Module: module, + Name: setting.Name, + Desc: setting.Desc, + Channels: StringToSlice(setting.Channels), + Clients: StringToSlice(setting.Clients), + Values: StringToSlice(setting.Values), + Status: setting.Status, + CreatedAt: setting.CreatedAt, + UpdatedAt: setting.UpdatedAt, + OfflineAt: setting.OfflineAt, + } +} + +// SettingInfosFrom create a slice of SettingInfo from a slice of schema.Setting +func SettingInfosFrom(settings []schema.Setting, product, module string) []SettingInfo { + res := make([]SettingInfo, len(settings)) + for i, l := range settings { + res[i] = SettingInfoFrom(l, product, module) + } + return res +} + +// SettingsInfoRes ... +type SettingsInfoRes struct { + SuccessResponseType + Result []SettingInfo `json:"result"` // 空数组也保留 } -// SettingsRes ... -type SettingsRes struct { - ResponseType - Result []schema.Setting `json:"result"` // 空数组也保留 +// SettingInfoRes ... +type SettingInfoRes struct { + SuccessResponseType + Result SettingInfo `json:"result"` // 空数组也保留 }