diff --git a/internal/database/database.go b/internal/database/database.go index 7edb9b30b..060402caf 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -123,7 +123,6 @@ func NewConnection(w logger.Writer) (*gorm.DB, error) { } // Initialize stores, sorted in alphabetical order. - Orgs = NewOrgsStore(db) Perms = NewPermsStore(db) Repos = NewReposStore(db) TwoFactors = &twoFactorsStore{DB: db} @@ -132,6 +131,7 @@ func NewConnection(w logger.Writer) (*gorm.DB, error) { return db, nil } +// DB is the database handler for the storage layer. type DB struct { db *gorm.DB } @@ -176,3 +176,7 @@ func (db *DB) LoginSources() *LoginSourcesStore { func (db *DB) Notices() *NoticesStore { return newNoticesStore(db.db) } + +func (db *DB) Organizations() *OrganizationsStore { + return newOrganizationsStoreStore(db.db) +} diff --git a/internal/database/organizations.go b/internal/database/organizations.go new file mode 100644 index 000000000..7c1cc68c1 --- /dev/null +++ b/internal/database/organizations.go @@ -0,0 +1,79 @@ +// Copyright 2022 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package database + +import ( + "context" + + "github.com/pkg/errors" + "gorm.io/gorm" + + "gogs.io/gogs/internal/dbutil" +) + +// OrganizationsStore is the storage layer for organizations. +type OrganizationsStore struct { + db *gorm.DB +} + +func newOrganizationsStoreStore(db *gorm.DB) *OrganizationsStore { + return &OrganizationsStore{db: db} +} + +type ListOrgsOptions struct { + // Filter by the membership with the given user ID. + MemberID int64 + // Whether to include private memberships. + IncludePrivateMembers bool +} + +// List returns a list of organizations filtered by options. +func (s *OrganizationsStore) List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error) { + if opts.MemberID <= 0 { + return nil, errors.New("MemberID must be greater than 0") + } + + /* + Equivalent SQL for PostgreSQL: + + SELECT * FROM "org" + JOIN org_user ON org_user.org_id = org.id + WHERE + org_user.uid = @memberID + [AND org_user.is_public = @includePrivateMembers] + ORDER BY org.id ASC + */ + tx := s.db.WithContext(ctx). + Joins(dbutil.Quote("JOIN org_user ON org_user.org_id = %s.id", "user")). + Where("org_user.uid = ?", opts.MemberID). + Order(dbutil.Quote("%s.id ASC", "user")) + if !opts.IncludePrivateMembers { + tx = tx.Where("org_user.is_public = ?", true) + } + + var orgs []*Organization + return orgs, tx.Find(&orgs).Error +} + +// SearchByName returns a list of organizations whose username or full name +// matches the given keyword case-insensitively. Results are paginated by given +// page and page size, and sorted by the given order (e.g. "id DESC"). A total +// count of all results is also returned. If the order is not given, it's up to +// the database to decide. +func (s *OrganizationsStore) SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error) { + return searchUserByName(ctx, s.db, UserTypeOrganization, keyword, page, pageSize, orderBy) +} + +// CountByUser returns the number of organizations the user is a member of. +func (s *OrganizationsStore) CountByUser(ctx context.Context, userID int64) (int64, error) { + var count int64 + return count, s.db.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error +} + +type Organization = User + +func (o *Organization) TableName() string { + return "user" +} diff --git a/internal/database/orgs_test.go b/internal/database/organizations_test.go similarity index 72% rename from internal/database/orgs_test.go rename to internal/database/organizations_test.go index abe868516..daffdbeae 100644 --- a/internal/database/orgs_test.go +++ b/internal/database/organizations_test.go @@ -21,24 +21,24 @@ func TestOrgs(t *testing.T) { t.Parallel() ctx := context.Background() - db := &orgsStore{ - DB: newTestDB(t, "orgsStore"), + s := &OrganizationsStore{ + db: newTestDB(t, "OrganizationsStore"), } for _, tc := range []struct { name string - test func(t *testing.T, ctx context.Context, db *orgsStore) + test func(t *testing.T, ctx context.Context, s *OrganizationsStore) }{ {"List", orgsList}, - {"SearchByName", orgsSearchByName}, - {"CountByUser", orgsCountByUser}, + {"SearchByName", organizationsSearchByName}, + {"CountByUser", organizationsCountByUser}, } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { - err := clearTables(t, db.DB) + err := clearTables(t, s.db) require.NoError(t, err) }) - tc.test(t, ctx, db) + tc.test(t, ctx, s) }) if t.Failed() { break @@ -46,8 +46,8 @@ func TestOrgs(t *testing.T) { } } -func orgsList(t *testing.T, ctx context.Context, db *orgsStore) { - usersStore := NewUsersStore(db.DB) +func orgsList(t *testing.T, ctx context.Context, s *OrganizationsStore) { + usersStore := NewUsersStore(s.db) alice, err := usersStore.Create(ctx, "alice", "alice@example.com", CreateUserOptions{}) require.NoError(t, err) bob, err := usersStore.Create(ctx, "bob", "bob@example.com", CreateUserOptions{}) @@ -58,18 +58,18 @@ func orgsList(t *testing.T, ctx context.Context, db *orgsStore) { require.NoError(t, err) org2, err := usersStore.Create(ctx, "org2", "org2@example.com", CreateUserOptions{}) require.NoError(t, err) - err = db.Exec( + err = s.db.Exec( dbutil.Quote("UPDATE %s SET type = ? WHERE id IN (?, ?)", "user"), UserTypeOrganization, org1.ID, org2.ID, ).Error require.NoError(t, err) // TODO: Use Orgs.Join to replace SQL hack when the method is available. - err = db.Exec(`INSERT INTO org_user (uid, org_id, is_public) VALUES (?, ?, ?)`, alice.ID, org1.ID, false).Error + err = s.db.Exec(`INSERT INTO org_user (uid, org_id, is_public) VALUES (?, ?, ?)`, alice.ID, org1.ID, false).Error require.NoError(t, err) - err = db.Exec(`INSERT INTO org_user (uid, org_id, is_public) VALUES (?, ?, ?)`, alice.ID, org2.ID, true).Error + err = s.db.Exec(`INSERT INTO org_user (uid, org_id, is_public) VALUES (?, ?, ?)`, alice.ID, org2.ID, true).Error require.NoError(t, err) - err = db.Exec(`INSERT INTO org_user (uid, org_id, is_public) VALUES (?, ?, ?)`, bob.ID, org2.ID, true).Error + err = s.db.Exec(`INSERT INTO org_user (uid, org_id, is_public) VALUES (?, ?, ?)`, bob.ID, org2.ID, true).Error require.NoError(t, err) tests := []struct { @@ -104,7 +104,7 @@ func orgsList(t *testing.T, ctx context.Context, db *orgsStore) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, err := db.List(ctx, test.opts) + got, err := s.List(ctx, test.opts) require.NoError(t, err) gotOrgNames := make([]string, len(got)) @@ -116,21 +116,21 @@ func orgsList(t *testing.T, ctx context.Context, db *orgsStore) { } } -func orgsSearchByName(t *testing.T, ctx context.Context, db *orgsStore) { +func organizationsSearchByName(t *testing.T, ctx context.Context, s *OrganizationsStore) { // TODO: Use Orgs.Create to replace SQL hack when the method is available. - usersStore := NewUsersStore(db.DB) + usersStore := NewUsersStore(s.db) org1, err := usersStore.Create(ctx, "org1", "org1@example.com", CreateUserOptions{FullName: "Acme Corp"}) require.NoError(t, err) org2, err := usersStore.Create(ctx, "org2", "org2@example.com", CreateUserOptions{FullName: "Acme Corp 2"}) require.NoError(t, err) - err = db.Exec( + err = s.db.Exec( dbutil.Quote("UPDATE %s SET type = ? WHERE id IN (?, ?)", "user"), UserTypeOrganization, org1.ID, org2.ID, ).Error require.NoError(t, err) t.Run("search for username org1", func(t *testing.T) { - orgs, count, err := db.SearchByName(ctx, "G1", 1, 1, "") + orgs, count, err := s.SearchByName(ctx, "G1", 1, 1, "") require.NoError(t, err) require.Len(t, orgs, int(count)) assert.Equal(t, int64(1), count) @@ -138,7 +138,7 @@ func orgsSearchByName(t *testing.T, ctx context.Context, db *orgsStore) { }) t.Run("search for username org2", func(t *testing.T) { - orgs, count, err := db.SearchByName(ctx, "G2", 1, 1, "") + orgs, count, err := s.SearchByName(ctx, "G2", 1, 1, "") require.NoError(t, err) require.Len(t, orgs, int(count)) assert.Equal(t, int64(1), count) @@ -146,14 +146,14 @@ func orgsSearchByName(t *testing.T, ctx context.Context, db *orgsStore) { }) t.Run("search for full name acme", func(t *testing.T) { - orgs, count, err := db.SearchByName(ctx, "ACME", 1, 10, "") + orgs, count, err := s.SearchByName(ctx, "ACME", 1, 10, "") require.NoError(t, err) require.Len(t, orgs, int(count)) assert.Equal(t, int64(2), count) }) t.Run("search for full name acme ORDER BY id DESC LIMIT 1", func(t *testing.T) { - orgs, count, err := db.SearchByName(ctx, "ACME", 1, 1, "id DESC") + orgs, count, err := s.SearchByName(ctx, "ACME", 1, 1, "id DESC") require.NoError(t, err) require.Len(t, orgs, 1) assert.Equal(t, int64(2), count) @@ -161,18 +161,18 @@ func orgsSearchByName(t *testing.T, ctx context.Context, db *orgsStore) { }) } -func orgsCountByUser(t *testing.T, ctx context.Context, db *orgsStore) { +func organizationsCountByUser(t *testing.T, ctx context.Context, s *OrganizationsStore) { // TODO: Use Orgs.Join to replace SQL hack when the method is available. - err := db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, 1, 1).Error + err := s.db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, 1, 1).Error require.NoError(t, err) - err = db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, 2, 1).Error + err = s.db.Exec(`INSERT INTO org_user (uid, org_id) VALUES (?, ?)`, 2, 1).Error require.NoError(t, err) - got, err := db.CountByUser(ctx, 1) + got, err := s.CountByUser(ctx, 1) require.NoError(t, err) assert.Equal(t, int64(1), got) - got, err = db.CountByUser(ctx, 404) + got, err = s.CountByUser(ctx, 404) require.NoError(t, err) assert.Equal(t, int64(0), got) } diff --git a/internal/database/orgs.go b/internal/database/orgs.go deleted file mode 100644 index e63ac94c8..000000000 --- a/internal/database/orgs.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2022 The Gogs Authors. All rights reserved. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package database - -import ( - "context" - - "github.com/pkg/errors" - "gorm.io/gorm" - - "gogs.io/gogs/internal/dbutil" -) - -// OrgsStore is the persistent interface for organizations. -type OrgsStore interface { - // List returns a list of organizations filtered by options. - List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error) - // SearchByName returns a list of organizations whose username or full name - // matches the given keyword case-insensitively. Results are paginated by given - // page and page size, and sorted by the given order (e.g. "id DESC"). A total - // count of all results is also returned. If the order is not given, it's up to - // the database to decide. - SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error) - - // CountByUser returns the number of organizations the user is a member of. - CountByUser(ctx context.Context, userID int64) (int64, error) -} - -var Orgs OrgsStore - -var _ OrgsStore = (*orgsStore)(nil) - -type orgsStore struct { - *gorm.DB -} - -// NewOrgsStore returns a persistent interface for orgs with given database -// connection. -func NewOrgsStore(db *gorm.DB) OrgsStore { - return &orgsStore{DB: db} -} - -type ListOrgsOptions struct { - // Filter by the membership with the given user ID. - MemberID int64 - // Whether to include private memberships. - IncludePrivateMembers bool -} - -func (s *orgsStore) List(ctx context.Context, opts ListOrgsOptions) ([]*Organization, error) { - if opts.MemberID <= 0 { - return nil, errors.New("MemberID must be greater than 0") - } - - /* - Equivalent SQL for PostgreSQL: - - SELECT * FROM "org" - JOIN org_user ON org_user.org_id = org.id - WHERE - org_user.uid = @memberID - [AND org_user.is_public = @includePrivateMembers] - ORDER BY org.id ASC - */ - tx := s.WithContext(ctx). - Joins(dbutil.Quote("JOIN org_user ON org_user.org_id = %s.id", "user")). - Where("org_user.uid = ?", opts.MemberID). - Order(dbutil.Quote("%s.id ASC", "user")) - if !opts.IncludePrivateMembers { - tx = tx.Where("org_user.is_public = ?", true) - } - - var orgs []*Organization - return orgs, tx.Find(&orgs).Error -} - -func (s *orgsStore) SearchByName(ctx context.Context, keyword string, page, pageSize int, orderBy string) ([]*Organization, int64, error) { - return searchUserByName(ctx, s.DB, UserTypeOrganization, keyword, page, pageSize, orderBy) -} - -func (s *orgsStore) CountByUser(ctx context.Context, userID int64) (int64, error) { - var count int64 - return count, s.WithContext(ctx).Model(&OrgUser{}).Where("uid = ?", userID).Count(&count).Error -} - -type Organization = User - -func (o *Organization) TableName() string { - return "user" -} diff --git a/internal/database/users.go b/internal/database/users.go index 61de0bd57..a6ebc4f6a 100644 --- a/internal/database/users.go +++ b/internal/database/users.go @@ -1491,7 +1491,7 @@ func (u *User) IsPublicMember(orgId int64) bool { // TODO(unknwon): This is also used in templates, which should be fixed by // having a dedicated type `template.User`. func (u *User) GetOrganizationCount() (int64, error) { - return Orgs.CountByUser(context.TODO(), u.ID) + return Handle.Organizations().CountByUser(context.TODO(), u.ID) } // ShortName truncates and returns the username at most in given length. diff --git a/internal/route/api/v1/org/org.go b/internal/route/api/v1/org/org.go index 92bb72db8..4587431b4 100644 --- a/internal/route/api/v1/org/org.go +++ b/internal/route/api/v1/org/org.go @@ -43,7 +43,7 @@ func CreateOrgForUser(c *context.APIContext, apiForm api.CreateOrgOption, user * } func listUserOrgs(c *context.APIContext, u *database.User, all bool) { - orgs, err := database.Orgs.List( + orgs, err := database.Handle.Organizations().List( c.Req.Context(), database.ListOrgsOptions{ MemberID: u.ID, diff --git a/internal/route/home.go b/internal/route/home.go index 8a2a25905..bcca302ae 100644 --- a/internal/route/home.go +++ b/internal/route/home.go @@ -115,7 +115,7 @@ func RenderUserSearch(c *context.Context, opts *UserSearchOptions) { } else { search := database.Users.SearchByName if opts.Type == database.UserTypeOrganization { - search = database.Orgs.SearchByName + search = database.Handle.Organizations().SearchByName } users, count, err = search(c.Req.Context(), keyword, page, opts.PageSize, opts.OrderBy) if err != nil { diff --git a/internal/route/repo/pull.go b/internal/route/repo/pull.go index 336b2b437..1859a6fa9 100644 --- a/internal/route/repo/pull.go +++ b/internal/route/repo/pull.go @@ -69,7 +69,7 @@ func parseBaseRepository(c *context.Context) *database.Repository { } c.Data["ForkFrom"] = baseRepo.Owner.Name + "/" + baseRepo.Name - orgs, err := database.Orgs.List( + orgs, err := database.Handle.Organizations().List( c.Req.Context(), database.ListOrgsOptions{ MemberID: c.User.ID, diff --git a/internal/route/user/home.go b/internal/route/user/home.go index 1f3d64da9..5fc0cdb23 100644 --- a/internal/route/user/home.go +++ b/internal/route/user/home.go @@ -40,7 +40,7 @@ func getDashboardContextUser(c *context.Context) *database.User { } c.Data["ContextUser"] = ctxUser - orgs, err := database.Orgs.List( + orgs, err := database.Handle.Organizations().List( c.Req.Context(), database.ListOrgsOptions{ MemberID: c.User.ID,