Skip to content

Commit 0f7fd83

Browse files
feat: add support for multiple github connectors (#616)
* chore(release): v0.1.0-alpha.71 [skip ci] * chore(release): update version.txt to v0.1.0-alpha.71 * feat: improvise github connector flow ux * fix: add missing translation keys to locales * feat: enable support for github connector settings * feat: add support for creating multiple github connectors --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 6aee921 commit 0f7fd83

File tree

18 files changed

+422
-31
lines changed

18 files changed

+422
-31
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package controller
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/go-fuego/fuego"
7+
"github.com/raghavyuva/nixopus-api/internal/features/github-connector/types"
8+
"github.com/raghavyuva/nixopus-api/internal/features/logger"
9+
"github.com/raghavyuva/nixopus-api/internal/utils"
10+
11+
shared_types "github.com/raghavyuva/nixopus-api/internal/types"
12+
)
13+
14+
func (c *GithubConnectorController) DeleteGithubConnector(f fuego.ContextWithBody[types.DeleteGithubConnectorRequest]) (*shared_types.Response, error) {
15+
deleteRequest, err := f.Body()
16+
17+
if err != nil {
18+
return nil, fuego.HTTPError{
19+
Err: err,
20+
Status: http.StatusBadRequest,
21+
}
22+
}
23+
24+
w, r := f.Response(), f.Request()
25+
26+
if !c.parseAndValidate(w, r, &deleteRequest) {
27+
// parseAndValidate already sent the error response, so return nil to prevent duplicate response
28+
return nil, nil
29+
}
30+
31+
user := utils.GetUser(w, r)
32+
33+
if user == nil {
34+
return nil, fuego.HTTPError{
35+
Err: nil,
36+
Status: http.StatusUnauthorized,
37+
}
38+
}
39+
40+
err = c.service.DeleteConnector(deleteRequest.ID, user.ID.String())
41+
if err != nil {
42+
c.logger.Log(logger.Error, err.Error(), "")
43+
if err == types.ErrConnectorDoesNotExist {
44+
return nil, fuego.HTTPError{
45+
Err: err,
46+
Status: http.StatusNotFound,
47+
}
48+
}
49+
if err == types.ErrPermissionDenied {
50+
return nil, fuego.HTTPError{
51+
Err: err,
52+
Status: http.StatusForbidden,
53+
}
54+
}
55+
return nil, fuego.HTTPError{
56+
Err: err,
57+
Status: http.StatusInternalServerError,
58+
}
59+
}
60+
61+
return &shared_types.Response{
62+
Status: "success",
63+
Message: "Github Connector deleted successfully",
64+
}, nil
65+
}
66+

api/internal/features/github-connector/controller/get_github_repositories.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func (c *GithubConnectorController) GetGithubRepositories(f fuego.ContextNoBody)
2525
q := r.URL.Query()
2626
page := 1
2727
pageSize := 10
28+
connectorID := q.Get("connector_id")
2829

2930
if v := q.Get("page"); v != "" {
3031
if p, err := strconv.Atoi(v); err == nil && p > 0 {
@@ -37,7 +38,7 @@ func (c *GithubConnectorController) GetGithubRepositories(f fuego.ContextNoBody)
3738
}
3839
}
3940

