diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..e115817 --- /dev/null +++ b/.env.template @@ -0,0 +1,6 @@ +APP_PORT=3003 +APP_ENV=development + +DB_URL=postgres://root:root@localhost:5432/johnjud_db + +SERVICE_FILE=localhost:3004 diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..7748e23 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,13 @@ +## Change made + +- [ ]  New features +- [ ]  Bug fixes +- [ ]  Breaking changes +## Describe what you have done +- +### New Features +- +### Fix +- +### Others +- \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8796d6..a0e9ba1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,7 +35,7 @@ jobs: - name: Set branch id: branch run: | - echo "::set-output name=BRANCH::${GITHUB_REF#refs/heads/}" + echo "BRANCH=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT - name: Log in to the Container Registry uses: docker/login-action@v1 @@ -48,6 +48,7 @@ jobs: uses: docker/build-push-action@v2 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }},${{ env.IMAGE_NAME }}:latest cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache diff --git a/.gitignore b/.gitignore index e921962..572dda1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ config.yaml .env .env.prod +.env.file # idea .idea diff --git a/Makefile b/Makefile index bc834cc..0dce9f5 100644 --- a/Makefile +++ b/Makefile @@ -13,4 +13,4 @@ test: go tool cover -html=coverage.out -o coverage.html server: - go run ./src/. + . ./tools/export-env.sh ; go run ./src/. diff --git a/config/config.example.yaml b/config/config.example.yaml deleted file mode 100644 index 34bc114..0000000 --- a/config/config.example.yaml +++ /dev/null @@ -1,13 +0,0 @@ -app: - port: 3003 - debug: true - -database: - host: localhost - port: 5432 - name: johnjud_db - username: root - password: root - -service: - file: localhost:3004 \ No newline at end of file diff --git a/config/file/config.example.yaml b/config/file/config.example.yaml deleted file mode 100644 index f1ecd10..0000000 --- a/config/file/config.example.yaml +++ /dev/null @@ -1,14 +0,0 @@ -app: - port: 3004 - debug: true - -database: - host: local-db - port: 5432 - name: johnjud_db - username: root - password: root - -s3: - bucket_name: - region: \ No newline at end of file diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml new file mode 100644 index 0000000..8d80e27 --- /dev/null +++ b/docker-compose-prod.yaml @@ -0,0 +1,55 @@ +version: "3.9" + +services: + backend: + container_name: johnjud-backend + restart: unless-stopped + build: . + ports: + - 3003:3003 + environment: + - APP_PORT=3003 + - APP_ENV=production + - DB_URL=postgres://root:root@johnjud-local-db:5432/johnjud_db + - SERVICE_FILE=localhost:3004 + networks: + - johnjud + local-file: + image: ghcr.io/isd-sgcu/johnjud-file + container_name: johnjud-file + depends_on: + - local-db + restart: unless-stopped + environment: + - APP_PORT=3004 + - APP_ENV=production + - DB_URL=postgres://root:root@johnjud-local-db:5432/johnjud_db + - BUCKET_ENDPOINT=BUCKET_ENDPOINT + - BUCKET_ACCESS_KEY=BUCKET_ACCESS_KEY + - BUCKET_SECRET_KEY=BUCKET_SECRET_KEY + - BUCKET_NAME=johnjud-pet-images + - BUCKET_USE_SSL=false + ports: + - "3004:3004" + networks: + - johnjud + + local-db: + image: postgres:15.1-alpine3.17 + container_name: johnjud-local-db + restart: unless-stopped + environment: + POSTGRES_USER: root + POSTGRES_PASSWORD: root + POSTGRES_DB: johnjud_db + volumes: + - postgres:/var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - johnjud + +volumes: + postgres: +networks: + johnjud: \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 707a72e..c40bdfe 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,14 +3,23 @@ version: "3.9" services: local-file: image: ghcr.io/isd-sgcu/johnjud-file - container_name: file + container_name: johnjud-file depends_on: - local-db restart: unless-stopped - volumes: - - ./config/file:/app/config + environment: + - APP_PORT=3004 + - APP_ENV=development + - DB_URL=postgres://root:root@johnjud-local-db:5432/johnjud_db + - BUCKET_ENDPOINT=BUCKET_ENDPOINT + - BUCKET_ACCESS_KEY=BUCKET_ACCESS_KEY + - BUCKET_SECRET_KEY=BUCKET_SECRET_KEY + - BUCKET_NAME=johnjud-pet-images + - BUCKET_USE_SSL=false ports: - "3004:3004" + networks: + - johnjud local-db: image: postgres:15.1-alpine3.17 @@ -24,6 +33,10 @@ services: - postgres:/var/lib/postgresql/data ports: - "5432:5432" + networks: + - johnjud volumes: - postgres: \ No newline at end of file + postgres: +networks: + johnjud: \ No newline at end of file diff --git a/go.mod b/go.mod index ade5ec4..ba7e136 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,9 @@ toolchain go1.21.5 require ( github.com/bxcodec/faker/v3 v3.8.1 github.com/google/uuid v1.5.0 - github.com/isd-sgcu/johnjud-go-proto v0.0.9 - github.com/pkg/errors v0.9.1 + github.com/isd-sgcu/johnjud-go-proto v0.5.0 github.com/rs/zerolog v1.31.0 - github.com/spf13/viper v1.18.2 + github.com/spf13/viper v1.18.1 github.com/stretchr/testify v1.8.4 google.golang.org/grpc v1.60.1 gorm.io/driver/postgres v1.5.4 diff --git a/go.sum b/go.sum index fc7670b..458a30d 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/isd-sgcu/johnjud-go-proto v0.0.9 h1:cFfZ2JSpW0jg94Iv5zHQJGnoekj0eCQe42SJaTpnp3c= -github.com/isd-sgcu/johnjud-go-proto v0.0.9/go.mod h1:1OK6aiCgtXQiLhxp0r6iLEejYIRpckWQZDrCZ9Trbo4= +github.com/isd-sgcu/johnjud-go-proto v0.5.0 h1:GgqRzWjya5p1yhfU/kpX8i4WL42+qT2TkyXZmssH6B4= +github.com/isd-sgcu/johnjud-go-proto v0.5.0/go.mod h1:1OK6aiCgtXQiLhxp0r6iLEejYIRpckWQZDrCZ9Trbo4= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= @@ -47,7 +47,6 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -69,8 +68,8 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM= +github.com/spf13/viper v1.18.1/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= diff --git a/src/app/model/pet/pet.model.go b/src/app/model/pet/pet.model.go index 3c88a37..ceff18a 100644 --- a/src/app/model/pet/pet.model.go +++ b/src/app/model/pet/pet.model.go @@ -8,18 +8,18 @@ import ( type Pet struct { model.Base Type string `json:"type" gorm:"tinytext"` - Species string `json:"species" gorm:"tinytext"` Name string `json:"name" gorm:"tinytext"` Birthdate string `json:"birthdate" gorm:"tinytext"` Gender pet.Gender `json:"gender" gorm:"tinytext" example:"male"` + Color string `json:"color" gorm:"tinytext"` + Pattern string `json:"pattern" gorm:"tinytext"` Habit string `json:"habit" gorm:"mediumtext"` Caption string `json:"caption" gorm:"mediumtext"` Status pet.Status `json:"status" gorm:"mediumtext" example:"findhome"` IsSterile bool `json:"is_sterile"` IsVaccinated bool `json:"is_vaccine"` IsVisible bool `json:"is_visible"` - IsClubPet bool `json:"is_club_pet"` - Background string `json:"background" gorm:"tinytext"` + Origin string `json:"origin" gorm:"tinytext"` Address string `json:"address" gorm:"tinytext"` Contact string `json:"contact" gorm:"tinytext"` AdoptBy string `json:"adopt_by" gorm:"tinytext"` diff --git a/src/app/repository/pet/pet.repository.go b/src/app/repository/pet/pet.repository.go index 2ee80f1..0f72ae1 100644 --- a/src/app/repository/pet/pet.repository.go +++ b/src/app/repository/pet/pet.repository.go @@ -18,7 +18,7 @@ func (r *Repository) FindAll(result *[]*pet.Pet) error { } func (r *Repository) FindOne(id string, result *pet.Pet) error { - return r.db.Model(&pet.Pet{}).Find(result, "id = ?", id).Error + return r.db.Model(&pet.Pet{}).First(result, "id = ?", id).Error } func (r *Repository) Create(in *pet.Pet) error { diff --git a/src/app/service/pet/pet.service.go b/src/app/service/pet/pet.service.go index 552f81b..92f4fcd 100644 --- a/src/app/service/pet/pet.service.go +++ b/src/app/service/pet/pet.service.go @@ -3,13 +3,10 @@ package pet import ( "context" "errors" + "fmt" - "time" - - "github.com/google/uuid" - "github.com/isd-sgcu/johnjud-backend/src/app/model" "github.com/isd-sgcu/johnjud-backend/src/app/model/pet" - petConst "github.com/isd-sgcu/johnjud-backend/src/constant/pet" + petUtils "github.com/isd-sgcu/johnjud-backend/src/app/utils/pet" proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/backend/pet/v1" image_proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/file/image/v1" "github.com/rs/zerolog/log" @@ -25,11 +22,11 @@ type Service struct { } type IRepository interface { - FindAll(result *[]*pet.Pet) error - FindOne(id string, result *pet.Pet) error - Create(in *pet.Pet) error - Update(id string, result *pet.Pet) error - Delete(id string) error + FindAll(*[]*pet.Pet) error + FindOne(string, *pet.Pet) error + Create(*pet.Pet) error + Update(string, *pet.Pet) error + Delete(string) error } type ImageService interface { @@ -52,7 +49,7 @@ func (s *Service) Delete(ctx context.Context, req *proto.DeletePetRequest) (*pro } func (s *Service) Update(_ context.Context, req *proto.UpdatePetRequest) (res *proto.UpdatePetResponse, err error) { - raw, err := DtoToRaw(req.Pet) + raw, err := petUtils.DtoToRaw(req.Pet) if err != nil { return nil, status.Error(codes.Internal, "error converting dto to raw") } @@ -66,9 +63,8 @@ func (s *Service) Update(_ context.Context, req *proto.UpdatePetRequest) (res *p if err != nil { return nil, status.Error(codes.Internal, "error querying image service") } - imageUrls := ExtractImageUrls(images) - return &proto.UpdatePetResponse{Pet: RawToDto(raw, imageUrls)}, nil + return &proto.UpdatePetResponse{Pet: petUtils.RawToDto(raw, images)}, nil } func (s *Service) ChangeView(_ context.Context, req *proto.ChangeViewPetRequest) (res *proto.ChangeViewPetResponse, err error) { @@ -76,7 +72,7 @@ func (s *Service) ChangeView(_ context.Context, req *proto.ChangeViewPetRequest) if err != nil { return nil, status.Error(codes.NotFound, "pet not found") } - pet, err := DtoToRaw(petData.Pet) + pet, err := petUtils.DtoToRaw(petData.Pet) if err != nil { return nil, status.Error(codes.Internal, "error converting dto to raw") } @@ -92,7 +88,8 @@ func (s *Service) ChangeView(_ context.Context, req *proto.ChangeViewPetRequest) func (s *Service) FindAll(_ context.Context, req *proto.FindAllPetRequest) (res *proto.FindAllPetResponse, err error) { var pets []*pet.Pet - var imageUrlsList [][]string + var imagesList [][]*image_proto.Image + metaData := proto.FindAllPetMetaData{} err = s.repository.FindAll(&pets) if err != nil { @@ -100,21 +97,21 @@ func (s *Service) FindAll(_ context.Context, req *proto.FindAllPetRequest) (res return nil, status.Error(codes.Unavailable, "Internal error") } + petUtils.FilterPet(&pets, req) + petUtils.PaginatePets(&pets, req.Page, req.PageSize, &metaData) + for _, pet := range pets { images, err := s.imageService.FindByPetId(pet.ID.String()) if err != nil { return nil, status.Error(codes.Internal, "error querying image service") } - imageUrls := ExtractImageUrls(images) - imageUrlsList = append(imageUrlsList, imageUrls) + imagesList = append(imagesList, images) } - - petWithImageUrls, err := RawToDtoList(&pets, imageUrlsList) + petWithImages, err := petUtils.RawToDtoList(&pets, imagesList, req) if err != nil { - return nil, status.Error(codes.Internal, "error converting raw to dto list") + return nil, status.Error(codes.Internal, fmt.Sprintf("error converting raw to dto list: %v", err)) } - - return &proto.FindAllPetResponse{Pets: petWithImageUrls}, nil + return &proto.FindAllPetResponse{Pets: petWithImages, Metadata: &metaData}, nil } func (s Service) FindOne(_ context.Context, req *proto.FindOnePetRequest) (res *proto.FindOnePetResponse, err error) { @@ -131,25 +128,24 @@ func (s Service) FindOne(_ context.Context, req *proto.FindOnePetRequest) (res * if err != nil { return nil, status.Error(codes.Internal, "error querying image service") } - imageUrls := ExtractImageUrls(images) - return &proto.FindOnePetResponse{Pet: RawToDto(&pet, imageUrls)}, err + return &proto.FindOnePetResponse{Pet: petUtils.RawToDto(&pet, images)}, err } func (s *Service) Create(_ context.Context, req *proto.CreatePetRequest) (res *proto.CreatePetResponse, err error) { - raw, err := DtoToRaw(req.Pet) + raw, err := petUtils.DtoToRaw(req.Pet) if err != nil { return nil, status.Error(codes.Internal, "error converting dto to raw: "+err.Error()) } - imgUrls := []string{} + images := []*image_proto.Image{} err = s.repository.Create(raw) if err != nil { return nil, status.Error(codes.Internal, "failed to create pet") } - return &proto.CreatePetResponse{Pet: RawToDto(raw, imgUrls)}, nil + return &proto.CreatePetResponse{Pet: petUtils.RawToDto(raw, images)}, nil } func (s *Service) AdoptPet(ctx context.Context, req *proto.AdoptPetRequest) (res *proto.AdoptPetResponse, err error) { @@ -157,7 +153,7 @@ func (s *Service) AdoptPet(ctx context.Context, req *proto.AdoptPetRequest) (res if err != nil { return nil, status.Error(codes.NotFound, "pet not found") } - pet, err := DtoToRaw(dtoPet.Pet) + pet, err := petUtils.DtoToRaw(dtoPet.Pet) if err != nil { return nil, status.Error(codes.Internal, "error converting dto to raw") } @@ -170,98 +166,3 @@ func (s *Service) AdoptPet(ctx context.Context, req *proto.AdoptPetRequest) (res return &proto.AdoptPetResponse{Success: true}, nil } - -func RawToDtoList(in *[]*pet.Pet, imageUrls [][]string) ([]*proto.Pet, error) { - var result []*proto.Pet - if len(*in) != len(imageUrls) { - return nil, errors.New("length of in and imageUrls have to be the same") - } - - for i, e := range *in { - result = append(result, RawToDto(e, imageUrls[i])) - } - return result, nil -} - -func RawToDto(in *pet.Pet, imgUrl []string) *proto.Pet { - return &proto.Pet{ - Id: in.ID.String(), - Type: in.Type, - Species: in.Species, - Name: in.Name, - Birthdate: in.Birthdate, - Gender: proto.Gender(in.Gender), - Habit: in.Habit, - Caption: in.Caption, - Status: proto.PetStatus(in.Status), - ImageUrls: imgUrl, - IsSterile: in.IsSterile, - IsVaccinated: in.IsVaccinated, - IsVisible: in.IsVisible, - IsClubPet: in.IsClubPet, - Background: in.Background, - Address: in.Address, - Contact: in.Contact, - AdoptBy: in.AdoptBy, - } -} - -func DtoToRaw(in *proto.Pet) (res *pet.Pet, err error) { - var id uuid.UUID - var gender petConst.Gender - var status petConst.Status - - if in.Id != "" { - id, err = uuid.Parse(in.Id) - if err != nil { - return nil, err - } - } - - switch in.Gender { - case 0: - gender = petConst.MALE - case 1: - gender = petConst.FEMALE - } - - switch in.Status { - case 0: - status = petConst.ADOPTED - case 1: - status = petConst.FINDHOME - } - - return &pet.Pet{ - Base: model.Base{ - ID: id, - CreatedAt: time.Time{}, - UpdatedAt: time.Time{}, - DeletedAt: gorm.DeletedAt{}, - }, - Type: in.Type, - Species: in.Species, - Name: in.Name, - Birthdate: in.Birthdate, - Gender: gender, - Habit: in.Habit, - Caption: in.Caption, - Status: status, - IsSterile: in.IsSterile, - IsVaccinated: in.IsVaccinated, - IsVisible: in.IsVisible, - IsClubPet: in.IsClubPet, - Background: in.Background, - Address: in.Address, - Contact: in.Contact, - AdoptBy: in.AdoptBy, - }, nil -} - -func ExtractImageUrls(in []*image_proto.Image) []string { - var result []string - for _, e := range in { - result = append(result, e.ImageUrl) - } - return result -} diff --git a/src/app/service/pet/pet.service_test.go b/src/app/service/pet/pet.service_test.go index 04d36b9..fa75c0c 100644 --- a/src/app/service/pet/pet.service_test.go +++ b/src/app/service/pet/pet.service_test.go @@ -37,9 +37,7 @@ type PetServiceTest struct { UpdatePetReqMock *proto.UpdatePetRequest ChangeViewPetReqMock *proto.ChangeViewPetRequest Images []*img_proto.Image - ImageUrls []string ImagesList [][]*img_proto.Image - ImageUrlsList [][]string ChangeAdoptBy *pet.Pet AdoptByReq *proto.AdoptPetRequest } @@ -50,6 +48,9 @@ func TestPetService(t *testing.T) { func (t *PetServiceTest) SetupTest() { var pets []*pet.Pet + genders := []petConst.Gender{petConst.MALE, petConst.FEMALE} + statuses := []petConst.Status{petConst.ADOPTED, petConst.FINDHOME} + for i := 0; i <= 3; i++ { pet := &pet.Pet{ Base: model.Base{ @@ -59,18 +60,18 @@ func (t *PetServiceTest) SetupTest() { DeletedAt: gorm.DeletedAt{}, }, Type: faker.Word(), - Species: faker.Word(), Name: faker.Name(), Birthdate: faker.Word(), - Gender: petConst.Gender(rand.Intn(1) + 1), + Gender: genders[rand.Intn(2)], + Color: faker.Word(), + Pattern: faker.Word(), Habit: faker.Paragraph(), Caption: faker.Paragraph(), - Status: petConst.Status(rand.Intn(1) + 1), + Status: statuses[rand.Intn(2)], IsSterile: true, IsVaccinated: true, IsVisible: true, - IsClubPet: true, - Background: faker.Paragraph(), + Origin: faker.Paragraph(), Address: faker.Paragraph(), Contact: faker.Paragraph(), AdoptBy: "", @@ -87,40 +88,31 @@ func (t *PetServiceTest) SetupTest() { imageUrls = append(imageUrls, url) } t.ImagesList = append(t.ImagesList, images) - t.ImageUrlsList = append(t.ImageUrlsList, imageUrls) pets = append(pets, pet) } t.Pets = pets t.Pet = pets[0] - - for _, images := range t.ImagesList { - for _, image := range images { - t.ImageUrls = append(t.ImageUrls, image.ImageUrl) - } - } - t.Images = t.ImagesList[0] - t.ImageUrls = t.ImageUrlsList[0] t.PetDto = &proto.Pet{ Id: t.Pet.ID.String(), Type: t.Pet.Type, - Species: t.Pet.Species, Name: t.Pet.Name, Birthdate: t.Pet.Birthdate, - Gender: proto.Gender(t.Pet.Gender), + Gender: string(t.Pet.Gender), + Color: t.Pet.Color, + Pattern: t.Pet.Pattern, Habit: t.Pet.Habit, Caption: t.Pet.Caption, - Status: proto.PetStatus(t.Pet.Status), + Status: string(t.Pet.Status), IsSterile: t.Pet.IsSterile, IsVaccinated: t.Pet.IsVaccinated, IsVisible: t.Pet.IsVisible, - IsClubPet: t.Pet.IsClubPet, - Background: t.Pet.Background, + Origin: t.Pet.Origin, Address: t.Pet.Address, Contact: t.Pet.Contact, - ImageUrls: t.ImageUrls, + Images: t.Images, } t.UpdatePet = &pet.Pet{ @@ -131,18 +123,18 @@ func (t *PetServiceTest) SetupTest() { DeletedAt: t.Pet.Base.DeletedAt, }, Type: t.Pet.Type, - Species: t.Pet.Species, Name: t.Pet.Name, Birthdate: t.Pet.Birthdate, Gender: t.Pet.Gender, + Color: t.Pet.Color, + Pattern: t.Pet.Pattern, Habit: t.Pet.Habit, Caption: t.Pet.Caption, Status: t.Pet.Status, IsSterile: t.Pet.IsSterile, IsVaccinated: t.Pet.IsVaccinated, IsVisible: t.Pet.IsVisible, - IsClubPet: t.Pet.IsClubPet, - Background: t.Pet.Background, + Origin: t.Pet.Origin, Address: t.Pet.Address, Contact: t.Pet.Contact, } @@ -155,18 +147,18 @@ func (t *PetServiceTest) SetupTest() { DeletedAt: t.Pet.Base.DeletedAt, }, Type: t.Pet.Type, - Species: t.Pet.Species, Name: t.Pet.Name, Birthdate: t.Pet.Birthdate, Gender: t.Pet.Gender, + Color: t.Pet.Color, + Pattern: t.Pet.Pattern, Habit: t.Pet.Habit, Caption: t.Pet.Caption, Status: t.Pet.Status, IsSterile: t.Pet.IsSterile, IsVaccinated: t.Pet.IsVaccinated, IsVisible: false, - IsClubPet: t.Pet.IsClubPet, - Background: t.Pet.Background, + Origin: t.Pet.Origin, Address: t.Pet.Address, Contact: t.Pet.Contact, } @@ -174,19 +166,19 @@ func (t *PetServiceTest) SetupTest() { t.CreatePetReqMock = &proto.CreatePetRequest{ Pet: &proto.Pet{ Type: t.Pet.Type, - Species: t.Pet.Species, Name: t.Pet.Name, Birthdate: t.Pet.Birthdate, - Gender: proto.Gender(t.Pet.Gender), + Gender: string(t.Pet.Gender), + Color: t.Pet.Color, + Pattern: t.Pet.Pattern, Habit: t.Pet.Habit, Caption: t.Pet.Caption, - Status: proto.PetStatus(t.Pet.Status), - ImageUrls: t.ImageUrls, + Status: string(t.Pet.Status), + Images: t.Images, IsSterile: t.Pet.IsSterile, IsVaccinated: t.Pet.IsVaccinated, IsVisible: t.Pet.IsVaccinated, - IsClubPet: t.Pet.IsClubPet, - Background: t.Pet.Background, + Origin: t.Pet.Origin, Address: t.Pet.Address, Contact: t.Pet.Contact, }, @@ -196,19 +188,19 @@ func (t *PetServiceTest) SetupTest() { Pet: &proto.Pet{ Id: t.Pet.ID.String(), Type: t.Pet.Type, - Species: t.Pet.Species, Name: t.Pet.Name, Birthdate: t.Pet.Birthdate, - Gender: proto.Gender(t.Pet.Gender), + Gender: string(t.Pet.Gender), + Color: t.Pet.Color, + Pattern: t.Pet.Pattern, Habit: t.Pet.Habit, Caption: t.Pet.Caption, - Status: proto.PetStatus(t.Pet.Status), - ImageUrls: t.ImageUrls, + Status: string(t.Pet.Status), + Images: t.Images, IsSterile: t.Pet.IsSterile, IsVaccinated: t.Pet.IsVaccinated, IsVisible: t.Pet.IsVisible, - IsClubPet: t.Pet.IsClubPet, - Background: t.Pet.Background, + Origin: t.Pet.Origin, Address: t.Pet.Address, Contact: t.Pet.Contact, }, @@ -227,18 +219,18 @@ func (t *PetServiceTest) SetupTest() { DeletedAt: t.Pet.Base.DeletedAt, }, Type: t.Pet.Type, - Species: t.Pet.Species, Name: t.Pet.Name, Birthdate: t.Pet.Birthdate, Gender: t.Pet.Gender, + Color: t.Pet.Color, + Pattern: t.Pet.Pattern, Habit: t.Pet.Habit, Caption: t.Pet.Caption, Status: t.Pet.Status, IsSterile: t.Pet.IsSterile, IsVaccinated: t.Pet.IsVaccinated, IsVisible: t.Pet.IsVisible, - IsClubPet: t.Pet.IsClubPet, - Background: t.Pet.Background, + Origin: t.Pet.Origin, Address: t.Pet.Address, Contact: t.Pet.Contact, AdoptBy: faker.UUIDDigit(), @@ -322,7 +314,15 @@ func (t *PetServiceTest) TestFindOneSuccess() { func (t *PetServiceTest) TestFindAllSuccess() { - want := &proto.FindAllPetResponse{Pets: t.createPetsDto(t.Pets, t.ImageUrlsList)} + want := &proto.FindAllPetResponse{ + Pets: t.createPetsDto(t.Pets, t.ImagesList), + Metadata: &proto.FindAllPetMetaData{ + Page: 1, + TotalPages: 1, + PageSize: int32(len(t.Pets)), + Total: int32(len(t.Pets)), + }, + } var petsIn []*pet.Pet @@ -337,7 +337,6 @@ func (t *PetServiceTest) TestFindAllSuccess() { srv := NewService(repo, imgSrv) actual, err := srv.FindAll(context.Background(), &proto.FindAllPetRequest{}) - assert.Nil(t.T(), err) assert.Equal(t.T(), want, actual) } @@ -360,6 +359,8 @@ func (t *PetServiceTest) TestFindOneNotFound() { func createPets() []*pet.Pet { var result []*pet.Pet + genders := []petConst.Gender{petConst.MALE, petConst.FEMALE} + statuses := []petConst.Status{petConst.ADOPTED, petConst.FINDHOME} for i := 0; i < rand.Intn(4)+1; i++ { r := &pet.Pet{ @@ -370,18 +371,18 @@ func createPets() []*pet.Pet { DeletedAt: gorm.DeletedAt{}, }, Type: faker.Word(), - Species: faker.Word(), Name: faker.Name(), Birthdate: faker.Word(), - Gender: petConst.Gender(rand.Intn(1) + 1), + Gender: genders[rand.Intn(2)], + Color: faker.Word(), + Pattern: faker.Word(), Habit: faker.Paragraph(), Caption: faker.Paragraph(), - Status: petConst.Status(rand.Intn(1) + 1), + Status: statuses[rand.Intn(2)], IsSterile: true, IsVaccinated: true, IsVisible: true, - IsClubPet: true, - Background: faker.Paragraph(), + Origin: faker.Paragraph(), Address: faker.Paragraph(), Contact: faker.Paragraph(), } @@ -391,26 +392,26 @@ func createPets() []*pet.Pet { return result } -func (t *PetServiceTest) createPetsDto(in []*pet.Pet, imageUrlsList [][]string) []*proto.Pet { +func (t *PetServiceTest) createPetsDto(in []*pet.Pet, imagesList [][]*img_proto.Image) []*proto.Pet { var result []*proto.Pet for i, p := range in { r := &proto.Pet{ Id: p.ID.String(), Type: p.Type, - Species: p.Species, Name: p.Name, Birthdate: p.Birthdate, - Gender: proto.Gender(p.Gender), + Gender: string(p.Gender), + Color: p.Color, + Pattern: p.Pattern, Habit: p.Habit, Caption: p.Caption, - Status: proto.PetStatus(p.Status), - ImageUrls: imageUrlsList[i], + Status: string(p.Status), + Images: imagesList[i], IsSterile: p.IsSterile, IsVaccinated: p.IsVaccinated, IsVisible: p.IsVisible, - IsClubPet: p.IsClubPet, - Background: p.Background, + Origin: p.Origin, Address: p.Address, Contact: p.Contact, } @@ -423,24 +424,24 @@ func (t *PetServiceTest) createPetsDto(in []*pet.Pet, imageUrlsList [][]string) func (t *PetServiceTest) TestCreateSuccess() { want := &proto.CreatePetResponse{Pet: t.PetDto} - want.Pet.ImageUrls = []string{} // when pet is first created, it has no images + want.Pet.Images = []*img_proto.Image{} // when pet is first created, it has no images repo := &mock.RepositoryMock{} in := &pet.Pet{ Type: t.Pet.Type, - Species: t.Pet.Species, Name: t.Pet.Name, Birthdate: t.Pet.Birthdate, Gender: t.Pet.Gender, + Color: t.Pet.Color, + Pattern: t.Pet.Pattern, Habit: t.Pet.Habit, Caption: t.Pet.Caption, Status: t.Pet.Status, IsSterile: t.Pet.IsSterile, IsVaccinated: t.Pet.IsVaccinated, IsVisible: t.Pet.IsVisible, - IsClubPet: t.Pet.IsClubPet, - Background: t.Pet.Background, + Origin: t.Pet.Origin, Address: t.Pet.Address, Contact: t.Pet.Contact, } @@ -461,18 +462,18 @@ func (t *PetServiceTest) TestCreateInternalErr() { in := &pet.Pet{ Type: t.Pet.Type, - Species: t.Pet.Species, Name: t.Pet.Name, Birthdate: t.Pet.Birthdate, Gender: t.Pet.Gender, + Color: t.Pet.Color, + Pattern: t.Pet.Pattern, Habit: t.Pet.Habit, Caption: t.Pet.Caption, Status: t.Pet.Status, IsSterile: t.Pet.IsSterile, IsVaccinated: t.Pet.IsVaccinated, IsVisible: t.Pet.IsVisible, - IsClubPet: t.Pet.IsClubPet, - Background: t.Pet.Background, + Origin: t.Pet.Origin, Address: t.Pet.Address, Contact: t.Pet.Contact, } diff --git a/src/app/utils/pet/pet.utils.go b/src/app/utils/pet/pet.utils.go new file mode 100644 index 0000000..8708acb --- /dev/null +++ b/src/app/utils/pet/pet.utils.go @@ -0,0 +1,206 @@ +package pet + +import ( + "errors" + "math" + "strings" + "time" + + "github.com/google/uuid" + "github.com/isd-sgcu/johnjud-backend/src/app/model" + "github.com/isd-sgcu/johnjud-backend/src/app/model/pet" + "github.com/isd-sgcu/johnjud-backend/src/constant" + petConst "github.com/isd-sgcu/johnjud-backend/src/constant/pet" + proto "github.com/isd-sgcu/johnjud-go-proto/johnjud/backend/pet/v1" + imageProto "github.com/isd-sgcu/johnjud-go-proto/johnjud/file/image/v1" + "gorm.io/gorm" +) + +func FilterPet(in *[]*pet.Pet, query *proto.FindAllPetRequest) error { + var results []*pet.Pet + for _, p := range *in { + res, err := filterAge(p, query.Age) + if err != nil { + return err + } + if !res { + continue + } + if query.Search != "" && !strings.Contains(p.Name, query.Search) { + continue + } + if query.Type != "" && p.Type != query.Type { + continue + } + if query.Gender != "" && p.Gender != petConst.Gender(query.Gender) { + continue + } + if query.Color != "" && p.Color != query.Color { + continue + } + if query.Pattern != "" && p.Pattern != query.Pattern { + continue + } + if query.Origin != "" && p.Origin != query.Origin { + continue + } + results = append(results, p) + } + *in = results + return nil +} + +func PaginatePets(pets *[]*pet.Pet, page int32, pageSize int32, metadata *proto.FindAllPetMetaData) error { + totalsPets := int32(len(*pets)) + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = totalsPets + } + start := (page - 1) * pageSize + end := start + pageSize + + if start > totalsPets { + *pets = []*pet.Pet{} + return nil + } + if end > totalsPets { + end = totalsPets + } + *pets = (*pets)[start:end] + + totalPages := int32(math.Ceil(float64(totalsPets) / float64(pageSize))) + + metadata.Page = page + metadata.PageSize = pageSize + metadata.Total = totalsPets + metadata.TotalPages = totalPages + return nil +} + +func RawToDtoList(in *[]*pet.Pet, images [][]*imageProto.Image, query *proto.FindAllPetRequest) ([]*proto.Pet, error) { + var result []*proto.Pet + if len(*in) != len(images) { + return nil, errors.New("length of in and imageUrls have to be the same") + } + + for i, p := range *in { + // TODO: create new filter image function this wont work + result = append(result, RawToDto(p, images[i])) + } + return result, nil +} + +func RawToDto(in *pet.Pet, images []*imageProto.Image) *proto.Pet { + return &proto.Pet{ + Id: in.ID.String(), + Type: in.Type, + Name: in.Name, + Birthdate: in.Birthdate, + Gender: string(in.Gender), + Color: in.Color, + Pattern: in.Pattern, + Habit: in.Habit, + Caption: in.Caption, + Status: string(in.Status), + Images: images, + IsSterile: in.IsSterile, + IsVaccinated: in.IsVaccinated, + IsVisible: in.IsVisible, + Origin: in.Origin, + Address: in.Address, + Contact: in.Contact, + AdoptBy: in.AdoptBy, + } +} + +func DtoToRaw(in *proto.Pet) (res *pet.Pet, err error) { + var id uuid.UUID + var gender petConst.Gender + var status petConst.Status + + if in.Id != "" { + id, err = uuid.Parse(in.Id) + if err != nil { + return nil, err + } + } + + switch in.Gender { + case string(petConst.MALE): + gender = petConst.MALE + case string(petConst.FEMALE): + gender = petConst.FEMALE + } + + switch in.Status { + case string(petConst.ADOPTED): + status = petConst.ADOPTED + case string(petConst.FINDHOME): + status = petConst.FINDHOME + } + + return &pet.Pet{ + Base: model.Base{ + ID: id, + CreatedAt: time.Time{}, + UpdatedAt: time.Time{}, + DeletedAt: gorm.DeletedAt{}, + }, + Type: in.Type, + Name: in.Name, + Birthdate: in.Birthdate, + Gender: gender, + Color: in.Color, + Pattern: in.Pattern, + Habit: in.Habit, + Caption: in.Caption, + Status: status, + IsSterile: in.IsSterile, + IsVaccinated: in.IsVaccinated, + IsVisible: in.IsVisible, + Origin: in.Origin, + Address: in.Address, + Contact: in.Contact, + AdoptBy: in.AdoptBy, + }, nil +} + +func ExtractImageUrls(in []*imageProto.Image) []string { + var result []string + for _, e := range in { + result = append(result, e.ImageUrl) + } + return result +} + +func parseDate(dateStr string) (time.Time, error) { + parsedTime, err := time.Parse(time.RFC3339, dateStr) + if err != nil { + return time.Time{}, err + } + return parsedTime, nil +} + +func filterAge(pet *pet.Pet, age string) (bool, error) { + birthdate, err := parseDate(pet.Birthdate) + if err != nil { + return false, err + } + + currYear := time.Now() + birthYear := birthdate + diff := currYear.Sub(birthYear).Hours() / constant.DAY / constant.YEAR + + switch age { + case "kitten": + return diff < 1, nil + case "adult": + return diff >= 1 && diff < 7, nil + case "senior": + return diff >= 7, nil + default: + return true, nil + } +} diff --git a/src/config/config.go b/src/config/config.go index fc6876a..9992687 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -1,50 +1,65 @@ package config import ( - "github.com/pkg/errors" "github.com/spf13/viper" ) type Database struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - Name string `mapstructure:"name"` - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - SSL string `mapstructure:"ssl"` + Url string `mapstructure:"URL"` } type App struct { - Port int `mapstructure:"port"` - Debug bool `mapstructure:"debug"` + Port int `mapstructure:"PORT"` + Env string `mapstructure:"ENV"` } type Service struct { - File string `mapstructure:"file"` + File string `mapstructure:"FILE"` } type Config struct { - App App `mapstructure:"app"` - Database Database `mapstructure:"database"` - Service Service `mapstructure:"service"` + App App + Database Database + Service Service } -func LoadConfig() (config *Config, err error) { - viper.AddConfigPath("./config") - viper.SetConfigName("config") - viper.SetConfigType("yaml") +func LoadConfig() (*Config, error) { + dbCfgLdr := viper.New() + dbCfgLdr.SetEnvPrefix("DB") + dbCfgLdr.AutomaticEnv() + dbCfgLdr.AllowEmptyEnv(false) + dbConfig := Database{} + if err := dbCfgLdr.Unmarshal(&dbConfig); err != nil { + return nil, err + } - viper.AutomaticEnv() + appCfgLdr := viper.New() + appCfgLdr.SetEnvPrefix("APP") + appCfgLdr.AutomaticEnv() + dbCfgLdr.AllowEmptyEnv(false) + appConfig := App{} + if err := appCfgLdr.Unmarshal(&appConfig); err != nil { + return nil, err + } - err = viper.ReadInConfig() - if err != nil { - return nil, errors.Wrap(err, "error occurs while reading the config") + serviceCfgLdr := viper.New() + serviceCfgLdr.SetEnvPrefix("SERVICE") + serviceCfgLdr.AutomaticEnv() + dbCfgLdr.AllowEmptyEnv(false) + serviceConfig := Service{} + if err := serviceCfgLdr.Unmarshal(&serviceConfig); err != nil { + return nil, err } - err = viper.Unmarshal(&config) - if err != nil { - return nil, errors.Wrap(err, "error occurs while unmarshal the config") + config := &Config{ + Database: dbConfig, + App: appConfig, + Service: serviceConfig, } - return + return config, nil +} + +func (ac *App) IsDevelopment() bool { + return ac.Env == "development" } diff --git a/src/constant/common.constant.go b/src/constant/common.constant.go new file mode 100644 index 0000000..1ac27a3 --- /dev/null +++ b/src/constant/common.constant.go @@ -0,0 +1,4 @@ +package constant + +const DAY = 24 +const YEAR = 365 diff --git a/src/constant/pet/pet.constant.go b/src/constant/pet/pet.constant.go index 577bed6..77e50c7 100644 --- a/src/constant/pet/pet.constant.go +++ b/src/constant/pet/pet.constant.go @@ -1,15 +1,15 @@ package pet -type Gender int +type Gender string const ( - MALE = 0 - FEMALE = 1 + MALE Gender = "male" + FEMALE Gender = "female" ) -type Status int +type Status string const ( - ADOPTED = 0 - FINDHOME = 1 + ADOPTED Status = "adopted" + FINDHOME Status = "findhome" ) diff --git a/src/database/postgresql.connection.go b/src/database/postgresql.connection.go index 8369137..2acc6de 100644 --- a/src/database/postgresql.connection.go +++ b/src/database/postgresql.connection.go @@ -1,9 +1,6 @@ package database import ( - "fmt" - "strconv" - "github.com/isd-sgcu/johnjud-backend/src/app/model/like" "github.com/isd-sgcu/johnjud-backend/src/app/model/pet" "github.com/isd-sgcu/johnjud-backend/src/app/model/user" @@ -14,15 +11,13 @@ import ( ) func InitPostgresDatabase(conf *config.Database, isDebug bool) (db *gorm.DB, err error) { - dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", conf.Host, strconv.Itoa(conf.Port), conf.Username, conf.Password, conf.Name, conf.SSL) - gormConf := &gorm.Config{} if !isDebug { gormConf.Logger = gormLogger.Default.LogMode(gormLogger.Silent) } - db, err = gorm.Open(postgres.Open(dsn), gormConf) + db, err = gorm.Open(postgres.Open(conf.Url), gormConf) if err != nil { return nil, err } diff --git a/src/main.go b/src/main.go index e23b1b4..7ee7b44 100644 --- a/src/main.go +++ b/src/main.go @@ -93,7 +93,7 @@ func main() { Msg("Failed to load config") } - db, err := database.InitPostgresDatabase(&conf.Database, conf.App.Debug) + db, err := database.InitPostgresDatabase(&conf.Database, conf.App.IsDevelopment()) if err != nil { log.Fatal(). Err(err). diff --git a/src/mocks/image/image.mock.go b/src/mocks/image/image.mock.go index 19aba0d..d631497 100644 --- a/src/mocks/image/image.mock.go +++ b/src/mocks/image/image.mock.go @@ -32,6 +32,16 @@ func (c *ClientMock) FindByPetId(_ context.Context, in *proto.FindImageByPetIdRe return res, args.Error(1) } +func (c *ClientMock) AssignPet(_ context.Context, in *proto.AssignPetRequest, _ ...grpc.CallOption) (res *proto.AssignPetResponse, err error) { + args := c.Called(in) + + if args.Get(0) != nil { + res = args.Get(0).(*proto.AssignPetResponse) + } + + return res, args.Error(1) +} + func (c *ClientMock) Delete(_ context.Context, in *proto.DeleteImageRequest, _ ...grpc.CallOption) (res *proto.DeleteImageResponse, err error) { args := c.Called(in) diff --git a/tools/export-env.sh b/tools/export-env.sh new file mode 100644 index 0000000..39688ee --- /dev/null +++ b/tools/export-env.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +## Usage: +## . ./export-env.sh ; $COMMAND +## . ./export-env.sh ; echo ${MINIENTREGA_FECHALIMITE} + +unamestr=$(uname) +if [ "$unamestr" = 'Linux' ]; then + + export $(grep -v '^#' .env | xargs -d '\n') + +elif [ "$unamestr" = 'FreeBSD' ] || [ "$unamestr" = 'Darwin' ]; then + + export $(grep -v '^#' .env | xargs -0) + +fi \ No newline at end of file