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  }