40-
repositories, totalCount, err := c.service.GetGithubRepositoriesPaginated(user.ID.String(), page, pageSize)
41+
repositories, totalCount, err := c.service.GetGithubRepositoriesPaginated(user.ID.String(), page, pageSize, connectorID)
4142
if err != nil {
4243
c.logger.Log(logger.Error, err.Error(), "")
4344
return nil, fuego.HTTPError{

api/internal/features/github-connector/controller/update_connector.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func (c *GithubConnectorController) UpdateGithubConnectorRequest(f fuego.Context
4141
}
4242
}
4343

44-
err = c.service.UpdateGithubConnectorRequest(UpdateConnectorRequest.InstallationID, user.ID.String())
44+
err = c.service.UpdateGithubConnectorRequest(UpdateConnectorRequest.InstallationID, user.ID.String(), UpdateConnectorRequest.ConnectorID)
4545
if err != nil {
4646
c.logger.Log(logger.Error, err.Error(), "")
4747
return nil, fuego.HTTPError{
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package service
2+
3+
import (
4+
"github.com/raghavyuva/nixopus-api/internal/features/github-connector/types"
5+
"github.com/raghavyuva/nixopus-api/internal/features/logger"
6+
)
7+
8+
// DeleteConnector deletes a GitHub connector for the given user.
9+
//
10+
// This method performs a soft delete on the connector by setting its DeletedAt field.
11+
// It verifies that the connector belongs to the user before deletion.
12+
//
13+
// Parameters:
14+
//
15+
// ConnectorID - the unique identifier of the connector to delete.
16+
// UserID - the unique identifier of the user who owns the connector.
17+
//
18+
// Returns:
19+
//
20+
// error - an error if the connector cannot be deleted or does not exist.
21+
func (c *GithubConnectorService) DeleteConnector(ConnectorID string, UserID string) error {
22+
// Verify connector exists and belongs to user
23+
connector, err := c.storage.GetConnector(ConnectorID)
24+
if err != nil {
25+
c.logger.Log(logger.Error, err.Error(), "")
26+
return types.ErrConnectorDoesNotExist
27+
}
28+
29+
if connector.UserID.String() != UserID {
30+
c.logger.Log(logger.Error, "User does not own this connector", "")
31+
return types.ErrPermissionDenied
32+
}
33+
34+
// Check if connector is already deleted
35+
if connector.DeletedAt != nil {
36+
c.logger.Log(logger.Error, "Connector already deleted", "")
37+
return types.ErrConnectorDoesNotExist
38+
}
39+
40+
// Perform soft delete
41+
err = c.storage.DeleteConnector(ConnectorID, UserID)
42+
if err != nil {
43+
c.logger.Log(logger.Error, err.Error(), "")
44+
return err
45+
}
46+
47+
return nil
48+
}
49+

api/internal/features/github-connector/service/get_github_repositories.go

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import (
1111
)
1212

1313
// GetGithubRepositoriesPaginated fetches repositories for the user's GitHub installation with pagination.
14-
func (c *GithubConnectorService) GetGithubRepositoriesPaginated(userID string, page int, pageSize int) ([]shared_types.GithubRepository, int, error) {
14+
// If connectorID is provided, it uses that specific connector. Otherwise, it finds a connector with a valid installation_id.
15+
func (c *GithubConnectorService) GetGithubRepositoriesPaginated(userID string, page int, pageSize int, connectorID string) ([]shared_types.GithubRepository, int, error) {
1516
connectors, err := c.storage.GetAllConnectors(userID)
1617
if err != nil {
1718
c.logger.Log(logger.Error, err.Error(), "")
@@ -23,8 +24,43 @@ func (c *GithubConnectorService) GetGithubRepositoriesPaginated(userID string, p
2324
return []shared_types.GithubRepository{}, 0, nil
2425
}
2526

26-
installation_id := connectors[0].InstallationID
27-
jwt := GenerateJwt(&connectors[0])
27+
var connectorToUse *shared_types.GithubConnector
28+
29+
// If connectorID is provided, find that specific connector
30+
if connectorID != "" {
31+
for i := range connectors {
32+
if connectors[i].ID.String() == connectorID {
33+
connectorToUse = &connectors[i]
34+
break
35+
}
36+
}
37+
if connectorToUse == nil {
38+
c.logger.Log(logger.Error, fmt.Sprintf("Connector with id %s not found for user", connectorID), userID)
39+
return nil, 0, fmt.Errorf("connector not found")
40+
}
41+
} else {
42+
// Find connector with valid installation_id (not empty)
43+
for i := range connectors {
44+
if connectors[i].InstallationID != "" && connectors[i].InstallationID != " " {
45+
connectorToUse = &connectors[i]
46+
break
47+
}
48+
}
49+
// If no connector with installation_id found, return error
50+
if connectorToUse == nil {
51+
c.logger.Log(logger.Error, "No connector with valid installation_id found for user", userID)
52+
return nil, 0, fmt.Errorf("no connector with valid installation found")
53+
}
54+
}
55+
56+
// Validate installation_id is not empty
57+
if connectorToUse.InstallationID == "" || connectorToUse.InstallationID == " " {
58+
c.logger.Log(logger.Error, fmt.Sprintf("Connector %s has empty installation_id", connectorToUse.ID.String()), userID)
59+
return nil, 0, fmt.Errorf("connector has no installation_id")
60+
}
61+
62+
installation_id := connectorToUse.InstallationID
63+
jwt := GenerateJwt(connectorToUse)
2864

2965
accessToken, err := c.getInstallationToken(jwt, installation_id)
3066
if err != nil {

api/internal/features/github-connector/service/update_connector.go

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,68 @@
11
package service
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/google/uuid"
8+
shared_types "github.com/raghavyuva/nixopus-api/internal/types"
9+
)
410

511
// UpdateGithubConnectorRequest updates the GitHub connector request for the given user ID.
612
//
7-
// The method first retrieves all GitHub connectors associated with the user ID.
8-
// If no connectors are found, the method simply returns.
9-
//
10-
// Otherwise, the method takes the ID of the first connector and updates the
11-
// associated GitHub app ID with the provided InstallationID.
13+
// If ConnectorID is provided, it updates that specific connector.
14+
// Otherwise, it finds the connector without an installation_id and updates that one.
15+
// If no connector without installation_id is found, it updates the first connector (backward compatibility).
1216
//
1317
// If any errors occur during the update process, the method returns the error.
14-
func (c *GithubConnectorService) UpdateGithubConnectorRequest(InstallationID string, UserID string) error {
15-
connector, err := c.storage.GetAllConnectors(UserID)
18+
func (c *GithubConnectorService) UpdateGithubConnectorRequest(InstallationID string, UserID string, ConnectorID string) error {
19+
connectors, err := c.storage.GetAllConnectors(UserID)
1620
if err != nil {
1721
fmt.Println(err)
1822
return err
1923
}
2024

21-
if len(connector) == 0 {
25+
if len(connectors) == 0 {
2226
fmt.Println("no connector found")
2327
return nil
2428
}
2529

26-
err = c.storage.UpdateConnector(connector[0].ID.String(), InstallationID)
30+
var connectorToUpdate *shared_types.GithubConnector
31+
32+
// If ConnectorID is provided, find and update that specific connector
33+
if ConnectorID != "" {
34+
// Validate UUID format
35+
if _, err := uuid.Parse(ConnectorID); err != nil {
36+
return fmt.Errorf("invalid connector_id format: %v", err)
37+
}
38+
39+
// Find the connector with matching ID
40+
for i := range connectors {
41+
if connectors[i].ID.String() == ConnectorID {
42+
connectorToUpdate = &connectors[i]
43+
break
44+
}
45+
}
46+
47+
if connectorToUpdate == nil {
48+
return fmt.Errorf("connector with id %s not found", ConnectorID)
49+
}
50+
} else {
51+
// Find connector without installation_id (newly created connector)
52+
for i := range connectors {
53+
if connectors[i].InstallationID == "" || strings.TrimSpace(connectors[i].InstallationID) == "" {
54+
connectorToUpdate = &connectors[i]
55+
break
56+
}
57+
}
58+
59+
// If no connector without installation_id found, use first connector
60+
if connectorToUpdate == nil {
61+
connectorToUpdate = &connectors[0]
62+
}
63+
}
64+
65+
err = c.storage.UpdateConnector(connectorToUpdate.ID.String(), InstallationID)
2766
if err != nil {
2867
return err
2968
}

api/internal/features/github-connector/storage/init.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type GithubConnectorStorage struct {
1414
type GithubConnectorRepository interface {
1515
CreateConnector(connector *shared_types.GithubConnector) error
1616
UpdateConnector(ConnectorID, InstallationID string) error
17+
DeleteConnector(ConnectorID string, UserID string) error
1718
GetConnector(ConnectorID string) (*shared_types.GithubConnector, error)
1819
GetAllConnectors(UserID string) ([]shared_types.GithubConnector, error)
1920
GetConnectorByAppID(AppID string) (*shared_types.GithubConnector, error)
@@ -92,7 +93,7 @@ func (s *GithubConnectorStorage) UpdateConnector(ConnectorID, InstallationID str
9293
// error - an error if the connector cannot be retrieved or does not exist.
9394
func (s *GithubConnectorStorage) GetConnector(ConnectorID string) (*shared_types.GithubConnector, error) {
9495
var connector shared_types.GithubConnector
95-
err := s.DB.NewSelect().Model(&connector).Where("id = ?", ConnectorID).Scan(s.Ctx)
96+
err := s.DB.NewSelect().Model(&connector).Where("id = ? AND deleted_at IS NULL", ConnectorID).Scan(s.Ctx)
9697
return &connector, err
9798
}
9899

@@ -112,10 +113,48 @@ func (s *GithubConnectorStorage) GetConnector(ConnectorID string) (*shared_types
112113
// error - an error if the connectors cannot be retrieved or do not exist.
113114
func (s *GithubConnectorStorage) GetAllConnectors(UserID string) ([]shared_types.GithubConnector, error) {
114115
var connectors []shared_types.GithubConnector
115-
err := s.DB.NewSelect().Model(&connectors).Where("user_id = ?", UserID).Scan(s.Ctx)
116+
err := s.DB.NewSelect().Model(&connectors).Where("user_id = ? AND deleted_at IS NULL", UserID).Scan(s.Ctx)
116117
return connectors, err
117118
}
118119

120+
// DeleteConnector performs a soft delete on a GitHub connector.
121+
//
122+
// The method sets the DeletedAt field to the current time, effectively
123+
// marking the connector as deleted without removing it from the database.
124+
// It also verifies that the connector belongs to the provided UserID.
125+
//
126+
// Parameters:
127+
//
128+
// ConnectorID - the unique identifier of the connector to delete.
129+
// UserID - the unique identifier of the user who owns the connector.
130+
//
131+
// Returns:
132+
//
133+
// error - an error if the connector cannot be deleted or does not exist.
134+
func (s *GithubConnectorStorage) DeleteConnector(ConnectorID string, UserID string) error {
135+
tx, err := s.DB.BeginTx(s.Ctx, nil)
136+
if err != nil {
137+
return err
138+
}
139+
defer tx.Rollback()
140+
141+
var connector shared_types.GithubConnector
142+
_, err = tx.NewUpdate().Model(&connector).
143+
Set("deleted_at = NOW()").
144+
Set("updated_at = NOW()").
145+
Where("id = ? AND user_id = ? AND deleted_at IS NULL", ConnectorID, UserID).
146+
Exec(s.Ctx)
147+
if err != nil {
148+
return err
149+
}
150+
151+
if err := tx.Commit(); err != nil {
152+
return err
153+
}
154+
155+
return nil
156+
}
157+
119158
// GetConnectorByAppID retrieves a GitHub connector by its GitHub app ID.
120159
//
121160
// The method queries the storage to find a GitHub connector associated with
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package types
2+
3+
type DeleteGithubConnectorRequest struct {
4+
ID string `json:"id" validate:"required"`
5+
}
6+

api/internal/features/github-connector/types/init.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type CreateGithubConnectorRequest struct {
1313

1414
type UpdateGithubConnectorRequest struct {
1515
InstallationID string `json:"installation_id"`
16+
ConnectorID string `json:"connector_id,omitempty"` // Optional: if provided, update this specific connector
1617
}
1718

1819
var (
@@ -22,6 +23,7 @@ var (
2223
ErrMissingClientSecret = errors.New("client_secret is required")
2324
ErrMissingWebhookSecret = errors.New("webhook_secret is required")
2425
ErrMissingInstallationID = errors.New("installation_id is required")
26+
ErrMissingID = errors.New("id is required")
2527
ErrInvalidRequestType = errors.New("invalid request type")
2628
ErrConnectorDoesNotExist = errors.New("connector does not exist")
2729
ErrNoConnectors = errors.New("no connectors found")

api/internal/features/github-connector/validation/validator.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func NewValidator(storage GithubConnectorRepository) *Validator {
2929
// The supported request types are:
3030
// - types.CreateGithubConnectorRequest
3131
// - types.UpdateGithubConnectorRequest
32+
// - types.DeleteGithubConnectorRequest
3233
//
3334
// If the request object is not of one of the above types, it returns
3435
// types.ErrInvalidRequestType.
@@ -38,6 +39,8 @@ func (v *Validator) ValidateRequest(req any) error {
3839
return v.validateCreateGithubConnectorRequest(*r)
3940
case *types.UpdateGithubConnectorRequest:
4041
return v.validateUpdateGithubConnectorRequest(*r)
42+
case *types.DeleteGithubConnectorRequest:
43+
return v.validateDeleteGithubConnectorRequest(*r)
4144
default:
4245
return types.ErrInvalidRequestType
4346
}
@@ -81,3 +84,11 @@ func (v *Validator) validateUpdateGithubConnectorRequest(req types.UpdateGithubC
8184

8285
return nil
8386
}
87+
88+
func (v *Validator) validateDeleteGithubConnectorRequest(req types.DeleteGithubConnectorRequest) error {
89+
if req.ID == "" {
90+
return types.ErrMissingID
91+
}
92+
93+
return nil
94+
}

0 commit comments

Comments
 (0)