From 3ed1efa6cf200d31980d7c41d2e5ca2d6a0d7ff7 Mon Sep 17 00:00:00 2001 From: Lei Da Date: Mon, 9 Dec 2024 17:37:56 +0800 Subject: [PATCH] searching and pagination organization --- .mockery.yaml | 1 + .../builder/store/database/mock_OrgStore.go | 93 ++++- .../component/mock_OrganizationComponent.go | 391 ++++++++++++++++++ builder/store/database/organization.go | 27 +- builder/store/database/organization_test.go | 22 +- cmd/csghub-server/cmd/trigger/fix_org_data.go | 2 +- user/component/organization.go | 57 ++- user/component/organization_test.go | 25 +- user/component/user.go | 75 ++-- user/component/user_test.go | 29 ++ user/handler/organization.go | 19 +- user/handler/organization_test.go | 51 +++ 12 files changed, 708 insertions(+), 84 deletions(-) create mode 100644 _mocks/opencsg.com/csghub-server/user/component/mock_OrganizationComponent.go create mode 100644 user/component/user_test.go create mode 100644 user/handler/organization_test.go diff --git a/.mockery.yaml b/.mockery.yaml index ec11083a..9bb5055b 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -20,6 +20,7 @@ packages: config: interfaces: MemberComponent: + OrganizationComponent: opencsg.com/csghub-server/builder/store/database: config: all: True diff --git a/_mocks/opencsg.com/csghub-server/builder/store/database/mock_OrgStore.go b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_OrgStore.go index 738d22eb..1c7aa592 100644 --- a/_mocks/opencsg.com/csghub-server/builder/store/database/mock_OrgStore.go +++ b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_OrgStore.go @@ -291,7 +291,7 @@ func (_c *MockOrgStore_GetUserBelongOrgs_Call) RunAndReturn(run func(context.Con } // GetUserOwnOrgs provides a mock function with given fields: ctx, username -func (_m *MockOrgStore) GetUserOwnOrgs(ctx context.Context, username string) ([]database.Organization, error) { +func (_m *MockOrgStore) GetUserOwnOrgs(ctx context.Context, username string) ([]database.Organization, int, error) { ret := _m.Called(ctx, username) if len(ret) == 0 { @@ -299,8 +299,9 @@ func (_m *MockOrgStore) GetUserOwnOrgs(ctx context.Context, username string) ([] } var r0 []database.Organization - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string) ([]database.Organization, error)); ok { + var r1 int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]database.Organization, int, error)); ok { return rf(ctx, username) } if rf, ok := ret.Get(0).(func(context.Context, string) []database.Organization); ok { @@ -311,13 +312,19 @@ func (_m *MockOrgStore) GetUserOwnOrgs(ctx context.Context, username string) ([] } } - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, string) int); ok { r1 = rf(ctx, username) } else { - r1 = ret.Error(1) + r1 = ret.Get(1).(int) } - return r0, r1 + if rf, ok := ret.Get(2).(func(context.Context, string) error); ok { + r2 = rf(ctx, username) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } // MockOrgStore_GetUserOwnOrgs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetUserOwnOrgs' @@ -339,12 +346,80 @@ func (_c *MockOrgStore_GetUserOwnOrgs_Call) Run(run func(ctx context.Context, us return _c } -func (_c *MockOrgStore_GetUserOwnOrgs_Call) Return(orgs []database.Organization, err error) *MockOrgStore_GetUserOwnOrgs_Call { - _c.Call.Return(orgs, err) +func (_c *MockOrgStore_GetUserOwnOrgs_Call) Return(orgs []database.Organization, total int, err error) *MockOrgStore_GetUserOwnOrgs_Call { + _c.Call.Return(orgs, total, err) + return _c +} + +func (_c *MockOrgStore_GetUserOwnOrgs_Call) RunAndReturn(run func(context.Context, string) ([]database.Organization, int, error)) *MockOrgStore_GetUserOwnOrgs_Call { + _c.Call.Return(run) + return _c +} + +// Search provides a mock function with given fields: ctx, search, per, page +func (_m *MockOrgStore) Search(ctx context.Context, search string, per int, page int) ([]database.Organization, int, error) { + ret := _m.Called(ctx, search, per, page) + + if len(ret) == 0 { + panic("no return value specified for Search") + } + + var r0 []database.Organization + var r1 int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, int, int) ([]database.Organization, int, error)); ok { + return rf(ctx, search, per, page) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int, int) []database.Organization); ok { + r0 = rf(ctx, search, per, page) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]database.Organization) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, int, int) int); ok { + r1 = rf(ctx, search, per, page) + } else { + r1 = ret.Get(1).(int) + } + + if rf, ok := ret.Get(2).(func(context.Context, string, int, int) error); ok { + r2 = rf(ctx, search, per, page) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockOrgStore_Search_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Search' +type MockOrgStore_Search_Call struct { + *mock.Call +} + +// Search is a helper method to define mock.On call +// - ctx context.Context +// - search string +// - per int +// - page int +func (_e *MockOrgStore_Expecter) Search(ctx interface{}, search interface{}, per interface{}, page interface{}) *MockOrgStore_Search_Call { + return &MockOrgStore_Search_Call{Call: _e.mock.On("Search", ctx, search, per, page)} +} + +func (_c *MockOrgStore_Search_Call) Run(run func(ctx context.Context, search string, per int, page int)) *MockOrgStore_Search_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(int), args[3].(int)) + }) + return _c +} + +func (_c *MockOrgStore_Search_Call) Return(orgs []database.Organization, total int, err error) *MockOrgStore_Search_Call { + _c.Call.Return(orgs, total, err) return _c } -func (_c *MockOrgStore_GetUserOwnOrgs_Call) RunAndReturn(run func(context.Context, string) ([]database.Organization, error)) *MockOrgStore_GetUserOwnOrgs_Call { +func (_c *MockOrgStore_Search_Call) RunAndReturn(run func(context.Context, string, int, int) ([]database.Organization, int, error)) *MockOrgStore_Search_Call { _c.Call.Return(run) return _c } diff --git a/_mocks/opencsg.com/csghub-server/user/component/mock_OrganizationComponent.go b/_mocks/opencsg.com/csghub-server/user/component/mock_OrganizationComponent.go new file mode 100644 index 00000000..9b56b50e --- /dev/null +++ b/_mocks/opencsg.com/csghub-server/user/component/mock_OrganizationComponent.go @@ -0,0 +1,391 @@ +// Code generated by mockery v2.49.1. DO NOT EDIT. + +package component + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + database "opencsg.com/csghub-server/builder/store/database" + + types "opencsg.com/csghub-server/common/types" +) + +// MockOrganizationComponent is an autogenerated mock type for the OrganizationComponent type +type MockOrganizationComponent struct { + mock.Mock +} + +type MockOrganizationComponent_Expecter struct { + mock *mock.Mock +} + +func (_m *MockOrganizationComponent) EXPECT() *MockOrganizationComponent_Expecter { + return &MockOrganizationComponent_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, req +func (_m *MockOrganizationComponent) Create(ctx context.Context, req *types.CreateOrgReq) (*types.Organization, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 *types.Organization + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.CreateOrgReq) (*types.Organization, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.CreateOrgReq) *types.Organization); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Organization) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.CreateOrgReq) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockOrganizationComponent_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockOrganizationComponent_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - req *types.CreateOrgReq +func (_e *MockOrganizationComponent_Expecter) Create(ctx interface{}, req interface{}) *MockOrganizationComponent_Create_Call { + return &MockOrganizationComponent_Create_Call{Call: _e.mock.On("Create", ctx, req)} +} + +func (_c *MockOrganizationComponent_Create_Call) Run(run func(ctx context.Context, req *types.CreateOrgReq)) *MockOrganizationComponent_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.CreateOrgReq)) + }) + return _c +} + +func (_c *MockOrganizationComponent_Create_Call) Return(_a0 *types.Organization, _a1 error) *MockOrganizationComponent_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockOrganizationComponent_Create_Call) RunAndReturn(run func(context.Context, *types.CreateOrgReq) (*types.Organization, error)) *MockOrganizationComponent_Create_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function with given fields: ctx, req +func (_m *MockOrganizationComponent) Delete(ctx context.Context, req *types.DeleteOrgReq) error { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *types.DeleteOrgReq) error); ok { + r0 = rf(ctx, req) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockOrganizationComponent_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockOrganizationComponent_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - req *types.DeleteOrgReq +func (_e *MockOrganizationComponent_Expecter) Delete(ctx interface{}, req interface{}) *MockOrganizationComponent_Delete_Call { + return &MockOrganizationComponent_Delete_Call{Call: _e.mock.On("Delete", ctx, req)} +} + +func (_c *MockOrganizationComponent_Delete_Call) Run(run func(ctx context.Context, req *types.DeleteOrgReq)) *MockOrganizationComponent_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.DeleteOrgReq)) + }) + return _c +} + +func (_c *MockOrganizationComponent_Delete_Call) Return(_a0 error) *MockOrganizationComponent_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockOrganizationComponent_Delete_Call) RunAndReturn(run func(context.Context, *types.DeleteOrgReq) error) *MockOrganizationComponent_Delete_Call { + _c.Call.Return(run) + return _c +} + +// FixOrgData provides a mock function with given fields: ctx, org +func (_m *MockOrganizationComponent) FixOrgData(ctx context.Context, org *database.Organization) (*database.Organization, error) { + ret := _m.Called(ctx, org) + + if len(ret) == 0 { + panic("no return value specified for FixOrgData") + } + + var r0 *database.Organization + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *database.Organization) (*database.Organization, error)); ok { + return rf(ctx, org) + } + if rf, ok := ret.Get(0).(func(context.Context, *database.Organization) *database.Organization); ok { + r0 = rf(ctx, org) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.Organization) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *database.Organization) error); ok { + r1 = rf(ctx, org) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockOrganizationComponent_FixOrgData_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FixOrgData' +type MockOrganizationComponent_FixOrgData_Call struct { + *mock.Call +} + +// FixOrgData is a helper method to define mock.On call +// - ctx context.Context +// - org *database.Organization +func (_e *MockOrganizationComponent_Expecter) FixOrgData(ctx interface{}, org interface{}) *MockOrganizationComponent_FixOrgData_Call { + return &MockOrganizationComponent_FixOrgData_Call{Call: _e.mock.On("FixOrgData", ctx, org)} +} + +func (_c *MockOrganizationComponent_FixOrgData_Call) Run(run func(ctx context.Context, org *database.Organization)) *MockOrganizationComponent_FixOrgData_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*database.Organization)) + }) + return _c +} + +func (_c *MockOrganizationComponent_FixOrgData_Call) Return(_a0 *database.Organization, _a1 error) *MockOrganizationComponent_FixOrgData_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockOrganizationComponent_FixOrgData_Call) RunAndReturn(run func(context.Context, *database.Organization) (*database.Organization, error)) *MockOrganizationComponent_FixOrgData_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function with given fields: ctx, orgName +func (_m *MockOrganizationComponent) Get(ctx context.Context, orgName string) (*types.Organization, error) { + ret := _m.Called(ctx, orgName) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *types.Organization + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*types.Organization, error)); ok { + return rf(ctx, orgName) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *types.Organization); ok { + r0 = rf(ctx, orgName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Organization) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, orgName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockOrganizationComponent_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockOrganizationComponent_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - orgName string +func (_e *MockOrganizationComponent_Expecter) Get(ctx interface{}, orgName interface{}) *MockOrganizationComponent_Get_Call { + return &MockOrganizationComponent_Get_Call{Call: _e.mock.On("Get", ctx, orgName)} +} + +func (_c *MockOrganizationComponent_Get_Call) Run(run func(ctx context.Context, orgName string)) *MockOrganizationComponent_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockOrganizationComponent_Get_Call) Return(_a0 *types.Organization, _a1 error) *MockOrganizationComponent_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockOrganizationComponent_Get_Call) RunAndReturn(run func(context.Context, string) (*types.Organization, error)) *MockOrganizationComponent_Get_Call { + _c.Call.Return(run) + return _c +} + +// Index provides a mock function with given fields: ctx, username, search, per, page +func (_m *MockOrganizationComponent) Index(ctx context.Context, username string, search string, per int, page int) ([]types.Organization, int, error) { + ret := _m.Called(ctx, username, search, per, page) + + if len(ret) == 0 { + panic("no return value specified for Index") + } + + var r0 []types.Organization + var r1 int + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int, int) ([]types.Organization, int, error)); ok { + return rf(ctx, username, search, per, page) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, int, int) []types.Organization); ok { + r0 = rf(ctx, username, search, per, page) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.Organization) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, int, int) int); ok { + r1 = rf(ctx, username, search, per, page) + } else { + r1 = ret.Get(1).(int) + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, int, int) error); ok { + r2 = rf(ctx, username, search, per, page) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockOrganizationComponent_Index_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Index' +type MockOrganizationComponent_Index_Call struct { + *mock.Call +} + +// Index is a helper method to define mock.On call +// - ctx context.Context +// - username string +// - search string +// - per int +// - page int +func (_e *MockOrganizationComponent_Expecter) Index(ctx interface{}, username interface{}, search interface{}, per interface{}, page interface{}) *MockOrganizationComponent_Index_Call { + return &MockOrganizationComponent_Index_Call{Call: _e.mock.On("Index", ctx, username, search, per, page)} +} + +func (_c *MockOrganizationComponent_Index_Call) Run(run func(ctx context.Context, username string, search string, per int, page int)) *MockOrganizationComponent_Index_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int), args[4].(int)) + }) + return _c +} + +func (_c *MockOrganizationComponent_Index_Call) Return(_a0 []types.Organization, _a1 int, _a2 error) *MockOrganizationComponent_Index_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *MockOrganizationComponent_Index_Call) RunAndReturn(run func(context.Context, string, string, int, int) ([]types.Organization, int, error)) *MockOrganizationComponent_Index_Call { + _c.Call.Return(run) + return _c +} + +// Update provides a mock function with given fields: ctx, req +func (_m *MockOrganizationComponent) Update(ctx context.Context, req *types.EditOrgReq) (*database.Organization, error) { + ret := _m.Called(ctx, req) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 *database.Organization + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.EditOrgReq) (*database.Organization, error)); ok { + return rf(ctx, req) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.EditOrgReq) *database.Organization); ok { + r0 = rf(ctx, req) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*database.Organization) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.EditOrgReq) error); ok { + r1 = rf(ctx, req) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockOrganizationComponent_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockOrganizationComponent_Update_Call struct { + *mock.Call +} + +// Update is a helper method to define mock.On call +// - ctx context.Context +// - req *types.EditOrgReq +func (_e *MockOrganizationComponent_Expecter) Update(ctx interface{}, req interface{}) *MockOrganizationComponent_Update_Call { + return &MockOrganizationComponent_Update_Call{Call: _e.mock.On("Update", ctx, req)} +} + +func (_c *MockOrganizationComponent_Update_Call) Run(run func(ctx context.Context, req *types.EditOrgReq)) *MockOrganizationComponent_Update_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.EditOrgReq)) + }) + return _c +} + +func (_c *MockOrganizationComponent_Update_Call) Return(_a0 *database.Organization, _a1 error) *MockOrganizationComponent_Update_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockOrganizationComponent_Update_Call) RunAndReturn(run func(context.Context, *types.EditOrgReq) (*database.Organization, error)) *MockOrganizationComponent_Update_Call { + _c.Call.Return(run) + return _c +} + +// NewMockOrganizationComponent creates a new instance of MockOrganizationComponent. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockOrganizationComponent(t interface { + mock.TestingT + Cleanup(func()) +}) *MockOrganizationComponent { + mock := &MockOrganizationComponent{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/builder/store/database/organization.go b/builder/store/database/organization.go index 68eec4b8..bcbf6e98 100644 --- a/builder/store/database/organization.go +++ b/builder/store/database/organization.go @@ -2,6 +2,8 @@ package database import ( "context" + "fmt" + "strings" "github.com/uptrace/bun" ) @@ -12,12 +14,13 @@ type orgStoreImpl struct { type OrgStore interface { Create(ctx context.Context, org *Organization, namepace *Namespace) (err error) - GetUserOwnOrgs(ctx context.Context, username string) (orgs []Organization, err error) + GetUserOwnOrgs(ctx context.Context, username string) (orgs []Organization, total int, err error) Update(ctx context.Context, org *Organization) (err error) Delete(ctx context.Context, path string) (err error) FindByPath(ctx context.Context, path string) (org Organization, err error) Exists(ctx context.Context, path string) (exists bool, err error) GetUserBelongOrgs(ctx context.Context, userID int64) (orgs []Organization, err error) + Search(ctx context.Context, search string, per, page int) (orgs []Organization, total int, err error) } func NewOrgStore() OrgStore { @@ -64,7 +67,7 @@ func (s *orgStoreImpl) Create(ctx context.Context, org *Organization, namepace * return } -func (s *orgStoreImpl) GetUserOwnOrgs(ctx context.Context, username string) (orgs []Organization, err error) { +func (s *orgStoreImpl) GetUserOwnOrgs(ctx context.Context, username string) (orgs []Organization, total int, err error) { query := s.db.Operator.Core. NewSelect(). Model(&orgs). @@ -76,6 +79,7 @@ func (s *orgStoreImpl) GetUserOwnOrgs(ctx context.Context, username string) (org } err = query.Scan(ctx, &orgs) + total = len(orgs) return } @@ -141,3 +145,22 @@ func (s *orgStoreImpl) GetUserBelongOrgs(ctx context.Context, userID int64) (org Scan(ctx, &orgs) return } + +func (s *orgStoreImpl) Search(ctx context.Context, search string, per int, page int) (orgs []Organization, total int, err error) { + search = strings.ToLower(search) + query := s.db.Operator.Core.NewSelect(). + Model(&orgs) + if search != "" { + query.Where("LOWER(name) like ? OR LOWER(path) like ?", fmt.Sprintf("%%%s%%", search), fmt.Sprintf("%%%s%%", search)) + } + total, err = query.Count(ctx) + if err != nil { + return + } + query.Order("id asc").Limit(per).Offset((page - 1) * per) + err = query.Scan(ctx, &orgs) + if err != nil { + return + } + return +} diff --git a/builder/store/database/organization_test.go b/builder/store/database/organization_test.go index 91a8ebf2..8a58f32b 100644 --- a/builder/store/database/organization_test.go +++ b/builder/store/database/organization_test.go @@ -16,10 +16,27 @@ func TestOrganizationStore_CRUD(t *testing.T) { store := database.NewOrgStoreWithDB(db) err := store.Create(ctx, &database.Organization{ - Name: "o1", + Name: "o1", + Nickname: "o1_nickname", }, &database.Namespace{Path: "o1"}) require.Nil(t, err) + //search with name + orgs, total, err := store.Search(ctx, "o1", 10, 1) + require.Nil(t, err) + require.Equal(t, 1, total) + require.Equal(t, "o1", orgs[0].Name) + //search with nickname + orgs, total, err = store.Search(ctx, "nickname", 10, 1) + require.Nil(t, err) + require.Equal(t, 1, total) + require.Equal(t, "o1_nickname", orgs[0].Nickname) + //empty search second page + orgs, total, err = store.Search(ctx, "nickname", 10, 2) + require.Nil(t, err) + require.Equal(t, 1, total) + require.Empty(t, orgs) + org := &database.Organization{} err = db.Core.NewSelect().Model(org).Where("path=?", "o1").Scan(ctx) require.Nil(t, err) @@ -63,9 +80,10 @@ func TestOrganizationStore_CRUD(t *testing.T) { err = store.Update(ctx, org) require.Nil(t, err) - orgs, err := store.GetUserOwnOrgs(ctx, "u1") + orgs, total, err = store.GetUserOwnOrgs(ctx, "u1") require.Nil(t, err) require.Equal(t, 1, len(orgs)) + require.Equal(t, 1, total) orgs, err = store.GetUserBelongOrgs(ctx, 321) require.Nil(t, err) diff --git a/cmd/csghub-server/cmd/trigger/fix_org_data.go b/cmd/csghub-server/cmd/trigger/fix_org_data.go index 42b0f379..e2373ad0 100644 --- a/cmd/csghub-server/cmd/trigger/fix_org_data.go +++ b/cmd/csghub-server/cmd/trigger/fix_org_data.go @@ -36,7 +36,7 @@ var fixOrgDataCmd = &cobra.Command{ orgComponent, _ := component.NewOrganizationComponent(cfg) // get all organizations - orgs, err = os.GetUserOwnOrgs(ctx, "") + orgs, _, err = os.GetUserOwnOrgs(ctx, "") for _, org := range orgs { req := new(types.CreateOrgReq) req.Name = org.Name diff --git a/user/component/organization.go b/user/component/organization.go index 292e8f47..f43a7d92 100644 --- a/user/component/organization.go +++ b/user/component/organization.go @@ -16,7 +16,7 @@ import ( type OrganizationComponent interface { FixOrgData(ctx context.Context, org *database.Organization) (*database.Organization, error) Create(ctx context.Context, req *types.CreateOrgReq) (*types.Organization, error) - Index(ctx context.Context, username string) ([]types.Organization, error) + Index(ctx context.Context, username, search string, per, page int) ([]types.Organization, int, error) Get(ctx context.Context, orgName string) (*types.Organization, error) Delete(ctx context.Context, req *types.DeleteOrgReq) error Update(ctx context.Context, req *types.EditOrgReq) (*database.Organization, error) @@ -24,9 +24,9 @@ type OrganizationComponent interface { func NewOrganizationComponent(config *config.Config) (OrganizationComponent, error) { c := &organizationComponentImpl{} - c.os = database.NewOrgStore() - c.ns = database.NewNamespaceStore() - c.us = database.NewUserStore() + c.orgStore = database.NewOrgStore() + c.nsStore = database.NewNamespaceStore() + c.userStore = database.NewUserStore() var err error c.gs, err = git.NewGitServer(config) if err != nil { @@ -44,10 +44,10 @@ func NewOrganizationComponent(config *config.Config) (OrganizationComponent, err } type organizationComponentImpl struct { - os database.OrgStore - ns database.NamespaceStore - us database.UserStore - gs gitserver.GitServer + orgStore database.OrgStore + nsStore database.NamespaceStore + userStore database.UserStore + gs gitserver.GitServer msc MemberComponent } @@ -74,12 +74,12 @@ func (c *organizationComponentImpl) FixOrgData(ctx context.Context, org *databas } func (c *organizationComponentImpl) Create(ctx context.Context, req *types.CreateOrgReq) (*types.Organization, error) { - user, err := c.us.FindByUsername(ctx, req.Username) + user, err := c.userStore.FindByUsername(ctx, req.Username) if err != nil { return nil, fmt.Errorf("failed to find user, error: %w", err) } - es, err := c.ns.Exists(ctx, req.Name) + es, err := c.nsStore.Exists(ctx, req.Name) if err != nil { return nil, err } @@ -99,7 +99,7 @@ func (c *organizationComponentImpl) Create(ctx context.Context, req *types.Creat Path: dbOrg.Name, UserID: user.ID, } - err = c.os.Create(ctx, dbOrg, namespace) + err = c.orgStore.Create(ctx, dbOrg, namespace) if err != nil { return nil, fmt.Errorf("failed create database organization, error: %w", err) } @@ -125,10 +125,27 @@ func (c *organizationComponentImpl) Create(ctx context.Context, req *types.Creat return org, err } -func (c *organizationComponentImpl) Index(ctx context.Context, username string) ([]types.Organization, error) { - dborgs, err := c.os.GetUserOwnOrgs(ctx, username) - if err != nil { - return nil, fmt.Errorf("failed to get organizations, error: %w", err) +func (c *organizationComponentImpl) Index(ctx context.Context, username, search string, per, page int) ([]types.Organization, int, error) { + var ( + err error + total int + u database.User + dborgs []database.Organization + ) + u, err = c.userStore.FindByUsername(ctx, username) + if err != nil { + return nil, 0, fmt.Errorf("failed to find user, error: %w", err) + } + if u.CanAdmin() { + dborgs, total, err = c.orgStore.Search(ctx, search, per, page) + if err != nil { + return nil, 0, fmt.Errorf("failed to get organizations for admin user, error: %w", err) + } + } else { + dborgs, total, err = c.orgStore.GetUserOwnOrgs(ctx, username) + if err != nil { + return nil, 0, fmt.Errorf("failed to get organizations for owner, error: %w", err) + } } var orgs []types.Organization for _, dborg := range dborgs { @@ -142,11 +159,11 @@ func (c *organizationComponentImpl) Index(ctx context.Context, username string) } orgs = append(orgs, org) } - return orgs, nil + return orgs, total, nil } func (c *organizationComponentImpl) Get(ctx context.Context, orgName string) (*types.Organization, error) { - dborg, err := c.os.FindByPath(ctx, orgName) + dborg, err := c.orgStore.FindByPath(ctx, orgName) if err != nil { return nil, fmt.Errorf("failed to get organizations by name, error: %w", err) } @@ -175,7 +192,7 @@ func (c *organizationComponentImpl) Delete(ctx context.Context, req *types.Delet if err != nil { return fmt.Errorf("failed to delete git organizations, error: %w", err) } - err = c.os.Delete(ctx, req.Name) + err = c.orgStore.Delete(ctx, req.Name) if err != nil { return fmt.Errorf("failed to delete database organizations, error: %w", err) } @@ -192,7 +209,7 @@ func (c *organizationComponentImpl) Update(ctx context.Context, req *types.EditO if !r.CanAdmin() { return nil, fmt.Errorf("current user does not have permission to edit the organization, current user: %s", req.CurrentUser) } - org, err := c.os.FindByPath(ctx, req.Name) + org, err := c.orgStore.FindByPath(ctx, req.Name) if err != nil { return nil, fmt.Errorf("organization does not exists, error: %w", err) } @@ -212,7 +229,7 @@ func (c *organizationComponentImpl) Update(ctx context.Context, req *types.EditO if req.OrgType != nil { org.OrgType = *req.OrgType } - err = c.os.Update(ctx, &org) + err = c.orgStore.Update(ctx, &org) if err != nil { return nil, fmt.Errorf("failed to update database organization, error: %w", err) } diff --git a/user/component/organization_test.go b/user/component/organization_test.go index 65af2c3f..1bdeb941 100644 --- a/user/component/organization_test.go +++ b/user/component/organization_test.go @@ -56,11 +56,11 @@ func TestOrganizationComponent_Create(t *testing.T) { } c := &organizationComponentImpl{ - us: mockUserStore, - ns: mockNamespaceStore, - gs: mockGitServer, - os: mockOrgStore, - msc: mockMemberComponent, + userStore: mockUserStore, + nsStore: mockNamespaceStore, + gs: mockGitServer, + orgStore: mockOrgStore, + msc: mockMemberComponent, } org, err := c.Create(context.Background(), req) require.NoError(t, err) @@ -88,14 +88,23 @@ func TestOrganizationComponent_Index(t *testing.T) { Verified: false, }) mockOrgStore := mockdb.NewMockOrgStore(t) - mockOrgStore.EXPECT().GetUserOwnOrgs(mock.Anything, "user1").Return(dbOrgs, nil).Once() + mockOrgStore.EXPECT().GetUserOwnOrgs(mock.Anything, "user1").Return(dbOrgs, len(dbOrgs), nil).Once() + + mockUserStore := mockdb.NewMockUserStore(t) + mockUserStore.EXPECT().FindByUsername(mock.Anything, "user1").Return(database.User{ + Username: "user1", + RoleMask: "", + }, nil) c := &organizationComponentImpl{ - os: mockOrgStore, + orgStore: mockOrgStore, + userStore: mockUserStore, } - expectedOrgs, err := c.Index(context.Background(), "user1") + expectedOrgs, total, err := c.Index(context.Background(), "user1", "", 10, 0) + require.NoError(t, err) require.Len(t, expectedOrgs, 2) + require.Equal(t, 2, total) require.Condition(t, func() bool { for i := 0; i < len(expectedOrgs); i++ { diff --git a/user/component/user.go b/user/component/user.go index 91a4b360..e014c19c 100644 --- a/user/component/user.go +++ b/user/component/user.go @@ -24,12 +24,12 @@ import ( const GitalyRepoNotFoundErr = "rpc error: code = NotFound desc = repository does not exist" type userComponentImpl struct { - us database.UserStore - os database.OrgStore - ns database.NamespaceStore - repo database.RepoStore - ds database.DeployTaskStore - ams database.AccountMeteringStore + userStore database.UserStore + orgStore database.OrgStore + nsStore database.NamespaceStore + repo database.RepoStore + ds database.DeployTaskStore + ams database.AccountMeteringStore gs gitserver.GitServer jwtc JwtComponent @@ -73,9 +73,9 @@ type UserComponent interface { func NewUserComponent(config *config.Config) (UserComponent, error) { var err error c := &userComponentImpl{} - c.us = database.NewUserStore() - c.os = database.NewOrgStore() - c.ns = database.NewNamespaceStore() + c.userStore = database.NewUserStore() + c.orgStore = database.NewOrgStore() + c.nsStore = database.NewNamespaceStore() c.repo = database.NewRepoStore() c.ds = database.NewDeployTaskStore() c.ams = database.NewAccountMeteringStore() @@ -175,7 +175,7 @@ func (c *userComponentImpl) createFromCasdoorUser(ctx context.Context, cu casdoo user.GitID = gsUserResp.GitID user.Password = gsUserResp.Password } - err = c.us.Create(ctx, user, namespace) + err = c.userStore.Create(ctx, user, namespace) if err != nil { newError := fmt.Errorf("failed to create user in db,error:%w", err) return nil, newError @@ -189,7 +189,7 @@ func (c *userComponentImpl) ChangeUserName(ctx context.Context, oldUserName, new return fmt.Errorf("user name can only be changed by user self, user: '%s', op user: '%s'", oldUserName, opUser) } - user, err := c.us.FindByUsername(ctx, oldUserName) + user, err := c.userStore.FindByUsername(ctx, oldUserName) if err != nil { return fmt.Errorf("failed to find user by old name in db,error:%w", err) } @@ -198,7 +198,7 @@ func (c *userComponentImpl) ChangeUserName(ctx context.Context, oldUserName, new return fmt.Errorf("user name can not be changed") } - newUser, err := c.us.FindByUsername(ctx, newUserName) + newUser, err := c.userStore.FindByUsername(ctx, newUserName) if err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("failed to find user by new name in db,error:%w", err) } @@ -206,7 +206,7 @@ func (c *userComponentImpl) ChangeUserName(ctx context.Context, oldUserName, new return fmt.Errorf("user name '%s' already exists", newUserName) } - err = c.us.ChangeUserName(ctx, oldUserName, newUserName) + err = c.userStore.ChangeUserName(ctx, oldUserName, newUserName) if err != nil { return fmt.Errorf("failed to change user name in db,error:%w", err) } @@ -232,7 +232,7 @@ func (c *userComponentImpl) ChangeUserName(ctx context.Context, oldUserName, new func (c *userComponentImpl) Update(ctx context.Context, req *types.UpdateUserRequest, opUser string) error { c.lazyInit() - user, err := c.us.FindByUsername(ctx, req.Username) + user, err := c.userStore.FindByUsername(ctx, req.Username) if err != nil { newError := fmt.Errorf("failed to find user by name in db,error:%w", err) return newError @@ -242,7 +242,7 @@ func (c *userComponentImpl) Update(ctx context.Context, req *types.UpdateUserReq } // need at least admin permission to update other user's info if req.Username != opUser { - opuser, err := c.us.FindByUsername(ctx, opUser) + opuser, err := c.userStore.FindByUsername(ctx, opUser) if err != nil { return fmt.Errorf("failed to find op user by name in db,user: '%s', error:%w", opUser, err) } @@ -261,7 +261,7 @@ func (c *userComponentImpl) Update(ctx context.Context, req *types.UpdateUserReq } c.setChangedProps(&user, req) - err = c.us.Update(ctx, &user) + err = c.userStore.Update(ctx, &user) if err != nil { newError := fmt.Errorf("failed to update database user '%s',error:%w", req.Username, err) return newError @@ -348,7 +348,7 @@ func (c *userComponentImpl) setChangedProps(user *database.User, req *types.Upda } func (c *userComponentImpl) Delete(ctx context.Context, operator, username string) error { - user, err := c.us.FindByUsername(ctx, username) + user, err := c.userStore.FindByUsername(ctx, username) if err != nil { newError := fmt.Errorf("failed to find user by name in db,error:%w", err) return newError @@ -380,7 +380,7 @@ func (c *userComponentImpl) Delete(ctx context.Context, operator, username strin } } // delete user from db - err = c.us.DeleteUserAndRelations(ctx, user) + err = c.userStore.DeleteUserAndRelations(ctx, user) if err != nil { return fmt.Errorf("failed to delete user and user relations: %v", err) } @@ -404,7 +404,7 @@ func (c *userComponentImpl) Delete(ctx context.Context, operator, username strin // - bool: True if the user has admin privileges, false otherwise. // - error: An error if the user cannot be found in the database. func (c *userComponentImpl) CanAdmin(ctx context.Context, username string) (bool, error) { - user, err := c.us.FindByUsername(ctx, username) + user, err := c.userStore.FindByUsername(ctx, username) if err != nil { newError := fmt.Errorf("failed to find user by name '%s' in db,error:%w", username, err) return false, newError @@ -419,9 +419,9 @@ func (c *userComponentImpl) GetInternal(ctx context.Context, userNameOrUUID stri var dbuser = new(database.User) var err error if useUUID { - dbuser, err = c.us.FindByUUID(ctx, userNameOrUUID) + dbuser, err = c.userStore.FindByUUID(ctx, userNameOrUUID) } else { - *dbuser, err = c.us.FindByUsername(ctx, userNameOrUUID) + *dbuser, err = c.userStore.FindByUsername(ctx, userNameOrUUID) } if err != nil { return nil, fmt.Errorf("failed to find user by name or uuid '%s' in db,error:%w", userNameOrUUID, err) @@ -433,9 +433,9 @@ func (c *userComponentImpl) Get(ctx context.Context, userNameOrUUID, visitorName var dbuser = new(database.User) var err error if useUUID { - dbuser, err = c.us.FindByUUID(ctx, userNameOrUUID) + dbuser, err = c.userStore.FindByUUID(ctx, userNameOrUUID) } else { - *dbuser, err = c.us.FindByUsername(ctx, userNameOrUUID) + *dbuser, err = c.userStore.FindByUsername(ctx, userNameOrUUID) } if err != nil { return nil, fmt.Errorf("failed to find user by name or uuid '%s' in db,error:%w", userNameOrUUID, err) @@ -460,13 +460,13 @@ func (c *userComponentImpl) Get(ctx context.Context, userNameOrUUID, visitorName } func (c *userComponentImpl) CheckOperatorAndUser(ctx context.Context, operator, username string) (bool, error) { - opUser, err := c.us.FindByUsername(ctx, operator) + opUser, err := c.userStore.FindByUsername(ctx, operator) if err != nil { newError := fmt.Errorf("failed to find operator by name in db,error:%w", err) return true, newError } - user, err := c.us.FindByUsername(ctx, username) + user, err := c.userStore.FindByUsername(ctx, username) if err != nil { newError := fmt.Errorf("failed to find user by name in db,error:%w", err) return true, newError @@ -483,20 +483,17 @@ func (c *userComponentImpl) CheckOperatorAndUser(ctx context.Context, operator, func (c *userComponentImpl) CheckIfUserHasOrgs(ctx context.Context, userName string) (bool, error) { var ( - err error - orgs []database.Organization + err error + total int ) - if orgs, err = c.os.GetUserOwnOrgs(ctx, userName); err != nil { + if _, total, err = c.orgStore.GetUserOwnOrgs(ctx, userName); err != nil { return false, fmt.Errorf("failed to find orgs by username in db,error:%w", err) } - if len(orgs) == 0 { - return false, nil - } - return true, nil + return total > 0, nil } func (c *userComponentImpl) CheckIffUserHasRunningOrBuildingDeployments(ctx context.Context, userName string) (bool, error) { - user, err := c.us.FindByUsername(ctx, userName) + user, err := c.userStore.FindByUsername(ctx, userName) if err != nil { return false, fmt.Errorf("failed to find user by username in db, error: %v", err) } @@ -511,7 +508,7 @@ func (c *userComponentImpl) CheckIffUserHasRunningOrBuildingDeployments(ctx cont } func (c *userComponentImpl) CheckIfUserHasBills(ctx context.Context, userName string) (bool, error) { - user, err := c.us.FindByUsername(ctx, userName) + user, err := c.userStore.FindByUsername(ctx, userName) if err != nil { return false, fmt.Errorf("failed to find user by username in db, error: %v", err) } @@ -543,7 +540,7 @@ func (c *userComponentImpl) buildUserInfo(ctx context.Context, dbuser *database. u.Roles = dbuser.Roles() } - dborgs, err := c.os.GetUserBelongOrgs(ctx, dbuser.ID) + dborgs, err := c.orgStore.GetUserBelongOrgs(ctx, dbuser.ID) if err != nil { return nil, fmt.Errorf("failed to get orgs for user %s,error:%w", dbuser.Username, err) } @@ -578,7 +575,7 @@ func (c *userComponentImpl) Index(ctx context.Context, visitorName, search strin onlyBasicInfo = true } - dbusers, count, err := c.us.IndexWithSearch(ctx, search, per, page) + dbusers, count, err := c.userStore.IndexWithSearch(ctx, search, per, page) if err != nil { newError := fmt.Errorf("failed to find user by name in db,error:%w", err) return nil, count, newError @@ -619,7 +616,7 @@ func (c *userComponentImpl) Signin(ctx context.Context, code, state string) (*ty } cu := claims.User - exists, err := c.us.IsExistByUUID(ctx, cu.Id) + exists, err := c.userStore.IsExistByUUID(ctx, cu.Id) if err != nil { return nil, "", fmt.Errorf("failed to check user existence by name in db,error:%w", err) } @@ -645,14 +642,14 @@ func (c *userComponentImpl) Signin(ctx context.Context, code, state string) (*ty }(dbu.Username) } else { // get user from db for username, as casdoor may have different username - dbu, err = c.us.FindByUUID(ctx, cu.Id) + dbu, err = c.userStore.FindByUUID(ctx, cu.Id) if err != nil { return nil, "", fmt.Errorf("failed to find user by uuid in db, uuid:%s, error:%w", cu.Id, err) } // update user login time asynchronously go func() { dbu.LastLoginAt = time.Now().Format("2006-01-02 15:04:05") - err := c.us.Update(ctx, dbu) + err := c.userStore.Update(ctx, dbu) if err != nil { slog.Error("failed to update user login time", "error", err, "username", dbu.Username) } diff --git a/user/component/user_test.go b/user/component/user_test.go new file mode 100644 index 00000000..bd51f27c --- /dev/null +++ b/user/component/user_test.go @@ -0,0 +1,29 @@ +package component + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + mockdb "opencsg.com/csghub-server/_mocks/opencsg.com/csghub-server/builder/store/database" + "opencsg.com/csghub-server/builder/store/database" +) + +func TestUserComponent_CheckIfUserHasOrgs(t *testing.T) { + mockOrgStore := mockdb.NewMockOrgStore(t) + mockOrgStore.EXPECT().GetUserOwnOrgs(context.TODO(), "user1").Return([]database.Organization{}, 0, nil) + mockOrgStore.EXPECT().GetUserOwnOrgs(context.TODO(), "user2").Return([]database.Organization{ + {ID: 1}, + }, 1, nil) + uc := &userComponentImpl{ + orgStore: mockOrgStore, + } + + has, err := uc.CheckIfUserHasOrgs(context.TODO(), "user1") + require.Nil(t, err) + require.False(t, has) + + has, err = uc.CheckIfUserHasOrgs(context.TODO(), "user2") + require.Nil(t, err) + require.True(t, has) +} diff --git a/user/handler/organization.go b/user/handler/organization.go index 46806816..b375987d 100644 --- a/user/handler/organization.go +++ b/user/handler/organization.go @@ -9,6 +9,7 @@ import ( "opencsg.com/csghub-server/api/httpbase" "opencsg.com/csghub-server/common/config" "opencsg.com/csghub-server/common/types" + "opencsg.com/csghub-server/common/utils/common" apicomponent "opencsg.com/csghub-server/component" "opencsg.com/csghub-server/user/component" ) @@ -115,15 +116,27 @@ func (h *OrganizationHandler) Get(ctx *gin.Context) { // @Router /organizations [get] func (h *OrganizationHandler) Index(ctx *gin.Context) { username := httpbase.GetCurrentUser(ctx) - orgs, err := h.c.Index(ctx, username) + search := ctx.Query("search") + per, page, err := common.GetPerAndPageFromContext(ctx) + if err != nil { + slog.Error("Failed to get per and page", slog.Any("error", err)) + httpbase.BadRequest(ctx, err.Error()) + return + } + orgs, total, err := h.c.Index(ctx, username, search, per, page) if err != nil { slog.Error("Failed to get organizations", slog.Any("error", err)) httpbase.ServerError(ctx, err) return } - slog.Info("Get organizations succeed") - httpbase.OK(ctx, orgs) + respData := gin.H{ + "data": orgs, + "total": total, + } + + slog.Info("Get organizations succeed", slog.String("username", username), slog.String("search", search), slog.Int("per", per), slog.Int("page", page)) + httpbase.OK(ctx, respData) } // DeleteOrganization godoc diff --git a/user/handler/organization_test.go b/user/handler/organization_test.go new file mode 100644 index 00000000..b0663bd2 --- /dev/null +++ b/user/handler/organization_test.go @@ -0,0 +1,51 @@ +package handler + +import ( + "encoding/json" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + mockcomp "opencsg.com/csghub-server/_mocks/opencsg.com/csghub-server/user/component" + "opencsg.com/csghub-server/api/httpbase" + "opencsg.com/csghub-server/common/types" +) + +func TestOrganizationHandler_Index(t *testing.T) { + response := httptest.NewRecorder() + ginc, _ := gin.CreateTestContext(response) + httpbase.SetCurrentUser(ginc, "user1") + ginc.Request = httptest.NewRequest("GET", "/api/v1/organizations?search=org1&per=10&page=1", nil) + + dborgs := []types.Organization{ + { + Name: "org1", + }, + } + mockOrgComp := mockcomp.NewMockOrganizationComponent(t) + mockOrgComp.EXPECT().Index(mock.Anything, "user1", "org1", 10, 1).Return(dborgs, 1, nil) + h := &OrganizationHandler{ + c: mockOrgComp, + } + h.Index(ginc) + require.Equal(t, 200, response.Code) + var r orgsResponse + err := json.Unmarshal(response.Body.Bytes(), &r) + require.Nil(t, err) + require.NotEmpty(t, r.Data) + require.Equal(t, 1, len(r.Data.Orgs)) + require.Equal(t, 1, r.Data.Total) +} + +type orgsResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data orgsResponseData `json:"data"` +} + +type orgsResponseData struct { + Orgs []types.Organization `json:"data"` + Total int `json:"total"` +}