package commands import ( "bufio" "context" "encoding/json" "fmt" "os" "strconv" "strings" "git.loyso.art/frx/eway/cmd/cli/components" "git.loyso.art/frx/eway/internal/entity" "git.loyso.art/frx/eway/internal/matcher" "github.com/rs/zerolog" "github.com/urfave/cli/v3" ) func ItemsCommandTree() *cli.Command { var h itemsHandlers return &cli.Command{ Name: "items", Usage: "Interact with items stored inside db", Commands: []*cli.Command{ newItemsGetCommand(h), newItemsCountCommand(h), newItemsUniqueParamsCommand(h), newItemsAggregateParametersCommand(h), newItemsFixSizesCommand(h), }, } } func newItemsGetCommand(h itemsHandlers) *cli.Command { cmd := cli.Command{ Name: "get", Usage: "gets goods item by its id", Flags: []cli.Flag{ &cli.StringFlag{ Name: "id", Usage: "id of the goods item. Either id or cart-id should be set", }, &cli.IntFlag{ Name: "cart-id", Usage: "cart-id of the item. Either cart-id or id should be set", }, }, } return cmdWithAction(cmd, h.Get) } func newItemsCountCommand(h itemsHandlers) *cli.Command { cmd := cli.Command{ Name: "count", Usage: "iterates over collection and counts number of items", Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "param-key-match", Usage: "filters by parameters with AND logic", }, }, } return cmdWithAction(cmd, h.Count) } func newItemsUniqueParamsCommand(h itemsHandlers) *cli.Command { cmd := cli.Command{ Name: "unique-params", Usage: "Show all stored unique param values", Description: "This command iterates over each item and collect keys of params in a dict and then" + " print it to the output. It's useful to find all unique parameters", } return cmdWithAction(cmd, h.UniqueParameters) } func newItemsAggregateParametersCommand(h itemsHandlers) *cli.Command { cmd := cli.Command{ Name: "aggregate-parameters", Usage: "Show all values of requested parameters", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "case-insensitive", Usage: "Ignores cases of keys", }, &cli.StringSliceFlag{ Name: "regex", Usage: "Registers regex to match", }, &cli.BoolFlag{ Name: "keys-only", Usage: "prints only keys", }, }, } return cmdWithAction(cmd, h.AggregateParameters) } func newItemsFixSizesCommand(h itemsHandlers) *cli.Command { cmd := cli.Command{ Name: "fix-sizes", Usage: "Iterates over params and sets sizes from parameters", } return cmdWithAction(cmd, h.FixSizes) } type itemsHandlers struct{} func (itemsHandlers) Get(ctx context.Context, cmd *cli.Command) error { r, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } id := cmd.String("id") cartID := cmd.Int("cart-id") if id == "" && cartID == 0 { return cli.Exit("oneof: id or cart-id should be set", 1) } else if id != "" && cartID != 0 { return cli.Exit("oneof: id or cart-id should be set", 1) } var item entity.GoodsItem if id != "" { item, err = r.GoodsItem().Get(ctx, id) } else { item, err = r.GoodsItem().GetByCart(ctx, cartID) } if err != nil { return fmt.Errorf("getting item: %w", err) } enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") err = enc.Encode(item) if err != nil { return fmt.Errorf("encoding item: %w", err) } return nil } func (itemsHandlers) Count(ctx context.Context, cmd *cli.Command) error { r, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } filters := cmd.StringSlice("param-key-match") m := matcher.NewRadix() patternMapped := make(map[string]empty, len(filters)) if len(filters) == 0 { m.Register("*") } else { for _, f := range filters { m.Register(f) } for _, pattern := range m.Patterns() { patternMapped[pattern] = empty{} } } var count int items, err := r.GoodsItem().List(ctx) if err != nil { return fmt.Errorf("getting items: %w", err) } for _, item := range items { seenPatterns := map[string]empty{} for k := range item.Parameters { pattern := m.MatchByPattern(k) if pattern == "" { continue } if _, ok := seenPatterns[pattern]; ok { continue } seenPatterns[pattern] = empty{} } if len(seenPatterns) == len(patternMapped) { count++ } } return nil } func (itemsHandlers) UniqueParameters(ctx context.Context, cmd *cli.Command) error { repository, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } knownParams := map[string]empty{} iter, err := repository.GoodsItem().ListIter(ctx, 1) if err != nil { return fmt.Errorf("getting list iter: %w", err) } for item := range iter { for k := range item.Parameters { knownParams[k] = empty{} } } bw := bufio.NewWriter(cmd.Writer) for paramName := range knownParams { _, err = bw.WriteString(paramName + "\n") if err != nil { return fmt.Errorf("unable to write: %w", err) } } return bw.Flush() } func (itemsHandlers) AggregateParameters(ctx context.Context, cmd *cli.Command) error { repository, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } log, err := components.GetLogger() if err != nil { return fmt.Errorf("getting logger: %w", err) } params := cmd.Args().Slice() opts := make([]matcher.RadixOpt, 0, 1) if cmd.Bool("case-insensitive") { opts = append(opts, matcher.RadixCaseInsensitive()) } m := matcher.NewRadix(opts...) for _, param := range params { log.Debug().Str("param", param).Msg("registering param") m.Register(param) } for _, regexp := range cmd.StringSlice("regex") { log.Debug().Str("regexp", regexp).Msg("registering regexp") m.RegisterRegexp(regexp) } requestedValues := make(map[string]map[string]empty, len(params)) requestedValuesByPattern := make(map[string]map[string]empty, len(params)) iter := getItemsIter(ctx, repository.GoodsItem()) for iter.Next() { item := iter.Get() for k, v := range item.Parameters { matchedPattern := m.MatchByPattern(k) if matchedPattern == "" { continue } values, ok := requestedValues[k] if !ok { values = make(map[string]empty) } values[v] = empty{} requestedValues[k] = values values, ok = requestedValuesByPattern[matchedPattern] if !ok { values = map[string]empty{} } values[v] = empty{} requestedValuesByPattern[matchedPattern] = values } } bw := bufio.NewWriter(cmd.Writer) _, _ = bw.WriteString("Matches:\n") if cmd.Bool("keys-only") { for k := range requestedValues { _, _ = bw.WriteString(k + "\n") } } else { for k, v := range requestedValues { _, _ = bw.WriteString(k + ": ") values := make([]string, 0, len(v)) for item := range v { values = append(values, strconv.Quote(item)) } valuesStr := "[" + strings.Join(values, ",") + "]\n" _, _ = bw.WriteString(valuesStr) } } _, _ = bw.WriteString("\nPatterns:\n") for _, pattern := range m.Patterns() { _, _ = bw.WriteString(pattern + "\n") } return bw.Flush() } func (itemsHandlers) FixSizes(ctx context.Context, cmd *cli.Command) error { repository, err := components.GetRepository() if err != nil { return fmt.Errorf("getting repository: %w", err) } matcher, err := components.GetDimensionMatcher() if err != nil { return fmt.Errorf("getting dimension matcher: %w", err) } dimensionDispatcher := dimensionDispatcher{m: matcher} log := zerolog.Ctx(ctx) toUpdate := make([]entity.GoodsItem, 0, 20_000) bus := getItemsIter(ctx, repository.GoodsItem()) for bus.Next() { item := bus.Get() var valueBeenUpdated bool for key, value := range item.Parameters { trimmedValue := strings.TrimSpace(value) trimmedKey := strings.TrimSpace(key) if trimmedKey != key || trimmedValue != value { log.Warn(). Str("old_key", key). Str("new_key", trimmedKey). Str("old_value", value). Str("new_value", trimmedValue). Msg("found mismatch") delete(item.Parameters, key) key = trimmedKey value = trimmedValue item.Parameters[key] = value valueBeenUpdated = true } updateValue := strings.HasSuffix(key, ":") if updateValue { key = strings.TrimSuffix(key, ":") item.Parameters[key] = value delete(item.Parameters, key+":") valueBeenUpdated = true } if dimensionDispatcher.dispatch(ctx, key, value, &item.Sizes) { valueBeenUpdated = true log.Debug().Str("key", key).Any("sizes", item.Sizes).Msg("been updated") } } var updated bool oldSizes := item.Sizes item.Sizes, updated = entity.FixupSizes(oldSizes) if updated { log.Info().Int64("cart", item.Cart).Any("old", oldSizes).Any("new", item.Sizes).Msg("sizes been fixed") } valueBeenUpdated = valueBeenUpdated || updated if valueBeenUpdated { toUpdate = append(toUpdate, item) } } if bus.Err() != nil { return fmt.Errorf("iterating: %w", bus.Err()) } _, err = repository.GoodsItem().UpsertMany(ctx, toUpdate...) if err != nil { return fmt.Errorf("updating items: %w", err) } log.Info().Int("count", len(toUpdate)).Msg("updated items") return nil }