feat: add farm api for farm setup

This commit is contained in:
Natthapol SERMSARAN 2025-02-14 03:05:24 +07:00
parent d6b73c9bb1
commit f3ded0f687
7 changed files with 187 additions and 22 deletions

View File

@ -25,6 +25,7 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // 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/magiconair/properties v1.8.7 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect github.com/mfridman/interpolate v0.0.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect

View File

@ -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/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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=

View File

@ -23,6 +23,7 @@ type api struct {
httpClient *http.Client httpClient *http.Client
userRepo domain.UserRepository userRepo domain.UserRepository
farmRepo domain.FarmRepository
} }
func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api { 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{} client := &http.Client{}
userRepository := repository.NewPostgresUser(pool) userRepository := repository.NewPostgresUser(pool)
farmRepository := repository.NewPostgresFarm(pool)
return &api{ return &api{
logger: logger, logger: logger,
httpClient: client, httpClient: client,
userRepo: userRepository, userRepo: userRepository,
farmRepo: farmRepository,
} }
} }
@ -70,6 +73,7 @@ func (a *api) Routes() *chi.Mux {
router.Group(func(r chi.Router) { router.Group(func(r chi.Router) {
api.UseMiddleware(m.AuthMiddleware(api)) api.UseMiddleware(m.AuthMiddleware(api))
a.registerHelloRoutes(r, api) a.registerHelloRoutes(r, api)
a.registerFarmRoutes(r, api)
}) })
return router return router

View File

@ -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
}

View File

@ -2,19 +2,20 @@ package domain
import ( import (
"context" "context"
"time"
validation "github.com/go-ozzo/ozzo-validation/v4" validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/google/uuid"
"time"
) )
type Farm struct { type Farm struct {
UUID string UUID string
Name string Name string
Lat float64 Lat []float64
Lon float64 Lon []float64
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
OwnerID string OwnerID string
PlantTypes []uuid.UUID
} }
func (f *Farm) Validate() error { func (f *Farm) Validate() error {
@ -28,6 +29,7 @@ func (f *Farm) Validate() error {
type FarmRepository interface { type FarmRepository interface {
GetByID(context.Context, string) (Farm, error) GetByID(context.Context, string) (Farm, error)
GetByOwnerID(context.Context, string) ([]Farm, error)
CreateOrUpdate(context.Context, *Farm) error CreateOrUpdate(context.Context, *Farm) error
Delete(context.Context, string) error Delete(context.Context, string) error
} }

View File

@ -2,11 +2,10 @@ package repository
import ( import (
"context" "context"
"strings"
"github.com/google/uuid"
"github.com/forfarm/backend/internal/domain" "github.com/forfarm/backend/internal/domain"
"github.com/google/uuid"
"github.com/lib/pq"
"strings"
) )
type postgresFarmRepository struct { type postgresFarmRepository struct {
@ -27,6 +26,7 @@ func (p *postgresFarmRepository) fetch(ctx context.Context, query string, args .
var farms []domain.Farm var farms []domain.Farm
for rows.Next() { for rows.Next() {
var f domain.Farm var f domain.Farm
var plantTypes pq.StringArray
if err := rows.Scan( if err := rows.Scan(
&f.UUID, &f.UUID,
&f.Name, &f.Name,
@ -35,9 +35,19 @@ func (p *postgresFarmRepository) fetch(ctx context.Context, query string, args .
&f.CreatedAt, &f.CreatedAt,
&f.UpdatedAt, &f.UpdatedAt,
&f.OwnerID, &f.OwnerID,
&plantTypes,
); err != nil { ); err != nil {
return nil, err 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) farms = append(farms, f)
} }
return farms, nil 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) { func (p *postgresFarmRepository) GetByID(ctx context.Context, uuid string) (domain.Farm, error) {
query := ` 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 FROM farms
WHERE uuid = $1` 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) { func (p *postgresFarmRepository) GetByOwnerID(ctx context.Context, ownerID string) ([]domain.Farm, error) {
query := ` 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 FROM farms
WHERE owner_id = $1` WHERE owner_id = $1`
@ -73,15 +83,21 @@ func (p *postgresFarmRepository) CreateOrUpdate(ctx context.Context, f *domain.F
f.UUID = uuid.New().String() f.UUID = uuid.New().String()
} }
plantTypes := make([]string, len(f.PlantTypes))
for i, pt := range f.PlantTypes {
plantTypes[i] = pt.String()
}
query := ` query := `
INSERT INTO farms (uuid, name, lat, lon, created_at, updated_at, owner_id) INSERT INTO farms (uuid, name, lat, lon, created_at, updated_at, owner_id, plant_types)
VALUES ($1, $2, $3, $4, NOW(), NOW(), $5) VALUES ($1, $2, $3, $4, NOW(), NOW(), $5, $6)
ON CONFLICT (uuid) DO UPDATE ON CONFLICT (uuid) DO UPDATE
SET name = EXCLUDED.name, SET name = EXCLUDED.name,
lat = EXCLUDED.lat, lat = EXCLUDED.lat,
lon = EXCLUDED.lon, lon = EXCLUDED.lon,
updated_at = NOW(), updated_at = NOW(),
owner_id = EXCLUDED.owner_id owner_id = EXCLUDED.owner_id,
plant_types = EXCLUDED.plant_types
RETURNING uuid, created_at, updated_at` RETURNING uuid, created_at, updated_at`
return p.conn.QueryRow( return p.conn.QueryRow(
@ -92,7 +108,8 @@ func (p *postgresFarmRepository) CreateOrUpdate(ctx context.Context, f *domain.F
f.Lat, f.Lat,
f.Lon, f.Lon,
f.OwnerID, 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 { func (p *postgresFarmRepository) Delete(ctx context.Context, uuid string) error {

View File

@ -43,8 +43,9 @@ CREATE TABLE plants (
CREATE TABLE farms ( CREATE TABLE farms (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(), uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL, name TEXT NOT NULL,
lat DOUBLE PRECISION NOT NULL, lat DOUBLE PRECISION[] NOT NULL,
lon DOUBLE PRECISION NOT NULL, lon DOUBLE PRECISION[] NOT NULL,
plant_types UUID[],
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
owner_id UUID NOT NULL, owner_id UUID NOT NULL,