diff --git a/.tools/nvim/__http__/console/registry-image.graphql.yml b/.tools/nvim/__http__/console/registry-image.graphql.yml index f1822e63c..9361c47bd 100644 --- a/.tools/nvim/__http__/console/registry-image.graphql.yml +++ b/.tools/nvim/__http__/console/registry-image.graphql.yml @@ -1,6 +1,7 @@ --- global: image: "imageName1:imageTag1" + query: "gitla" --- label: List Registry Images @@ -70,3 +71,18 @@ variables: tag: "latest" --- + +label: Search Registry Images +query: |+ + query Core_searchRegistryImages($query: String!) { + core_searchRegistryImages(query: $query) { + accountName + imageName + imageTag + meta + } + } +variables: + query: "{{.query}}" +--- + diff --git a/apps/console/internal/app/graph/generated/generated.go b/apps/console/internal/app/graph/generated/generated.go index f67d3e540..7806f949c 100644 --- a/apps/console/internal/app/graph/generated/generated.go +++ b/apps/console/internal/app/graph/generated/generated.go @@ -830,6 +830,7 @@ type ComplexityRoot struct { CoreResyncManagedResource func(childComplexity int, msvcName string, name string) int CoreResyncRouter func(childComplexity int, envName string, name string) int CoreResyncSecret func(childComplexity int, envName string, name string) int + CoreSearchRegistryImages func(childComplexity int, query string) int InfraGetClusterManagedService func(childComplexity int, name string) int InfraListClusterManagedServices func(childComplexity int, search *model.SearchClusterManagedService, pagination *repos.CursorPagination) int __resolve__service func(childComplexity int) int @@ -1114,6 +1115,7 @@ type QueryResolver interface { CoreGetRegistryImageURL(ctx context.Context) (*model.RegistryImageURL, error) CoreGetRegistryImage(ctx context.Context, image string) (*entities.RegistryImage, error) CoreListRegistryImages(ctx context.Context, pq *repos.CursorPagination) (*model.RegistryImagePaginatedRecords, error) + CoreSearchRegistryImages(ctx context.Context, query string) ([]*entities.RegistryImage, error) CoreListApps(ctx context.Context, envName string, search *model.SearchApps, pq *repos.CursorPagination) (*model.AppPaginatedRecords, error) CoreGetApp(ctx context.Context, envName string, name string) (*entities.App, error) CoreResyncApp(ctx context.Context, envName string, name string) (bool, error) @@ -5003,6 +5005,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.CoreResyncSecret(childComplexity, args["envName"].(string), args["name"].(string)), true + case "Query.core_searchRegistryImages": + if e.complexity.Query.CoreSearchRegistryImages == nil { + break + } + + args, err := ec.field_Query_core_searchRegistryImages_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.CoreSearchRegistryImages(childComplexity, args["query"].(string)), true + case "Query.infra_getClusterManagedService": if e.complexity.Query.InfraGetClusterManagedService == nil { break @@ -5926,6 +5940,7 @@ type Query { core_getRegistryImageURL: RegistryImageURL! @isLoggedInAndVerified @hasAccount core_getRegistryImage(image: String!,): RegistryImage @isLoggedInAndVerified @hasAccount core_listRegistryImages(pq: CursorPaginationIn): RegistryImagePaginatedRecords @isLoggedInAndVerified @hasAccount + core_searchRegistryImages(query: String!): [RegistryImage!]! @isLoggedInAndVerified @hasAccount core_listApps(envName: String!, search: SearchApps, pq: CursorPaginationIn): AppPaginatedRecords @isLoggedInAndVerified @hasAccount core_getApp(envName: String!, name: String!): App @isLoggedInAndVerified @hasAccount @@ -9252,6 +9267,21 @@ func (ec *executionContext) field_Query_core_resyncSecret_args(ctx context.Conte return args, nil } +func (ec *executionContext) field_Query_core_searchRegistryImages_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["query"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("query")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["query"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_infra_getClusterManagedService_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -33114,6 +33144,107 @@ func (ec *executionContext) fieldContext_Query_core_listRegistryImages(ctx conte return fc, nil } +func (ec *executionContext) _Query_core_searchRegistryImages(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_core_searchRegistryImages(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + directive0 := func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().CoreSearchRegistryImages(rctx, fc.Args["query"].(string)) + } + directive1 := func(ctx context.Context) (interface{}, error) { + if ec.directives.IsLoggedInAndVerified == nil { + return nil, errors.New("directive isLoggedInAndVerified is not implemented") + } + return ec.directives.IsLoggedInAndVerified(ctx, nil, directive0) + } + directive2 := func(ctx context.Context) (interface{}, error) { + if ec.directives.HasAccount == nil { + return nil, errors.New("directive hasAccount is not implemented") + } + return ec.directives.HasAccount(ctx, nil, directive1) + } + + tmp, err := directive2(rctx) + if err != nil { + return nil, graphql.ErrorOnPath(ctx, err) + } + if tmp == nil { + return nil, nil + } + if data, ok := tmp.([]*entities.RegistryImage); ok { + return data, nil + } + return nil, fmt.Errorf(`unexpected type %T from directive, should be []*github.com/kloudlite/api/apps/console/internal/entities.RegistryImage`, tmp) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*entities.RegistryImage) + fc.Result = res + return ec.marshalNRegistryImage2ᚕᚖgithubᚗcomᚋkloudliteᚋapiᚋappsᚋconsoleᚋinternalᚋentitiesᚐRegistryImageᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_core_searchRegistryImages(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "accountName": + return ec.fieldContext_RegistryImage_accountName(ctx, field) + case "creationTime": + return ec.fieldContext_RegistryImage_creationTime(ctx, field) + case "id": + return ec.fieldContext_RegistryImage_id(ctx, field) + case "imageName": + return ec.fieldContext_RegistryImage_imageName(ctx, field) + case "imageTag": + return ec.fieldContext_RegistryImage_imageTag(ctx, field) + case "markedForDeletion": + return ec.fieldContext_RegistryImage_markedForDeletion(ctx, field) + case "meta": + return ec.fieldContext_RegistryImage_meta(ctx, field) + case "recordVersion": + return ec.fieldContext_RegistryImage_recordVersion(ctx, field) + case "updateTime": + return ec.fieldContext_RegistryImage_updateTime(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type RegistryImage", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_core_searchRegistryImages_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query_core_listApps(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_core_listApps(ctx, field) if err != nil { @@ -51650,6 +51781,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "core_searchRegistryImages": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_core_searchRegistryImages(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "core_listApps": field := field @@ -55230,6 +55383,50 @@ func (ec *executionContext) marshalNPageInfo2ᚖgithubᚗcomᚋkloudliteᚋapi return ec._PageInfo(ctx, sel, v) } +func (ec *executionContext) marshalNRegistryImage2ᚕᚖgithubᚗcomᚋkloudliteᚋapiᚋappsᚋconsoleᚋinternalᚋentitiesᚐRegistryImageᚄ(ctx context.Context, sel ast.SelectionSet, v []*entities.RegistryImage) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNRegistryImage2ᚖgithubᚗcomᚋkloudliteᚋapiᚋappsᚋconsoleᚋinternalᚋentitiesᚐRegistryImage(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalNRegistryImage2ᚖgithubᚗcomᚋkloudliteᚋapiᚋappsᚋconsoleᚋinternalᚋentitiesᚐRegistryImage(ctx context.Context, sel ast.SelectionSet, v *entities.RegistryImage) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { diff --git a/apps/console/internal/app/graph/schema.graphqls b/apps/console/internal/app/graph/schema.graphqls index ede08f51c..d95c85a68 100644 --- a/apps/console/internal/app/graph/schema.graphqls +++ b/apps/console/internal/app/graph/schema.graphqls @@ -119,6 +119,7 @@ type Query { core_getRegistryImageURL: RegistryImageURL! @isLoggedInAndVerified @hasAccount core_getRegistryImage(image: String!,): RegistryImage @isLoggedInAndVerified @hasAccount core_listRegistryImages(pq: CursorPaginationIn): RegistryImagePaginatedRecords @isLoggedInAndVerified @hasAccount + core_searchRegistryImages(query: String!): [RegistryImage!]! @isLoggedInAndVerified @hasAccount core_listApps(envName: String!, search: SearchApps, pq: CursorPaginationIn): AppPaginatedRecords @isLoggedInAndVerified @hasAccount core_getApp(envName: String!, name: String!): App @isLoggedInAndVerified @hasAccount diff --git a/apps/console/internal/app/graph/schema.resolvers.go b/apps/console/internal/app/graph/schema.resolvers.go index 92719307f..1bb3a1eb0 100644 --- a/apps/console/internal/app/graph/schema.resolvers.go +++ b/apps/console/internal/app/graph/schema.resolvers.go @@ -7,9 +7,8 @@ package graph import ( "context" "fmt" - "time" - "github.com/kloudlite/api/pkg/errors" + "time" "github.com/kloudlite/api/apps/console/internal/app/graph/generated" "github.com/kloudlite/api/apps/console/internal/app/graph/model" @@ -588,6 +587,19 @@ func (r *queryResolver) CoreListRegistryImages(ctx context.Context, pq *repos.Cu return fn.JsonConvertP[model.RegistryImagePaginatedRecords](pImages) } +// CoreSearchRegistryImages is the resolver for the core_searchRegistryImages field. +func (r *queryResolver) CoreSearchRegistryImages(ctx context.Context, query string) ([]*entities.RegistryImage, error) { + cc, err := toConsoleContext(ctx) + if err != nil { + return nil, errors.NewE(err) + } + images, err := r.Domain.SearchRegistryImages(cc, query) + if err != nil { + return nil, errors.NewE(err) + } + return images, nil +} + // CoreListApps is the resolver for the core_listApps field. func (r *queryResolver) CoreListApps(ctx context.Context, envName string, search *model.SearchApps, pq *repos.CursorPagination) (*model.AppPaginatedRecords, error) { cc, err := toConsoleContext(ctx) @@ -1060,7 +1072,5 @@ func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResol // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } -type ( - mutationResolver struct{ *Resolver } - queryResolver struct{ *Resolver } -) +type mutationResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } diff --git a/apps/console/internal/domain/api.go b/apps/console/internal/domain/api.go index 072b847cb..2c50d6065 100644 --- a/apps/console/internal/domain/api.go +++ b/apps/console/internal/domain/api.go @@ -185,6 +185,7 @@ type Domain interface { DeleteRegistryImage(ctx ConsoleContext, image string) error CreateRegistryImage(ctx context.Context, accountName string, image string, meta map[string]any) (*entities.RegistryImage, error) ListRegistryImages(ctx ConsoleContext, pq repos.CursorPagination) (*repos.PaginatedRecord[*entities.RegistryImage], error) + SearchRegistryImages(ctx ConsoleContext, query string) ([]*entities.RegistryImage, error) ListApps(ctx ResourceContext, search map[string]repos.MatchFilter, pq repos.CursorPagination) (*repos.PaginatedRecord[*entities.App], error) GetApp(ctx ResourceContext, name string) (*entities.App, error) diff --git a/apps/console/internal/domain/registry-image.go b/apps/console/internal/domain/registry-image.go index 9e1304b19..19105a571 100644 --- a/apps/console/internal/domain/registry-image.go +++ b/apps/console/internal/domain/registry-image.go @@ -35,6 +35,30 @@ func encodeAccessToken(accountName string, tokenSecret string) string { return base64.StdEncoding.EncodeToString([]byte(info)) } +func generatePartialWords(word string) []string { + var partials []string + for i := 3; i <= len(word); i++ { + partials = append(partials, word[:i]) + } + return partials +} + +func generateAutocompleteWords(meta map[string]any) string { + metaString := "" + for _, value := range meta { + metaString += fmt.Sprintf("%s ", value) + } + + words := strings.Fields(metaString) + var autocompleteWords []string + for _, word := range words { + partials := generatePartialWords(word) + autocompleteWords = append(autocompleteWords, partials...) + } + + return strings.Join(autocompleteWords, " ") +} + func getImageNameTag(image string) (string, string) { parts := strings.Split(image, ":") @@ -66,13 +90,34 @@ func (d *domain) CreateRegistryImage(ctx context.Context, accountName string, im ImageName: imageName, ImageTag: imageTag, Meta: meta, + MetaData: generateAutocompleteWords(meta), }) if err != nil { return nil, errors.NewE(err) } return createdImage, nil +} + +func (d *domain) SearchRegistryImages(ctx ConsoleContext, query string) ([]*entities.RegistryImage, error) { + if err := d.canPerformActionInAccount(ctx, iamT.ListRegistryImages); err != nil { + return nil, errors.NewE(err) + } + + filters := repos.Filter{ + fields.AccountName: ctx.AccountName, + "$text": map[string]any{"$search": query}, + } + + searchedImages, err := d.registryImageRepo.Find(ctx, repos.Query{ + Filter: filters, + Limit: fn.New(int64(10)), + }) + if err != nil { + return nil, errors.NewE(err) + } + return searchedImages, nil } func (d *domain) DeleteRegistryImage(ctx ConsoleContext, image string) error { diff --git a/apps/console/internal/entities/registry-image.go b/apps/console/internal/entities/registry-image.go index b01d4927a..d22b7ced7 100644 --- a/apps/console/internal/entities/registry-image.go +++ b/apps/console/internal/entities/registry-image.go @@ -12,6 +12,7 @@ type RegistryImage struct { ImageName string `json:"imageName"` ImageTag string `json:"imageTag"` Meta map[string]any `json:"meta"` + MetaData string `json:"metadata" graphql:"ignore"` } type RegistryImageURL struct { @@ -34,4 +35,10 @@ var RegistryImageIndexes = []repos.IndexField{ }, Unique: true, }, + { + Field: []repos.IndexKey{ + {Key: fields.AccountName, Value: repos.IndexAsc}, + {Key: fc.Metadata, Value: repos.IndexAsc, IsText: true}, + }, + }, } diff --git a/pkg/repos/db-repo-mongo.go b/pkg/repos/db-repo-mongo.go index 0735473a3..0b4e66c83 100644 --- a/pkg/repos/db-repo-mongo.go +++ b/pkg/repos/db-repo-mongo.go @@ -625,6 +625,11 @@ func (repo *dbRepo[T]) IndexFields(ctx context.Context, indices []IndexField) er // READ MORE @ https://www.mongodb.com/docs/manual/tutorial/manage-indexes/#modify-an-index indexName := "" for _, field := range f.Field { + if field.IsText { + b = append(b, bson.E{Key: field.Key, Value: "text"}) + indexName = buildIndexName(indexName, field.Key, 1) + continue + } switch field.Value { case IndexAsc: b = append(b, bson.E{Key: field.Key, Value: 1}) @@ -635,7 +640,9 @@ func (repo *dbRepo[T]) IndexFields(ctx context.Context, indices []IndexField) er } } - indexModel := mongo.IndexModel{Keys: b, Options: &options.IndexOptions{Unique: &f.Unique, Name: &indexName}} + indexModel := mongo.IndexModel{ + Keys: b, Options: &options.IndexOptions{Unique: &f.Unique, Name: &indexName}, + } _, err := repo.db.Collection(repo.collectionName).Indexes().CreateOne(ctx, indexModel) if err != nil { diff --git a/pkg/repos/db-repo.go b/pkg/repos/db-repo.go index 3b4dd045c..7641ee833 100644 --- a/pkg/repos/db-repo.go +++ b/pkg/repos/db-repo.go @@ -128,8 +128,9 @@ const ( ) type IndexKey struct { - Key string - Value indexOrder + Key string + Value indexOrder + IsText bool } type IndexField struct {