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

     1  package repo
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/jmoiron/sqlx"
     9  
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    15  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    16  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    17  )
    18  
    19  // UpserterGlobal is an interface for upserting global entities without tenant or entities with tenant embedded in them.
    20  type UpserterGlobal interface {
    21  	UpsertGlobal(ctx context.Context, dbEntity interface{}) error
    22  }
    23  
    24  // Upserter is an interface for upserting entities with externally managed tenant accesses (m2m table or view)
    25  type Upserter interface {
    26  	Upsert(ctx context.Context, resourceType resource.Type, tenant string, dbEntity interface{}) (string, error)
    27  }
    28  
    29  type upserter struct {
    30  	tableName          string
    31  	insertColumns      []string
    32  	conflictingColumns []string
    33  	updateColumns      []string
    34  	isTrusted          bool
    35  }
    36  
    37  type upserterGlobal struct {
    38  	tableName          string
    39  	resourceType       resource.Type
    40  	tenantColumn       *string
    41  	insertColumns      []string
    42  	conflictingColumns []string
    43  	updateColumns      []string
    44  }
    45  
    46  // NewUpserter is a constructor for Upserter about entities with externally managed tenant accesses (m2m table or view)
    47  func NewUpserter(tableName string, insertColumns []string, conflictingColumns []string, updateColumns []string) Upserter {
    48  	return &upserter{
    49  		tableName:          tableName,
    50  		insertColumns:      insertColumns,
    51  		conflictingColumns: conflictingColumns,
    52  		updateColumns:      updateColumns,
    53  	}
    54  }
    55  
    56  // NewTrustedUpserter is a constructor for Upserter about entities with externally managed tenant accesses (m2m table or view) which ignores the tenant isolation
    57  func NewTrustedUpserter(tableName string, insertColumns []string, conflictingColumns []string, updateColumns []string) Upserter {
    58  	return &upserter{
    59  		tableName:          tableName,
    60  		insertColumns:      insertColumns,
    61  		conflictingColumns: conflictingColumns,
    62  		updateColumns:      updateColumns,
    63  		isTrusted:          true,
    64  	}
    65  }
    66  
    67  // NewUpserterGlobal is a constructor for UpserterGlobal about global entities without tenant.
    68  func NewUpserterGlobal(resourceType resource.Type, tableName string, insertColumns []string, conflictingColumns []string, updateColumns []string) UpserterGlobal {
    69  	return &upserterGlobal{
    70  		resourceType:       resourceType,
    71  		tableName:          tableName,
    72  		insertColumns:      insertColumns,
    73  		conflictingColumns: conflictingColumns,
    74  		updateColumns:      updateColumns,
    75  	}
    76  }
    77  
    78  // NewUpserterWithEmbeddedTenant is a constructor for Upserter about entities with tenant embedded in them.
    79  func NewUpserterWithEmbeddedTenant(resourceType resource.Type, tableName string, insertColumns []string, conflictingColumns []string, updateColumns []string, tenantColumn string) UpserterGlobal {
    80  	return &upserterGlobal{
    81  		resourceType:       resourceType,
    82  		tableName:          tableName,
    83  		tenantColumn:       &tenantColumn,
    84  		insertColumns:      insertColumns,
    85  		conflictingColumns: conflictingColumns,
    86  		updateColumns:      updateColumns,
    87  	}
    88  }
    89  
    90  // Upsert adds a new entity in the Compass DB in case it does not exist. If it already exists, updates it.
    91  // This upserter is suitable for resources that have m2m tenant relation as it does maintain tenant accesses.
    92  func (u *upserter) Upsert(ctx context.Context, resourceType resource.Type, tenant string, dbEntity interface{}) (string, error) {
    93  	return u.unsafeUpsert(ctx, resourceType, tenant, dbEntity)
    94  }
    95  
    96  func (u *upserter) unsafeUpsert(ctx context.Context, resourceType resource.Type, tenant string, dbEntity interface{}) (string, error) {
    97  	if dbEntity == nil {
    98  		return "", apperrors.NewInternalError("item cannot be nil")
    99  	}
   100  
   101  	persist, err := persistence.FromCtx(ctx)
   102  	if err != nil {
   103  		return "", err
   104  	}
   105  
   106  	query := buildQuery(u.tableName, u.insertColumns, u.conflictingColumns, u.updateColumns)
   107  	if !u.isTrusted {
   108  		query, err = u.addTenantIsolation(query, resourceType, tenant)
   109  		if err != nil {
   110  			return "", err
   111  		}
   112  	}
   113  	query += " RETURNING id;"
   114  
   115  	if entityWithExternalTenant, ok := dbEntity.(EntityWithExternalTenant); ok {
   116  		dbEntity = entityWithExternalTenant.DecorateWithTenantID(tenant)
   117  	}
   118  
   119  	preparedQuery, args, err := sqlx.Named(query, dbEntity)
   120  	if err != nil {
   121  		return "", err
   122  	}
   123  
   124  	preparedQuery = sqlx.Rebind(sqlx.DOLLAR, preparedQuery)
   125  	upsertedID := ""
   126  
   127  	log.C(ctx).Debugf("Executing DB query: %s", preparedQuery)
   128  	err = persist.GetContext(ctx, &upsertedID, preparedQuery, args...)
   129  	if err = persistence.MapSQLError(ctx, err, resourceType, resource.Upsert, "while upserting row to '%s' table", u.tableName); err != nil {
   130  		return "", err
   131  	}
   132  
   133  	var id string
   134  	if identifiable, ok := dbEntity.(Identifiable); ok {
   135  		id = identifiable.GetID()
   136  	}
   137  
   138  	if len(id) == 0 {
   139  		return "", apperrors.NewInternalError("id cannot be empty, check if the entity implements Identifiable")
   140  	}
   141  
   142  	if resourceType.IsTopLevel() {
   143  		if err = u.upsertTenantAccess(ctx, resourceType, upsertedID, tenant); err != nil {
   144  			return "", err
   145  		}
   146  	}
   147  
   148  	return upsertedID, nil
   149  }
   150  
   151  func (u *upserter) upsertTenantAccess(ctx context.Context, resourceType resource.Type, resourceID string, tenant string) error {
   152  	m2mTable, ok := resourceType.TenantAccessTable()
   153  	if !ok {
   154  		return errors.Errorf("entity %s does not have access table", resourceType)
   155  	}
   156  
   157  	return UpsertTenantAccessRecursively(ctx, m2mTable, &TenantAccess{
   158  		TenantID:   tenant,
   159  		ResourceID: resourceID,
   160  		Owner:      true,
   161  	})
   162  }
   163  
   164  func (u *upserter) addTenantIsolation(query string, resourceType resource.Type, tenant string) (string, error) {
   165  	var stmtBuilder strings.Builder
   166  
   167  	stmtBuilder.WriteString(query)
   168  
   169  	tenantIsolationCondition, err := NewTenantIsolationConditionForNamedArgs(resourceType, tenant, true)
   170  	if err != nil {
   171  		return "", err
   172  	}
   173  
   174  	tenantIsolationStatement := strings.Replace(tenantIsolationCondition.GetQueryPart(), "(", fmt.Sprintf("(%s.", u.tableName), 1)
   175  	stmtBuilder.WriteString(" WHERE ")
   176  	stmtBuilder.WriteString(tenantIsolationStatement)
   177  
   178  	return stmtBuilder.String(), nil
   179  }
   180  
   181  // UpsertGlobal adds a new entity in the Compass DB in case it does not exist. If it already exists, updates it.
   182  // This upserter is not suitable for resources that have m2m tenant relation as it does not maintain tenant accesses.
   183  // Use it for global scoped resources or resources with embedded tenant_id only.
   184  func (u *upserterGlobal) UpsertGlobal(ctx context.Context, dbEntity interface{}) error {
   185  	return u.unsafeUpsert(ctx, u.resourceType, dbEntity)
   186  }
   187  
   188  func (u *upserterGlobal) unsafeUpsert(ctx context.Context, resourceType resource.Type, dbEntity interface{}) error {
   189  	if dbEntity == nil {
   190  		return apperrors.NewInternalError("item cannot be nil")
   191  	}
   192  
   193  	persist, err := persistence.FromCtx(ctx)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	query := buildQuery(u.tableName, u.insertColumns, u.conflictingColumns, u.updateColumns)
   199  	if u.tenantColumn != nil {
   200  		query = u.addTenantIsolation(query)
   201  	}
   202  
   203  	log.C(ctx).Warnf("Executing DB query: %s", query)
   204  	_, err = persist.NamedExecContext(ctx, query, dbEntity)
   205  	err = persistence.MapSQLError(ctx, err, resourceType, resource.Upsert, "while upserting row to '%s' table", u.tableName)
   206  	return err
   207  }
   208  
   209  func (u *upserterGlobal) addTenantIsolation(query string) string {
   210  	var stmtBuilder strings.Builder
   211  
   212  	stmtBuilder.WriteString(query)
   213  
   214  	stmtBuilder.WriteString(" WHERE ")
   215  	stmtBuilder.WriteString(fmt.Sprintf(" %s.%s = :%s", u.tableName, *u.tenantColumn, *u.tenantColumn))
   216  
   217  	return stmtBuilder.String()
   218  }
   219  
   220  func buildQuery(tableName string, insertColumns []string, conflictingColumns []string, updateColumns []string) string {
   221  	var stmtBuilder strings.Builder
   222  
   223  	values := make([]string, 0, len(insertColumns))
   224  	for _, c := range insertColumns {
   225  		values = append(values, fmt.Sprintf(":%s", c))
   226  	}
   227  
   228  	update := make([]string, 0, len(updateColumns))
   229  	for _, c := range updateColumns {
   230  		update = append(update, fmt.Sprintf("%[1]s=EXCLUDED.%[1]s", c))
   231  	}
   232  	stmtWithoutUpsert := fmt.Sprintf("INSERT INTO %s ( %s ) VALUES ( %s )", tableName, strings.Join(insertColumns, ", "), strings.Join(values, ", "))
   233  	stmtWithUpsert := fmt.Sprintf("%s ON CONFLICT ( %s ) DO UPDATE SET %s", stmtWithoutUpsert, strings.Join(conflictingColumns, ", "), strings.Join(update, ", "))
   234  
   235  	stmtBuilder.WriteString(stmtWithUpsert)
   236  	return stmtBuilder.String()
   237  }