package adapter import ( "context" "database/sql" "errors" "fmt" "time" "gitea.dwysokinski.me/twhelp/core/internal/bun/bunmodel" "gitea.dwysokinski.me/twhelp/core/internal/domain" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect" ) type VillageBunRepository struct { db bun.IDB } func NewVillageBunRepository(db bun.IDB) *VillageBunRepository { return &VillageBunRepository{db: db} } func (repo *VillageBunRepository) CreateOrUpdate(ctx context.Context, params ...domain.CreateVillageParams) error { if len(params) == 0 { return nil } now := time.Now() villages := make(bunmodel.Villages, 0, len(params)) for _, p := range params { base := p.Base() villages = append(villages, bunmodel.Village{ ID: base.ID(), ServerKey: p.ServerKey(), Name: base.Name(), Points: base.Points(), X: base.X(), Y: base.Y(), Continent: base.Continent(), Bonus: base.Bonus(), PlayerID: base.PlayerID(), ProfileURL: base.ProfileURL().String(), CreatedAt: now, }) } q := repo.db.NewInsert(). Model(&villages) //nolint:exhaustive switch q.Dialect().Name() { case dialect.PG: q = q.On("CONFLICT ON CONSTRAINT villages_pkey DO UPDATE") case dialect.SQLite: q = q.On("CONFLICT(id, server_key) DO UPDATE") default: q = q.Err(errors.New("unsupported dialect")) } if _, err := q. Set("name = EXCLUDED.name"). Set("points = EXCLUDED.points"). Set("x = EXCLUDED.x"). Set("y = EXCLUDED.y"). Set("continent = EXCLUDED.continent"). Set("bonus = EXCLUDED.bonus"). Set("player_id = EXCLUDED.player_id"). Set("profile_url = EXCLUDED.profile_url"). Returning(""). Exec(ctx); err != nil { return fmt.Errorf("something went wrong while inserting villages into the db: %w", err) } return nil } func (repo *VillageBunRepository) List( ctx context.Context, params domain.ListVillagesParams, ) (domain.ListVillagesResult, error) { var villages bunmodel.Villages if err := repo.db.NewSelect(). Model(&villages). Apply(listVillagesParamsApplier{params: params}.apply). Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { return domain.ListVillagesResult{}, fmt.Errorf("couldn't select villages from the db: %w", err) } converted, err := villages.ToDomain() if err != nil { return domain.ListVillagesResult{}, err } return domain.NewListVillagesResult(separateListResultAndNext(converted, params.Limit())) } func (repo *VillageBunRepository) ListWithRelations( ctx context.Context, params domain.ListVillagesParams, ) (domain.ListVillagesWithRelationsResult, error) { var villages bunmodel.Villages if err := repo.db.NewSelect(). Model(&villages). Apply(listVillagesParamsApplier{params: params}.apply). Relation("Player", func(q *bun.SelectQuery) *bun.SelectQuery { return q.Column(bunmodel.PlayerMetaColumns...) }). Relation("Player.Tribe", func(q *bun.SelectQuery) *bun.SelectQuery { return q.Column(bunmodel.TribeMetaColumns...) }). Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { return domain.ListVillagesWithRelationsResult{}, fmt.Errorf("couldn't select players from the db: %w", err) } converted, err := villages.ToDomainWithRelations() if err != nil { return domain.ListVillagesWithRelationsResult{}, err } return domain.NewListVillagesWithRelationsResult(separateListResultAndNext(converted, params.Limit())) } func (repo *VillageBunRepository) Delete(ctx context.Context, serverKey string, ids ...int) error { if len(ids) == 0 { return nil } if _, err := repo.db.NewDelete(). Model((*bunmodel.Village)(nil)). Where("id IN (?)", bun.In(ids)). Where("server_key = ?", serverKey). Returning(""). Exec(ctx); err != nil { return fmt.Errorf("couldn't delete villages: %w", err) } return nil } type listVillagesParamsApplier struct { params domain.ListVillagesParams } //nolint:gocyclo func (a listVillagesParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { if ids := a.params.IDs(); len(ids) > 0 { q = q.Where("village.id IN (?)", bun.In(ids)) } if serverKeys := a.params.ServerKeys(); len(serverKeys) > 0 { q = q.Where("village.server_key IN (?)", bun.In(serverKeys)) } if coords := a.params.Coords(); len(coords) > 0 { converted := make([][]int, 0, len(coords)) for _, c := range coords { converted = append(converted, []int{c.X(), c.Y()}) } q = q.Where("(village.x, village.y) in (?)", bun.In(converted)) } if playerIDs := a.params.PlayerIDs(); len(playerIDs) > 0 { q = q.Where("village.player_id IN (?)", bun.In(playerIDs)) } if tribeIDs := a.params.TribeIDs(); len(tribeIDs) > 0 { q = q.Relation("Player.Tribe", func(q *bun.SelectQuery) *bun.SelectQuery { return q.ExcludeColumn("*") }). Where("player__tribe.id IN (?)", bun.In(tribeIDs)) } for _, s := range a.params.Sort() { column, dir, err := a.sortToColumnAndDirection(s) if err != nil { return q.Err(err) } q.OrderExpr("? ?", column, dir.Bun()) } return q.Limit(a.params.Limit() + 1).Apply(a.applyCursor) } func (a listVillagesParamsApplier) applyCursor(q *bun.SelectQuery) *bun.SelectQuery { cursor := a.params.Cursor() if cursor.IsZero() { return q } sort := a.params.Sort() cursorApplier := cursorPaginationApplier{ data: make([]cursorPaginationApplierDataElement, 0, len(sort)), } for _, s := range sort { var err error var el cursorPaginationApplierDataElement el.column, el.direction, err = a.sortToColumnAndDirection(s) if err != nil { return q.Err(err) } switch s { case domain.VillageSortIDASC, domain.VillageSortIDDESC: el.value = cursor.ID() el.unique = true case domain.VillageSortServerKeyASC, domain.VillageSortServerKeyDESC: el.value = cursor.ServerKey() default: return q.Err(fmt.Errorf("%s: %w", s.String(), errInvalidSortValue)) } cursorApplier.data = append(cursorApplier.data, el) } return q.Apply(cursorApplier.apply) } func (a listVillagesParamsApplier) sortToColumnAndDirection( s domain.VillageSort, ) (bun.Safe, sortDirection, error) { switch s { case domain.VillageSortIDASC: return "village.id", sortDirectionASC, nil case domain.VillageSortIDDESC: return "village.id", sortDirectionDESC, nil case domain.VillageSortServerKeyASC: return "village.server_key", sortDirectionASC, nil case domain.VillageSortServerKeyDESC: return "village.server_key", sortDirectionDESC, nil default: return "", 0, fmt.Errorf("%s: %w", s.String(), errInvalidSortValue) } }