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

     1  package api
     2  
     3  import (
     4  	"context"
     5  
     6  	"github.com/kyma-incubator/compass/components/director/internal/domain/bundlereferences"
     7  	"github.com/kyma-incubator/compass/components/director/pkg/pagination"
     8  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
     9  
    10  	"github.com/kyma-incubator/compass/components/director/internal/model"
    11  	"github.com/kyma-incubator/compass/components/director/internal/repo"
    12  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  const apiDefTable string = `"public"."api_definitions"`
    17  
    18  var (
    19  	bundleColumn  = "bundle_id"
    20  	idColumn      = "id"
    21  	apiDefColumns = []string{"id", "app_id", "app_template_version_id", "package_id", "name", "description", "group_name", "ord_id", "local_tenant_id",
    22  		"short_description", "system_instance_aware", "policy_level", "custom_policy_level", "api_protocol", "tags", "countries", "links", "api_resource_links", "release_status",
    23  		"sunset_date", "changelog_entries", "labels", "visibility", "disabled", "part_of_products", "line_of_business",
    24  		"industry", "version_value", "version_deprecated", "version_deprecated_since", "version_for_removal", "ready", "created_at", "updated_at", "deleted_at", "error", "implementation_standard", "custom_implementation_standard", "custom_implementation_standard_description", "target_urls", "extensible", "successors", "resource_hash", "hierarchy", "supported_use_cases", "documentation_labels"}
    25  	idColumns        = []string{"id"}
    26  	updatableColumns = []string{"package_id", "name", "description", "group_name", "ord_id", "local_tenant_id",
    27  		"short_description", "system_instance_aware", "policy_level", "custom_policy_level", "api_protocol", "tags", "countries", "links", "api_resource_links", "release_status",
    28  		"sunset_date", "changelog_entries", "labels", "visibility", "disabled", "part_of_products", "line_of_business",
    29  		"industry", "version_value", "version_deprecated", "version_deprecated_since", "version_for_removal", "ready", "created_at", "updated_at", "deleted_at", "error", "implementation_standard", "custom_implementation_standard", "custom_implementation_standard_description", "target_urls", "extensible", "successors", "resource_hash", "hierarchy", "supported_use_cases", "documentation_labels"}
    30  )
    31  
    32  // APIDefinitionConverter converts APIDefinitions between the model.APIDefinition service-layer representation and the repo-layer representation Entity.
    33  //
    34  //go:generate mockery --name=APIDefinitionConverter --output=automock --outpkg=automock --case=underscore --disable-version-string
    35  type APIDefinitionConverter interface {
    36  	FromEntity(entity *Entity) *model.APIDefinition
    37  	ToEntity(apiModel *model.APIDefinition) *Entity
    38  }
    39  
    40  type pgRepository struct {
    41  	creator               repo.Creator
    42  	creatorGlobal         repo.CreatorGlobal
    43  	singleGetter          repo.SingleGetter
    44  	singleGetterGlobal    repo.SingleGetterGlobal
    45  	pageableQuerier       repo.PageableQuerier
    46  	bundleRefQueryBuilder repo.QueryBuilderGlobal
    47  	lister                repo.Lister
    48  	listerGlobal          repo.ListerGlobal
    49  	updater               repo.Updater
    50  	updaterGlobal         repo.UpdaterGlobal
    51  	deleter               repo.Deleter
    52  	deleterGlobal         repo.DeleterGlobal
    53  	existQuerier          repo.ExistQuerier
    54  	conv                  APIDefinitionConverter
    55  }
    56  
    57  // NewRepository returns a new entity responsible for repo-layer APIDefinitions operations.
    58  func NewRepository(conv APIDefinitionConverter) *pgRepository {
    59  	return &pgRepository{
    60  		singleGetter:          repo.NewSingleGetter(apiDefTable, apiDefColumns),
    61  		singleGetterGlobal:    repo.NewSingleGetterGlobal(resource.API, apiDefTable, apiDefColumns),
    62  		pageableQuerier:       repo.NewPageableQuerier(apiDefTable, apiDefColumns),
    63  		bundleRefQueryBuilder: repo.NewQueryBuilderGlobal(resource.BundleReference, bundlereferences.BundleReferenceTable, []string{bundlereferences.APIDefIDColumn}),
    64  		lister:                repo.NewLister(apiDefTable, apiDefColumns),
    65  		listerGlobal:          repo.NewListerGlobal(resource.API, apiDefTable, apiDefColumns),
    66  		creator:               repo.NewCreator(apiDefTable, apiDefColumns),
    67  		creatorGlobal:         repo.NewCreatorGlobal(resource.API, apiDefTable, apiDefColumns),
    68  		updater:               repo.NewUpdater(apiDefTable, updatableColumns, idColumns),
    69  		updaterGlobal:         repo.NewUpdaterGlobal(resource.API, apiDefTable, updatableColumns, idColumns),
    70  		deleter:               repo.NewDeleter(apiDefTable),
    71  		deleterGlobal:         repo.NewDeleterGlobal(resource.API, apiDefTable),
    72  		existQuerier:          repo.NewExistQuerier(apiDefTable),
    73  		conv:                  conv,
    74  	}
    75  }
    76  
    77  // APIDefCollection is an array of Entities
    78  type APIDefCollection []Entity
    79  
    80  // Len returns the length of the collection
    81  func (r APIDefCollection) Len() int {
    82  	return len(r)
    83  }
    84  
    85  // ListByBundleIDs retrieves all APIDefinitions for a Bundle in pages. Each Bundle is extracted from the input array of bundleIDs. The input bundleReferences array is used for getting the appropriate APIDefinition IDs.
    86  func (r *pgRepository) ListByBundleIDs(ctx context.Context, tenantID string, bundleIDs []string, bundleRefs []*model.BundleReference, totalCounts map[string]int, pageSize int, cursor string) ([]*model.APIDefinitionPage, error) {
    87  	apiDefIDs := make([]string, 0, len(bundleRefs))
    88  	for _, ref := range bundleRefs {
    89  		apiDefIDs = append(apiDefIDs, *ref.ObjectID)
    90  	}
    91  
    92  	var conditions repo.Conditions
    93  	if len(apiDefIDs) > 0 {
    94  		conditions = repo.Conditions{
    95  			repo.NewInConditionForStringValues("id", apiDefIDs),
    96  		}
    97  	}
    98  
    99  	var apiDefCollection APIDefCollection
   100  	err := r.lister.List(ctx, resource.API, tenantID, &apiDefCollection, conditions...)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	refsByBundleID, apiDefsByAPIDefID := r.groupEntitiesByID(bundleRefs, apiDefCollection)
   106  
   107  	offset, err := pagination.DecodeOffsetCursor(cursor)
   108  	if err != nil {
   109  		return nil, errors.Wrap(err, "while decoding page cursor")
   110  	}
   111  
   112  	apiDefPages := make([]*model.APIDefinitionPage, 0, len(bundleIDs))
   113  	for _, bundleID := range bundleIDs {
   114  		ids := getAPIDefIDsForBundle(refsByBundleID[bundleID])
   115  		apiDefs := getAPIDefsForBundle(ids, apiDefsByAPIDefID)
   116  
   117  		hasNextPage := false
   118  		endCursor := ""
   119  		if totalCounts[bundleID] > offset+len(apiDefs) {
   120  			hasNextPage = true
   121  			endCursor = pagination.EncodeNextOffsetCursor(offset, pageSize)
   122  		}
   123  
   124  		page := &pagination.Page{
   125  			StartCursor: cursor,
   126  			EndCursor:   endCursor,
   127  			HasNextPage: hasNextPage,
   128  		}
   129  
   130  		apiDefPages = append(apiDefPages, &model.APIDefinitionPage{Data: apiDefs, TotalCount: totalCounts[bundleID], PageInfo: page})
   131  	}
   132  
   133  	return apiDefPages, nil
   134  }
   135  
   136  // ListByResourceID lists all APIDefinitions for a given resource ID and resource type.
   137  func (r *pgRepository) ListByResourceID(ctx context.Context, tenantID string, resourceType resource.Type, resourceID string) ([]*model.APIDefinition, error) {
   138  	apiCollection := APIDefCollection{}
   139  
   140  	var condition repo.Condition
   141  	var err error
   142  	if resourceType == resource.Application {
   143  		condition = repo.NewEqualCondition("app_id", resourceID)
   144  		err = r.lister.ListWithSelectForUpdate(ctx, resource.API, tenantID, &apiCollection, condition)
   145  	} else {
   146  		condition = repo.NewEqualCondition("app_template_version_id", resourceID)
   147  		err = r.listerGlobal.ListGlobalWithSelectForUpdate(ctx, &apiCollection, condition)
   148  	}
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	apis := make([]*model.APIDefinition, 0, apiCollection.Len())
   154  	for _, api := range apiCollection {
   155  		apiModel := r.conv.FromEntity(&api)
   156  		apis = append(apis, apiModel)
   157  	}
   158  
   159  	return apis, nil
   160  }
   161  
   162  // ListByApplicationIDPage lists all APIDefinitions for a given application ID with paging.
   163  func (r *pgRepository) ListByApplicationIDPage(ctx context.Context, tenantID string, appID string, pageSize int, cursor string) (*model.APIDefinitionPage, error) {
   164  	var apiDefCollection APIDefCollection
   165  	page, totalCount, err := r.pageableQuerier.List(ctx, resource.API, tenantID, pageSize, cursor, idColumn, &apiDefCollection, repo.NewEqualCondition("app_id", appID))
   166  	if err != nil {
   167  		return nil, errors.Wrap(err, "while decoding page cursor")
   168  	}
   169  
   170  	items := make([]*model.APIDefinition, 0, len(apiDefCollection))
   171  	for _, api := range apiDefCollection {
   172  		m := r.conv.FromEntity(&api)
   173  		items = append(items, m)
   174  	}
   175  
   176  	return &model.APIDefinitionPage{
   177  		Data:       items,
   178  		TotalCount: totalCount,
   179  		PageInfo:   page,
   180  	}, nil
   181  }
   182  
   183  // GetByID retrieves the APIDefinition with matching ID from the Compass storage.
   184  func (r *pgRepository) GetByID(ctx context.Context, tenantID string, id string) (*model.APIDefinition, error) {
   185  	var apiDefEntity Entity
   186  	err := r.singleGetter.Get(ctx, resource.API, tenantID, repo.Conditions{repo.NewEqualCondition("id", id)}, repo.NoOrderBy, &apiDefEntity)
   187  	if err != nil {
   188  		return nil, errors.Wrap(err, "while getting APIDefinition")
   189  	}
   190  
   191  	apiDefModel := r.conv.FromEntity(&apiDefEntity)
   192  
   193  	return apiDefModel, nil
   194  }
   195  
   196  // GetByIDGlobal gets an APIDefinition by ID without tenant isolation
   197  func (r *pgRepository) GetByIDGlobal(ctx context.Context, id string) (*model.APIDefinition, error) {
   198  	var apiDefEntity Entity
   199  	err := r.singleGetterGlobal.GetGlobal(ctx, repo.Conditions{repo.NewEqualCondition("id", id)}, repo.NoOrderBy, &apiDefEntity)
   200  	if err != nil {
   201  		return nil, errors.Wrap(err, "while getting APIDefinition")
   202  	}
   203  
   204  	apiDefModel := r.conv.FromEntity(&apiDefEntity)
   205  
   206  	return apiDefModel, nil
   207  }
   208  
   209  // GetForBundle gets an APIDefinition by its id.
   210  // the bundleID remains for backwards compatibility above in the layers; we are sure that the correct API will be fetched because there can't be two records with the same ID
   211  func (r *pgRepository) GetForBundle(ctx context.Context, tenant string, id string, bundleID string) (*model.APIDefinition, error) {
   212  	return r.GetByID(ctx, tenant, id)
   213  }
   214  
   215  // Create creates an APIDefinition.
   216  func (r *pgRepository) Create(ctx context.Context, tenant string, item *model.APIDefinition) error {
   217  	if item == nil {
   218  		return apperrors.NewInternalError("item cannot be nil")
   219  	}
   220  
   221  	entity := r.conv.ToEntity(item)
   222  	err := r.creator.Create(ctx, resource.API, tenant, entity)
   223  	if err != nil {
   224  		return errors.Wrap(err, "while saving entity to db")
   225  	}
   226  
   227  	return nil
   228  }
   229  
   230  // CreateGlobal creates an APIDefinition.
   231  func (r *pgRepository) CreateGlobal(ctx context.Context, item *model.APIDefinition) error {
   232  	if item == nil {
   233  		return apperrors.NewInternalError("item cannot be nil")
   234  	}
   235  
   236  	entity := r.conv.ToEntity(item)
   237  	err := r.creatorGlobal.Create(ctx, entity)
   238  	if err != nil {
   239  		return errors.Wrap(err, "while saving entity to db")
   240  	}
   241  
   242  	return nil
   243  }
   244  
   245  // CreateMany creates many APIDefinitions.
   246  func (r *pgRepository) CreateMany(ctx context.Context, tenant string, items []*model.APIDefinition) error {
   247  	for index, item := range items {
   248  		entity := r.conv.ToEntity(item)
   249  
   250  		err := r.creator.Create(ctx, resource.API, tenant, entity)
   251  		if err != nil {
   252  			return errors.Wrapf(err, "while persisting %d item", index)
   253  		}
   254  	}
   255  
   256  	return nil
   257  }
   258  
   259  // Update updates an APIDefinition.
   260  func (r *pgRepository) Update(ctx context.Context, tenant string, item *model.APIDefinition) error {
   261  	if item == nil {
   262  		return apperrors.NewInternalError("item cannot be nil")
   263  	}
   264  
   265  	entity := r.conv.ToEntity(item)
   266  
   267  	return r.updater.UpdateSingle(ctx, resource.API, tenant, entity)
   268  }
   269  
   270  // UpdateGlobal updates an existing APIDefinition without tenant isolation.
   271  func (r *pgRepository) UpdateGlobal(ctx context.Context, item *model.APIDefinition) error {
   272  	if item == nil {
   273  		return apperrors.NewInternalError("item cannot be nil")
   274  	}
   275  
   276  	entity := r.conv.ToEntity(item)
   277  
   278  	return r.updaterGlobal.UpdateSingleGlobal(ctx, entity)
   279  }
   280  
   281  // Exists checks if an APIDefinition with a given ID exists.
   282  func (r *pgRepository) Exists(ctx context.Context, tenantID, id string) (bool, error) {
   283  	return r.existQuerier.Exists(ctx, resource.API, tenantID, repo.Conditions{repo.NewEqualCondition("id", id)})
   284  }
   285  
   286  // Delete deletes an APIDefinition by its ID.
   287  func (r *pgRepository) Delete(ctx context.Context, tenantID string, id string) error {
   288  	return r.deleter.DeleteOne(ctx, resource.API, tenantID, repo.Conditions{repo.NewEqualCondition("id", id)})
   289  }
   290  
   291  // DeleteGlobal deletes an APIDefinition by its ID without tenant isolation.
   292  func (r *pgRepository) DeleteGlobal(ctx context.Context, id string) error {
   293  	return r.deleterGlobal.DeleteOneGlobal(ctx, repo.Conditions{repo.NewEqualCondition("id", id)})
   294  }
   295  
   296  // DeleteAllByBundleID deletes all APIDefinitions for a given bundle ID.
   297  func (r *pgRepository) DeleteAllByBundleID(ctx context.Context, tenantID, bundleID string) error {
   298  	subqueryConditions := repo.Conditions{
   299  		repo.NewEqualCondition(bundleColumn, bundleID),
   300  		repo.NewNotNullCondition(bundlereferences.APIDefIDColumn),
   301  	}
   302  	subquery, args, err := r.bundleRefQueryBuilder.BuildQueryGlobal(false, subqueryConditions...)
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	inOperatorConditions := repo.Conditions{
   308  		repo.NewInConditionForSubQuery(idColumn, subquery, args),
   309  	}
   310  
   311  	return r.deleter.DeleteMany(ctx, resource.API, tenantID, inOperatorConditions)
   312  }
   313  
   314  func getAPIDefIDsForBundle(refs []*model.BundleReference) []string {
   315  	result := make([]string, 0, len(refs))
   316  	for _, ref := range refs {
   317  		result = append(result, *ref.ObjectID)
   318  	}
   319  	return result
   320  }
   321  
   322  func getAPIDefsForBundle(ids []string, defs map[string]*model.APIDefinition) []*model.APIDefinition {
   323  	result := make([]*model.APIDefinition, 0, len(ids))
   324  	if len(defs) > 0 {
   325  		for _, id := range ids {
   326  			result = append(result, defs[id])
   327  		}
   328  	}
   329  	return result
   330  }
   331  
   332  func (r *pgRepository) groupEntitiesByID(bundleRefs []*model.BundleReference, apiDefCollection APIDefCollection) (map[string][]*model.BundleReference, map[string]*model.APIDefinition) {
   333  	refsByBundleID := map[string][]*model.BundleReference{}
   334  	for _, ref := range bundleRefs {
   335  		refsByBundleID[*ref.BundleID] = append(refsByBundleID[*ref.BundleID], ref)
   336  	}
   337  
   338  	apiDefsByAPIDefID := map[string]*model.APIDefinition{}
   339  	for _, apiDefEnt := range apiDefCollection {
   340  		m := r.conv.FromEntity(&apiDefEnt)
   341  		apiDefsByAPIDefID[apiDefEnt.ID] = m
   342  	}
   343  
   344  	return refsByBundleID, apiDefsByAPIDefID
   345  }