From 05f1ea6ec64c8bde530da79a9b6cacaa57e12ece Mon Sep 17 00:00:00 2001 From: Wil Simpson Date: Tue, 31 Dec 2024 23:23:32 -0600 Subject: [PATCH] feat: implement fieldmask --- api | 2 +- go.mod | 10 +-- go.sum | 12 +++ pkg/model/character/character.go | 28 ++++++ pkg/repository/character.go | 11 ++- pkg/repository/character_pgx.go | 113 ++++++++++++------------ pkg/repository/character_test.go | 80 ++++++++++++++--- pkg/repository/mocks/character.go | 56 +++--------- pkg/repository/repository_suite_test.go | 13 +-- pkg/service/character.go | 22 ++--- pkg/service/character_test.go | 20 ++--- pkg/service/mocks/character.go | 21 +++-- pkg/srv/context.go | 4 +- pkg/srv/context_test.go | 12 +-- pkg/srv/grpc.go | 47 ++++++++-- pkg/srv/grpc_test.go | 13 ++- sro.character.json | 4 +- 17 files changed, 292 insertions(+), 176 deletions(-) diff --git a/api b/api index dfbab03..2bce3c5 160000 --- a/api +++ b/api @@ -1 +1 @@ -Subproject commit dfbab03aebc0e2ee0ea1405ff76984404a6edf62 +Subproject commit 2bce3c564b9ef2c7ae3f2f0e2af5d63bf8603d91 diff --git a/go.mod b/go.mod index 8a43bff..4ad6ad2 100644 --- a/go.mod +++ b/go.mod @@ -4,21 +4,21 @@ go 1.23.3 require ( ariga.io/atlas-provider-gorm v0.5.0 - github.com/ShatteredRealms/go-common-service v0.9.18 + github.com/ShatteredRealms/go-common-service v0.9.20 github.com/WilSimpson/gocloak/v13 v13.11.3 github.com/go-faker/faker/v4 v4.5.0 github.com/golang-migrate/migrate/v4 v4.18.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 github.com/jackc/pgx/v5 v5.7.1 github.com/mitranim/gow v0.0.0-20231026105220-af11a6e1e9eb - github.com/onsi/ginkgo/v2 v2.22.1 - github.com/onsi/gomega v1.36.1 + github.com/onsi/ginkgo/v2 v2.22.2 + github.com/onsi/gomega v1.36.2 github.com/solo-io/protoc-gen-openapi v0.2.5 github.com/spf13/cobra-cli v1.3.0 go.mongodb.org/mongo-driver v1.17.1 go.opentelemetry.io/otel v1.33.0 go.uber.org/mock v0.5.0 - google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 + google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def google.golang.org/grpc v1.69.2 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 google.golang.org/protobuf v1.36.1 @@ -287,7 +287,7 @@ require ( golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/tools v0.28.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241230172942-26aa7a208def // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4a9b20c..93d5229 100644 --- a/go.sum +++ b/go.sum @@ -128,6 +128,10 @@ github.com/ShatteredRealms/go-common-service v0.9.16 h1:+dLwg6Zcs76ULvH3991G7pMk github.com/ShatteredRealms/go-common-service v0.9.16/go.mod h1:AxfNgvbQ0T/gN3FASOaHlOjL0tEZldyDokLMg1G+V8c= github.com/ShatteredRealms/go-common-service v0.9.18 h1:uHDB29vwvPjD9nXBshzRIzYsZ5MDXARXah5tkZfiUNw= github.com/ShatteredRealms/go-common-service v0.9.18/go.mod h1:AxfNgvbQ0T/gN3FASOaHlOjL0tEZldyDokLMg1G+V8c= +github.com/ShatteredRealms/go-common-service v0.9.19 h1:bBNtc8lKC/BiIFHJXHR5la/Z3D0HOt9E35Q/85i2fuI= +github.com/ShatteredRealms/go-common-service v0.9.19/go.mod h1:AxfNgvbQ0T/gN3FASOaHlOjL0tEZldyDokLMg1G+V8c= +github.com/ShatteredRealms/go-common-service v0.9.20 h1:wlXY34D9cxWRScnU7IwCyAdleiYWxlpgshTJQfd4ldA= +github.com/ShatteredRealms/go-common-service v0.9.20/go.mod h1:AxfNgvbQ0T/gN3FASOaHlOjL0tEZldyDokLMg1G+V8c= github.com/TwiN/go-away v1.6.14 h1:gjFP+6/A36gmj0NpYX0Sz9hrdU0KtHwtNWYnsJgV4fo= github.com/TwiN/go-away v1.6.14/go.mod h1:d+Gv3XuqjIeFqXYuAIzlyNoDzr1vNsP5B/hRY3u/VLs= github.com/WilSimpson/gocloak/v13 v13.11.3 h1:5Y2j7x3SD8wnwir2dBEOr6XY2Yo6wPQQ24hfkig6KXA= @@ -790,12 +794,16 @@ github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM= github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -1514,10 +1522,14 @@ google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484 h1: google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484/go.mod h1:KRUmxRI4JmbpAm8gcZM4Jsffi859fo5LQjILwuqj9z8= google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8 h1:st3LcW/BPi75W4q1jJTEor/QWwbNlPlDG0JTn6XhZu0= google.golang.org/genproto/googleapis/api v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:klhJGKFyG8Tn50enBn7gizg4nXGXJ+jqEREdCWaPcV4= +google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def h1:0Km0hi+g2KXbXL0+riZzSCKz23f4MmwicuEb00JeonI= +google.golang.org/genproto/googleapis/api v0.0.0-20241230172942-26aa7a208def/go.mod h1:u2DoMSpCXjrzqLdobRccQMc9wrnMAJ1DLng0a2yqM2Q= google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484 h1:Z7FRVJPSMaHQxD0uXU8WdgFh8PseLM8Q8NzhnpMrBhQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20241216192217-9240e9c98484/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241230172942-26aa7a208def h1:4P81qv5JXI/sDNae2ClVx88cgDDA6DPilADkG9tYKz8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241230172942-26aa7a208def/go.mod h1:bdAgzvd4kFrpykc5/AC2eLUiegK9T/qxZHD4hXYf/ho= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/pkg/model/character/character.go b/pkg/model/character/character.go index a25dc32..dcabd18 100644 --- a/pkg/model/character/character.go +++ b/pkg/model/character/character.go @@ -9,8 +9,10 @@ import ( "github.com/ShatteredRealms/character-service/pkg/model/game" "github.com/ShatteredRealms/character-service/pkg/pb" commongame "github.com/ShatteredRealms/go-common-service/pkg/model/game" + "github.com/ShatteredRealms/go-common-service/pkg/util" goaway "github.com/TwiN/go-away" "github.com/google/uuid" + fieldmask_utils "github.com/mennanov/fieldmask-utils" ) const ( @@ -135,6 +137,17 @@ func (c *Character) ToPb() *pb.Character { return char } +func (c *Character) ToPbWithMask(paths []string) (*pb.Character, error) { + mask, err := fieldmask_utils.MaskFromPaths(paths, util.PascalCase) + if err != nil { + return nil, err + } + + outPb := &pb.Character{} + err = fieldmask_utils.StructToStruct(mask, c.ToPb(), outPb) + return outPb, err +} + func (c Characters) ToPb() *pb.Characters { resp := &pb.Characters{Characters: make([]*pb.Character, len(c))} for idx, character := range c { @@ -145,3 +158,18 @@ func (c Characters) ToPb() *pb.Characters { return resp } + +func (c Characters) ToPbWithMask(paths []string) (*pb.Characters, error) { + var err error + resp := &pb.Characters{Characters: make([]*pb.Character, len(c))} + for idx, character := range c { + if character != nil { + resp.Characters[idx], err = character.ToPbWithMask(paths) + if err != nil { + return nil, err + } + } + } + + return resp, nil +} diff --git a/pkg/repository/character.go b/pkg/repository/character.go index dac09d1..0b9736d 100644 --- a/pkg/repository/character.go +++ b/pkg/repository/character.go @@ -4,15 +4,18 @@ import ( "context" "github.com/ShatteredRealms/character-service/pkg/model/character" + "github.com/ShatteredRealms/go-common-service/pkg/pb" "github.com/google/uuid" ) type CharacterRepository interface { - GetCharacterById(ctx context.Context, characterId *uuid.UUID) (*character.Character, error) + // GetCharacters gets the of character that matches the provided filters. If more than one character matches the filters, an error is returned. + GetCharacter(ctx context.Context, matchFilters map[string]interface{}) (*character.Character, error) - GetCharacters(ctx context.Context) (character.Characters, error) - GetDeletedCharacters(ctx context.Context) (character.Characters, error) - GetCharactersByOwner(ctx context.Context, ownerId *uuid.UUID) (character.Characters, error) + // GetCharacters gets a list of characters that match the provided filters and returns the total number of characters that match the filters. + // If no characters match the filters, an empty list is returned. + // Deleted characters are only returned if deleted is true, and non-deleted characters if true. + GetCharacters(ctx context.Context, matchFilters map[string]interface{}, queryFilter *pb.QueryFilters, deleted bool) (character.Characters, int, error) CreateCharacter(ctx context.Context, newCharacter *character.Character) (*character.Character, error) UpdateCharacter(ctx context.Context, updatedCharacter *character.Character) (*character.Character, error) diff --git a/pkg/repository/character_pgx.go b/pkg/repository/character_pgx.go index 8a96cdd..32afc36 100644 --- a/pkg/repository/character_pgx.go +++ b/pkg/repository/character_pgx.go @@ -4,9 +4,12 @@ import ( "context" "errors" "fmt" + "strings" "github.com/ShatteredRealms/character-service/pkg/common" "github.com/ShatteredRealms/character-service/pkg/model/character" + "github.com/ShatteredRealms/go-common-service/pkg/log" + "github.com/ShatteredRealms/go-common-service/pkg/pb" "github.com/ShatteredRealms/go-common-service/pkg/repository" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -128,68 +131,61 @@ func (p *pgxCharacterRepository) DeleteCharactersByOwner(ctx context.Context, ow return outCharacters, tx.Commit(ctx) } -// GetCharacterById implements CharacterRepository. -func (p *pgxCharacterRepository) GetCharacterById(ctx context.Context, characterId *uuid.UUID) (*character.Character, error) { - if characterId == nil { - return nil, ErrNilId +func QueryCharacters(ctx context.Context, tx pgx.Tx, matchFilters map[string]interface{}, queryFilter *pb.QueryFilters, deleted bool) (pgx.Rows, int, error) { + total := -1 + builder := strings.Builder{} + params := make([]interface{}, 0, len(matchFilters)) + builder.WriteString("FROM characters WHERE (deleted_at IS ") + if deleted { + builder.WriteString("NOT NULL") + } else { + builder.WriteString("NULL") } - - tx, err := p.conn.Begin(ctx) - defer tx.Rollback(ctx) - if err != nil { - return nil, err + for key, value := range matchFilters { + builder.WriteString(" AND ") + builder.WriteString(key) + builder.WriteString("=") + builder.WriteString(fmt.Sprintf("$%d", len(params)+1)) + params = append(params, value) } + builder.WriteString(")") - rows, _ := tx.Query(ctx, - "SELECT * FROM characters WHERE id = $1", - characterId) - outCharacter, err := pgx.CollectExactlyOneRow(rows, pgx.RowToAddrOfStructByName[character.Character]) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, nil + if queryFilter != nil { + rows, err := tx.Query(ctx, "SELECT COUNT(*) "+builder.String(), params...) + if err != nil { + return nil, -1, err + } + if rows.Next() { + err = rows.Scan(&total) + rows.Close() } - return nil, err - } - return outCharacter, tx.Commit(ctx) -} + log.Logger.Infof("total: %d", total) -// GetCharacters implements CharacterRepository. -func (p *pgxCharacterRepository) GetCharacters(ctx context.Context) (character.Characters, error) { - tx, err := p.conn.Begin(ctx) - defer tx.Rollback(ctx) - if err != nil { - return nil, err - } - - rows, _ := tx.Query(ctx, "SELECT * FROM characters WHERE deleted_at IS NULL") - outCharacters, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[character.Character]) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - return nil, nil + if queryFilter.Limit > 0 { + builder.WriteString(fmt.Sprintf(" LIMIT %d", queryFilter.Limit)) + } + if queryFilter.Offset > 0 { + builder.WriteString(fmt.Sprintf(" OFFSET %d", queryFilter.Offset)) } - return nil, err } - - return outCharacters, tx.Commit(ctx) + rows, err := tx.Query(ctx, "SELECT * "+builder.String()+";", params...) + return rows, total, err } -// GetCharactersByOwner implements CharacterRepository. -func (p *pgxCharacterRepository) GetCharactersByOwner(ctx context.Context, ownerId *uuid.UUID) (character.Characters, error) { - if ownerId == nil { - return nil, ErrNilId - } - +// GetCharacter implements CharacterRepository. +func (p *pgxCharacterRepository) GetCharacter(ctx context.Context, matchFilters map[string]interface{}) (*character.Character, error) { tx, err := p.conn.Begin(ctx) defer tx.Rollback(ctx) if err != nil { return nil, err } - rows, _ := tx.Query(ctx, - "SELECT * FROM characters WHERE (owner_id = $1 AND deleted_at IS NULL)", - ownerId) - outCharacters, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[character.Character]) + rows, _, err := QueryCharacters(ctx, tx, matchFilters, nil, false) + if err != nil { + return nil, err + } + outCharacter, err := pgx.CollectExactlyOneRow(rows, pgx.RowToAddrOfStructByNameLax[character.Character]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, nil @@ -197,27 +193,30 @@ func (p *pgxCharacterRepository) GetCharactersByOwner(ctx context.Context, owner return nil, err } - return outCharacters, tx.Commit(ctx) + return outCharacter, tx.Commit(ctx) } -// GetDeletedCharacters implements CharacterRepository. -func (p *pgxCharacterRepository) GetDeletedCharacters(ctx context.Context) (character.Characters, error) { +// GetCharacters implements CharacterRepository. +func (p *pgxCharacterRepository) GetCharacters(ctx context.Context, matchFilters map[string]interface{}, queryFilter *pb.QueryFilters, deleted bool) (character.Characters, int, error) { tx, err := p.conn.Begin(ctx) defer tx.Rollback(ctx) if err != nil { - return nil, err + return nil, -1, err } - rows, _ := tx.Query(ctx, "SELECT * FROM characters WHERE deleted_at IS NOT NULL") + rows, total, err := QueryCharacters(ctx, tx, matchFilters, queryFilter, deleted) + if err != nil { + return nil, -1, err + } outCharacters, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[character.Character]) if err != nil { if errors.Is(err, pgx.ErrNoRows) { - return nil, nil + return nil, 0, nil } - return nil, err + return nil, -1, err } - return outCharacters, tx.Commit(ctx) + return outCharacters, total, tx.Commit(ctx) } // UpdateCharacter implements CharacterRepository. @@ -233,9 +232,9 @@ func (p *pgxCharacterRepository) UpdateCharacter(ctx context.Context, c *charact } rows, _ := tx.Query(ctx, - `UPDATE - characters - SET + `UPDATE + characters + SET owner_id = $2, name = $3, gender = $4, diff --git a/pkg/repository/character_test.go b/pkg/repository/character_test.go index 3306564..0a7134c 100644 --- a/pkg/repository/character_test.go +++ b/pkg/repository/character_test.go @@ -6,14 +6,19 @@ import ( "github.com/go-faker/faker/v4" "github.com/google/uuid" + fieldmask_utils "github.com/mennanov/fieldmask-utils" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/ShatteredRealms/character-service/pkg/common" "github.com/ShatteredRealms/character-service/pkg/model/character" "github.com/ShatteredRealms/character-service/pkg/model/game" + "github.com/ShatteredRealms/character-service/pkg/pb" "github.com/ShatteredRealms/character-service/pkg/repository" cgame "github.com/ShatteredRealms/go-common-service/pkg/model/game" + cpb "github.com/ShatteredRealms/go-common-service/pkg/pb" + "github.com/ShatteredRealms/go-common-service/pkg/util" ) var _ = Describe("Character Repository", func() { @@ -80,27 +85,26 @@ var _ = Describe("Character Repository", func() { c = outC - outChars, err := repo.GetCharacters(ctx) + outChars, _, err := repo.GetCharacters(ctx, map[string]interface{}{"id": c.Id}, nil, false) Expect(err).To(BeNil()) Expect(outChars).To(ContainElement(c)) }) Describe("GetCharacterById", func() { It("should return nothing if there are no matches", func(ctx SpecContext) { - outC, err := repo.GetCharacterById(ctx, nil) + outC, err := repo.GetCharacter(ctx, map[string]interface{}{"id": "a"}) Expect(err).NotTo(BeNil()) - Expect(errors.Is(err, common.ErrRequestInvalid)).To(BeTrue()) Expect(outC).To(BeNil()) id, err := uuid.NewV7() Expect(err).NotTo(HaveOccurred()) - outC, err = repo.GetCharacterById(ctx, &id) + outC, err = repo.GetCharacter(ctx, map[string]interface{}{"id": id}) Expect(err).To(BeNil()) Expect(outC).To(BeNil()) }) It("should return a character if there is a match", func(ctx SpecContext) { - outC, err := repo.GetCharacterById(ctx, &c.Id) + outC, err := repo.GetCharacter(ctx, map[string]interface{}{"id": c.Id}) Expect(err).To(BeNil()) Expect(outC).NotTo(BeNil()) ExpectCharactersEquals(outC, c) @@ -109,28 +113,76 @@ var _ = Describe("Character Repository", func() { Describe("GetCharacters", func() { It("should return characters", func(ctx SpecContext) { - outChars, err := repo.GetCharacters(ctx) + outChars, total, err := repo.GetCharacters(ctx, map[string]interface{}{}, &cpb.QueryFilters{}, false) Expect(err).To(BeNil()) Expect(len(outChars) > 0).To(BeTrue()) + Expect(outChars).To(HaveLen(total)) + }) + + It("should work with proto filter limit", func(ctx SpecContext) { + c.Id = uuid.Nil + c.Name = faker.Username() + "a" + _, err := repo.CreateCharacter(ctx, c) + Expect(err).To(BeNil()) + + outChars, total, err := repo.GetCharacters(ctx, map[string]interface{}{}, &cpb.QueryFilters{Limit: 1}, false) + Expect(err).To(BeNil()) + Expect(outChars).To(HaveLen(1)) + Expect(total > 1).To(BeTrue()) + }) + It("should work with proto filter offset", func(ctx SpecContext) { + c.Id = uuid.Nil + c.Name = faker.Username() + "a" + _, err := repo.CreateCharacter(ctx, c) + Expect(err).To(BeNil()) + + outChars, total, err := repo.GetCharacters(ctx, map[string]interface{}{}, &cpb.QueryFilters{Limit: 1}, false) + Expect(err).To(BeNil()) + Expect(outChars).To(HaveLen(1)) + Expect(total > 1).To(BeTrue()) + + outChars2, total2, err := repo.GetCharacters(ctx, map[string]interface{}{}, &cpb.QueryFilters{Limit: 1, Offset: 1}, false) + Expect(err).To(BeNil()) + Expect(outChars2).To(HaveLen(1)) + Expect(total2 > 1).To(BeTrue()) + Expect(outChars[0].Id).NotTo(Equal(outChars2[0].Id)) + }) + + It("should work with proto match filters", func(ctx SpecContext) { + request := &pb.Character{Id: c.Id.String(), Name: "a"} + pbmask := &fieldmaskpb.FieldMask{Paths: []string{"id"}} + mask, err := fieldmask_utils.MaskFromProtoFieldMask(pbmask, util.PascalCase) + Expect(err).To(BeNil()) + + matchFilters := make(map[string]interface{}) + Expect(fieldmask_utils.StructToMap(mask, request, matchFilters)).To(Succeed()) + + outChars, _, err := repo.GetCharacters(ctx, matchFilters, &cpb.QueryFilters{}, false) + Expect(err).To(BeNil()) + Expect(outChars).To(HaveLen(1)) + Expect(outChars[0].Id).To(Equal(c.Id)) }) }) Describe("GetCharactersByOwner", func() { It("should return nothing if there are no matches", func(ctx SpecContext) { - outChars, err := repo.GetCharactersByOwner(ctx, nil) + outChars, total, err := repo.GetCharacters(ctx, map[string]interface{}{"owner_id": "a"}, &cpb.QueryFilters{}, false) Expect(err).NotTo(BeNil()) - Expect(errors.Is(err, common.ErrRequestInvalid)).To(BeTrue()) Expect(outChars).To(HaveLen(0)) + // Expect(outChars).To(HaveLen(total)) id := uuid.New() - outChars, err = repo.GetCharactersByOwner(ctx, &id) + outChars, total, err = repo.GetCharacters(ctx, map[string]interface{}{"owner_id": id.String()}, &cpb.QueryFilters{}, false) + Expect(err).To(BeNil()) Expect(outChars).To(HaveLen(0)) + Expect(outChars).To(HaveLen(total)) }) It("should return a character if there is a match", func(ctx SpecContext) { - outChars, err := repo.GetCharactersByOwner(ctx, &c.OwnerId) + outChars, total, err := repo.GetCharacters(ctx, map[string]interface{}{"owner_id": c.OwnerId}, &cpb.QueryFilters{}, false) Expect(err).To(BeNil()) Expect(outChars).To(HaveLen(1)) + Expect(outChars).To(HaveLen(total)) outC := outChars[0] Expect(err).To(BeNil()) @@ -169,9 +221,11 @@ var _ = Describe("Character Repository", func() { Expect(outC).NotTo(BeNil()) ExpectCharactersEquals(outC, c) - outChars, err := repo.GetCharacters(ctx) + outChars, total, err := repo.GetCharacters(ctx, map[string]interface{}{}, &cpb.QueryFilters{}, false) Expect(err).To(BeNil()) Expect(outChars).NotTo(ContainElement(c)) + Expect(len(outChars) > 0).To(BeTrue()) + Expect(outChars).To(HaveLen(total)) }) }) @@ -192,9 +246,11 @@ var _ = Describe("Character Repository", func() { Expect(outC).NotTo(BeNil()) ExpectCharactersEquals(outC, c) - outChars, err = repo.GetCharacters(ctx) + outChars, total, err := repo.GetCharacters(ctx, map[string]interface{}{}, &cpb.QueryFilters{}, false) Expect(err).To(BeNil()) Expect(outChars).NotTo(ContainElement(c)) + Expect(len(outChars) > 0).To(BeTrue()) + Expect(outChars).To(HaveLen(total)) }) }) }) diff --git a/pkg/repository/mocks/character.go b/pkg/repository/mocks/character.go index a7a080a..0203ada 100644 --- a/pkg/repository/mocks/character.go +++ b/pkg/repository/mocks/character.go @@ -14,6 +14,7 @@ import ( reflect "reflect" character "github.com/ShatteredRealms/character-service/pkg/model/character" + pb "github.com/ShatteredRealms/go-common-service/pkg/pb" uuid "github.com/google/uuid" gomock "go.uber.org/mock/gomock" ) @@ -87,64 +88,35 @@ func (mr *MockCharacterRepositoryMockRecorder) DeleteCharactersByOwner(ctx, owne return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCharactersByOwner", reflect.TypeOf((*MockCharacterRepository)(nil).DeleteCharactersByOwner), ctx, ownerId) } -// GetCharacterById mocks base method. -func (m *MockCharacterRepository) GetCharacterById(ctx context.Context, characterId *uuid.UUID) (*character.Character, error) { +// GetCharacter mocks base method. +func (m *MockCharacterRepository) GetCharacter(ctx context.Context, matchFilters map[string]any) (*character.Character, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCharacterById", ctx, characterId) + ret := m.ctrl.Call(m, "GetCharacter", ctx, matchFilters) ret0, _ := ret[0].(*character.Character) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetCharacterById indicates an expected call of GetCharacterById. -func (mr *MockCharacterRepositoryMockRecorder) GetCharacterById(ctx, characterId any) *gomock.Call { +// GetCharacter indicates an expected call of GetCharacter. +func (mr *MockCharacterRepositoryMockRecorder) GetCharacter(ctx, matchFilters any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCharacterById", reflect.TypeOf((*MockCharacterRepository)(nil).GetCharacterById), ctx, characterId) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCharacter", reflect.TypeOf((*MockCharacterRepository)(nil).GetCharacter), ctx, matchFilters) } // GetCharacters mocks base method. -func (m *MockCharacterRepository) GetCharacters(ctx context.Context) (character.Characters, error) { +func (m *MockCharacterRepository) GetCharacters(ctx context.Context, matchFilters map[string]any, queryFilter *pb.QueryFilters, deleted bool) (character.Characters, int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCharacters", ctx) + ret := m.ctrl.Call(m, "GetCharacters", ctx, matchFilters, queryFilter, deleted) ret0, _ := ret[0].(character.Characters) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetCharacters indicates an expected call of GetCharacters. -func (mr *MockCharacterRepositoryMockRecorder) GetCharacters(ctx any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCharacters", reflect.TypeOf((*MockCharacterRepository)(nil).GetCharacters), ctx) -} - -// GetCharactersByOwner mocks base method. -func (m *MockCharacterRepository) GetCharactersByOwner(ctx context.Context, ownerId *uuid.UUID) (character.Characters, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCharactersByOwner", ctx, ownerId) - ret0, _ := ret[0].(character.Characters) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetCharactersByOwner indicates an expected call of GetCharactersByOwner. -func (mr *MockCharacterRepositoryMockRecorder) GetCharactersByOwner(ctx, ownerId any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCharactersByOwner", reflect.TypeOf((*MockCharacterRepository)(nil).GetCharactersByOwner), ctx, ownerId) -} - -// GetDeletedCharacters mocks base method. -func (m *MockCharacterRepository) GetDeletedCharacters(ctx context.Context) (character.Characters, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetDeletedCharacters", ctx) - ret0, _ := ret[0].(character.Characters) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetDeletedCharacters indicates an expected call of GetDeletedCharacters. -func (mr *MockCharacterRepositoryMockRecorder) GetDeletedCharacters(ctx any) *gomock.Call { +func (mr *MockCharacterRepositoryMockRecorder) GetCharacters(ctx, matchFilters, queryFilter, deleted any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeletedCharacters", reflect.TypeOf((*MockCharacterRepository)(nil).GetDeletedCharacters), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCharacters", reflect.TypeOf((*MockCharacterRepository)(nil).GetCharacters), ctx, matchFilters, queryFilter, deleted) } // UpdateCharacter mocks base method. diff --git a/pkg/repository/repository_suite_test.go b/pkg/repository/repository_suite_test.go index 6dc2b73..f755e89 100644 --- a/pkg/repository/repository_suite_test.go +++ b/pkg/repository/repository_suite_test.go @@ -25,6 +25,7 @@ type initializeData struct { } var ( + _ = log.Logger hook *test.Hook pgCloseFunc func() error @@ -37,8 +38,12 @@ var ( func TestRepository(t *testing.T) { var err error - SynchronizedBeforeSuite(func(ctx SpecContext) []byte { + + BeforeEach(func() { log.Logger, hook = test.NewNullLogger() + }) + SynchronizedBeforeSuite(func(ctx SpecContext) []byte { + // log.Logger, hook = test.NewNullLogger() var pgPort string pgCloseFunc, pgPort, err = testsro.SetupPostgresWithDocker() @@ -79,7 +84,7 @@ func TestRepository(t *testing.T) { return buf.Bytes() }, func(ctx SpecContext, inBytes []byte) { - log.Logger, hook = test.NewNullLogger() + // log.Logger, hook = test.NewNullLogger() dec := gob.NewDecoder(bytes.NewBuffer(inBytes)) Expect(dec.Decode(&data)).To(Succeed()) @@ -93,10 +98,6 @@ func TestRepository(t *testing.T) { Expect(migrater).NotTo(BeNil()) }) - BeforeEach(func() { - log.Logger, hook = test.NewNullLogger() - }) - SynchronizedAfterSuite(func() { }, func() { pgCloseFunc() diff --git a/pkg/service/character.go b/pkg/service/character.go index 1ee5358..58cf49c 100644 --- a/pkg/service/character.go +++ b/pkg/service/character.go @@ -15,9 +15,9 @@ var ( ) type CharacterService interface { - GetCharacters(ctx context.Context) (character.Characters, error) - GetDeletedCharacters(ctx context.Context) (character.Characters, error) - GetCharactersByOwner(ctx context.Context, ownerId string) (character.Characters, error) + GetCharacters(ctx context.Context) (character.Characters, int, error) + GetDeletedCharacters(ctx context.Context) (character.Characters, int, error) + GetCharactersByOwner(ctx context.Context, ownerId string) (character.Characters, int, error) GetCharacterById(ctx context.Context, characterId *uuid.UUID) (*character.Character, error) @@ -90,24 +90,24 @@ func (c *characterService) EditCharacter(ctx context.Context, character *charact // GetCharacterById implements CharacterService. func (c *characterService) GetCharacterById(ctx context.Context, characterId *uuid.UUID) (*character.Character, error) { - return c.repo.GetCharacterById(ctx, characterId) + return c.repo.GetCharacter(ctx, map[string]interface{}{"id": characterId}) } // GetCharacters implements CharacterService. -func (c *characterService) GetCharacters(ctx context.Context) (character.Characters, error) { - return c.repo.GetCharacters(ctx) +func (c *characterService) GetCharacters(ctx context.Context) (character.Characters, int, error) { + return c.repo.GetCharacters(ctx, nil, nil, false) } // GetDeletedCharacters implements CharacterService. -func (c *characterService) GetDeletedCharacters(ctx context.Context) (character.Characters, error) { - return c.repo.GetDeletedCharacters(ctx) +func (c *characterService) GetDeletedCharacters(ctx context.Context) (character.Characters, int, error) { + return c.repo.GetCharacters(ctx, nil, nil, true) } // GetCharactersByOwner implements CharacterService. -func (c *characterService) GetCharactersByOwner(ctx context.Context, ownerId string) (character.Characters, error) { +func (c *characterService) GetCharactersByOwner(ctx context.Context, ownerId string) (character.Characters, int, error) { id, err := uuid.Parse(ownerId) if err != nil { - return nil, ErrInvalidOwnerId + return nil, -1, ErrInvalidOwnerId } - return c.repo.GetCharactersByOwner(ctx, &id) + return c.repo.GetCharacters(ctx, map[string]interface{}{"owner_id": id}, nil, false) } diff --git a/pkg/service/character_test.go b/pkg/service/character_test.go index d65dba4..c345d25 100644 --- a/pkg/service/character_test.go +++ b/pkg/service/character_test.go @@ -146,13 +146,13 @@ var _ = Describe("CharacterS", func() { }) Describe("GetCharacterById", func() { It("should fail if the repo fails", func(ctx SpecContext) { - repo.EXPECT().GetCharacterById(gomock.Eq(ctx), gomock.Eq(&c.Id)).Return(nil, errors.New("repo")) + repo.EXPECT().GetCharacter(gomock.Eq(ctx), gomock.Any()).Return(nil, errors.New("repo")) outC, err := svc.GetCharacterById(ctx, &c.Id) Expect(err).To(HaveOccurred()) Expect(outC).To(BeNil()) }) It("should return the results of the repo if it succeeds", func(ctx SpecContext) { - repo.EXPECT().GetCharacterById(gomock.Eq(ctx), gomock.Eq(&c.Id)).Return(c, nil) + repo.EXPECT().GetCharacter(gomock.Eq(ctx), gomock.Any()).Return(c, nil) outC, err := svc.GetCharacterById(ctx, &c.Id) Expect(err).NotTo(HaveOccurred()) Expect(outC).NotTo(BeNil()) @@ -161,14 +161,14 @@ var _ = Describe("CharacterS", func() { }) Describe("GetCharacters", func() { It("should fail if the character does not exist", func(ctx SpecContext) { - repo.EXPECT().GetCharacters(gomock.Eq(ctx)).Return(nil, errors.New("repo")) - outChars, err := svc.GetCharacters(ctx) + repo.EXPECT().GetCharacters(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Eq(false)).Return(nil, -1, errors.New("repo")) + outChars, _, err := svc.GetCharacters(ctx) Expect(err).To(HaveOccurred()) Expect(outChars).To(BeNil()) }) It("should return the results of the repo if it succeeds", func(ctx SpecContext) { - repo.EXPECT().GetCharacters(gomock.Eq(ctx)).Return(character.Characters{c}, nil) - outChars, err := svc.GetCharacters(ctx) + repo.EXPECT().GetCharacters(gomock.Eq(ctx), gomock.Any(), gomock.Any(), gomock.Eq(false)).Return(character.Characters{c}, 1, nil) + outChars, _, err := svc.GetCharacters(ctx) Expect(err).NotTo(HaveOccurred()) Expect(outChars).NotTo(BeNil()) Expect(outChars).To(HaveLen(1)) @@ -177,14 +177,14 @@ var _ = Describe("CharacterS", func() { }) Describe("GetCharactersByOwner", func() { It("should fail if the character does not exist", func(ctx SpecContext) { - repo.EXPECT().GetCharactersByOwner(gomock.Eq(ctx), gomock.Eq(&c.OwnerId)).Return(nil, errors.New("repo")) - outChars, err := svc.GetCharactersByOwner(ctx, c.OwnerId.String()) + repo.EXPECT().GetCharacters(gomock.Eq(ctx), gomock.Any(), gomock.Any(), false).Return(nil, -1, errors.New("repo")) + outChars, _, err := svc.GetCharactersByOwner(ctx, c.OwnerId.String()) Expect(err).To(HaveOccurred()) Expect(outChars).To(BeNil()) }) It("should return the results of the repo if it succeeds", func(ctx SpecContext) { - repo.EXPECT().GetCharactersByOwner(gomock.Eq(ctx), gomock.Eq(&c.OwnerId)).Return(character.Characters{c}, nil) - outChars, err := svc.GetCharactersByOwner(ctx, c.OwnerId.String()) + repo.EXPECT().GetCharacters(gomock.Eq(ctx), gomock.Any(), gomock.Any(), false).Return(character.Characters{c}, 1, nil) + outChars, _, err := svc.GetCharactersByOwner(ctx, c.OwnerId.String()) Expect(err).NotTo(HaveOccurred()) Expect(outChars).NotTo(BeNil()) Expect(outChars).To(HaveLen(1)) diff --git a/pkg/service/mocks/character.go b/pkg/service/mocks/character.go index b2ce347..2fc09b6 100644 --- a/pkg/service/mocks/character.go +++ b/pkg/service/mocks/character.go @@ -118,12 +118,13 @@ func (mr *MockCharacterServiceMockRecorder) GetCharacterById(ctx, characterId an } // GetCharacters mocks base method. -func (m *MockCharacterService) GetCharacters(ctx context.Context) (character.Characters, error) { +func (m *MockCharacterService) GetCharacters(ctx context.Context) (character.Characters, int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetCharacters", ctx) ret0, _ := ret[0].(character.Characters) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetCharacters indicates an expected call of GetCharacters. @@ -133,12 +134,13 @@ func (mr *MockCharacterServiceMockRecorder) GetCharacters(ctx any) *gomock.Call } // GetCharactersByOwner mocks base method. -func (m *MockCharacterService) GetCharactersByOwner(ctx context.Context, ownerId string) (character.Characters, error) { +func (m *MockCharacterService) GetCharactersByOwner(ctx context.Context, ownerId string) (character.Characters, int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetCharactersByOwner", ctx, ownerId) ret0, _ := ret[0].(character.Characters) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetCharactersByOwner indicates an expected call of GetCharactersByOwner. @@ -148,12 +150,13 @@ func (mr *MockCharacterServiceMockRecorder) GetCharactersByOwner(ctx, ownerId an } // GetDeletedCharacters mocks base method. -func (m *MockCharacterService) GetDeletedCharacters(ctx context.Context) (character.Characters, error) { +func (m *MockCharacterService) GetDeletedCharacters(ctx context.Context) (character.Characters, int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetDeletedCharacters", ctx) ret0, _ := ret[0].(character.Characters) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret1, _ := ret[1].(int) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 } // GetDeletedCharacters indicates an expected call of GetDeletedCharacters. diff --git a/pkg/srv/context.go b/pkg/srv/context.go index f4a22c1..689ebf9 100644 --- a/pkg/srv/context.go +++ b/pkg/srv/context.go @@ -67,11 +67,11 @@ func (c *CharacterContext) ResetCharacterBus() commonsrv.WriterResetCallback { return func(ctx context.Context) error { ctx, span := c.Context.Tracer.Start(ctx, "character.reset_character_bus") defer span.End() - chars, err := c.CharacterService.GetCharacters(ctx) + chars, _, err := c.CharacterService.GetCharacters(ctx) if err != nil { return fmt.Errorf("get characters: %w", err) } - deletedChars, err := c.CharacterService.GetDeletedCharacters(ctx) + deletedChars, _, err := c.CharacterService.GetDeletedCharacters(ctx) if err != nil { return fmt.Errorf("get deleted characters: %w", err) } diff --git a/pkg/srv/context_test.go b/pkg/srv/context_test.go index 187a134..26b07a3 100644 --- a/pkg/srv/context_test.go +++ b/pkg/srv/context_test.go @@ -39,21 +39,21 @@ var _ = Describe("CharacterContext", func() { expectedErr := errors.New(faker.Username()) It("should return an error if getting characters fails", func(ctx SpecContext) { - mockCharService.EXPECT().GetCharacters(gomock.Any()).Return(nil, expectedErr) + mockCharService.EXPECT().GetCharacters(gomock.Any()).Return(nil, -1, expectedErr) outErr = fn(ctx) }) When("getting characters succeeded", func() { BeforeEach(func() { - mockCharService.EXPECT().GetCharacters(gomock.Any()).Return([]*character.Character{NewCharacter()}, nil) + mockCharService.EXPECT().GetCharacters(gomock.Any()).Return([]*character.Character{NewCharacter()}, 1, nil) }) It("should error if getting deleted characters fails", func(ctx SpecContext) { - mockCharService.EXPECT().GetDeletedCharacters(gomock.Any()).Return(nil, expectedErr) + mockCharService.EXPECT().GetDeletedCharacters(gomock.Any()).Return(nil, -1, expectedErr) outErr = fn(ctx) }) When("getting deleted characters succeeds", func() { BeforeEach(func() { - mockCharService.EXPECT().GetDeletedCharacters(gomock.Any()).Return([]*character.Character{NewCharacter()}, nil) + mockCharService.EXPECT().GetDeletedCharacters(gomock.Any()).Return([]*character.Character{NewCharacter()}, 1, nil) }) It("should return error when writing to the bus fails", func(ctx SpecContext) { characterBusWriterMock.EXPECT().PublishMany(gomock.Any(), gomock.Any()).Return(expectedErr) @@ -78,8 +78,8 @@ var _ = Describe("CharacterContext", func() { GinkgoWriter.Printf("Deleted character: %v\n", deletedChars[idx].Id) } - mockCharService.EXPECT().GetCharacters(gomock.Any()).Return(existingChars, nil) - mockCharService.EXPECT().GetDeletedCharacters(gomock.Any()).Return(deletedChars, nil) + mockCharService.EXPECT().GetCharacters(gomock.Any()).Return(existingChars, len(existingChars), nil) + mockCharService.EXPECT().GetDeletedCharacters(gomock.Any()).Return(deletedChars, len(existingChars), nil) characterBusWriterMock.EXPECT(). PublishMany(gomock.Any(), gomock.Any()). Return(nil). diff --git a/pkg/srv/grpc.go b/pkg/srv/grpc.go index 27f2bb2..e6ec012 100644 --- a/pkg/srv/grpc.go +++ b/pkg/srv/grpc.go @@ -295,38 +295,53 @@ func (s *characterServiceServer) EditCharacter(ctx context.Context, request *pb. // GetCharacter implements pb.CharacterServiceServer. func (s *characterServiceServer) GetCharacter(ctx context.Context, request *pb.GetCharacterRequest) (*pb.Character, error) { + err := s.validateMaskRequest(ctx, request.Mask.Paths) + if err != nil { + return nil, err + } + character, err := s.validateCharacterPermissions(ctx, request.Id, RoleGetCharactersSelf, RoleGetCharactersAll) if err != nil { return nil, err } - return character.ToPb(), nil + return character.ToPbWithMask(request.Mask.Paths) } // GetCharacters implements pb.CharacterServiceServer. func (s *characterServiceServer) GetCharacters(ctx context.Context, request *pb.GetCharactersRequest) (*pb.Characters, error) { - err := s.validateRole(ctx, RoleGetCharactersAll) + err := s.validateMaskRequest(ctx, request.Mask.Paths) + if err != nil { + return nil, err + } + + err = s.validateRole(ctx, RoleGetCharactersAll) if err != nil { return nil, err } - characters, err := s.Context.CharacterService.GetCharacters(ctx) + characters, _, err := s.Context.CharacterService.GetCharacters(ctx) if err != nil { log.Logger.WithContext(ctx).Errorf("code %v: %e", ErrCharacterGet, err) return nil, status.Error(codes.Internal, ErrCharacterGet.Error()) } - return characters.ToPb(), nil + return characters.ToPbWithMask(request.Mask.Paths) } // GetCharactersForUser implements pb.CharacterServiceServer. func (s *characterServiceServer) GetCharactersForUser(ctx context.Context, request *pb.GetUserCharactersRequest) (*pb.Characters, error) { - err := s.validateUserPermissions(ctx, request.OwnerId, RoleGetCharactersSelf, RoleGetCharactersAll) + err := s.validateMaskRequest(ctx, request.Mask.Paths) if err != nil { return nil, err } - characters, err := s.Context.CharacterService.GetCharactersByOwner(ctx, request.OwnerId) + err = s.validateUserPermissions(ctx, request.OwnerId, RoleGetCharactersSelf, RoleGetCharactersAll) + if err != nil { + return nil, err + } + + characters, _, err := s.Context.CharacterService.GetCharactersByOwner(ctx, request.OwnerId) if err != nil { log.Logger.WithContext(ctx).Errorf("code %v: %v", ErrCharacterGet, err) return nil, status.Error(codes.Internal, ErrCharacterGet.Error()) @@ -334,3 +349,23 @@ func (s *characterServiceServer) GetCharactersForUser(ctx context.Context, reque return characters.ToPb(), nil } + +func (s *characterServiceServer) validateMaskRequest(ctx context.Context, paths []string) error { + if len(paths) == 0 { + err := s.validateRole(ctx, RoleEditCharacter) + if err != nil { + return err + } + } + + for _, path := range paths { + if path == "play_time" || path == "updated_at" || path == "location" { + err := s.validateRole(ctx, RoleEditCharacter) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/srv/grpc_test.go b/pkg/srv/grpc_test.go index 869dcb9..48a9777 100644 --- a/pkg/srv/grpc_test.go +++ b/pkg/srv/grpc_test.go @@ -17,6 +17,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/ShatteredRealms/character-service/pkg/model/character" "github.com/ShatteredRealms/character-service/pkg/model/game" @@ -259,7 +260,8 @@ var _ = Describe("Grpc Server", func() { function: "GetCharacter (owner)", fn: func(ctx context.Context) (any, error) { return server.GetCharacter(ctx, &pb.GetCharacterRequest{ - Id: userChar.Id.String(), + Id: userChar.Id.String(), + Mask: &fieldmaskpb.FieldMask{}, }) }, roles: []*gocloak.Role{ @@ -272,7 +274,8 @@ var _ = Describe("Grpc Server", func() { fn: func(ctx context.Context) (any, error) { mockCharService.EXPECT().GetCharacterById(ctx, &adminChar.Id).Return(adminChar, nil).AnyTimes() return server.GetCharacter(ctx, &pb.GetCharacterRequest{ - Id: adminChar.Id.String(), + Id: adminChar.Id.String(), + Mask: &fieldmaskpb.FieldMask{}, }) }, roles: []*gocloak.Role{ @@ -283,7 +286,9 @@ var _ = Describe("Grpc Server", func() { { function: "GetCharacters", fn: func(ctx context.Context) (any, error) { - return server.GetCharacters(ctx, &pb.GetCharactersRequest{}) + return server.GetCharacters(ctx, &pb.GetCharactersRequest{ + Mask: &fieldmaskpb.FieldMask{}, + }) }, roles: []*gocloak.Role{ srv.RoleGetCharactersAll, @@ -295,6 +300,7 @@ var _ = Describe("Grpc Server", func() { fn: func(ctx context.Context) (any, error) { return server.GetCharactersForUser(ctx, &pb.GetUserCharactersRequest{ OwnerId: userChar.OwnerId.String(), + Mask: &fieldmaskpb.FieldMask{}, }) }, roles: []*gocloak.Role{ @@ -307,6 +313,7 @@ var _ = Describe("Grpc Server", func() { fn: func(ctx context.Context) (any, error) { return server.GetCharactersForUser(ctx, &pb.GetUserCharactersRequest{ OwnerId: adminChar.Id.String(), + Mask: &fieldmaskpb.FieldMask{}, }) }, roles: []*gocloak.Role{ diff --git a/sro.character.json b/sro.character.json index 200d5c1..ea17314 100644 --- a/sro.character.json +++ b/sro.character.json @@ -292,7 +292,7 @@ "properties": { "filters": { "properties": { - "count": { + "limit": { "format": "int32", "type": "integer" }, @@ -322,7 +322,7 @@ "properties": { "filters": { "properties": { - "count": { + "limit": { "format": "int32", "type": "integer" },