diff --git a/internal/api/context.go b/internal/api/context.go index f50468f80..aabf2a535 100644 --- a/internal/api/context.go +++ b/internal/api/context.go @@ -30,6 +30,7 @@ type Store interface { CreateInstallation(installation *model.Installation) error GetInstallation(installationID string, includeGroupConfig, includeGroupConfigOverrides bool) (*model.Installation, error) GetInstallations(filter *model.InstallationFilter, includeGroupConfig, includeGroupConfigOverrides bool) ([]*model.Installation, error) + GetInstallationsCount(includeDeleted bool) (int, error) UpdateInstallation(installation *model.Installation) error LockInstallation(installationID, lockerID string) (bool, error) UnlockInstallation(installationID, lockerID string, force bool) (bool, error) diff --git a/internal/api/installation.go b/internal/api/installation.go index 6a59e080e..e5ee935fd 100644 --- a/internal/api/installation.go +++ b/internal/api/installation.go @@ -21,6 +21,7 @@ func initInstallation(apiRouter *mux.Router, context *Context) { installationsRouter := apiRouter.PathPrefix("/installations").Subrouter() installationsRouter.Handle("", addContext(handleGetInstallations)).Methods("GET") + installationsRouter.Handle("/count", addContext(handleGetNumberOfInstallations)).Methods("GET") installationsRouter.Handle("", addContext(handleCreateInstallation)).Methods("POST") installationRouter := apiRouter.PathPrefix("/installation/{installation:[A-Za-z0-9]{26}}").Subrouter() @@ -109,6 +110,25 @@ func handleGetInstallations(c *Context, w http.ResponseWriter, r *http.Request) outputJSON(c, w, installations) } +// handlerGetNumberOfInstallations responds to GET /api/installations/count, returning the +// number of non-deleted installations +func handleGetNumberOfInstallations(c *Context, w http.ResponseWriter, r *http.Request) { + includeDeleted, err := parseBool(r.URL, "include_deleted", false) + if err != nil { + includeDeleted = false + } + installationsCount, err := c.Store.GetInstallationsCount(includeDeleted) + if err != nil { + c.Logger.WithError(err).Error("failed to query the number of installations") + w.WriteHeader(http.StatusInternalServerError) + return + } + result := model.InstallationsCount{Count: installationsCount} + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + outputJSON(c, w, result) +} + // handleCreateInstallation responds to POST /api/installations, beginning the process of creating // a new installation. func handleCreateInstallation(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/installation_test.go b/internal/api/installation_test.go index de095f2cf..ff57087bc 100644 --- a/internal/api/installation_test.go +++ b/internal/api/installation_test.go @@ -259,6 +259,32 @@ func TestGetInstallations(t *testing.T) { }) } }) + + t.Run("get installations count", func(t *testing.T) { + testCases := []struct { + Description string + IncludeDeleted bool + Expected int + }{ + { + "count without deleted", + false, + 3, + }, + { + "count with deleted", + true, + 4, + }, + } + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + installations, err := client.GetInstallationsCount(testCase.IncludeDeleted) + require.NoError(t, err) + require.Equal(t, testCase.Expected, installations) + }) + } + }) }) } diff --git a/internal/store/installation.go b/internal/store/installation.go index e00138bd9..91c1682e9 100644 --- a/internal/store/installation.go +++ b/internal/store/installation.go @@ -144,6 +144,25 @@ func (sqlStore *SQLStore) GetInstallations(filter *model.InstallationFilter, inc return installations, nil } +// GetInstallationsCount returns the number of installations filtered by the deletedat +// field +func (sqlStore *SQLStore) GetInstallationsCount(includeDeleted bool) (int, error) { + builder := sq.Select("COUNT(*) as InstallationsCount").From("Installation") + if !includeDeleted { + builder = builder.Where("DeleteAt = 0") + } + var numberOfInstallations int + query, _, err := builder.ToSql() + if err != nil { + return 0, errors.Wrap(err, "failed to parse query for installations count") + } + err = sqlStore.get(sqlStore.db, &numberOfInstallations, query) + if err != nil { + return 0, errors.Wrap(err, "failed to query for installations count") + } + return numberOfInstallations, nil +} + // GetUnlockedInstallationsPendingWork returns an unlocked installation in a pending state. func (sqlStore *SQLStore) GetUnlockedInstallationsPendingWork() ([]*model.Installation, error) { builder := installationSelect. diff --git a/model/client.go b/model/client.go index 65f2fa6ea..1f7b5c30f 100644 --- a/model/client.go +++ b/model/client.go @@ -406,6 +406,30 @@ func (c *Client) GetInstallations(request *GetInstallationsRequest) ([]*Installa } } +// GetInstallationsCount returns then number of installations filtered by deleted field +func (c *Client) GetInstallationsCount(includeDeleted bool) (int, error) { + u, err := url.Parse(c.buildURL("/api/installations/count")) + if err != nil { + return 0, err + } + if includeDeleted { + q := u.Query() + q.Add("include_deleted", "true") + u.RawQuery = q.Encode() + } + resp, err := c.doGet(u.String()) + if err != nil { + return 0, errors.Wrap(err, "problem getting installations count") + } + defer closeBody(resp) + switch resp.StatusCode { + case http.StatusOK: + return InstallationsCountFromReader(resp.Body) + default: + return 0, errors.Errorf("failed with status code %d", resp.StatusCode) + } +} + // UpdateInstallation updates an installation. func (c *Client) UpdateInstallation(installationID string, request *PatchInstallationRequest) (*Installation, error) { resp, err := c.doPut(c.buildURL("/api/installation/%s/mattermost", installationID), request) diff --git a/model/installation.go b/model/installation.go index d2503e126..062498ac3 100644 --- a/model/installation.go +++ b/model/installation.go @@ -44,6 +44,11 @@ type Installation struct { configMergeGroupSequence int64 } +// InstallationsCount represents the number of installations +type InstallationsCount struct { + Count int +} + // InstallationFilter describes the parameters used to constrain a set of installations. type InstallationFilter struct { OwnerID string @@ -149,3 +154,16 @@ func InstallationsFromReader(reader io.Reader) ([]*Installation, error) { return installations, nil } + +// InstallationsCountFromReader decodes a json-encoded installations count data from the +// given io.Reader +func InstallationsCountFromReader(reader io.Reader) (int, error) { + installationsCount := InstallationsCount{} + decoder := json.NewDecoder(reader) + err := decoder.Decode(&installationsCount) + if err != nil && err != io.EOF { + return 0, err + } + + return installationsCount.Count, nil +}