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 }