Skip to content

Commit c13898f

Browse files
feat: add tag filter support in app listing
(cherry picked from commit 57e4fd6b65185e2f3f6add7d7010f4c1ebdcf1fc)
1 parent c0b293d commit c13898f

5 files changed

Lines changed: 165 additions & 69 deletions

File tree

api/restHandler/app/appList/AppListingRestHandler.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,9 +279,8 @@ func (handler AppListingRestHandlerImpl) FetchJobOverviewCiPipelines(w http.Resp
279279
common.WriteJsonResp(w, err, jobCi, http.StatusOK)
280280
}
281281

282-
// validateAndNormalizeFetchAppListingRequest applies request-level validation first,
283-
// then tag-filter business validation, and finally normalization.
284-
func (handler AppListingRestHandlerImpl) validateAndNormalizeFetchAppListingRequest(w http.ResponseWriter, r *http.Request, fetchAppListingRequest *app.FetchAppListingRequest) bool {
282+
// validateFetchAppListingRequest performs request and business-rule validation.
283+
func (handler AppListingRestHandlerImpl) validateFetchAppListingRequest(w http.ResponseWriter, r *http.Request, fetchAppListingRequest *app.FetchAppListingRequest) bool {
285284
err := handler.validator.Struct(*fetchAppListingRequest)
286285
if err != nil {
287286
handler.logger.Errorw("validation err, FetchAppsByEnvironment", "err", err, "payload", fetchAppListingRequest)
@@ -294,10 +293,14 @@ func (handler AppListingRestHandlerImpl) validateAndNormalizeFetchAppListingRequ
294293
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
295294
return false
296295
}
297-
fetchAppListingRequest.TagFilters = handler.appListingService.NormalizeTagFilters(fetchAppListingRequest.TagFilters)
298296
return true
299297
}
300298

299+
// normalizeFetchAppListingRequest applies request normalization after validation.
300+
func (handler AppListingRestHandlerImpl) normalizeFetchAppListingRequest(fetchAppListingRequest *app.FetchAppListingRequest) {
301+
fetchAppListingRequest.TagFilters = handler.appListingService.NormalizeTagFilters(fetchAppListingRequest.TagFilters)
302+
}
303+
301304
func (handler AppListingRestHandlerImpl) FetchAppsByEnvironmentV2(w http.ResponseWriter, r *http.Request) {
302305
//Allow CORS here By * or specific origin
303306
util3.SetupCorsOriginHeader(&w)
@@ -353,9 +356,10 @@ func (handler AppListingRestHandlerImpl) FetchAppsByEnvironmentV2(w http.Respons
353356
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
354357
return
355358
}
356-
if !handler.validateAndNormalizeFetchAppListingRequest(w, r, &fetchAppListingRequest) {
359+
if !handler.validateFetchAppListingRequest(w, r, &fetchAppListingRequest) {
357360
return
358361
}
362+
handler.normalizeFetchAppListingRequest(&fetchAppListingRequest)
359363
newCtx, span = otel.Tracer("fetchAppListingRequest").Start(newCtx, "GetNamespaceClusterMapping")
360364
_, _, err = fetchAppListingRequest.GetNamespaceClusterMapping()
361365
span.End()

internal/sql/repository/AppListingRepository.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,14 @@ func (impl *AppListingRepositoryImpl) FetchAppsByEnvironmentV2(appListingFilter
217217

218218
if string(appListingFilter.SortBy) == helper.LastDeployedSortBy {
219219

220-
query, queryParams := impl.appListingRepositoryQueryBuilder.GetAppIdsQueryWithPaginationForLastDeployedSearch(appListingFilter)
220+
query, queryParams, err := impl.appListingRepositoryQueryBuilder.GetAppIdsQueryWithPaginationForLastDeployedSearch(appListingFilter)
221+
if err != nil {
222+
impl.Logger.Errorw("error in building appIds query with appList filter", "err", err, "filter", appListingFilter)
223+
return appEnvArr, appsSize, err
224+
}
221225
impl.Logger.Debug("GetAppIdsQueryWithPaginationForLastDeployedSearch query ", query)
222226
start := time.Now()
223-
_, err := impl.dbConnection.Query(&lastDeployedTimeDTO, query, queryParams...)
227+
_, err = impl.dbConnection.Query(&lastDeployedTimeDTO, query, queryParams...)
224228
middleware.AppListingDuration.WithLabelValues("getAppIdsQueryWithPaginationForLastDeployedSearch", "devtron").Observe(time.Since(start).Seconds())
225229
if err != nil || len(lastDeployedTimeDTO) == 0 {
226230
if err != nil {
@@ -235,7 +239,11 @@ func (impl *AppListingRepositoryImpl) FetchAppsByEnvironmentV2(appListingFilter
235239
appIdsFound[i] = obj.AppId
236240
}
237241
appListingFilter.AppIds = appIdsFound
238-
appContainerQuery, appContainerQueryParams := impl.appListingRepositoryQueryBuilder.GetQueryForAppEnvContainers(appListingFilter)
242+
appContainerQuery, appContainerQueryParams, err := impl.appListingRepositoryQueryBuilder.GetQueryForAppEnvContainers(appListingFilter)
243+
if err != nil {
244+
impl.Logger.Errorw("error in building appEnv query with appList filter", "err", err, "filter", appListingFilter)
245+
return appEnvArr, appsSize, err
246+
}
239247
impl.Logger.Debug("GetQueryForAppEnvContainers query ", query)
240248
_, err = impl.dbConnection.Query(&appEnvContainer, appContainerQuery, appContainerQueryParams...)
241249
if err != nil {
@@ -247,10 +255,14 @@ func (impl *AppListingRepositoryImpl) FetchAppsByEnvironmentV2(appListingFilter
247255

248256
// to get all the appIds in appEnvs allowed for user and filtered by the appListing filter and sorted by name
249257
appIdCountDtos := make([]*AppView.AppEnvironmentContainer, 0)
250-
appIdCountQuery, appIdCountQueryParams := impl.appListingRepositoryQueryBuilder.GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter)
258+
appIdCountQuery, appIdCountQueryParams, appsErr := impl.appListingRepositoryQueryBuilder.GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter)
259+
if appsErr != nil {
260+
impl.Logger.Errorw("error in building appIds query with appList filter", "err", appsErr, "filter", appListingFilter)
261+
return appEnvContainer, appsSize, appsErr
262+
}
251263
impl.Logger.Debug("GetAppIdsQueryWithPaginationForAppNameSearch query ", appIdCountQuery)
252264
start := time.Now()
253-
_, appsErr := impl.dbConnection.Query(&appIdCountDtos, appIdCountQuery, appIdCountQueryParams...)
265+
_, appsErr = impl.dbConnection.Query(&appIdCountDtos, appIdCountQuery, appIdCountQueryParams...)
254266
middleware.AppListingDuration.WithLabelValues("getAppIdsQueryWithPaginationForAppNameSearch", "devtron").Observe(time.Since(start).Seconds())
255267
if appsErr != nil || len(appIdCountDtos) == 0 {
256268
if appsErr != nil {
@@ -268,7 +280,11 @@ func (impl *AppListingRepositoryImpl) FetchAppsByEnvironmentV2(appListingFilter
268280
appListingFilter.AppIds = uniqueAppIds
269281
// set appids required for this page in the filter and get the appEnv containers of these apps
270282
appListingFilter.AppIds = uniqueAppIds
271-
appsEnvquery, appsEnvQueryParams := impl.appListingRepositoryQueryBuilder.GetQueryForAppEnvContainers(appListingFilter)
283+
appsEnvquery, appsEnvQueryParams, appsErr := impl.appListingRepositoryQueryBuilder.GetQueryForAppEnvContainers(appListingFilter)
284+
if appsErr != nil {
285+
impl.Logger.Errorw("error in building appEnv query with appList filter", "err", appsErr, "filter", appListingFilter)
286+
return appEnvContainer, appsSize, appsErr
287+
}
272288
impl.Logger.Debug("GetQueryForAppEnvContainers query: ", appsEnvquery)
273289
start = time.Now()
274290
_, appsErr = impl.dbConnection.Query(&appEnvContainer, appsEnvquery, appsEnvQueryParams...)

internal/sql/repository/helper/AppListingRepositoryQueryBuilder.go

Lines changed: 70 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,18 @@ func NewAppListingRepositoryQueryBuilder(logger *zap.SugaredLogger) AppListingRe
4545
}
4646

4747
type AppListingFilter struct {
48-
Environments []int `json:"environments"`
49-
Statuses []string `json:"statutes"`
50-
Teams []int `json:"teams"`
51-
AppStatuses []string `json:"appStatuses"`
52-
TagFilters []TagFilter `json:"tagFilters"`
53-
AppNameSearch string `json:"appNameSearch"`
54-
SortOrder SortOrder `json:"sortOrder"`
55-
SortBy SortBy `json:"sortBy"`
56-
Offset int `json:"offset"`
57-
Size int `json:"size"`
58-
DeploymentGroupId int `json:"deploymentGroupId"`
59-
AppIds []int `json:"-"` // internal use only
48+
Environments []int `json:"environments"`
49+
Statuses []string `json:"statutes"`
50+
Teams []int `json:"teams"`
51+
AppStatuses []string `json:"appStatuses"`
52+
TagFilters *[]TagFilter `json:"tagFilters"`
53+
AppNameSearch string `json:"appNameSearch"`
54+
SortOrder SortOrder `json:"sortOrder"`
55+
SortBy SortBy `json:"sortBy"`
56+
Offset int `json:"offset"`
57+
Size int `json:"size"`
58+
DeploymentGroupId int `json:"deploymentGroupId"`
59+
AppIds []int `json:"-"` // internal use only
6060
}
6161

6262
type SortBy string
@@ -170,14 +170,17 @@ func getAppListingCommonQueryString() string {
170170
" LEFT JOIN app_status aps on aps.app_id = a.id and p.environment_id = aps.env_id "
171171
}
172172

173-
func (impl AppListingRepositoryQueryBuilder) GetQueryForAppEnvContainers(appListingFilter AppListingFilter) (string, []interface{}) {
173+
func (impl AppListingRepositoryQueryBuilder) GetQueryForAppEnvContainers(appListingFilter AppListingFilter) (string, []interface{}, error) {
174174
query := "SELECT p.environment_id , a.id AS app_id, a.app_name,p.id as pipeline_id, a.team_id ,aps.status as app_status "
175-
queryTemp, queryParams := impl.TestForCommonAppFilter(appListingFilter)
175+
queryTemp, queryParams, err := impl.TestForCommonAppFilter(appListingFilter)
176+
if err != nil {
177+
return "", nil, err
178+
}
176179
query += queryTemp
177-
return query, queryParams
180+
return query, queryParams, nil
178181
}
179182

180-
func (impl AppListingRepositoryQueryBuilder) CommonJoinSubQuery(appListingFilter AppListingFilter) (string, []interface{}) {
183+
func (impl AppListingRepositoryQueryBuilder) CommonJoinSubQuery(appListingFilter AppListingFilter) (string, []interface{}, error) {
181184
var queryParams []interface{}
182185
query := ` LEFT JOIN pipeline p ON a.id=p.app_id and p.deleted=?
183186
LEFT JOIN deployment_config dc ON ( p.app_id=dc.app_id and p.environment_id=dc.environment_id and dc.active=? )
@@ -186,16 +189,22 @@ func (impl AppListingRepositoryQueryBuilder) CommonJoinSubQuery(appListingFilter
186189
if appListingFilter.DeploymentGroupId != 0 {
187190
query = query + " INNER JOIN deployment_group_app dga ON a.id = dga.app_id "
188191
}
189-
whereCondition, whereConditionParams := impl.buildAppListingWhereCondition(appListingFilter)
192+
whereCondition, whereConditionParams, err := impl.buildAppListingWhereCondition(appListingFilter)
193+
if err != nil {
194+
return "", nil, err
195+
}
190196
query = query + whereCondition
191197
queryParams = append(queryParams, whereConditionParams...)
192-
return query, queryParams
198+
return query, queryParams, nil
193199
}
194200

195-
func (impl AppListingRepositoryQueryBuilder) TestForCommonAppFilter(appListingFilter AppListingFilter) (string, []interface{}) {
196-
queryTemp, queryParams := impl.CommonJoinSubQuery(appListingFilter)
201+
func (impl AppListingRepositoryQueryBuilder) TestForCommonAppFilter(appListingFilter AppListingFilter) (string, []interface{}, error) {
202+
queryTemp, queryParams, err := impl.CommonJoinSubQuery(appListingFilter)
203+
if err != nil {
204+
return "", nil, err
205+
}
197206
query := " FROM app a " + queryTemp
198-
return query, queryParams
207+
return query, queryParams, nil
199208
}
200209

201210
func (impl AppListingRepositoryQueryBuilder) BuildAppListingQueryLastDeploymentTimeV2(pipelineIDs []int) (string, []interface{}) {
@@ -211,8 +220,11 @@ func (impl AppListingRepositoryQueryBuilder) BuildAppListingQueryLastDeploymentT
211220
return query, queryParams
212221
}
213222

214-
func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForLastDeployedSearch(appListingFilter AppListingFilter) (string, []interface{}) {
215-
join, queryParams := impl.CommonJoinSubQuery(appListingFilter)
223+
func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForLastDeployedSearch(appListingFilter AppListingFilter) (string, []interface{}, error) {
224+
join, queryParams, err := impl.CommonJoinSubQuery(appListingFilter)
225+
if err != nil {
226+
return "", nil, err
227+
}
216228
countQuery := " (SELECT count(distinct(a.id)) as count FROM app a " + join + ") AS total_count "
217229
// appending query params for count query as well
218230
queryParams = append(queryParams, queryParams...)
@@ -234,12 +246,15 @@ func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForLast
234246
}
235247
query += " LIMIT ? OFFSET ? "
236248
queryParams = append(queryParams, appListingFilter.Size, appListingFilter.Offset)
237-
return query, queryParams
249+
return query, queryParams, nil
238250
}
239251

240-
func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter AppListingFilter) (string, []interface{}) {
252+
func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForAppNameSearch(appListingFilter AppListingFilter) (string, []interface{}, error) {
241253
orderByClause := impl.buildAppListingSortBy(appListingFilter)
242-
join, queryParams := impl.CommonJoinSubQuery(appListingFilter)
254+
join, queryParams, err := impl.CommonJoinSubQuery(appListingFilter)
255+
if err != nil {
256+
return "", nil, err
257+
}
243258
countQuery := "( SELECT count(distinct(a.id)) as count FROM app a" + join + " ) as total_count"
244259
query := "SELECT DISTINCT(a.id) as app_id, a.app_name, " + countQuery +
245260
" FROM app a " + join
@@ -250,7 +265,7 @@ func (impl AppListingRepositoryQueryBuilder) GetAppIdsQueryWithPaginationForAppN
250265
//adding queryParams two times because join query is used in countQuery and mainQuery two times
251266
queryParams = append(queryParams, queryParams...)
252267
queryParams = append(queryParams, appListingFilter.Size, appListingFilter.Offset)
253-
return query, queryParams
268+
return query, queryParams, nil
254269
}
255270

256271
func (impl AppListingRepositoryQueryBuilder) buildAppListingSortBy(appListingFilter AppListingFilter) string {
@@ -263,7 +278,7 @@ func (impl AppListingRepositoryQueryBuilder) buildAppListingSortBy(appListingFil
263278
return orderByCondition
264279
}
265280

266-
func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appListingFilter AppListingFilter) (string, []interface{}) {
281+
func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appListingFilter AppListingFilter) (string, []interface{}, error) {
267282
var queryParams []interface{}
268283
whereCondition := " WHERE a.active = ? and a.app_type = ? "
269284
queryParams = append(queryParams, true, CustomApp)
@@ -313,30 +328,39 @@ func (impl AppListingRepositoryQueryBuilder) buildAppListingWhereCondition(appLi
313328
}
314329
// Tag filters are AND-combined for now as requested by product.
315330
// Each row translates to a correlated EXISTS/NOT EXISTS on app_label.
316-
tagWhereCondition, tagQueryParams := impl.buildTagFiltersWhereConditionAND(appListingFilter.TagFilters)
331+
tagWhereCondition, tagQueryParams, err := impl.buildTagFiltersWhereConditionAND(appListingFilter.TagFilters)
332+
if err != nil {
333+
return "", nil, err
334+
}
317335
whereCondition += tagWhereCondition
318-
queryParams = append(queryParams, tagQueryParams...)
336+
if len(tagQueryParams) > 0 {
337+
queryParams = append(queryParams, tagQueryParams...)
338+
}
319339

320340
if len(appListingFilter.AppIds) > 0 {
321341
whereCondition += " and a.id IN (?) "
322342
queryParams = append(queryParams, pg.In(appListingFilter.AppIds))
323343
}
324-
return whereCondition, queryParams
344+
345+
return whereCondition, queryParams, nil
325346
}
326347

327-
func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionAND(tagFilters []TagFilter) (string, []interface{}) {
328-
if len(tagFilters) == 0 {
329-
return "", nil
348+
func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionAND(tagFilters *[]TagFilter) (string, []interface{}, error) {
349+
if tagFilters == nil || len(*tagFilters) == 0 {
350+
return "", make([]interface{}, 0), nil
330351
}
331352
var queryBuilder strings.Builder
332-
queryParams := make([]interface{}, 0, len(tagFilters)*2)
333-
for _, tagFilter := range tagFilters {
334-
predicate, predicateParams := impl.buildTagFilterPredicate(tagFilter)
353+
queryParams := make([]interface{}, 0, len(*tagFilters)*2)
354+
for _, tagFilter := range *tagFilters {
355+
predicate, predicateParams, err := impl.buildTagFilterPredicate(tagFilter)
356+
if err != nil {
357+
return "", nil, err
358+
}
335359
queryBuilder.WriteString(" and ")
336360
queryBuilder.WriteString(predicate)
337361
queryParams = append(queryParams, predicateParams...)
338362
}
339-
return queryBuilder.String(), queryParams
363+
return queryBuilder.String(), queryParams, nil
340364
}
341365

342366
// buildTagFilterPredicate converts one UI tag filter row into a SQL predicate.
@@ -347,38 +371,36 @@ func (impl AppListingRepositoryQueryBuilder) buildTagFiltersWhereConditionAND(ta
347371
// - DOES_NOT_CONTAIN: key exists with at least one value not containing target substring.
348372
// - EXISTS: key exists.
349373
// - DOES_NOT_EXIST: key does not exist.
350-
func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter TagFilter) (string, []interface{}) {
374+
func (impl AppListingRepositoryQueryBuilder) buildTagFilterPredicate(tagFilter TagFilter) (string, []interface{}, error) {
351375
value := ""
352376
if tagFilter.Value != nil {
353377
value = *tagFilter.Value
354378
}
355379
switch tagFilter.Operator {
356380
case TagFilterOperatorEquals:
357381
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value = ?)",
358-
[]interface{}{tagFilter.Key, value}
382+
[]interface{}{tagFilter.Key, value}, nil
359383
case TagFilterOperatorDoesNotEqual:
360384
// Best-practice semantics for multi-value keys:
361385
// include app when key exists and at least one value is different from target.
362386
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value <> ?)",
363-
[]interface{}{tagFilter.Key, value}
387+
[]interface{}{tagFilter.Key, value}, nil
364388
case TagFilterOperatorContains:
365389
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value LIKE ? ESCAPE '\\')",
366-
[]interface{}{tagFilter.Key, buildContainsPattern(value)}
390+
[]interface{}{tagFilter.Key, buildContainsPattern(value)}, nil
367391
case TagFilterOperatorDoesNotContain:
368392
// Best-practice semantics for multi-value keys:
369393
// include app when key exists and at least one value does not contain target.
370394
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ? and al.value NOT LIKE ? ESCAPE '\\')",
371-
[]interface{}{tagFilter.Key, buildContainsPattern(value)}
395+
[]interface{}{tagFilter.Key, buildContainsPattern(value)}, nil
372396
case TagFilterOperatorExists:
373397
return "EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)",
374-
[]interface{}{tagFilter.Key}
398+
[]interface{}{tagFilter.Key}, nil
375399
case TagFilterOperatorDoesNotExist:
376400
return "NOT EXISTS (SELECT 1 FROM app_label al WHERE al.app_id = a.id and al.key = ?)",
377-
[]interface{}{tagFilter.Key}
401+
[]interface{}{tagFilter.Key}, nil
378402
default:
379-
// Invalid operator should never reach here due request validation.
380-
// Returning false condition keeps query safe if validation is bypassed.
381-
return "1 = 0", nil
403+
return "", nil, fmt.Errorf("unsupported tag filter operator: %s", tagFilter.Operator)
382404
}
383405
}
384406

0 commit comments

Comments
 (0)