github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/repo/list_pageable.go (about) 1 package repo 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 8 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 9 10 "github.com/kyma-incubator/compass/components/director/pkg/resource" 11 12 "github.com/kyma-incubator/compass/components/director/pkg/pagination" 13 "github.com/kyma-incubator/compass/components/director/pkg/persistence" 14 "github.com/pkg/errors" 15 ) 16 17 // PageableQuerier is an interface for listing with paging of tenant scoped entities with either externally managed tenant accesses (m2m table or view) or embedded tenant in them. 18 type PageableQuerier interface { 19 List(ctx context.Context, resourceType resource.Type, tenant string, pageSize int, cursor string, orderByColumn string, dest Collection, additionalConditions ...Condition) (*pagination.Page, int, error) 20 } 21 22 // PageableQuerierGlobal is an interface for listing with paging of global entities. 23 type PageableQuerierGlobal interface { 24 ListGlobal(ctx context.Context, pageSize int, cursor string, orderByColumn string, dest Collection) (*pagination.Page, int, error) 25 ListGlobalWithAdditionalConditions(ctx context.Context, pageSize int, cursor string, orderByColumn string, dest Collection, conditions *ConditionTree) (*pagination.Page, int, error) 26 ListGlobalWithSelectForUpdate(ctx context.Context, pageSize int, cursor string, orderByColumn string, dest Collection) (*pagination.Page, int, error) 27 } 28 29 type universalPageableQuerier struct { 30 tableName string 31 selectedColumns string 32 tenantColumn *string 33 resourceType resource.Type 34 } 35 36 // NewPageableQuerierWithEmbeddedTenant is a constructor for PageableQuerier about entities with tenant embedded in them. 37 func NewPageableQuerierWithEmbeddedTenant(tableName string, tenantColumn string, selectedColumns []string) PageableQuerier { 38 return &universalPageableQuerier{ 39 tableName: tableName, 40 selectedColumns: strings.Join(selectedColumns, ", "), 41 tenantColumn: &tenantColumn, 42 } 43 } 44 45 // NewPageableQuerier is a constructor for PageableQuerier about entities with externally managed tenant accesses (m2m table or view) 46 func NewPageableQuerier(tableName string, selectedColumns []string) PageableQuerier { 47 return &universalPageableQuerier{ 48 tableName: tableName, 49 selectedColumns: strings.Join(selectedColumns, ", "), 50 } 51 } 52 53 // NewPageableQuerierGlobal is a constructor for PageableQuerierGlobal about global entities. 54 func NewPageableQuerierGlobal(resourceType resource.Type, tableName string, selectedColumns []string) PageableQuerierGlobal { 55 return &universalPageableQuerier{ 56 tableName: tableName, 57 selectedColumns: strings.Join(selectedColumns, ", "), 58 resourceType: resourceType, 59 } 60 } 61 62 // Collection is an interface for a collection of entities. 63 type Collection interface { 64 Len() int 65 } 66 67 // List lists a page of tenant scoped entities with tenant isolation subquery. 68 // If the tenantColumn is configured the isolation is based on equal condition on tenantColumn. 69 // If the tenantColumn is not configured an entity with externally managed tenant accesses in m2m table / view is assumed. 70 func (g *universalPageableQuerier) List(ctx context.Context, resourceType resource.Type, tenant string, pageSize int, cursor string, orderByColumn string, dest Collection, additionalConditions ...Condition) (*pagination.Page, int, error) { 71 if tenant == "" { 72 return nil, -1, apperrors.NewTenantRequiredError() 73 } 74 75 if g.tenantColumn != nil { 76 additionalConditions = append(Conditions{NewEqualCondition(*g.tenantColumn, tenant)}, additionalConditions...) 77 return g.list(ctx, resourceType, pageSize, cursor, orderByColumn, dest, NoLock, And(ConditionTreesFromConditions(additionalConditions)...)) 78 } 79 80 tenantIsolation, err := NewTenantIsolationCondition(resourceType, tenant, false) 81 if err != nil { 82 return nil, -1, err 83 } 84 85 additionalConditions = append(additionalConditions, tenantIsolation) 86 87 return g.list(ctx, resourceType, pageSize, cursor, orderByColumn, dest, NoLock, And(ConditionTreesFromConditions(additionalConditions)...)) 88 } 89 90 // ListGlobal lists a page of global entities without tenant isolation. 91 func (g *universalPageableQuerier) ListGlobal(ctx context.Context, pageSize int, cursor string, orderByColumn string, dest Collection) (*pagination.Page, int, error) { 92 return g.list(ctx, g.resourceType, pageSize, cursor, orderByColumn, dest, NoLock, nil) 93 } 94 95 // ListGlobalWithSelectForUpdate lists a page of global entities without tenant isolation. 96 func (g *universalPageableQuerier) ListGlobalWithSelectForUpdate(ctx context.Context, pageSize int, cursor string, orderByColumn string, dest Collection) (*pagination.Page, int, error) { 97 return g.list(ctx, g.resourceType, pageSize, cursor, orderByColumn, dest, ForUpdateLock, nil) 98 } 99 100 // ListGlobalWithAdditionalConditions lists a page of global entities without tenant isolation. 101 func (g *universalPageableQuerier) ListGlobalWithAdditionalConditions(ctx context.Context, pageSize int, cursor string, orderByColumn string, dest Collection, conditions *ConditionTree) (*pagination.Page, int, error) { 102 return g.list(ctx, g.resourceType, pageSize, cursor, orderByColumn, dest, NoLock, conditions) 103 } 104 105 func (g *universalPageableQuerier) list(ctx context.Context, resourceType resource.Type, pageSize int, cursor string, orderByColumn string, dest Collection, lockClause string, conditions *ConditionTree) (*pagination.Page, int, error) { 106 persist, err := persistence.FromCtx(ctx) 107 if err != nil { 108 return nil, -1, err 109 } 110 111 offset, err := pagination.DecodeOffsetCursor(cursor) 112 if err != nil { 113 return nil, -1, errors.Wrap(err, "while decoding page cursor") 114 } 115 116 paginationSQL, err := pagination.ConvertOffsetLimitAndOrderedColumnToSQL(pageSize, offset, orderByColumn) 117 if err != nil { 118 return nil, -1, errors.Wrap(err, "while converting offset and limit to cursor") 119 } 120 121 query, args, err := buildSelectQueryFromTree(g.tableName, g.selectedColumns, conditions, OrderByParams{}, lockClause, true) 122 if err != nil { 123 return nil, -1, errors.Wrap(err, "while building list query") 124 } 125 126 // TODO: Refactor query builder 127 var stmtWithPagination string 128 if IsLockClauseProvided(lockClause) { 129 stmtWithPagination = strings.ReplaceAll(query, lockClause, paginationSQL+" "+lockClause) 130 } else { 131 stmtWithPagination = fmt.Sprintf("%s %s", query, paginationSQL) 132 } 133 134 err = persist.SelectContext(ctx, dest, stmtWithPagination, args...) 135 if err != nil { 136 return nil, -1, persistence.MapSQLError(ctx, err, resourceType, resource.List, "while fetching list page of objects from '%s' table", g.tableName) 137 } 138 139 var countQuery = query 140 if IsLockClauseProvided(lockClause) { 141 countQuery = strings.ReplaceAll(query, " "+lockClause, "") 142 } 143 totalCount, err := g.getTotalCount(ctx, resourceType, persist, countQuery, args) 144 if err != nil { 145 return nil, -1, err 146 } 147 148 hasNextPage, endCursor := g.getNextPageAndCursor(totalCount, offset, pageSize, dest.Len()) 149 return &pagination.Page{ 150 StartCursor: cursor, 151 EndCursor: endCursor, 152 HasNextPage: hasNextPage, 153 }, totalCount, nil 154 } 155 156 func (g *universalPageableQuerier) getNextPageAndCursor(totalCount, offset, pageSize, totalLen int) (bool, string) { 157 hasNextPage := false 158 endCursor := "" 159 if totalCount > offset+totalLen { 160 hasNextPage = true 161 endCursor = pagination.EncodeNextOffsetCursor(offset, pageSize) 162 } 163 return hasNextPage, endCursor 164 } 165 166 func (g *universalPageableQuerier) getTotalCount(ctx context.Context, resourceType resource.Type, persist persistence.PersistenceOp, query string, args []interface{}) (int, error) { 167 stmt := strings.Replace(query, g.selectedColumns, "COUNT(*)", 1) 168 var totalCount int 169 err := persist.GetContext(ctx, &totalCount, stmt, args...) 170 if err != nil { 171 return -1, persistence.MapSQLError(ctx, err, resourceType, resource.List, "while counting objects from '%s' table", g.tableName) 172 } 173 return totalCount, nil 174 } 175 176 // IsLockClauseProvided true if there is a non-empty lock clause provided, and false otherwise. 177 func IsLockClauseProvided(lockClause string) bool { 178 return strings.TrimSpace(lockClause) != NoLock 179 }