mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-19 14:04:08 +01:00
Merge pull request #4 from ForFarmTeam/feature-crop-management
Feature crop management
This commit is contained in:
commit
3a36fd2db9
@ -7,6 +7,7 @@ require (
|
|||||||
github.com/go-chi/chi/v5 v5.2.1
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgx/v5 v5.7.2
|
github.com/jackc/pgx/v5 v5.7.2
|
||||||
|
|||||||
@ -19,6 +19,8 @@ github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
|||||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
|
||||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||||
|
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
|||||||
@ -23,6 +23,7 @@ type api struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
|
||||||
userRepo domain.UserRepository
|
userRepo domain.UserRepository
|
||||||
|
cropRepo domain.CroplandRepository
|
||||||
farmRepo domain.FarmRepository
|
farmRepo domain.FarmRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +31,9 @@ func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api {
|
|||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|
||||||
|
// Initialize repositories for users and croplands
|
||||||
userRepository := repository.NewPostgresUser(pool)
|
userRepository := repository.NewPostgresUser(pool)
|
||||||
|
croplandRepository := repository.NewPostgresCropland(pool)
|
||||||
farmRepository := repository.NewPostgresFarm(pool)
|
farmRepository := repository.NewPostgresFarm(pool)
|
||||||
|
|
||||||
return &api{
|
return &api{
|
||||||
@ -38,6 +41,7 @@ func NewAPI(ctx context.Context, logger *slog.Logger, pool *pgxpool.Pool) *api {
|
|||||||
httpClient: client,
|
httpClient: client,
|
||||||
|
|
||||||
userRepo: userRepository,
|
userRepo: userRepository,
|
||||||
|
cropRepo: croplandRepository,
|
||||||
farmRepo: farmRepository,
|
farmRepo: farmRepository,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -66,14 +70,18 @@ func (a *api) Routes() *chi.Mux {
|
|||||||
config := huma.DefaultConfig("ForFarm Public API", "v1.0.0")
|
config := huma.DefaultConfig("ForFarm Public API", "v1.0.0")
|
||||||
api := humachi.New(router, config)
|
api := humachi.New(router, config)
|
||||||
|
|
||||||
|
// Register Authentication Routes
|
||||||
router.Group(func(r chi.Router) {
|
router.Group(func(r chi.Router) {
|
||||||
a.registerAuthRoutes(r, api)
|
a.registerAuthRoutes(r, api)
|
||||||
|
a.registerCropRoutes(r, api)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Register Cropland Routes, including Auth Middleware if required
|
||||||
router.Group(func(r chi.Router) {
|
router.Group(func(r chi.Router) {
|
||||||
|
// Apply Authentication middleware to the Cropland routes
|
||||||
api.UseMiddleware(m.AuthMiddleware(api))
|
api.UseMiddleware(m.AuthMiddleware(api))
|
||||||
a.registerHelloRoutes(r, api)
|
a.registerHelloRoutes(r, api)
|
||||||
a.registerFarmRoutes(r, api)
|
a.registerFarmRoutes(r, api)
|
||||||
})
|
})
|
||||||
|
|
||||||
return router
|
return router
|
||||||
|
|||||||
209
backend/internal/api/crop.go
Normal file
209
backend/internal/api/crop.go
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/danielgtaylor/huma/v2"
|
||||||
|
"github.com/forfarm/backend/internal/domain"
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Register the crop routes
|
||||||
|
func (a *api) registerCropRoutes(_ chi.Router, api huma.API) {
|
||||||
|
tags := []string{"crop"}
|
||||||
|
|
||||||
|
prefix := "/crop"
|
||||||
|
|
||||||
|
// Register GET /crop
|
||||||
|
huma.Register(api, huma.Operation{
|
||||||
|
OperationID: "getAllCroplands",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: prefix,
|
||||||
|
Tags: tags,
|
||||||
|
}, a.getAllCroplandsHandler)
|
||||||
|
|
||||||
|
// Register GET /crop/{uuid}
|
||||||
|
huma.Register(api, huma.Operation{
|
||||||
|
OperationID: "getCroplandByID",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: prefix + "/{uuid}",
|
||||||
|
Tags: tags,
|
||||||
|
}, a.getCroplandByIDHandler)
|
||||||
|
|
||||||
|
// Register GET /crop/farm/{farm_id}
|
||||||
|
huma.Register(api, huma.Operation{
|
||||||
|
OperationID: "getAllCroplandsByFarmID",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
Path: prefix + "/farm/{farm_id}",
|
||||||
|
Tags: tags,
|
||||||
|
}, a.getAllCroplandsByFarmIDHandler)
|
||||||
|
|
||||||
|
// Register POST /crop (Create or Update)
|
||||||
|
huma.Register(api, huma.Operation{
|
||||||
|
OperationID: "createOrUpdateCropland",
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: prefix,
|
||||||
|
Tags: tags,
|
||||||
|
}, a.createOrUpdateCroplandHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response structure for all croplands
|
||||||
|
type GetCroplandsOutput struct {
|
||||||
|
Body struct {
|
||||||
|
Croplands []domain.Cropland `json:"croplands"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response structure for single cropland by ID
|
||||||
|
type GetCroplandByIDOutput struct {
|
||||||
|
Body struct {
|
||||||
|
Cropland domain.Cropland `json:"cropland"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request structure for creating or updating a cropland
|
||||||
|
type CreateOrUpdateCroplandInput struct {
|
||||||
|
Body struct {
|
||||||
|
UUID string `json:"uuid,omitempty"` // Optional for create, required for update
|
||||||
|
Name string `json:"name"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
LandSize float64 `json:"land_size"`
|
||||||
|
GrowthStage string `json:"growth_stage"`
|
||||||
|
PlantID string `json:"plant_id"`
|
||||||
|
FarmID string `json:"farm_id"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response structure for creating or updating a cropland
|
||||||
|
type CreateOrUpdateCroplandOutput struct {
|
||||||
|
Body struct {
|
||||||
|
Cropland domain.Cropland `json:"cropland"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCroplands handles GET /crop endpoint
|
||||||
|
func (a *api) getAllCroplandsHandler(ctx context.Context, input *struct{}) (*GetCroplandsOutput, error) {
|
||||||
|
resp := &GetCroplandsOutput{}
|
||||||
|
|
||||||
|
// Fetch all croplands without filtering by farmID
|
||||||
|
croplands, err := a.cropRepo.GetAll(ctx) // Use the GetAll method
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Body.Croplands = croplands
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCroplandByID handles GET /crop/{uuid} endpoint
|
||||||
|
func (a *api) getCroplandByIDHandler(ctx context.Context, input *struct {
|
||||||
|
UUID string `path:"uuid" example:"550e8400-e29b-41d4-a716-446655440000"`
|
||||||
|
}) (*GetCroplandByIDOutput, error) {
|
||||||
|
resp := &GetCroplandByIDOutput{}
|
||||||
|
|
||||||
|
// Validate the UUID format
|
||||||
|
if input.UUID == "" {
|
||||||
|
return nil, huma.Error400BadRequest("UUID parameter is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the UUID is in a valid format
|
||||||
|
_, err := uuid.FromString(input.UUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, huma.Error400BadRequest("invalid UUID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch cropland by ID
|
||||||
|
cropland, err := a.cropRepo.GetByID(ctx, input.UUID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, domain.ErrNotFound) {
|
||||||
|
return nil, huma.Error404NotFound("cropland not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Body.Cropland = cropland
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCroplandsByFarmID handles GET /crop/farm/{farm_id} endpoint
|
||||||
|
func (a *api) getAllCroplandsByFarmIDHandler(ctx context.Context, input *struct {
|
||||||
|
FarmID string `path:"farm_id" example:"550e8400-e29b-41d4-a716-446655440000"`
|
||||||
|
}) (*GetCroplandsOutput, error) {
|
||||||
|
resp := &GetCroplandsOutput{}
|
||||||
|
|
||||||
|
// Validate the FarmID format
|
||||||
|
if input.FarmID == "" {
|
||||||
|
return nil, huma.Error400BadRequest("FarmID parameter is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the FarmID is in a valid format
|
||||||
|
_, err := uuid.FromString(input.FarmID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, huma.Error400BadRequest("invalid FarmID format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch croplands by FarmID
|
||||||
|
croplands, err := a.cropRepo.GetByFarmID(ctx, input.FarmID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Body.Croplands = croplands
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdateCropland handles POST /crop endpoint
|
||||||
|
func (a *api) createOrUpdateCroplandHandler(ctx context.Context, input *CreateOrUpdateCroplandInput) (*CreateOrUpdateCroplandOutput, error) {
|
||||||
|
resp := &CreateOrUpdateCroplandOutput{}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if input.Body.Name == "" {
|
||||||
|
return nil, huma.Error400BadRequest("name is required")
|
||||||
|
}
|
||||||
|
if input.Body.Status == "" {
|
||||||
|
return nil, huma.Error400BadRequest("status is required")
|
||||||
|
}
|
||||||
|
if input.Body.GrowthStage == "" {
|
||||||
|
return nil, huma.Error400BadRequest("growth_stage is required")
|
||||||
|
}
|
||||||
|
if input.Body.PlantID == "" {
|
||||||
|
return nil, huma.Error400BadRequest("plant_id is required")
|
||||||
|
}
|
||||||
|
if input.Body.FarmID == "" {
|
||||||
|
return nil, huma.Error400BadRequest("farm_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate UUID if provided
|
||||||
|
if input.Body.UUID != "" {
|
||||||
|
_, err := uuid.FromString(input.Body.UUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, huma.Error400BadRequest("invalid UUID format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map input to domain.Cropland
|
||||||
|
cropland := &domain.Cropland{
|
||||||
|
UUID: input.Body.UUID,
|
||||||
|
Name: input.Body.Name,
|
||||||
|
Status: input.Body.Status,
|
||||||
|
Priority: input.Body.Priority,
|
||||||
|
LandSize: input.Body.LandSize,
|
||||||
|
GrowthStage: input.Body.GrowthStage,
|
||||||
|
PlantID: input.Body.PlantID,
|
||||||
|
FarmID: input.Body.FarmID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update the cropland
|
||||||
|
err := a.cropRepo.CreateOrUpdate(ctx, cropland)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the created/updated cropland
|
||||||
|
resp.Body.Cropland = *cropland
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
@ -31,6 +31,8 @@ func (c *Cropland) Validate() error {
|
|||||||
|
|
||||||
type CroplandRepository interface {
|
type CroplandRepository interface {
|
||||||
GetByID(context.Context, string) (Cropland, error)
|
GetByID(context.Context, string) (Cropland, error)
|
||||||
|
GetByFarmID(ctx context.Context, farmID string) ([]Cropland, error)
|
||||||
|
GetAll(ctx context.Context) ([]Cropland, error) // Add this method
|
||||||
CreateOrUpdate(context.Context, *Cropland) error
|
CreateOrUpdate(context.Context, *Cropland) error
|
||||||
Delete(context.Context, string) error
|
Delete(context.Context, string) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,18 +77,18 @@ func (p *postgresCroplandRepository) CreateOrUpdate(ctx context.Context, c *doma
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
INSERT INTO croplands (uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at)
|
INSERT INTO croplands (uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
||||||
ON CONFLICT (uuid) DO UPDATE
|
ON CONFLICT (uuid) DO UPDATE
|
||||||
SET name = EXCLUDED.name,
|
SET name = EXCLUDED.name,
|
||||||
status = EXCLUDED.status,
|
status = EXCLUDED.status,
|
||||||
priority = EXCLUDED.priority,
|
priority = EXCLUDED.priority,
|
||||||
land_size = EXCLUDED.land_size,
|
land_size = EXCLUDED.land_size,
|
||||||
growth_stage = EXCLUDED.growth_stage,
|
growth_stage = EXCLUDED.growth_stage,
|
||||||
plant_id = EXCLUDED.plant_id,
|
plant_id = EXCLUDED.plant_id,
|
||||||
farm_id = EXCLUDED.farm_id,
|
farm_id = EXCLUDED.farm_id,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
RETURNING uuid, created_at, updated_at`
|
RETURNING uuid, created_at, updated_at`
|
||||||
|
|
||||||
return p.conn.QueryRow(
|
return p.conn.QueryRow(
|
||||||
ctx,
|
ctx,
|
||||||
@ -101,11 +101,18 @@ func (p *postgresCroplandRepository) CreateOrUpdate(ctx context.Context, c *doma
|
|||||||
c.GrowthStage,
|
c.GrowthStage,
|
||||||
c.PlantID,
|
c.PlantID,
|
||||||
c.FarmID,
|
c.FarmID,
|
||||||
).Scan(&c.CreatedAt, &c.UpdatedAt)
|
).Scan(&c.UUID, &c.CreatedAt, &c.UpdatedAt) // Fixed Scan call
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *postgresCroplandRepository) Delete(ctx context.Context, uuid string) error {
|
func (p *postgresCroplandRepository) Delete(ctx context.Context, uuid string) error {
|
||||||
query := `DELETE FROM croplands WHERE uuid = $1`
|
query := `DELETE FROM croplands WHERE uuid = $1`
|
||||||
_, err := p.conn.Exec(ctx, query, uuid)
|
_, err := p.conn.Exec(ctx, query, uuid)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *postgresCroplandRepository) GetAll(ctx context.Context) ([]domain.Cropland, error) {
|
||||||
|
query := `
|
||||||
|
SELECT uuid, name, status, priority, land_size, growth_stage, plant_id, farm_id, created_at, updated_at
|
||||||
|
FROM croplands`
|
||||||
|
|
||||||
|
return p.fetch(ctx, query)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user