From f3ded0f6873a47925b406830221fe4920298860f Mon Sep 17 00:00:00 2001 From: Natthapol SERMSARAN Date: Fri, 14 Feb 2025 03:05:24 +0700 Subject: [PATCH] feat: add farm api for farm setup --- backend/go.mod | 1 + backend/go.sum | 2 + backend/internal/api/api.go | 6 +- backend/internal/api/farm.go | 138 ++++++++++++++++++ backend/internal/domain/farm.go | 20 +-- backend/internal/repository/postgres_farm.go | 37 +++-- .../00002_create_farm_and_cropland_tables.sql | 5 +- 7 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 backend/internal/api/farm.go diff --git a/backend/go.mod b/backend/go.mod index ab3fb1f..7aa25ad 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -25,6 +25,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 633f502..d0cbd55 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -46,6 +46,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= diff --git a/backend/internal/api/api.go b/backend/internal/api/api.go index c254bab..9b69188 100644 --- a/backend/internal/api/api.go +++ b/backend/internal/api/api.go @@ -23,6 +23,7 @@ type api struct { httpClient *http.Client userRepo domain.UserRepository + farmRepo domain.FarmRepository } func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api { @@ -30,12 +31,14 @@ func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api { client := &http.Client{} userRepository := repository.NewPostgresUser(pool) + farmRepository := repository.NewPostgresFarm(pool) return &api{ logger: logger, httpClient: client, userRepo: userRepository, + farmRepo: farmRepository, } } @@ -69,7 +72,8 @@ func (a *api) Routes() *chi.Mux { router.Group(func(r chi.Router) { api.UseMiddleware(m.AuthMiddleware(api)) - a.registerHelloRoutes(r, api) + a.registerHelloRoutes(r, api) + a.registerFarmRoutes(r, api) }) return router diff --git a/backend/internal/api/farm.go b/backend/internal/api/farm.go new file mode 100644 index 0000000..ecaf840 --- /dev/null +++ b/backend/internal/api/farm.go @@ -0,0 +1,138 @@ +package api + +import ( + "context" + "net/http" + + "github.com/danielgtaylor/huma/v2" + "github.com/forfarm/backend/internal/domain" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" +) + +func (a *api) registerFarmRoutes(_ chi.Router, api huma.API) { + tags := []string{"farm"} + prefix := "/farm" + + huma.Register(api, huma.Operation{ + OperationID: "createFarm", + Method: http.MethodPost, + Path: prefix, + Tags: tags, + }, a.createFarmHandler) + + huma.Register(api, huma.Operation{ + OperationID: "getFarmsByOwner", + Method: http.MethodGet, + Path: prefix + "/owner/{owner_id}", + Tags: tags, + }, a.getFarmsByOwnerHandler) + + huma.Register(api, huma.Operation{ + OperationID: "getFarmByID", + Method: http.MethodGet, + Path: prefix + "/{farm_id}", + Tags: tags, + }, a.getFarmByIDHandler) + + huma.Register(api, huma.Operation{ + OperationID: "deleteFarm", + Method: http.MethodDelete, + Path: prefix + "/{farm_id}", + Tags: tags, + }, a.deleteFarmHandler) +} + +type CreateFarmInput struct { + Header string `header:"Authorization" required:"true" example:"Bearer token"` + Body struct { + Name string `json:"name"` + Lat []float64 `json:"lat"` + Lon []float64 `json:"lon"` + OwnerID string `json:"owner_id"` + PlantTypes []uuid.UUID `json:"plant_types"` + } +} + +type CreateFarmOutput struct { + Body struct { + UUID string `json:"uuid"` + } +} + +func (a *api) createFarmHandler(ctx context.Context, input *CreateFarmInput) (*CreateFarmOutput, error) { + farm := &domain.Farm{ + Name: input.Body.Name, + Lat: input.Body.Lat, + Lon: input.Body.Lon, + OwnerID: input.Body.OwnerID, + PlantTypes: input.Body.PlantTypes, + } + + err := a.farmRepo.CreateOrUpdate(ctx, farm) + if err != nil { + return nil, err + } + + return &CreateFarmOutput{Body: struct { + UUID string `json:"uuid"` + }{UUID: farm.UUID}}, nil +} + +type GetFarmsByOwnerInput struct { + Header string `header:"Authorization" required:"true" example:"Bearer token"` + OwnerID string `path:"owner_id"` +} + +type GetFarmsByOwnerOutput struct { + Body []domain.Farm +} + +func (a *api) getFarmsByOwnerHandler(ctx context.Context, input *GetFarmsByOwnerInput) (*GetFarmsByOwnerOutput, error) { + farms, err := a.farmRepo.GetByOwnerID(ctx, input.OwnerID) + if err != nil { + return nil, err + } + + return &GetFarmsByOwnerOutput{Body: farms}, nil +} + +type GetFarmByIDInput struct { + Header string `header:"Authorization" required:"true" example:"Bearer token"` + FarmID string `path:"farm_id"` +} + +type GetFarmByIDOutput struct { + Body domain.Farm +} + +func (a *api) getFarmByIDHandler(ctx context.Context, input *GetFarmByIDInput) (*GetFarmByIDOutput, error) { + farm, err := a.farmRepo.GetByID(ctx, input.FarmID) + if err != nil { + return nil, err + } + + return &GetFarmByIDOutput{Body: farm}, nil +} + +type DeleteFarmInput struct { + Header string `header:"Authorization" required:"true" example:"Bearer token"` + FarmID string `path:"farm_id"` +} + +type DeleteFarmOutput struct { + Body struct { + Message string `json:"message"` + } +} + +func (a *api) deleteFarmHandler(ctx context.Context, input *DeleteFarmInput) (*DeleteFarmOutput, error) { + err := a.farmRepo.Delete(ctx, input.FarmID) + if err != nil { + return nil, err + } + + return &DeleteFarmOutput{Body: struct { + Message string `json:"message"` + }{Message: "Farm deleted successfully"}}, nil +} diff --git a/backend/internal/domain/farm.go b/backend/internal/domain/farm.go index 1dd2aaf..666c2c1 100644 --- a/backend/internal/domain/farm.go +++ b/backend/internal/domain/farm.go @@ -2,19 +2,20 @@ package domain import ( "context" - "time" - validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/google/uuid" + "time" ) type Farm struct { - UUID string - Name string - Lat float64 - Lon float64 - CreatedAt time.Time - UpdatedAt time.Time - OwnerID string + UUID string + Name string + Lat []float64 + Lon []float64 + CreatedAt time.Time + UpdatedAt time.Time + OwnerID string + PlantTypes []uuid.UUID } func (f *Farm) Validate() error { @@ -28,6 +29,7 @@ func (f *Farm) Validate() error { type FarmRepository interface { GetByID(context.Context, string) (Farm, error) + GetByOwnerID(context.Context, string) ([]Farm, error) CreateOrUpdate(context.Context, *Farm) error Delete(context.Context, string) error } diff --git a/backend/internal/repository/postgres_farm.go b/backend/internal/repository/postgres_farm.go index e0fa6a4..44ae54f 100644 --- a/backend/internal/repository/postgres_farm.go +++ b/backend/internal/repository/postgres_farm.go @@ -2,11 +2,10 @@ package repository import ( "context" - "strings" - - "github.com/google/uuid" - "github.com/forfarm/backend/internal/domain" + "github.com/google/uuid" + "github.com/lib/pq" + "strings" ) type postgresFarmRepository struct { @@ -27,6 +26,7 @@ func (p *postgresFarmRepository) fetch(ctx context.Context, query string, args . var farms []domain.Farm for rows.Next() { var f domain.Farm + var plantTypes pq.StringArray if err := rows.Scan( &f.UUID, &f.Name, @@ -35,9 +35,19 @@ func (p *postgresFarmRepository) fetch(ctx context.Context, query string, args . &f.CreatedAt, &f.UpdatedAt, &f.OwnerID, + &plantTypes, ); err != nil { return nil, err } + + for _, plantTypeStr := range plantTypes { + plantTypeUUID, err := uuid.Parse(plantTypeStr) + if err != nil { + return nil, err + } + f.PlantTypes = append(f.PlantTypes, plantTypeUUID) + } + farms = append(farms, f) } return farms, nil @@ -45,7 +55,7 @@ func (p *postgresFarmRepository) fetch(ctx context.Context, query string, args . func (p *postgresFarmRepository) GetByID(ctx context.Context, uuid string) (domain.Farm, error) { query := ` - SELECT uuid, name, lat, lon, created_at, updated_at, owner_id + SELECT uuid, name, lat, lon, created_at, updated_at, owner_id, plant_types FROM farms WHERE uuid = $1` @@ -61,7 +71,7 @@ func (p *postgresFarmRepository) GetByID(ctx context.Context, uuid string) (doma func (p *postgresFarmRepository) GetByOwnerID(ctx context.Context, ownerID string) ([]domain.Farm, error) { query := ` - SELECT uuid, name, lat, lon, created_at, updated_at, owner_id + SELECT uuid, name, lat, lon, created_at, updated_at, owner_id, plant_types FROM farms WHERE owner_id = $1` @@ -73,15 +83,21 @@ func (p *postgresFarmRepository) CreateOrUpdate(ctx context.Context, f *domain.F f.UUID = uuid.New().String() } + plantTypes := make([]string, len(f.PlantTypes)) + for i, pt := range f.PlantTypes { + plantTypes[i] = pt.String() + } + query := ` - INSERT INTO farms (uuid, name, lat, lon, created_at, updated_at, owner_id) - VALUES ($1, $2, $3, $4, NOW(), NOW(), $5) + INSERT INTO farms (uuid, name, lat, lon, created_at, updated_at, owner_id, plant_types) + VALUES ($1, $2, $3, $4, NOW(), NOW(), $5, $6) ON CONFLICT (uuid) DO UPDATE SET name = EXCLUDED.name, lat = EXCLUDED.lat, lon = EXCLUDED.lon, updated_at = NOW(), - owner_id = EXCLUDED.owner_id + owner_id = EXCLUDED.owner_id, + plant_types = EXCLUDED.plant_types RETURNING uuid, created_at, updated_at` return p.conn.QueryRow( @@ -92,7 +108,8 @@ func (p *postgresFarmRepository) CreateOrUpdate(ctx context.Context, f *domain.F f.Lat, f.Lon, f.OwnerID, - ).Scan(&f.CreatedAt, &f.UpdatedAt) + pq.StringArray(plantTypes), + ).Scan(&f.UUID, &f.CreatedAt, &f.UpdatedAt) } func (p *postgresFarmRepository) Delete(ctx context.Context, uuid string) error { diff --git a/backend/migrations/00002_create_farm_and_cropland_tables.sql b/backend/migrations/00002_create_farm_and_cropland_tables.sql index 8f2df4b..bd5a114 100644 --- a/backend/migrations/00002_create_farm_and_cropland_tables.sql +++ b/backend/migrations/00002_create_farm_and_cropland_tables.sql @@ -43,8 +43,9 @@ CREATE TABLE plants ( CREATE TABLE farms ( uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, - lat DOUBLE PRECISION NOT NULL, - lon DOUBLE PRECISION NOT NULL, + lat DOUBLE PRECISION[] NOT NULL, + lon DOUBLE PRECISION[] NOT NULL, + plant_types UUID[], created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), owner_id UUID NOT NULL,