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

     1  package application
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
     9  
    10  	"github.com/kyma-incubator/compass/components/director/pkg/graphql"
    11  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    12  	"github.com/kyma-incubator/compass/components/director/pkg/operation"
    13  
    14  	"github.com/google/uuid"
    15  	"github.com/kyma-incubator/compass/components/director/internal/domain/label"
    16  	"github.com/kyma-incubator/compass/components/director/internal/labelfilter"
    17  	"github.com/kyma-incubator/compass/components/director/internal/model"
    18  	"github.com/kyma-incubator/compass/components/director/internal/repo"
    19  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  const (
    24  	applicationTable string = `public.applications`
    25  	// listeningApplicationsView provides a structured view of applications that have a Webhook or their ApplicationTemplate has a Webhook
    26  	listeningApplicationsView = `listening_applications`
    27  )
    28  
    29  var (
    30  	applicationColumns    = []string{"id", "app_template_id", "system_number", "local_tenant_id", "name", "description", "status_condition", "status_timestamp", "system_status", "healthcheck_url", "integration_system_id", "provider_name", "base_url", "application_namespace", "labels", "ready", "created_at", "updated_at", "deleted_at", "error", "correlation_ids", "tags", "documentation_labels", "tenant_business_type_id"}
    31  	updatableColumns      = []string{"name", "description", "status_condition", "status_timestamp", "system_status", "healthcheck_url", "integration_system_id", "provider_name", "base_url", "application_namespace", "labels", "ready", "created_at", "updated_at", "deleted_at", "error", "correlation_ids", "tags", "documentation_labels", "system_number", "local_tenant_id"}
    32  	upsertableColumns     = []string{"name", "description", "status_condition", "system_status", "provider_name", "base_url", "application_namespace", "labels", "tenant_business_type_id"}
    33  	matchingSystemColumns = []string{"system_number"}
    34  )
    35  
    36  // EntityConverter missing godoc
    37  //
    38  //go:generate mockery --name=EntityConverter --output=automock --outpkg=automock --case=underscore --disable-version-string
    39  type EntityConverter interface {
    40  	ToEntity(in *model.Application) (*Entity, error)
    41  	FromEntity(entity *Entity) *model.Application
    42  }
    43  
    44  type pgRepository struct {
    45  	existQuerier          repo.ExistQuerier
    46  	ownerExistQuerier     repo.ExistQuerier
    47  	singleGetter          repo.SingleGetter
    48  	globalGetter          repo.SingleGetterGlobal
    49  	globalDeleter         repo.DeleterGlobal
    50  	lister                repo.Lister
    51  	listeningAppsLister   repo.Lister
    52  	listerGlobal          repo.ListerGlobal
    53  	deleter               repo.Deleter
    54  	pageableQuerier       repo.PageableQuerier
    55  	globalPageableQuerier repo.PageableQuerierGlobal
    56  	creator               repo.Creator
    57  	updater               repo.Updater
    58  	globalUpdater         repo.UpdaterGlobal
    59  	upserter              repo.Upserter
    60  	trustedUpserter       repo.Upserter
    61  	conv                  EntityConverter
    62  }
    63  
    64  // NewRepository missing godoc
    65  func NewRepository(conv EntityConverter) *pgRepository {
    66  	return &pgRepository{
    67  		existQuerier:          repo.NewExistQuerier(applicationTable),
    68  		ownerExistQuerier:     repo.NewExistQuerierWithOwnerCheck(applicationTable),
    69  		singleGetter:          repo.NewSingleGetter(applicationTable, applicationColumns),
    70  		globalGetter:          repo.NewSingleGetterGlobal(resource.Application, applicationTable, applicationColumns),
    71  		globalDeleter:         repo.NewDeleterGlobal(resource.Application, applicationTable),
    72  		deleter:               repo.NewDeleter(applicationTable),
    73  		lister:                repo.NewLister(applicationTable, applicationColumns),
    74  		listeningAppsLister:   repo.NewLister(listeningApplicationsView, applicationColumns),
    75  		listerGlobal:          repo.NewListerGlobal(resource.Application, applicationTable, applicationColumns),
    76  		pageableQuerier:       repo.NewPageableQuerier(applicationTable, applicationColumns),
    77  		globalPageableQuerier: repo.NewPageableQuerierGlobal(resource.Application, applicationTable, applicationColumns),
    78  		creator:               repo.NewCreator(applicationTable, applicationColumns),
    79  		updater:               repo.NewUpdater(applicationTable, updatableColumns, []string{"id"}),
    80  		globalUpdater:         repo.NewUpdaterGlobal(resource.Application, applicationTable, updatableColumns, []string{"id"}),
    81  		upserter:              repo.NewUpserter(applicationTable, applicationColumns, matchingSystemColumns, upsertableColumns),
    82  		trustedUpserter:       repo.NewTrustedUpserter(applicationTable, applicationColumns, matchingSystemColumns, upsertableColumns),
    83  		conv:                  conv,
    84  	}
    85  }
    86  
    87  // Exists missing godoc
    88  func (r *pgRepository) Exists(ctx context.Context, tenant, id string) (bool, error) {
    89  	return r.existQuerier.Exists(ctx, resource.Application, tenant, repo.Conditions{repo.NewEqualCondition("id", id)})
    90  }
    91  
    92  // OwnerExists checks if application with given id and tenant exists and has owner access
    93  func (r *pgRepository) OwnerExists(ctx context.Context, tenant, id string) (bool, error) {
    94  	return r.ownerExistQuerier.Exists(ctx, resource.Application, tenant, repo.Conditions{repo.NewEqualCondition("id", id)})
    95  }
    96  
    97  // Delete missing godoc
    98  func (r *pgRepository) Delete(ctx context.Context, tenant, id string) error {
    99  	opMode := operation.ModeFromCtx(ctx)
   100  	if opMode == graphql.OperationModeAsync {
   101  		app, err := r.GetByID(ctx, tenant, id)
   102  		if err != nil {
   103  			return err
   104  		}
   105  
   106  		app.SetReady(false)
   107  		app.SetError("")
   108  		if app.GetDeletedAt().IsZero() { // Needed for the tests but might be useful for the production also
   109  			app.SetDeletedAt(time.Now())
   110  		}
   111  
   112  		return r.Update(ctx, tenant, app)
   113  	}
   114  
   115  	return r.deleter.DeleteOne(ctx, resource.Application, tenant, repo.Conditions{repo.NewEqualCondition("id", id)})
   116  }
   117  
   118  // DeleteGlobal missing godoc
   119  func (r *pgRepository) DeleteGlobal(ctx context.Context, id string) error {
   120  	opMode := operation.ModeFromCtx(ctx)
   121  	if opMode == graphql.OperationModeAsync {
   122  		app, err := r.GetGlobalByID(ctx, id)
   123  		if err != nil {
   124  			return err
   125  		}
   126  
   127  		app.SetReady(false)
   128  		app.SetError("")
   129  		if app.DeletedAt.IsZero() { // Needed for the tests but might be useful for the production also
   130  			app.SetDeletedAt(time.Now())
   131  		}
   132  
   133  		return r.globalUpdater.UpdateSingleGlobal(ctx, app)
   134  	}
   135  
   136  	return r.globalDeleter.DeleteOneGlobal(ctx, repo.Conditions{repo.NewEqualCondition("id", id)})
   137  }
   138  
   139  // GetByID missing godoc
   140  func (r *pgRepository) GetByID(ctx context.Context, tenant, id string) (*model.Application, error) {
   141  	var appEnt Entity
   142  	if err := r.singleGetter.Get(ctx, resource.Application, tenant, repo.Conditions{repo.NewEqualCondition("id", id)}, repo.NoOrderBy, &appEnt); err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	appModel := r.conv.FromEntity(&appEnt)
   147  
   148  	return appModel, nil
   149  }
   150  
   151  // GetByIDForUpdate returns the application with matching ID from the Compass DB and locks it exclusively until the transaction is finished.
   152  func (r *pgRepository) GetByIDForUpdate(ctx context.Context, tenant, id string) (*model.Application, error) {
   153  	var appEnt Entity
   154  	if err := r.singleGetter.GetForUpdate(ctx, resource.Application, tenant, repo.Conditions{repo.NewEqualCondition("id", id)}, repo.NoOrderBy, &appEnt); err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	appModel := r.conv.FromEntity(&appEnt)
   159  
   160  	return appModel, nil
   161  }
   162  
   163  // GetBySystemNumber returns an application retrieved by systemNumber from the Compass DB
   164  func (r *pgRepository) GetBySystemNumber(ctx context.Context, tenant, systemNumber string) (*model.Application, error) {
   165  	var appEnt Entity
   166  	if err := r.singleGetter.Get(ctx, resource.Application, tenant, repo.Conditions{repo.NewEqualCondition("system_number", systemNumber)}, repo.NoOrderBy, &appEnt); err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	appModel := r.conv.FromEntity(&appEnt)
   171  
   172  	return appModel, nil
   173  }
   174  
   175  // GetGlobalByID missing godoc
   176  func (r *pgRepository) GetGlobalByID(ctx context.Context, id string) (*model.Application, error) {
   177  	var appEnt Entity
   178  	if err := r.globalGetter.GetGlobal(ctx, repo.Conditions{repo.NewEqualCondition("id", id)}, repo.NoOrderBy, &appEnt); err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	appModel := r.conv.FromEntity(&appEnt)
   183  
   184  	return appModel, nil
   185  }
   186  
   187  // GetByFilter retrieves Application matching on the given label filters
   188  func (r *pgRepository) GetByFilter(ctx context.Context, tenant string, filter []*labelfilter.LabelFilter) (*model.Application, error) {
   189  	var appEnt Entity
   190  
   191  	tenantID, err := uuid.Parse(tenant)
   192  	if err != nil {
   193  		return nil, errors.Wrap(err, "while parsing tenant as UUID")
   194  	}
   195  
   196  	filterSubquery, args, err := label.FilterQuery(model.ApplicationLabelableObject, label.IntersectSet, tenantID, filter)
   197  	if err != nil {
   198  		return nil, errors.Wrap(err, "while building filter query")
   199  	}
   200  
   201  	var conditions repo.Conditions
   202  	if filterSubquery != "" {
   203  		conditions = append(conditions, repo.NewInConditionForSubQuery("id", filterSubquery, args))
   204  	}
   205  
   206  	if err = r.singleGetter.Get(ctx, resource.Application, tenant, conditions, repo.NoOrderBy, &appEnt); err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	appModel := r.conv.FromEntity(&appEnt)
   211  
   212  	return appModel, nil
   213  }
   214  
   215  // ListAll missing godoc
   216  func (r *pgRepository) ListAll(ctx context.Context, tenantID string) ([]*model.Application, error) {
   217  	var entities EntityCollection
   218  
   219  	err := r.lister.List(ctx, resource.Application, tenantID, &entities)
   220  
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	return r.multipleFromEntities(entities)
   226  }
   227  
   228  // ListAllByFilter retrieves all applications matching on the given label filters
   229  func (r *pgRepository) ListAllByFilter(ctx context.Context, tenant string, filter []*labelfilter.LabelFilter) ([]*model.Application, error) {
   230  	var entities EntityCollection
   231  
   232  	tenantID, err := uuid.Parse(tenant)
   233  	if err != nil {
   234  		return nil, errors.Wrap(err, "while parsing tenant as UUID")
   235  	}
   236  
   237  	filterSubquery, args, err := label.FilterQuery(model.ApplicationLabelableObject, label.IntersectSet, tenantID, filter)
   238  	if err != nil {
   239  		return nil, errors.Wrap(err, "while building filter query")
   240  	}
   241  
   242  	var conditions repo.Conditions
   243  	if filterSubquery != "" {
   244  		conditions = append(conditions, repo.NewInConditionForSubQuery("id", filterSubquery, args))
   245  	}
   246  
   247  	if err = r.lister.List(ctx, resource.Application, tenant, &entities, conditions...); err != nil {
   248  		return nil, err
   249  	}
   250  
   251  	return r.multipleFromEntities(entities)
   252  }
   253  
   254  // ListAllByApplicationTemplateID retrieves all applications which have the given app template id
   255  func (r *pgRepository) ListAllByApplicationTemplateID(ctx context.Context, applicationTemplateID string) ([]*model.Application, error) {
   256  	var appsCollection EntityCollection
   257  
   258  	conditions := repo.Conditions{
   259  		repo.NewEqualCondition("app_template_id", applicationTemplateID),
   260  	}
   261  	if err := r.listerGlobal.ListGlobal(ctx, &appsCollection, conditions...); err != nil {
   262  		return nil, err
   263  	}
   264  
   265  	items := make([]*model.Application, 0, len(appsCollection))
   266  
   267  	for _, appEnt := range appsCollection {
   268  		m := r.conv.FromEntity(&appEnt)
   269  		items = append(items, m)
   270  	}
   271  
   272  	return items, nil
   273  }
   274  
   275  // List missing godoc
   276  func (r *pgRepository) List(ctx context.Context, tenant string, filter []*labelfilter.LabelFilter, pageSize int, cursor string) (*model.ApplicationPage, error) {
   277  	var appsCollection EntityCollection
   278  	tenantID, err := uuid.Parse(tenant)
   279  	if err != nil {
   280  		return nil, errors.Wrap(err, "while parsing tenant as UUID")
   281  	}
   282  	filterSubquery, args, err := label.FilterQuery(model.ApplicationLabelableObject, label.IntersectSet, tenantID, filter)
   283  	if err != nil {
   284  		return nil, errors.Wrap(err, "while building filter query")
   285  	}
   286  
   287  	var conditions repo.Conditions
   288  	if filterSubquery != "" {
   289  		conditions = append(conditions, repo.NewInConditionForSubQuery("id", filterSubquery, args))
   290  	}
   291  
   292  	page, totalCount, err := r.pageableQuerier.List(ctx, resource.Application, tenant, pageSize, cursor, "id", &appsCollection, conditions...)
   293  
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	items := make([]*model.Application, 0, len(appsCollection))
   299  
   300  	for _, appEnt := range appsCollection {
   301  		m := r.conv.FromEntity(&appEnt)
   302  		items = append(items, m)
   303  	}
   304  	return &model.ApplicationPage{
   305  		Data:       items,
   306  		TotalCount: totalCount,
   307  		PageInfo:   page}, nil
   308  }
   309  
   310  // ListGlobal missing godoc
   311  func (r *pgRepository) ListGlobal(ctx context.Context, pageSize int, cursor string) (*model.ApplicationPage, error) {
   312  	var appsCollection EntityCollection
   313  
   314  	page, totalCount, err := r.globalPageableQuerier.ListGlobal(ctx, pageSize, cursor, "id", &appsCollection)
   315  
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  
   320  	items := make([]*model.Application, 0, len(appsCollection))
   321  
   322  	for _, appEnt := range appsCollection {
   323  		m := r.conv.FromEntity(&appEnt)
   324  		items = append(items, m)
   325  	}
   326  	return &model.ApplicationPage{
   327  		Data:       items,
   328  		TotalCount: totalCount,
   329  		PageInfo:   page}, nil
   330  }
   331  
   332  // ListByScenarios missing godoc
   333  func (r *pgRepository) ListByScenarios(ctx context.Context, tenant uuid.UUID, scenarios []string, pageSize int, cursor string, hidingSelectors map[string][]string) (*model.ApplicationPage, error) {
   334  	var appsCollection EntityCollection
   335  
   336  	// Scenarios query part
   337  	scenariosFilters := make([]*labelfilter.LabelFilter, 0, len(scenarios))
   338  	for _, scenarioValue := range scenarios {
   339  		query := fmt.Sprintf(`$[*] ? (@ == "%s")`, scenarioValue)
   340  		scenariosFilters = append(scenariosFilters, labelfilter.NewForKeyWithQuery(model.ScenariosKey, query))
   341  	}
   342  
   343  	scenariosSubquery, scenariosArgs, err := label.FilterQuery(model.ApplicationLabelableObject, label.UnionSet, tenant, scenariosFilters)
   344  	if err != nil {
   345  		return nil, errors.Wrap(err, "while creating scenarios filter query")
   346  	}
   347  
   348  	// Application Hide query part
   349  	var appHideFilters []*labelfilter.LabelFilter
   350  	for key, values := range hidingSelectors {
   351  		for _, value := range values {
   352  			appHideFilters = append(appHideFilters, labelfilter.NewForKeyWithQuery(key, fmt.Sprintf(`"%s"`, value)))
   353  		}
   354  	}
   355  
   356  	appHideSubquery, appHideArgs, err := label.FilterSubquery(model.ApplicationLabelableObject, label.ExceptSet, tenant, appHideFilters)
   357  	if err != nil {
   358  		return nil, errors.Wrap(err, "while creating scenarios filter query")
   359  	}
   360  
   361  	// Combining both queries
   362  	combinedQuery := scenariosSubquery + appHideSubquery
   363  	combinedArgs := append(scenariosArgs, appHideArgs...)
   364  
   365  	var conditions repo.Conditions
   366  	if combinedQuery != "" {
   367  		conditions = append(conditions, repo.NewInConditionForSubQuery("id", combinedQuery, combinedArgs))
   368  	}
   369  
   370  	page, totalCount, err := r.pageableQuerier.List(ctx, resource.Application, tenant.String(), pageSize, cursor, "id", &appsCollection, conditions...)
   371  
   372  	if err != nil {
   373  		return nil, err
   374  	}
   375  
   376  	items := make([]*model.Application, 0, len(appsCollection))
   377  
   378  	for _, appEnt := range appsCollection {
   379  		m := r.conv.FromEntity(&appEnt)
   380  		items = append(items, m)
   381  	}
   382  	return &model.ApplicationPage{
   383  		Data:       items,
   384  		TotalCount: totalCount,
   385  		PageInfo:   page}, nil
   386  }
   387  
   388  // ListByScenariosNoPaging lists all applications that are in any of the given scenarios
   389  func (r *pgRepository) ListByScenariosNoPaging(ctx context.Context, tenant string, scenarios []string) ([]*model.Application, error) {
   390  	tenantUUID, err := uuid.Parse(tenant)
   391  	if err != nil {
   392  		return nil, apperrors.NewInvalidDataError("tenantID is not UUID")
   393  	}
   394  
   395  	var entities EntityCollection
   396  
   397  	// Scenarios query part
   398  	scenariosFilters := make([]*labelfilter.LabelFilter, 0, len(scenarios))
   399  	for _, scenarioValue := range scenarios {
   400  		query := fmt.Sprintf(`$[*] ? (@ == "%s")`, scenarioValue)
   401  		scenariosFilters = append(scenariosFilters, labelfilter.NewForKeyWithQuery(model.ScenariosKey, query))
   402  	}
   403  
   404  	scenariosSubquery, scenariosArgs, err := label.FilterQuery(model.ApplicationLabelableObject, label.UnionSet, tenantUUID, scenariosFilters)
   405  	if err != nil {
   406  		return nil, errors.Wrap(err, "while creating scenarios filter query")
   407  	}
   408  
   409  	var conditions repo.Conditions
   410  	if scenariosSubquery != "" {
   411  		conditions = append(conditions, repo.NewInConditionForSubQuery("id", scenariosSubquery, scenariosArgs))
   412  	}
   413  
   414  	if err = r.lister.List(ctx, resource.Application, tenant, &entities, conditions...); err != nil {
   415  		return nil, err
   416  	}
   417  
   418  	items := make([]*model.Application, 0, len(entities))
   419  
   420  	for _, appEnt := range entities {
   421  		m := r.conv.FromEntity(&appEnt)
   422  		items = append(items, m)
   423  	}
   424  
   425  	return items, nil
   426  }
   427  
   428  // ListByScenariosAndIDs lists all apps with given IDs that are in any of the given scenarios
   429  func (r *pgRepository) ListByScenariosAndIDs(ctx context.Context, tenant string, scenarios []string, ids []string) ([]*model.Application, error) {
   430  	if len(scenarios) == 0 || len(ids) == 0 {
   431  		return nil, nil
   432  	}
   433  	tenantUUID, err := uuid.Parse(tenant)
   434  	if err != nil {
   435  		return nil, apperrors.NewInvalidDataError("tenantID is not UUID")
   436  	}
   437  
   438  	var entities EntityCollection
   439  
   440  	// Scenarios query part
   441  	scenariosFilters := make([]*labelfilter.LabelFilter, 0, len(scenarios))
   442  	for _, scenarioValue := range scenarios {
   443  		query := fmt.Sprintf(`$[*] ? (@ == "%s")`, scenarioValue)
   444  		scenariosFilters = append(scenariosFilters, labelfilter.NewForKeyWithQuery(model.ScenariosKey, query))
   445  	}
   446  
   447  	scenariosSubquery, scenariosArgs, err := label.FilterQuery(model.ApplicationLabelableObject, label.UnionSet, tenantUUID, scenariosFilters)
   448  	if err != nil {
   449  		return nil, errors.Wrap(err, "while creating scenarios filter query")
   450  	}
   451  
   452  	var conditions repo.Conditions
   453  	if scenariosSubquery != "" {
   454  		conditions = append(conditions, repo.NewInConditionForSubQuery("id", scenariosSubquery, scenariosArgs))
   455  	}
   456  
   457  	conditions = append(conditions, repo.NewInConditionForStringValues("id", ids))
   458  
   459  	if err := r.lister.List(ctx, resource.Application, tenant, &entities, conditions...); err != nil {
   460  		return nil, err
   461  	}
   462  
   463  	items := make([]*model.Application, 0, len(entities))
   464  
   465  	for _, appEnt := range entities {
   466  		m := r.conv.FromEntity(&appEnt)
   467  		items = append(items, m)
   468  	}
   469  
   470  	return items, nil
   471  }
   472  
   473  // ListAllByIDs lists all apps with given IDs
   474  func (r *pgRepository) ListAllByIDs(ctx context.Context, tenantID string, ids []string) ([]*model.Application, error) {
   475  	if len(ids) == 0 {
   476  		return nil, nil
   477  	}
   478  
   479  	var entities EntityCollection
   480  	err := r.lister.List(ctx, resource.Application, tenantID, &entities, repo.NewInConditionForStringValues("id", ids))
   481  
   482  	if err != nil {
   483  		return nil, err
   484  	}
   485  
   486  	return r.multipleFromEntities(entities)
   487  }
   488  
   489  // ListListeningApplications lists all application that either have webhook of type whType, or their application template has a webhook of type whType
   490  func (r *pgRepository) ListListeningApplications(ctx context.Context, tenant string, whType model.WebhookType) ([]*model.Application, error) {
   491  	var entities EntityCollection
   492  
   493  	conditions := repo.Conditions{
   494  		repo.NewEqualCondition("webhook_type", whType),
   495  	}
   496  
   497  	if err := r.listeningAppsLister.List(ctx, resource.Application, tenant, &entities, conditions...); err != nil {
   498  		return nil, err
   499  	}
   500  
   501  	return r.multipleFromEntities(entities)
   502  }
   503  
   504  // Create missing godoc
   505  func (r *pgRepository) Create(ctx context.Context, tenant string, model *model.Application) error {
   506  	if model == nil {
   507  		return apperrors.NewInternalError("model can not be empty")
   508  	}
   509  
   510  	log.C(ctx).Debugf("Converting Application model with id %s to entity", model.ID)
   511  	appEnt, err := r.conv.ToEntity(model)
   512  	if err != nil {
   513  		return errors.Wrap(err, "while converting to Application entity")
   514  	}
   515  
   516  	log.C(ctx).Debugf("Persisting Application entity with id %s to db", model.ID)
   517  	return r.creator.Create(ctx, resource.Application, tenant, appEnt)
   518  }
   519  
   520  // Update missing godoc
   521  func (r *pgRepository) Update(ctx context.Context, tenant string, model *model.Application) error {
   522  	return r.updateSingle(ctx, tenant, model, false)
   523  }
   524  
   525  // Upsert inserts application for given tenant or update it if it already exists
   526  func (r *pgRepository) Upsert(ctx context.Context, tenant string, model *model.Application) (string, error) {
   527  	return r.genericUpsert(ctx, tenant, model, r.upserter)
   528  }
   529  
   530  // TrustedUpsert inserts application for given tenant or update it if it already exists ignoring tenant isolation
   531  func (r *pgRepository) TrustedUpsert(ctx context.Context, tenant string, model *model.Application) (string, error) {
   532  	return r.genericUpsert(ctx, tenant, model, r.trustedUpserter)
   533  }
   534  
   535  // TechnicalUpdate missing godoc
   536  func (r *pgRepository) TechnicalUpdate(ctx context.Context, model *model.Application) error {
   537  	return r.updateSingle(ctx, "", model, true)
   538  }
   539  
   540  func (r *pgRepository) genericUpsert(ctx context.Context, tenant string, model *model.Application, upserter repo.Upserter) (string, error) {
   541  	if model == nil {
   542  		return "", apperrors.NewInternalError("model can not be empty")
   543  	}
   544  
   545  	log.C(ctx).Debugf("Converting Application model with id %s to entity", model.ID)
   546  	appEnt, err := r.conv.ToEntity(model)
   547  	if err != nil {
   548  		return "", errors.Wrap(err, "while converting to Application entity")
   549  	}
   550  
   551  	log.C(ctx).Debugf("Upserting Application entity with id %s to db", model.ID)
   552  	return upserter.Upsert(ctx, resource.Application, tenant, appEnt)
   553  }
   554  
   555  func (r *pgRepository) updateSingle(ctx context.Context, tenant string, model *model.Application, isTechnical bool) error {
   556  	if model == nil {
   557  		return apperrors.NewInternalError("model can not be empty")
   558  	}
   559  
   560  	log.C(ctx).Debugf("Converting Application model with id %s to entity", model.ID)
   561  	appEnt, err := r.conv.ToEntity(model)
   562  	if err != nil {
   563  		return errors.Wrap(err, "while converting to Application entity")
   564  	}
   565  
   566  	log.C(ctx).Debugf("Persisting updated Application entity with id %s to db", model.ID)
   567  	if isTechnical {
   568  		return r.globalUpdater.TechnicalUpdate(ctx, appEnt)
   569  	}
   570  	return r.updater.UpdateSingle(ctx, resource.Application, tenant, appEnt)
   571  }
   572  
   573  func (r *pgRepository) multipleFromEntities(entities EntityCollection) ([]*model.Application, error) {
   574  	items := make([]*model.Application, 0, len(entities))
   575  	for _, ent := range entities {
   576  		m := r.conv.FromEntity(&ent)
   577  		items = append(items, m)
   578  	}
   579  	return items, nil
   580  }