ForFarm/backend/internal/api/farm.go

314 lines
9.9 KiB
Go

package api
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"github.com/danielgtaylor/huma/v2"
"github.com/forfarm/backend/internal/domain"
"github.com/go-chi/chi/v5"
)
// registerFarmRoutes defines endpoints for farm operations.
func (a *api) registerFarmRoutes(_ chi.Router, api huma.API) {
tags := []string{"farm"}
prefix := "/farms"
huma.Register(api, huma.Operation{
OperationID: "getAllFarms",
Method: http.MethodGet,
Path: prefix,
Tags: tags,
}, a.getAllFarmsHandler)
huma.Register(api, huma.Operation{
OperationID: "getFarmByID",
Method: http.MethodGet,
Path: prefix + "/{farmId}",
Tags: tags,
}, a.getFarmByIDHandler)
huma.Register(api, huma.Operation{
OperationID: "createFarm",
Method: http.MethodPost,
Path: prefix,
Tags: tags,
}, a.createFarmHandler)
huma.Register(api, huma.Operation{
OperationID: "updateFarm",
Method: http.MethodPut,
Path: prefix + "/{farmId}",
Tags: tags,
}, a.updateFarmHandler)
huma.Register(api, huma.Operation{
OperationID: "deleteFarm",
Method: http.MethodDelete,
Path: prefix + "/{farmId}",
Tags: tags,
}, a.deleteFarmHandler)
}
//
// Input and Output types
//
type CreateFarmInput struct {
Header string `header:"Authorization" required:"true" example:"Bearer token"`
Body struct {
Name string `json:"name" required:"true"`
Lat float64 `json:"lat" required:"true"`
Lon float64 `json:"lon" required:"true"`
FarmType string `json:"farmType,omitempty"`
TotalSize string `json:"totalSize,omitempty"`
}
}
type CreateFarmOutput struct {
Body struct {
UUID string `json:"uuid"`
}
}
type GetAllFarmsInput struct {
Header string `header:"Authorization" required:"true" example:"Bearer token"`
}
type GetAllFarmsOutput struct {
Body []domain.Farm `json:"farms"`
}
type GetFarmByIDInput struct {
Header string `header:"Authorization" required:"true" example:"Bearer token"`
FarmID string `path:"farmId" required:"true"`
}
type GetFarmByIDOutput struct {
Body domain.Farm `json:"farm"`
}
type UpdateFarmInput struct {
Header string `header:"Authorization" required:"true" example:"Bearer token"`
FarmID string `path:"farmId" required:"true"`
Body struct {
Name *string `json:"name,omitempty"`
Lat *float64 `json:"lat,omitempty"`
Lon *float64 `json:"lon,omitempty"`
FarmType *string `json:"farmType,omitempty"`
TotalSize *string `json:"totalSize,omitempty"`
}
}
type UpdateFarmOutput struct {
Body domain.Farm `json:"farm"`
}
type DeleteFarmInput struct {
Header string `header:"Authorization" required:"true" example:"Bearer token"`
FarmID string `path:"farmId" required:"true"`
}
type DeleteFarmOutput struct {
Body struct {
Message string `json:"message"`
}
}
//
// API Handlers
//
func (a *api) createFarmHandler(ctx context.Context, input *CreateFarmInput) (*CreateFarmOutput, error) {
userID, err := a.getUserIDFromHeader(input.Header)
if err != nil {
return nil, huma.Error401Unauthorized("Authentication failed", err)
}
farm := &domain.Farm{
Name: input.Body.Name,
Lat: input.Body.Lat,
Lon: input.Body.Lon,
FarmType: input.Body.FarmType,
TotalSize: input.Body.TotalSize,
OwnerID: userID,
}
// Validate the farm object (optional but recommended)
// if err := farm.Validate(); err != nil {
// return nil, huma.Error422UnprocessableEntity("Validation failed", err)
// }
fmt.Println("Creating farm:", farm) // Keep for debugging if needed
if err := a.farmRepo.CreateOrUpdate(ctx, farm); err != nil {
a.logger.Error("Failed to create farm in database", "error", err, "ownerId", userID, "farmName", farm.Name)
return nil, huma.Error500InternalServerError("Failed to create farm")
}
a.logger.Info("Farm created successfully", "farmId", farm.UUID, "ownerId", userID)
return &CreateFarmOutput{
Body: struct {
UUID string `json:"uuid"`
}{UUID: farm.UUID},
}, nil
}
func (a *api) getAllFarmsHandler(ctx context.Context, input *GetAllFarmsInput) (*GetAllFarmsOutput, error) {
userID, err := a.getUserIDFromHeader(input.Header)
if err != nil {
return nil, huma.Error401Unauthorized("Authentication failed", err)
}
farms, err := a.farmRepo.GetByOwnerID(ctx, userID)
if err != nil {
a.logger.Error("Failed to get farms by owner ID", "ownerId", userID, "error", err)
return nil, huma.Error500InternalServerError("Failed to retrieve farms")
}
// Handle case where user has no farms (return empty list, not error)
if farms == nil {
farms = []domain.Farm{}
}
return &GetAllFarmsOutput{Body: farms}, nil
}
func (a *api) getFarmByIDHandler(ctx context.Context, input *GetFarmByIDInput) (*GetFarmByIDOutput, error) {
userID, err := a.getUserIDFromHeader(input.Header)
if err != nil {
return nil, huma.Error401Unauthorized("Authentication failed", err)
}
farm, err := a.farmRepo.GetByID(ctx, input.FarmID)
if err != nil {
if errors.Is(err, domain.ErrNotFound) || errors.Is(err, sql.ErrNoRows) { // Handle pgx ErrNoRows too
a.logger.Warn("Farm not found", "farmId", input.FarmID, "requestingUserId", userID)
return nil, huma.Error404NotFound("Farm not found")
}
a.logger.Error("Failed to get farm by ID", "farmId", input.FarmID, "error", err)
return nil, huma.Error500InternalServerError("Failed to retrieve farm")
}
if farm.OwnerID != userID {
a.logger.Warn("Unauthorized attempt to access farm", "farmId", input.FarmID, "requestingUserId", userID, "ownerId", farm.OwnerID)
return nil, huma.Error403Forbidden("You are not authorized to view this farm")
}
return &GetFarmByIDOutput{Body: *farm}, nil
}
func (a *api) updateFarmHandler(ctx context.Context, input *UpdateFarmInput) (*UpdateFarmOutput, error) {
userID, err := a.getUserIDFromHeader(input.Header)
if err != nil {
return nil, huma.Error401Unauthorized("Authentication failed", err)
}
farm, err := a.farmRepo.GetByID(ctx, input.FarmID)
if err != nil {
if errors.Is(err, domain.ErrNotFound) || errors.Is(err, sql.ErrNoRows) {
a.logger.Warn("Attempt to update non-existent farm", "farmId", input.FarmID, "requestingUserId", userID)
return nil, huma.Error404NotFound("Farm not found")
}
a.logger.Error("Failed to get farm for update", "farmId", input.FarmID, "error", err)
return nil, huma.Error500InternalServerError("Failed to retrieve farm for update")
}
if farm.OwnerID != userID {
a.logger.Warn("Unauthorized attempt to update farm", "farmId", input.FarmID, "requestingUserId", userID, "ownerId", farm.OwnerID)
return nil, huma.Error403Forbidden("You are not authorized to update this farm")
}
// Apply updates selectively
updated := false
if input.Body.Name != nil && *input.Body.Name != "" && *input.Body.Name != farm.Name {
farm.Name = *input.Body.Name
updated = true
}
if input.Body.Lat != nil && *input.Body.Lat != farm.Lat {
farm.Lat = *input.Body.Lat
updated = true
}
if input.Body.Lon != nil && *input.Body.Lon != farm.Lon {
farm.Lon = *input.Body.Lon
updated = true
}
if input.Body.FarmType != nil && *input.Body.FarmType != farm.FarmType {
farm.FarmType = *input.Body.FarmType
updated = true
}
if input.Body.TotalSize != nil && *input.Body.TotalSize != farm.TotalSize {
farm.TotalSize = *input.Body.TotalSize
updated = true
}
if !updated {
a.logger.Info("No changes detected for farm update", "farmId", input.FarmID)
// Return the existing farm data as no update was needed
return &UpdateFarmOutput{Body: *farm}, nil
}
// Validate updated farm object (optional but recommended)
// if err := farm.Validate(); err != nil {
// return nil, huma.Error422UnprocessableEntity("Validation failed after update", err)
// }
if err = a.farmRepo.CreateOrUpdate(ctx, farm); err != nil {
a.logger.Error("Failed to update farm in database", "farmId", input.FarmID, "error", err)
return nil, huma.Error500InternalServerError("Failed to update farm")
}
a.logger.Info("Farm updated successfully", "farmId", farm.UUID, "ownerId", userID)
// Fetch the updated farm again to ensure we return the latest state (including UpdatedAt)
updatedFarm, fetchErr := a.farmRepo.GetByID(ctx, input.FarmID)
if fetchErr != nil {
a.logger.Error("Failed to fetch farm after update", "farmId", input.FarmID, "error", fetchErr)
// Return the potentially stale data from 'farm' as a fallback, but log the error
return &UpdateFarmOutput{Body: *farm}, nil
}
return &UpdateFarmOutput{Body: *updatedFarm}, nil
}
func (a *api) deleteFarmHandler(ctx context.Context, input *DeleteFarmInput) (*DeleteFarmOutput, error) {
userID, err := a.getUserIDFromHeader(input.Header)
if err != nil {
return nil, huma.Error401Unauthorized("Authentication failed", err)
}
farm, err := a.farmRepo.GetByID(ctx, input.FarmID)
if err != nil {
if errors.Is(err, domain.ErrNotFound) || errors.Is(err, sql.ErrNoRows) {
a.logger.Warn("Attempt to delete non-existent farm", "farmId", input.FarmID, "requestingUserId", userID)
// Consider returning 204 No Content if delete is idempotent
return nil, huma.Error404NotFound("Farm not found")
}
a.logger.Error("Failed to get farm for deletion", "farmId", input.FarmID, "error", err)
return nil, huma.Error500InternalServerError("Failed to retrieve farm for deletion")
}
if farm.OwnerID != userID {
a.logger.Warn("Unauthorized attempt to delete farm", "farmId", input.FarmID, "requestingUserId", userID, "ownerId", farm.OwnerID)
return nil, huma.Error403Forbidden("You are not authorized to delete this farm")
}
if err := a.farmRepo.Delete(ctx, input.FarmID); err != nil {
a.logger.Error("Failed to delete farm from database", "farmId", input.FarmID, "error", err)
// Consider potential FK constraint errors if crops aren't deleted automatically
return nil, huma.Error500InternalServerError("Failed to delete farm")
}
a.logger.Info("Farm deleted successfully", "farmId", input.FarmID, "ownerId", userID)
return &DeleteFarmOutput{
Body: struct {
Message string `json:"message"`
}{Message: "Farm deleted successfully"},
}, nil
}