From ab5af433d9475c5812eed222d0e71f4e6b050258 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Thu, 12 Sep 2024 11:17:00 +0300 Subject: [PATCH] Use central entity tables to list remote repos (#4429) This is part of generalizing entity registration, as other providers will need to work with this mechanism. As part of the migration, this moved the logic to use our new entity model. Related-To: https://github.com/stacklok/minder/issues/4331 Signed-off-by: Juan Antonio Osorio --- database/query/entities.sql | 4 +- internal/controlplane/handlers_entities.go | 24 +++---- .../controlplane/handlers_entities_test.go | 30 +++++++-- .../controlplane/handlers_repositories.go | 42 ++++++++---- .../handlers_repositories_test.go | 34 ++++++---- internal/db/entities.sql.go | 7 +- internal/providers/manager/manager.go | 17 +++-- internal/providers/manager/manager_test.go | 5 +- internal/providers/manager/mock/manager.go | 5 +- .../repositories/mock/fixtures/service.go | 4 +- internal/repositories/mock/service.go | 10 +-- internal/repositories/service.go | 67 ++++++++++++++----- 12 files changed, 169 insertions(+), 80 deletions(-) diff --git a/database/query/entities.sql b/database/query/entities.sql index cf33e90e4b..394ced4d81 100644 --- a/database/query/entities.sql +++ b/database/query/entities.sql @@ -74,7 +74,9 @@ LIMIT 1; -- name: GetEntitiesByType :many SELECT * FROM entity_instances -WHERE entity_instances.entity_type = $1 AND entity_instances.project_id = ANY(sqlc.arg(projects)::uuid[]); +WHERE entity_instances.entity_type = $1 + AND entity_instances.provider_id = sqlc.arg(provider_id) + AND entity_instances.project_id = ANY(sqlc.arg(projects)::uuid[]); -- GetEntitiesByProvider retrieves all entities of a given provider. -- this is how one would get all repositories, artifacts, etc. for a given provider. diff --git a/internal/controlplane/handlers_entities.go b/internal/controlplane/handlers_entities.go index bd628a9c44..725cdc7664 100644 --- a/internal/controlplane/handlers_entities.go +++ b/internal/controlplane/handlers_entities.go @@ -55,8 +55,9 @@ func (s *Server) ReconcileEntityRegistration( return nil, util.UserVisibleError(codes.InvalidArgument, "entity type %s not supported", entityType) } - providerName := in.GetContext().GetProvider() - provs, errorProvs, err := s.providerManager.BulkInstantiateByTrait(ctx, projectID, db.ProviderTypeRepoLister, providerName) + providerNameParam := in.GetContext().GetProvider() + provs, errorProvs, err := s.providerManager.BulkInstantiateByTrait( + ctx, projectID, db.ProviderTypeRepoLister, providerNameParam) if err != nil { pErr := providers.ErrProviderNotFoundBy{} if errors.As(err, &pErr) { @@ -65,22 +66,15 @@ func (s *Server) ReconcileEntityRegistration( return nil, providerError(err) } - for providerName, provider := range provs { - // Explicitly fetch the provider here as we need its ID for posting the event. - pvr, err := s.providerStore.GetByName(ctx, projectID, providerName) - if err != nil { - errorProvs = append(errorProvs, providerName) - continue - } - - repos, err := s.fetchRepositoriesForProvider(ctx, projectID, providerName, provider) + for providerID, providerT := range provs { + repos, err := s.fetchRepositoriesForProvider(ctx, projectID, providerID, providerT.Name, providerT.Provider) if err != nil { l.Error(). - Str("providerName", providerName). + Str("providerName", providerT.Name). Str("projectID", projectID.String()). Err(err). Msg("error fetching repositories for provider") - errorProvs = append(errorProvs, providerName) + errorProvs = append(errorProvs, providerT.Name) continue } @@ -89,11 +83,11 @@ func (s *Server) ReconcileEntityRegistration( continue } - msg, err := createEntityMessage(ctx, &l, projectID, pvr.ID, repo.GetName(), repo.GetOwner()) + msg, err := createEntityMessage(ctx, &l, projectID, providerID, repo.GetName(), repo.GetOwner()) if err != nil { l.Error().Err(err). Int64("repoID", repo.RepoId). - Str("providerName", providerName). + Str("providerName", providerT.Name). Msg("error creating registration entity message") // This message will not be sent, but we can continue with the rest. continue diff --git a/internal/controlplane/handlers_entities_test.go b/internal/controlplane/handlers_entities_test.go index 98f63fae7c..c84819e8df 100644 --- a/internal/controlplane/handlers_entities_test.go +++ b/internal/controlplane/handlers_entities_test.go @@ -18,6 +18,7 @@ import ( "context" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -25,10 +26,10 @@ import ( "github.com/stacklok/minder/internal/engine/engcontext" mockevents "github.com/stacklok/minder/internal/events/mock" mockgh "github.com/stacklok/minder/internal/providers/github/mock" + "github.com/stacklok/minder/internal/providers/manager" mockmanager "github.com/stacklok/minder/internal/providers/manager/mock" rf "github.com/stacklok/minder/internal/repositories/mock/fixtures" pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" - provinfv1 "github.com/stacklok/minder/pkg/providers/v1" ) func TestServer_ReconcileEntityRegistration(t *testing.T) { @@ -58,7 +59,12 @@ func TestServer_ReconcileEntityRegistration(t *testing.T) { providerManager := mockmanager.NewMockProviderManager(ctrl) providerManager.EXPECT().BulkInstantiateByTrait( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - ).Return(map[string]provinfv1.Provider{provider.Name: mockgh.NewMockGitHub(ctrl)}, []string{}, nil).Times(1) + ).Return(map[uuid.UUID]manager.NameProviderTuple{ + uuid.New(): { + Name: provider.Name, + Provider: mockgh.NewMockGitHub(ctrl), + }, + }, []string{}, nil).Times(1) return providerManager }, EventerSetup: func(ctrl *gomock.Controller) *mockevents.MockInterface { @@ -80,7 +86,12 @@ func TestServer_ReconcileEntityRegistration(t *testing.T) { providerManager := mockmanager.NewMockProviderManager(ctrl) providerManager.EXPECT().BulkInstantiateByTrait( gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), - ).Return(map[string]provinfv1.Provider{provider.Name: mockgh.NewMockGitHub(ctrl)}, []string{}, nil).Times(1) + ).Return(map[uuid.UUID]manager.NameProviderTuple{ + uuid.New(): { + Name: provider.Name, + Provider: mockgh.NewMockGitHub(ctrl), + }, + }, []string{}, nil).Times(1) return providerManager }, ExpectedError: "cannot register entities for providers: [github]", @@ -101,22 +112,27 @@ func TestServer_ReconcileEntityRegistration(t *testing.T) { Project: engcontext.Project{ID: projectID}, }) - manager := mockmanager.NewMockProviderManager(ctrl) + mgr := mockmanager.NewMockProviderManager(ctrl) if scenario.ProviderSetup != nil && scenario.GitHubSetup != nil { prov := scenario.GitHubSetup(ctrl) - manager.EXPECT().BulkInstantiateByTrait( + mgr.EXPECT().BulkInstantiateByTrait( gomock.Any(), gomock.Eq(projectID), gomock.Eq(db.ProviderTypeRepoLister), gomock.Eq(""), - ).Return(map[string]provinfv1.Provider{provider.Name: prov}, []string{}, nil) + ).Return(map[uuid.UUID]manager.NameProviderTuple{ + uuid.New(): { + Name: provider.Name, + Provider: prov, + }, + }, []string{}, nil) } server := createServer( ctrl, scenario.RepoServiceSetup, scenario.ProviderFails, - manager, + mgr, ) if scenario.EventerSetup != nil { diff --git a/internal/controlplane/handlers_repositories.go b/internal/controlplane/handlers_repositories.go index e087741fbf..35c28b64fe 100644 --- a/internal/controlplane/handlers_repositories.go +++ b/internal/controlplane/handlers_repositories.go @@ -29,6 +29,7 @@ import ( "github.com/stacklok/minder/internal/db" "github.com/stacklok/minder/internal/engine/engcontext" + "github.com/stacklok/minder/internal/entities/properties" "github.com/stacklok/minder/internal/logger" "github.com/stacklok/minder/internal/projects/features" "github.com/stacklok/minder/internal/providers" @@ -318,7 +319,8 @@ func (s *Server) ListRemoteRepositoriesFromProvider( logger.BusinessRecord(ctx).Project = projectID providerName := in.GetContext().GetProvider() - provs, errorProvs, err := s.providerManager.BulkInstantiateByTrait(ctx, projectID, db.ProviderTypeRepoLister, providerName) + provs, errorProvs, err := s.providerManager.BulkInstantiateByTrait( + ctx, projectID, db.ProviderTypeRepoLister, providerName) if err != nil { pErr := providers.ErrProviderNotFoundBy{} if errors.As(err, &pErr) { @@ -331,11 +333,13 @@ func (s *Server) ListRemoteRepositoriesFromProvider( Results: []*pb.UpstreamRepositoryRef{}, } - for providerName, provider := range provs { - results, err := s.fetchRepositoriesForProvider(ctx, projectID, providerName, provider) + for providerID, providerT := range provs { + results, err := s.fetchRepositoriesForProvider( + ctx, projectID, providerID, providerT.Name, providerT.Provider) if err != nil { - zerolog.Ctx(ctx).Error().Err(err).Msgf("error listing repositories for provider %s in project %s", providerName, projectID) - errorProvs = append(errorProvs, providerName) + zerolog.Ctx(ctx).Error().Err(err). + Msgf("error listing repositories for provider %s in project %s", providerT.Name, projectID) + errorProvs = append(errorProvs, providerT.Name) continue } out.Results = append(out.Results, results...) @@ -355,11 +359,12 @@ func (s *Server) ListRemoteRepositoriesFromProvider( func (s *Server) fetchRepositoriesForProvider( ctx context.Context, projectID uuid.UUID, + providerID uuid.UUID, providerName string, provider v1.Provider, ) ([]*pb.UpstreamRepositoryRef, error) { zerolog.Ctx(ctx).Trace(). - Str("provider", providerName). + Str("provider_id", providerID.String()). Str("project_id", projectID.String()). Msg("listing repositories") @@ -378,12 +383,12 @@ func (s *Server) fetchRepositoriesForProvider( registeredRepos, err := s.repos.ListRepositories( ctx, projectID, - providerName, + providerID, ) if err != nil { zerolog.Ctx(ctx).Error(). - Str("projectID", projectID.String()). - Str("providerName", providerName). + Str("project_id", projectID.String()). + Str("provider_id", providerID.String()). Err(err).Msg("cannot list registered repositories") return nil, util.UserVisibleError( codes.Internal, @@ -391,13 +396,26 @@ func (s *Server) fetchRepositoriesForProvider( ) } - registered := make(map[int64]bool) + registered := make(map[string]bool) for _, repo := range registeredRepos { - registered[repo.RepoID] = true + uidP := repo.Properties.GetProperty(properties.PropertyUpstreamID) + if uidP == nil { + zerolog.Ctx(ctx).Warn(). + Str("entity_id", repo.Entity.ID.String()). + Str("entity_name", repo.Entity.Name). + Str("provider_id", providerID.String()). + Str("project_id", projectID.String()). + Msg("repository has no upstream ID") + continue + } + registered[uidP.GetString()] = true } for _, result := range results { - result.Registered = registered[result.RepoId] + // TEMPORARY: This will be changed to use properties. + // for now, we transform the repo ID to a string + uid := fmt.Sprintf("%d", result.RepoId) + result.Registered = registered[uid] } return results, nil diff --git a/internal/controlplane/handlers_repositories_test.go b/internal/controlplane/handlers_repositories_test.go index 816baa1ab1..9f80eb13f8 100644 --- a/internal/controlplane/handlers_repositories_test.go +++ b/internal/controlplane/handlers_repositories_test.go @@ -19,6 +19,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "testing" "github.com/google/uuid" @@ -30,6 +31,8 @@ import ( "github.com/stacklok/minder/internal/config/server" "github.com/stacklok/minder/internal/db" "github.com/stacklok/minder/internal/engine/engcontext" + "github.com/stacklok/minder/internal/entities/models" + "github.com/stacklok/minder/internal/entities/properties" "github.com/stacklok/minder/internal/providers" ghprovider "github.com/stacklok/minder/internal/providers/github/clients" mockgh "github.com/stacklok/minder/internal/providers/github/mock" @@ -223,19 +226,24 @@ func TestServer_ListRemoteRepositoriesFromProvider(t *testing.T) { }) prov := scenario.GitHubSetup(ctrl) - manager := mockmanager.NewMockProviderManager(ctrl) - manager.EXPECT().BulkInstantiateByTrait( + mgr := mockmanager.NewMockProviderManager(ctrl) + mgr.EXPECT().BulkInstantiateByTrait( gomock.Any(), gomock.Eq(projectID), gomock.Eq(db.ProviderTypeRepoLister), gomock.Eq(""), - ).Return(map[string]provinfv1.Provider{provider.Name: prov}, []string{}, nil) + ).Return(map[uuid.UUID]manager.NameProviderTuple{ + provider.ID: { + Name: provider.Name, + Provider: prov, + }, + }, []string{}, nil) server := createServer( ctrl, scenario.RepoServiceSetup, scenario.ProviderFails, - manager, + mgr, ) projectIDStr := projectID.String() @@ -420,14 +428,16 @@ var ( } ) -func simpleDbRepository(name string, id int64) db.Repository { - return db.Repository{ - ID: uuid.UUID{}, - Provider: ghprovider.Github, - RepoOwner: repoOwner, - RepoName: name, - RepoID: id, - } +func simpleDbRepository(name string, id int64) *models.EntityWithProperties { + //nolint:errcheck // this shouldn't fail + props, _ := properties.NewProperties(map[string]any{ + "repo_id": id, + properties.PropertyUpstreamID: fmt.Sprintf("%d", id), + }) + return models.NewEntityWithPropertiesFromInstance(models.EntityInstance{ + ID: uuid.UUID{}, + Name: name, + }, props) } func simpleUpstreamRepositoryRef(name string, id int64, registered bool) *pb.UpstreamRepositoryRef { diff --git a/internal/db/entities.sql.go b/internal/db/entities.sql.go index d64582b718..4371d6aff1 100644 --- a/internal/db/entities.sql.go +++ b/internal/db/entities.sql.go @@ -283,18 +283,21 @@ func (q *Queries) GetEntitiesByProvider(ctx context.Context, providerID uuid.UUI const getEntitiesByType = `-- name: GetEntitiesByType :many SELECT id, entity_type, name, project_id, provider_id, created_at, originated_from FROM entity_instances -WHERE entity_instances.entity_type = $1 AND entity_instances.project_id = ANY($2::uuid[]) +WHERE entity_instances.entity_type = $1 + AND entity_instances.provider_id = $2 + AND entity_instances.project_id = ANY($3::uuid[]) ` type GetEntitiesByTypeParams struct { EntityType Entities `json:"entity_type"` + ProviderID uuid.UUID `json:"provider_id"` Projects []uuid.UUID `json:"projects"` } // GetEntitiesByType retrieves all entities of a given type for a project or hierarchy of projects. // this is how one would get all repositories, artifacts, etc. func (q *Queries) GetEntitiesByType(ctx context.Context, arg GetEntitiesByTypeParams) ([]EntityInstance, error) { - rows, err := q.db.QueryContext(ctx, getEntitiesByType, arg.EntityType, pq.Array(arg.Projects)) + rows, err := q.db.QueryContext(ctx, getEntitiesByType, arg.EntityType, arg.ProviderID, pq.Array(arg.Projects)) if err != nil { return nil, err } diff --git a/internal/providers/manager/manager.go b/internal/providers/manager/manager.go index 2c4d9e8e2a..99d43dab77 100644 --- a/internal/providers/manager/manager.go +++ b/internal/providers/manager/manager.go @@ -33,6 +33,12 @@ import ( //go:generate go run go.uber.org/mock/mockgen -package mock_$GOPACKAGE -destination=./mock/$GOFILE -source=./$GOFILE +// NameProviderTuple is a tuple of a provider name and the provider instance +type NameProviderTuple struct { + Name string + Provider v1.Provider +} + // ProviderManager encapsulates operations for manipulating Provider instances type ProviderManager interface { // CreateFromConfig creates a new Provider instance in the database with a given configuration or the provider default @@ -55,7 +61,7 @@ type ProviderManager interface { projectID uuid.UUID, trait db.ProviderType, name string, - ) (map[string]v1.Provider, []string, error) + ) (map[uuid.UUID]NameProviderTuple, []string, error) // DeleteByID deletes the specified instance of the Provider, and // carries out any cleanup needed. DeleteByID(ctx context.Context, providerID uuid.UUID, projectID uuid.UUID) error @@ -217,13 +223,13 @@ func (p *providerManager) BulkInstantiateByTrait( projectID uuid.UUID, trait db.ProviderType, name string, -) (map[string]v1.Provider, []string, error) { +) (map[uuid.UUID]NameProviderTuple, []string, error) { providerConfigs, err := p.store.GetByTraitInHierarchy(ctx, projectID, name, trait) if err != nil { return nil, nil, fmt.Errorf("error retrieving db records: %w", err) } - result := make(map[string]v1.Provider, len(providerConfigs)) + result := make(map[uuid.UUID]NameProviderTuple, len(providerConfigs)) failedProviders := []string{} for _, config := range providerConfigs { provider, err := p.buildFromDBRecord(ctx, &config) @@ -232,7 +238,10 @@ func (p *providerManager) BulkInstantiateByTrait( failedProviders = append(failedProviders, config.Name) continue } - result[config.Name] = provider + result[config.ID] = NameProviderTuple{ + Name: config.Name, + Provider: provider, + } } return result, failedProviders, nil diff --git a/internal/providers/manager/manager_test.go b/internal/providers/manager/manager_test.go index 70d2ba2f9b..3a52d52b51 100644 --- a/internal/providers/manager/manager_test.go +++ b/internal/providers/manager/manager_test.go @@ -379,7 +379,10 @@ func TestProviderManager_BulkInstantiateByTrait(t *testing.T) { } else { require.Len(t, success, 1) require.Empty(t, fail) - require.Equal(t, provider, success[scenario.Provider.Name]) + require.Equal(t, manager.NameProviderTuple{ + Name: scenario.Provider.Name, + Provider: provider, + }, success[scenario.Provider.ID]) } }) } diff --git a/internal/providers/manager/mock/manager.go b/internal/providers/manager/mock/manager.go index 8c59b2a950..0f0ee9b09a 100644 --- a/internal/providers/manager/mock/manager.go +++ b/internal/providers/manager/mock/manager.go @@ -16,6 +16,7 @@ import ( uuid "github.com/google/uuid" db "github.com/stacklok/minder/internal/db" + manager "github.com/stacklok/minder/internal/providers/manager" v1 "github.com/stacklok/minder/pkg/providers/v1" gomock "go.uber.org/mock/gomock" ) @@ -44,10 +45,10 @@ func (m *MockProviderManager) EXPECT() *MockProviderManagerMockRecorder { } // BulkInstantiateByTrait mocks base method. -func (m *MockProviderManager) BulkInstantiateByTrait(ctx context.Context, projectID uuid.UUID, trait db.ProviderType, name string) (map[string]v1.Provider, []string, error) { +func (m *MockProviderManager) BulkInstantiateByTrait(ctx context.Context, projectID uuid.UUID, trait db.ProviderType, name string) (map[uuid.UUID]manager.NameProviderTuple, []string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BulkInstantiateByTrait", ctx, projectID, trait, name) - ret0, _ := ret[0].(map[string]v1.Provider) + ret0, _ := ret[0].(map[uuid.UUID]manager.NameProviderTuple) ret1, _ := ret[1].([]string) ret2, _ := ret[2].(error) return ret0, ret1, ret2 diff --git a/internal/repositories/mock/fixtures/service.go b/internal/repositories/mock/fixtures/service.go index 121c09e344..9e8def0c35 100644 --- a/internal/repositories/mock/fixtures/service.go +++ b/internal/repositories/mock/fixtures/service.go @@ -23,7 +23,7 @@ import ( "github.com/google/uuid" "go.uber.org/mock/gomock" - "github.com/stacklok/minder/internal/db" + "github.com/stacklok/minder/internal/entities/models" mockghrepo "github.com/stacklok/minder/internal/repositories/mock" pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" ) @@ -131,7 +131,7 @@ func WithFailedDeleteByName(err error) func(RepoServiceMock) { } func WithSuccessfulListRepositories( - repositories ...db.Repository, + repositories ...*models.EntityWithProperties, ) func(RepoServiceMock) { return func(mock RepoServiceMock) { mock.EXPECT(). diff --git a/internal/repositories/mock/service.go b/internal/repositories/mock/service.go index f8fa6e45ef..aafe708916 100644 --- a/internal/repositories/mock/service.go +++ b/internal/repositories/mock/service.go @@ -117,18 +117,18 @@ func (mr *MockRepositoryServiceMockRecorder) GetRepositoryByName(ctx, repoOwner, } // ListRepositories mocks base method. -func (m *MockRepositoryService) ListRepositories(ctx context.Context, projectID uuid.UUID, providerName string) ([]db.Repository, error) { +func (m *MockRepositoryService) ListRepositories(ctx context.Context, projectID, providerID uuid.UUID) ([]*models.EntityWithProperties, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListRepositories", ctx, projectID, providerName) - ret0, _ := ret[0].([]db.Repository) + ret := m.ctrl.Call(m, "ListRepositories", ctx, projectID, providerID) + ret0, _ := ret[0].([]*models.EntityWithProperties) ret1, _ := ret[1].(error) return ret0, ret1 } // ListRepositories indicates an expected call of ListRepositories. -func (mr *MockRepositoryServiceMockRecorder) ListRepositories(ctx, projectID, providerName any) *gomock.Call { +func (mr *MockRepositoryServiceMockRecorder) ListRepositories(ctx, projectID, providerID any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositories", reflect.TypeOf((*MockRepositoryService)(nil).ListRepositories), ctx, projectID, providerName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRepositories", reflect.TypeOf((*MockRepositoryService)(nil).ListRepositories), ctx, projectID, providerID) } // RefreshRepositoryByUpstreamID mocks base method. diff --git a/internal/repositories/service.go b/internal/repositories/service.go index 06bbfff00a..9832ec9115 100644 --- a/internal/repositories/service.go +++ b/internal/repositories/service.go @@ -78,14 +78,12 @@ type RepositoryService interface { ) error // ListRepositories retrieves all repositories for the - // specific provider and project. Ideally, we would take - // provider ID instead of name. Name is used for backwards - // compatibility with the API endpoint which calls it. + // specific provider and project. ListRepositories( ctx context.Context, projectID uuid.UUID, - providerName string, - ) ([]db.Repository, error) + providerID uuid.UUID, + ) ([]*models.EntityWithProperties, error) // GetRepositoryById retrieves a repository by its ID and project. GetRepositoryById(ctx context.Context, repositoryID uuid.UUID, projectID uuid.UUID) (db.Repository, error) @@ -229,18 +227,53 @@ func (r *repositoryService) CreateRepository( func (r *repositoryService) ListRepositories( ctx context.Context, projectID uuid.UUID, - providerName string, -) ([]db.Repository, error) { - return r.store.ListRepositoriesByProjectID( - ctx, - db.ListRepositoriesByProjectIDParams{ - ProjectID: projectID, - Provider: sql.NullString{ - String: providerName, - Valid: providerName != "", - }, - }, - ) + providerID uuid.UUID, +) (ents []*models.EntityWithProperties, outErr error) { + tx, err := r.store.BeginTransaction() + if err != nil { + return nil, fmt.Errorf("error starting transaction: %w", err) + } + + defer func() { + if outErr != nil { + if err := tx.Rollback(); err != nil { + log.Printf("error rolling back transaction: %v", err) + } + } + }() + + qtx := r.store.GetQuerierWithTransaction(tx) + + repoEnts, err := qtx.GetEntitiesByType(ctx, db.GetEntitiesByTypeParams{ + EntityType: db.EntitiesRepository, + ProviderID: providerID, + Projects: []uuid.UUID{projectID}, + }) + if err != nil { + return nil, fmt.Errorf("error fetching repositories: %w", err) + } + + ents = make([]*models.EntityWithProperties, 0, len(repoEnts)) + for _, ent := range repoEnts { + ewp, err := r.propSvc.EntityWithProperties(ctx, ent.ID, qtx) + if err != nil { + return nil, fmt.Errorf("error fetching properties for repository: %w", err) + } + + if err := r.propSvc.RetrieveAllPropertiesForEntity(ctx, ewp, r.providerManager, qtx); err != nil { + return nil, fmt.Errorf("error fetching properties for repository: %w", err) + } + + ents = append(ents, ewp) + } + + // We care about commiting the transaction since the `RetrieveAllPropertiesForEntity` + // call above may have modified the properties of the entities + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("error committing transaction: %w", err) + } + + return ents, nil } func (r *repositoryService) GetRepositoryById(