github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/repo/list_union.go (about)

     1  package repo
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  
     7  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
     8  
     9  	"github.com/kyma-incubator/compass/components/director/pkg/pagination"
    10  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    11  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    12  	"github.com/pkg/errors"
    13  )
    14  
    15  // UnionLister is an interface for listing tenant scoped entities with either externally managed tenant accesses (m2m table or view) or embedded tenant in them.
    16  // It lists entities based on multiple parent queries. For each parent a separate list with a separate tenant isolation subquery is created and the end result is a union of all the results.
    17  type UnionLister interface {
    18  	// List stores the result into dest and returns the total count of tuples for each id from ids
    19  	List(ctx context.Context, resourceType resource.Type, tenant string, ids []string, idsColumn string, pageSize int, cursor string, orderBy OrderByParams, dest Collection, additionalConditions ...Condition) (map[string]int, error)
    20  }
    21  
    22  // UnionListerGlobal is an interface for listing global entities.
    23  // It lists entities based on multiple parent queries. For each parent a separate list with a separate tenant isolation subquery is created and the end result is a union of all the results.
    24  type UnionListerGlobal interface {
    25  	ListGlobal(ctx context.Context, ids []string, idsColumn string, pageSize int, cursor string, orderBy OrderByParams, dest Collection, additionalConditions ...Condition) (map[string]int, error)
    26  	SetSelectedColumns(selectedColumns []string)
    27  	Clone() *unionLister
    28  }
    29  
    30  type unionLister struct {
    31  	tableName       string
    32  	selectedColumns string
    33  	tenantColumn    *string
    34  	resourceType    resource.Type
    35  }
    36  
    37  // NewUnionListerWithEmbeddedTenant is a constructor for UnionLister about entities with tenant embedded in them.
    38  func NewUnionListerWithEmbeddedTenant(tableName string, tenantColumn string, selectedColumns []string) UnionLister {
    39  	return &unionLister{
    40  		tableName:       tableName,
    41  		selectedColumns: strings.Join(selectedColumns, ", "),
    42  		tenantColumn:    &tenantColumn,
    43  	}
    44  }
    45  
    46  // NewUnionLister is a constructor for UnionLister about entities with externally managed tenant accesses (m2m table or view)
    47  func NewUnionLister(tableName string, selectedColumns []string) UnionLister {
    48  	return &unionLister{
    49  		tableName:       tableName,
    50  		selectedColumns: strings.Join(selectedColumns, ", "),
    51  	}
    52  }
    53  
    54  // NewUnionListerGlobal is a constructor for UnionListerGlobal about global entities.
    55  func NewUnionListerGlobal(resourceType resource.Type, tableName string, selectedColumns []string) UnionListerGlobal {
    56  	return &unionLister{
    57  		tableName:       tableName,
    58  		selectedColumns: strings.Join(selectedColumns, ", "),
    59  		resourceType:    resourceType,
    60  	}
    61  }
    62  
    63  // SetSelectedColumns sets the selected columns for the lister
    64  func (l *unionLister) SetSelectedColumns(selectedColumns []string) {
    65  	l.selectedColumns = strings.Join(selectedColumns, ", ")
    66  }
    67  
    68  // Clone returns a copy of the lister
    69  func (l *unionLister) Clone() *unionLister {
    70  	var clonedLister unionLister
    71  
    72  	clonedLister.resourceType = l.resourceType
    73  	clonedLister.tableName = l.tableName
    74  	clonedLister.selectedColumns = l.selectedColumns
    75  	clonedLister.tenantColumn = l.tenantColumn
    76  
    77  	return &clonedLister
    78  }
    79  
    80  // List lists tenant scoped entities based on multiple parent queries. For each parent a separate list with a separate tenant isolation subquery is created and the end result is a union of all the results.
    81  // If the tenantColumn is configured the isolation is based on equal condition on tenantColumn.
    82  // If the tenantColumn is not configured an entity with externally managed tenant accesses in m2m table / view is assumed.
    83  func (l *unionLister) List(ctx context.Context, resourceType resource.Type, tenant string, ids []string, idscolumn string, pageSize int, cursor string, orderBy OrderByParams, dest Collection, additionalConditions ...Condition) (map[string]int, error) {
    84  	if tenant == "" {
    85  		return nil, apperrors.NewTenantRequiredError()
    86  	}
    87  
    88  	if l.tenantColumn != nil {
    89  		additionalConditions = append(Conditions{NewEqualCondition(*l.tenantColumn, tenant)}, additionalConditions...)
    90  		return l.list(ctx, resourceType, pageSize, cursor, orderBy, ids, idscolumn, dest, additionalConditions...)
    91  	}
    92  
    93  	tenantIsolation, err := NewTenantIsolationCondition(resourceType, tenant, false)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  
    98  	additionalConditions = append(additionalConditions, tenantIsolation)
    99  
   100  	return l.list(ctx, resourceType, pageSize, cursor, orderBy, ids, idscolumn, dest, additionalConditions...)
   101  }
   102  
   103  // ListGlobal lists global entities without tenant isolation.
   104  func (l *unionLister) ListGlobal(ctx context.Context, ids []string, idscolumn string, pageSize int, cursor string, orderBy OrderByParams, dest Collection, additionalConditions ...Condition) (map[string]int, error) {
   105  	return l.list(ctx, l.resourceType, pageSize, cursor, orderBy, ids, idscolumn, dest, additionalConditions...)
   106  }
   107  
   108  type queryStruct struct {
   109  	args      []interface{}
   110  	statement string
   111  }
   112  
   113  func (l *unionLister) list(ctx context.Context, resourceType resource.Type, pageSize int, cursor string, orderBy OrderByParams, ids []string, idsColumn string, dest Collection, conditions ...Condition) (map[string]int, error) {
   114  	persist, err := persistence.FromCtx(ctx)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	offset, err := pagination.DecodeOffsetCursor(cursor)
   120  	if err != nil {
   121  		return nil, errors.Wrap(err, "while decoding page cursor")
   122  	}
   123  
   124  	queries, err := l.buildQueries(ids, idsColumn, conditions, orderBy, pageSize, offset)
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	stmts := make([]string, 0, len(queries))
   130  	for _, q := range queries {
   131  		stmts = append(stmts, q.statement)
   132  	}
   133  
   134  	args := make([]interface{}, 0, len(queries))
   135  	for _, q := range queries {
   136  		args = append(args, q.args...)
   137  	}
   138  
   139  	query := buildUnionQuery(stmts)
   140  
   141  	err = persist.SelectContext(ctx, dest, query, args...)
   142  	if err != nil {
   143  		return nil, persistence.MapSQLError(ctx, err, resourceType, resource.List, "while fetching list page of objects from '%s' table", l.tableName)
   144  	}
   145  
   146  	totalCount, err := l.getTotalCount(ctx, resourceType, persist, idsColumn, []string{idsColumn}, OrderByParams{NewAscOrderBy(idsColumn)}, conditions)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	return totalCount, nil
   152  }
   153  
   154  func (l *unionLister) buildQueries(ids []string, idsColumn string, conditions []Condition, orderBy OrderByParams, limit int, offset int) ([]queryStruct, error) {
   155  	queries := make([]queryStruct, 0, len(ids))
   156  	for _, id := range ids {
   157  		query, args, err := buildSelectQueryWithLimitAndOffset(l.tableName, l.selectedColumns, append(conditions, NewEqualCondition(idsColumn, id)), orderBy, limit, offset, false)
   158  		if err != nil {
   159  			return nil, errors.Wrap(err, "while building list query")
   160  		}
   161  
   162  		queries = append(queries, queryStruct{
   163  			args:      args,
   164  			statement: query,
   165  		})
   166  	}
   167  	return queries, nil
   168  }
   169  
   170  type idToCount struct {
   171  	ID    string `db:"id"`
   172  	Count int    `db:"total_count"`
   173  }
   174  
   175  func (l *unionLister) getTotalCount(ctx context.Context, resourceType resource.Type, persist persistence.PersistenceOp, idsColumn string, groupBy GroupByParams, orderBy OrderByParams, conditions Conditions) (map[string]int, error) {
   176  	query, args, err := buildCountQuery(l.tableName, idsColumn, conditions, groupBy, orderBy, true)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	var counts []idToCount
   182  	err = persist.SelectContext(ctx, &counts, query, args...)
   183  	if err != nil {
   184  		return nil, persistence.MapSQLError(ctx, err, resourceType, resource.List, "while counting objects from '%s' table", l.tableName)
   185  	}
   186  
   187  	totalCount := make(map[string]int)
   188  	for _, c := range counts {
   189  		totalCount[c.ID] = c.Count
   190  	}
   191  
   192  	return totalCount, nil
   193  }