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

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  
     7  	"github.com/kyma-incubator/compass/components/director/pkg/log"
     8  
     9  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    10  
    11  	ord "github.com/kyma-incubator/compass/components/director/internal/open_resource_discovery"
    12  	"github.com/kyma-incubator/compass/components/director/pkg/str"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    15  
    16  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    17  	"github.com/kyma-incubator/compass/components/director/internal/model"
    18  	"github.com/kyma-incubator/compass/components/director/internal/timestamp"
    19  	"github.com/pkg/errors"
    20  )
    21  
    22  // APIRepository is responsible for the repo-layer APIDefinition operations.
    23  //
    24  //go:generate mockery --name=APIRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    25  type APIRepository interface {
    26  	GetByID(ctx context.Context, tenantID, id string) (*model.APIDefinition, error)
    27  	GetByIDGlobal(ctx context.Context, id string) (*model.APIDefinition, error)
    28  	GetForBundle(ctx context.Context, tenant string, id string, bundleID string) (*model.APIDefinition, error)
    29  	Exists(ctx context.Context, tenant, id string) (bool, error)
    30  	ListByBundleIDs(ctx context.Context, tenantID string, bundleIDs []string, bundleRefs []*model.BundleReference, counts map[string]int, pageSize int, cursor string) ([]*model.APIDefinitionPage, error)
    31  	ListByApplicationIDPage(ctx context.Context, tenantID string, appID string, pageSize int, cursor string) (*model.APIDefinitionPage, error)
    32  	ListByResourceID(ctx context.Context, tenantID string, resourceType resource.Type, resourceID string) ([]*model.APIDefinition, error)
    33  	CreateMany(ctx context.Context, tenant string, item []*model.APIDefinition) error
    34  	Create(ctx context.Context, tenant string, item *model.APIDefinition) error
    35  	CreateGlobal(ctx context.Context, item *model.APIDefinition) error
    36  	Update(ctx context.Context, tenant string, item *model.APIDefinition) error
    37  	UpdateGlobal(ctx context.Context, item *model.APIDefinition) error
    38  	Delete(ctx context.Context, tenantID string, id string) error
    39  	DeleteAllByBundleID(ctx context.Context, tenantID, bundleID string) error
    40  	DeleteGlobal(ctx context.Context, id string) error
    41  }
    42  
    43  // UIDService is responsible for generating GUIDs, which will be used as internal apiDefinition IDs when they are created.
    44  //
    45  //go:generate mockery --name=UIDService --output=automock --outpkg=automock --case=underscore --disable-version-string
    46  type UIDService interface {
    47  	Generate() string
    48  }
    49  
    50  // SpecService is responsible for the service-layer Specification operations.
    51  //
    52  //go:generate mockery --name=SpecService --output=automock --outpkg=automock --case=underscore --disable-version-string
    53  type SpecService interface {
    54  	CreateByReferenceObjectID(ctx context.Context, in model.SpecInput, resourceType resource.Type, objectType model.SpecReferenceObjectType, objectID string) (string, error)
    55  	UpdateByReferenceObjectID(ctx context.Context, id string, in model.SpecInput, resourceType resource.Type, objectType model.SpecReferenceObjectType, objectID string) error
    56  	GetByReferenceObjectID(ctx context.Context, resourceType resource.Type, objectType model.SpecReferenceObjectType, objectID string) (*model.Spec, error)
    57  	RefetchSpec(ctx context.Context, id string, objectType model.SpecReferenceObjectType) (*model.Spec, error)
    58  	ListFetchRequestsByReferenceObjectIDs(ctx context.Context, tenant string, objectIDs []string, objectType model.SpecReferenceObjectType) ([]*model.FetchRequest, error)
    59  }
    60  
    61  // BundleReferenceService is responsible for the service-layer BundleReference operations.
    62  //
    63  //go:generate mockery --name=BundleReferenceService --output=automock --outpkg=automock --case=underscore --disable-version-string
    64  type BundleReferenceService interface {
    65  	GetForBundle(ctx context.Context, objectType model.BundleReferenceObjectType, objectID, bundleID *string) (*model.BundleReference, error)
    66  	CreateByReferenceObjectID(ctx context.Context, in model.BundleReferenceInput, objectType model.BundleReferenceObjectType, objectID, bundleID *string) error
    67  	UpdateByReferenceObjectID(ctx context.Context, in model.BundleReferenceInput, objectType model.BundleReferenceObjectType, objectID, bundleID *string) error
    68  	DeleteByReferenceObjectID(ctx context.Context, objectType model.BundleReferenceObjectType, objectID, bundleID *string) error
    69  	ListByBundleIDs(ctx context.Context, objectType model.BundleReferenceObjectType, bundleIDs []string, pageSize int, cursor string) ([]*model.BundleReference, map[string]int, error)
    70  }
    71  
    72  type service struct {
    73  	repo                   APIRepository
    74  	uidService             UIDService
    75  	specService            SpecService
    76  	bundleReferenceService BundleReferenceService
    77  	timestampGen           timestamp.Generator
    78  }
    79  
    80  // NewService returns a new object responsible for service-layer APIDefinition operations.
    81  func NewService(repo APIRepository, uidService UIDService, specService SpecService, bundleReferenceService BundleReferenceService) *service {
    82  	return &service{
    83  		repo:                   repo,
    84  		uidService:             uidService,
    85  		specService:            specService,
    86  		bundleReferenceService: bundleReferenceService,
    87  		timestampGen:           timestamp.DefaultGenerator,
    88  	}
    89  }
    90  
    91  // ListByBundleIDs lists all APIDefinitions in pages for a given array of bundle IDs.
    92  func (s *service) ListByBundleIDs(ctx context.Context, bundleIDs []string, pageSize int, cursor string) ([]*model.APIDefinitionPage, error) {
    93  	tnt, err := tenant.LoadFromContext(ctx)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  
    98  	if pageSize < 1 || pageSize > 200 {
    99  		return nil, apperrors.NewInvalidDataError("page size must be between 1 and 200")
   100  	}
   101  
   102  	bundleRefs, counts, err := s.bundleReferenceService.ListByBundleIDs(ctx, model.BundleAPIReference, bundleIDs, pageSize, cursor)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  
   107  	return s.repo.ListByBundleIDs(ctx, tnt, bundleIDs, bundleRefs, counts, pageSize, cursor)
   108  }
   109  
   110  // ListByApplicationID lists all APIDefinitions for a given application ID.
   111  func (s *service) ListByApplicationID(ctx context.Context, appID string) ([]*model.APIDefinition, error) {
   112  	tnt, err := tenant.LoadFromContext(ctx)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	return s.repo.ListByResourceID(ctx, tnt, resource.Application, appID)
   118  }
   119  
   120  // ListByApplicationTemplateVersionID lists all APIDefinitions for a given application template version ID.
   121  func (s *service) ListByApplicationTemplateVersionID(ctx context.Context, appTemplateVersionID string) ([]*model.APIDefinition, error) {
   122  	return s.repo.ListByResourceID(ctx, "", resource.ApplicationTemplateVersion, appTemplateVersionID)
   123  }
   124  
   125  // ListByApplicationIDPage lists all APIDefinitions for a given application ID with paging.
   126  func (s *service) ListByApplicationIDPage(ctx context.Context, appID string, pageSize int, cursor string) (*model.APIDefinitionPage, error) {
   127  	tnt, err := tenant.LoadFromContext(ctx)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	if pageSize < 1 || pageSize > 200 {
   133  		return nil, apperrors.NewInvalidDataError("page size must be between 1 and 200")
   134  	}
   135  
   136  	return s.repo.ListByApplicationIDPage(ctx, tnt, appID, pageSize, cursor)
   137  }
   138  
   139  // Get returns the APIDefinition by its ID.
   140  func (s *service) Get(ctx context.Context, id string) (*model.APIDefinition, error) {
   141  	tnt, err := tenant.LoadFromContext(ctx)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	api, err := s.repo.GetByID(ctx, tnt, id)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	return api, nil
   152  }
   153  
   154  // GetForBundle returns an APIDefinition by its ID and a bundle ID.
   155  func (s *service) GetForBundle(ctx context.Context, id string, bundleID string) (*model.APIDefinition, error) {
   156  	tnt, err := tenant.LoadFromContext(ctx)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  
   161  	apiDefinition, err := s.repo.GetForBundle(ctx, tnt, id, bundleID)
   162  	if err != nil {
   163  		return nil, errors.Wrapf(err, "while getting API definition with id %q", id)
   164  	}
   165  
   166  	return apiDefinition, nil
   167  }
   168  
   169  // CreateInBundle creates an APIDefinition. This function is used in the graphQL flow.
   170  func (s *service) CreateInBundle(ctx context.Context, resourceType resource.Type, resourceID string, bundleID string, in model.APIDefinitionInput, spec *model.SpecInput) (string, error) {
   171  	return s.Create(ctx, resourceType, resourceID, &bundleID, nil, in, []*model.SpecInput{spec}, nil, 0, "")
   172  }
   173  
   174  // CreateInApplication creates an APIDefinition in the context of an Application without Bundle
   175  func (s *service) CreateInApplication(ctx context.Context, appID string, in model.APIDefinitionInput, spec *model.SpecInput) (string, error) {
   176  	return s.Create(ctx, resource.Application, appID, nil, nil, in, []*model.SpecInput{spec}, nil, 0, "")
   177  }
   178  
   179  // Create creates APIDefinition/s. This function is used both in the ORD scenario and is re-used in CreateInBundle but with "null" ORD specific arguments.
   180  func (s *service) Create(ctx context.Context, resourceType resource.Type, resourceID string, bundleID, packageID *string, in model.APIDefinitionInput, specs []*model.SpecInput, defaultTargetURLPerBundle map[string]string, apiHash uint64, defaultBundleID string) (string, error) {
   181  	id := s.uidService.Generate()
   182  	api := in.ToAPIDefinition(id, resourceType, resourceID, packageID, apiHash)
   183  
   184  	enrichAPIProtocol(api, specs)
   185  
   186  	if err := s.createAPI(ctx, api, resourceType); err != nil {
   187  		return "", errors.Wrap(err, "while creating api")
   188  	}
   189  
   190  	if err := s.processSpecs(ctx, api.ID, specs, resourceType); err != nil {
   191  		return "", errors.Wrap(err, "while processing specs")
   192  	}
   193  
   194  	if err := s.createBundleReferenceObject(ctx, api.ID, bundleID, defaultBundleID, api.TargetURLs, defaultTargetURLPerBundle); err != nil {
   195  		return "", errors.Wrap(err, "while creating bundle reference object")
   196  	}
   197  
   198  	return id, nil
   199  }
   200  
   201  // Update updates an APIDefinition. This function is used in the graphQL flow.
   202  func (s *service) Update(ctx context.Context, resourceType resource.Type, id string, in model.APIDefinitionInput, specIn *model.SpecInput) error {
   203  	return s.UpdateInManyBundles(ctx, resourceType, id, in, specIn, nil, nil, nil, 0, "")
   204  }
   205  
   206  // UpdateInManyBundles updates APIDefinition/s. This function is used both in the ORD scenario and is re-used in Update but with "null" ORD specific arguments.
   207  func (s *service) UpdateInManyBundles(ctx context.Context, resourceType resource.Type, id string, in model.APIDefinitionInput, specIn *model.SpecInput, defaultTargetURLPerBundleForUpdate map[string]string, defaultTargetURLPerBundleForCreation map[string]string, bundleIDsForDeletion []string, apiHash uint64, defaultBundleID string) error {
   208  	api, err := s.getAPI(ctx, id, resourceType)
   209  	if err != nil {
   210  		return errors.Wrapf(err, "while getting API with ID %s for %s", id, resourceType)
   211  	}
   212  
   213  	resourceID := getParentResourceID(api)
   214  	api = in.ToAPIDefinition(id, resourceType, resourceID, api.PackageID, apiHash)
   215  
   216  	err = s.updateAPI(ctx, api, resourceType)
   217  	if err != nil {
   218  		return errors.Wrapf(err, "while updating API with ID %s for %s", id, resourceType)
   219  	}
   220  
   221  	if err = s.updateReferences(ctx, api, in.TargetURLs, defaultTargetURLPerBundleForUpdate, defaultBundleID); err != nil {
   222  		return err
   223  	}
   224  
   225  	if err = s.createBundleReferences(ctx, api, defaultTargetURLPerBundleForCreation, defaultBundleID); err != nil {
   226  		return err
   227  	}
   228  
   229  	if err = s.deleteBundleIDs(ctx, &api.ID, bundleIDsForDeletion); err != nil {
   230  		return err
   231  	}
   232  
   233  	if specIn != nil {
   234  		return s.handleSpecsInAPI(ctx, api.ID, specIn, resourceType)
   235  	}
   236  
   237  	return nil
   238  }
   239  
   240  // UpdateForApplication updates an APIDefinition for Application without being in a Bundle
   241  func (s *service) UpdateForApplication(ctx context.Context, id string, in model.APIDefinitionInput, specIn *model.SpecInput) error {
   242  	tnt, err := tenant.LoadFromContext(ctx)
   243  	if err != nil {
   244  		return err
   245  	}
   246  
   247  	api, err := s.Get(ctx, id)
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	api = in.ToAPIDefinition(id, resource.Application, str.PtrStrToStr(api.ApplicationID), api.PackageID, 0)
   253  
   254  	if err = s.repo.Update(ctx, tnt, api); err != nil {
   255  		return errors.Wrapf(err, "while updating APIDefinition with id %s", id)
   256  	}
   257  
   258  	if specIn != nil {
   259  		return s.handleSpecsInAPI(ctx, id, specIn, resource.Application)
   260  	}
   261  
   262  	return nil
   263  }
   264  
   265  // Delete deletes the APIDefinition by its ID.
   266  func (s *service) Delete(ctx context.Context, resourceType resource.Type, id string) error {
   267  	if err := s.deleteAPI(ctx, id, resourceType); err != nil {
   268  		return errors.Wrapf(err, "while deleting APIDefinition with id %s", id)
   269  	}
   270  
   271  	log.C(ctx).Infof("Successfully deleted APIDefinition with id %s", id)
   272  
   273  	return nil
   274  }
   275  
   276  // DeleteAllByBundleID deletes all APIDefinitions for a given bundle ID
   277  func (s *service) DeleteAllByBundleID(ctx context.Context, bundleID string) error {
   278  	tnt, err := tenant.LoadFromContext(ctx)
   279  	if err != nil {
   280  		return err
   281  	}
   282  
   283  	err = s.repo.DeleteAllByBundleID(ctx, tnt, bundleID)
   284  	if err != nil {
   285  		return errors.Wrapf(err, "while deleting APIDefinitions for Bundle with id %q", bundleID)
   286  	}
   287  
   288  	return nil
   289  }
   290  
   291  // ListFetchRequests lists all FetchRequests for given specification IDs
   292  func (s *service) ListFetchRequests(ctx context.Context, specIDs []string) ([]*model.FetchRequest, error) {
   293  	tnt, err := tenant.LoadFromContext(ctx)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	fetchRequests, err := s.specService.ListFetchRequestsByReferenceObjectIDs(ctx, tnt, specIDs, model.APISpecReference)
   299  	if err != nil {
   300  		if apperrors.IsNotFoundError(err) {
   301  			return nil, nil
   302  		}
   303  		return nil, err
   304  	}
   305  
   306  	return fetchRequests, nil
   307  }
   308  
   309  func (s *service) updateBundleReferences(ctx context.Context, api *model.APIDefinition, defaultTargetURLPerBundleForUpdate map[string]string, defaultBundleID string) error {
   310  	for crrBndlID, defaultTargetURL := range defaultTargetURLPerBundleForUpdate {
   311  		bundleRefInput := &model.BundleReferenceInput{
   312  			APIDefaultTargetURL: &defaultTargetURL,
   313  		}
   314  		if defaultBundleID != "" && defaultBundleID == crrBndlID {
   315  			isDefaultBundle := true
   316  			bundleRefInput.IsDefaultBundle = &isDefaultBundle
   317  		}
   318  
   319  		err := s.bundleReferenceService.UpdateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &api.ID, &crrBndlID)
   320  		if err != nil {
   321  			return err
   322  		}
   323  	}
   324  	return nil
   325  }
   326  
   327  func (s *service) createBundleReferences(ctx context.Context, api *model.APIDefinition, defaultTargetURLPerBundleForCreation map[string]string, defaultBundleID string) error {
   328  	for crrBndlID, defaultTargetURL := range defaultTargetURLPerBundleForCreation {
   329  		bundleRefInput := &model.BundleReferenceInput{
   330  			APIDefaultTargetURL: &defaultTargetURL,
   331  		}
   332  		if defaultBundleID != "" && crrBndlID == defaultBundleID {
   333  			isDefaultBundle := true
   334  			bundleRefInput.IsDefaultBundle = &isDefaultBundle
   335  		}
   336  
   337  		err := s.bundleReferenceService.CreateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &api.ID, &crrBndlID)
   338  		if err != nil {
   339  			return err
   340  		}
   341  	}
   342  	return nil
   343  }
   344  
   345  func (s *service) deleteBundleIDs(ctx context.Context, apiID *string, bundleIDsForDeletion []string) error {
   346  	for _, bundleID := range bundleIDsForDeletion {
   347  		err := s.bundleReferenceService.DeleteByReferenceObjectID(ctx, model.BundleAPIReference, apiID, &bundleID)
   348  		if err != nil {
   349  			return err
   350  		}
   351  	}
   352  	return nil
   353  }
   354  
   355  func (s *service) handleSpecsInAPI(ctx context.Context, id string, specIn *model.SpecInput, resourceType resource.Type) error {
   356  	dbSpec, err := s.specService.GetByReferenceObjectID(ctx, resourceType, model.APISpecReference, id)
   357  	if err != nil {
   358  		return errors.Wrapf(err, "while getting spec for APIDefinition with id %q", id)
   359  	}
   360  
   361  	if dbSpec == nil {
   362  		_, err = s.specService.CreateByReferenceObjectID(ctx, *specIn, resourceType, model.APISpecReference, id)
   363  		return err
   364  	}
   365  
   366  	return s.specService.UpdateByReferenceObjectID(ctx, dbSpec.ID, *specIn, resourceType, model.APISpecReference, id)
   367  }
   368  
   369  func (s *service) updateReferences(ctx context.Context, api *model.APIDefinition, targetURLs json.RawMessage, defaultTargetURLPerBundleForUpdate map[string]string, defaultBundleID string) error {
   370  	// when defaultTargetURLPerBundle == nil we are in the graphQL flow
   371  	if defaultTargetURLPerBundleForUpdate == nil {
   372  		bundleRefInput := &model.BundleReferenceInput{
   373  			APIDefaultTargetURL: str.Ptr(ExtractTargetURLFromJSONArray(targetURLs)),
   374  		}
   375  		return s.bundleReferenceService.UpdateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &api.ID, nil)
   376  	}
   377  
   378  	return s.updateBundleReferences(ctx, api, defaultTargetURLPerBundleForUpdate, defaultBundleID)
   379  }
   380  
   381  func (s *service) processSpecs(ctx context.Context, apiID string, specs []*model.SpecInput, resourceType resource.Type) error {
   382  	for _, spec := range specs {
   383  		if spec == nil {
   384  			continue
   385  		}
   386  
   387  		if _, err := s.specService.CreateByReferenceObjectID(ctx, *spec, resourceType, model.APISpecReference, apiID); err != nil {
   388  			return err
   389  		}
   390  	}
   391  
   392  	return nil
   393  }
   394  
   395  func (s *service) createBundleReferenceObject(ctx context.Context, apiID string, bundleID *string, defaultBundleID string, targetURLs json.RawMessage, defaultTargetURLPerBundle map[string]string) error {
   396  	// when defaultTargetURLPerBundle == nil we are in the graphQL flow
   397  	if defaultTargetURLPerBundle == nil && bundleID != nil {
   398  		bundleRefInput := &model.BundleReferenceInput{
   399  			APIDefaultTargetURL: str.Ptr(ExtractTargetURLFromJSONArray(targetURLs)),
   400  		}
   401  		return s.bundleReferenceService.CreateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &apiID, bundleID)
   402  	}
   403  
   404  	for crrBndlID, defaultTargetURL := range defaultTargetURLPerBundle {
   405  		bundleRefInput := &model.BundleReferenceInput{
   406  			APIDefaultTargetURL: &defaultTargetURL,
   407  		}
   408  		if defaultBundleID != "" && crrBndlID == defaultBundleID {
   409  			isDefaultBundle := true
   410  			bundleRefInput.IsDefaultBundle = &isDefaultBundle
   411  		}
   412  		if err := s.bundleReferenceService.CreateByReferenceObjectID(ctx, *bundleRefInput, model.BundleAPIReference, &apiID, &crrBndlID); err != nil {
   413  			return err
   414  		}
   415  	}
   416  
   417  	return nil
   418  }
   419  
   420  func (s *service) getAPI(ctx context.Context, id string, resourceType resource.Type) (*model.APIDefinition, error) {
   421  	if resourceType.IsTenantIgnorable() {
   422  		return s.repo.GetByIDGlobal(ctx, id)
   423  	}
   424  	return s.Get(ctx, id)
   425  }
   426  
   427  func (s *service) updateAPI(ctx context.Context, api *model.APIDefinition, resourceType resource.Type) error {
   428  	if resourceType.IsTenantIgnorable() {
   429  		return s.repo.UpdateGlobal(ctx, api)
   430  	}
   431  
   432  	tnt, err := tenant.LoadFromContext(ctx)
   433  	if err != nil {
   434  		return err
   435  	}
   436  	return s.repo.Update(ctx, tnt, api)
   437  }
   438  
   439  func (s *service) createAPI(ctx context.Context, api *model.APIDefinition, resourceType resource.Type) error {
   440  	if resourceType.IsTenantIgnorable() {
   441  		return s.repo.CreateGlobal(ctx, api)
   442  	}
   443  
   444  	tnt, err := tenant.LoadFromContext(ctx)
   445  	if err != nil {
   446  		return err
   447  	}
   448  
   449  	return s.repo.Create(ctx, tnt, api)
   450  }
   451  
   452  func (s *service) deleteAPI(ctx context.Context, apiID string, resourceType resource.Type) error {
   453  	if resourceType.IsTenantIgnorable() {
   454  		return s.repo.DeleteGlobal(ctx, apiID)
   455  	}
   456  
   457  	tnt, err := tenant.LoadFromContext(ctx)
   458  	if err != nil {
   459  		return err
   460  	}
   461  	return s.repo.Delete(ctx, tnt, apiID)
   462  }
   463  
   464  func getParentResourceID(api *model.APIDefinition) string {
   465  	if api.ApplicationTemplateVersionID != nil {
   466  		return *api.ApplicationTemplateVersionID
   467  	} else if api.ApplicationID != nil {
   468  		return *api.ApplicationID
   469  	}
   470  
   471  	return ""
   472  }
   473  
   474  func enrichAPIProtocol(api *model.APIDefinition, specs []*model.SpecInput) {
   475  	if len(specs) > 0 && specs[0] != nil && specs[0].APIType != nil {
   476  		switch *specs[0].APIType {
   477  		case model.APISpecTypeOdata:
   478  			protocol := ord.APIProtocolODataV2
   479  			api.APIProtocol = &protocol
   480  		case model.APISpecTypeOpenAPI:
   481  			protocol := ord.APIProtocolRest
   482  			api.APIProtocol = &protocol
   483  		}
   484  	}
   485  }