Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ app.*.map.json
/android/app/profile
/android/app/release


#ignore data dir
data/

# ignore build outputs
bin/
27 changes: 22 additions & 5 deletions backend/auth/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package auth
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"errors"
"net/http"
Expand Down Expand Up @@ -34,11 +35,27 @@ func (f *fakeDB) CreateUser(ctx context.Context, email, passHash string) (int64,
}

// Unused methods to satisfy interface:
func (f *fakeDB) Connect(dsn string) error { return nil }
func (f *fakeDB) Close() error { return nil }
func (f *fakeDB) Migrate() error { return nil }
func (f *fakeDB) SomeUnusedMethod1() { /* no-op */ }
func (f *fakeDB) SomeUnusedMethod2(arg interface{}) (interface{}, error) { return nil, nil }
func (f *fakeDB) Connect(dsn string) error { return nil }
func (f *fakeDB) Close() error { return nil }
func (f *fakeDB) Migrate() error { return nil }
func (f *fakeDB) SearchSpecies(ctx context.Context, query string, limit, offset int) ([]*model.Species, error) {
return nil, nil
}
func (f *fakeDB) GetSpeciesByID(ctx context.Context, speciesID int64) (*model.Species, error) {
return nil, nil
}
func (f *fakeDB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
return nil, nil
}
func (f *fakeDB) CreatePlant(ctx context.Context, p *model.Plant) (int64, error) { return 0, nil }
func (f *fakeDB) GetPlantByID(ctx context.Context, userID, plantID int64) (*model.Plant, error) {
return nil, nil
}
func (f *fakeDB) ListPlants(ctx context.Context, userID int64, limit, offset int) ([]*model.Plant, error) {
return nil, nil
}
func (f *fakeDB) UpdatePlant(ctx context.Context, p *model.Plant) error { return nil }
func (f *fakeDB) DeletePlant(ctx context.Context, userID, plantID int64) error { return nil }

//-----------------------
// Tests for HandleLogin
Expand Down
30 changes: 26 additions & 4 deletions backend/db/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,47 @@ package db

import (
"context"
"database/sql"

"github.com/anish-chanda/ferna/model"
)

type Database interface {
// user auth methods
// ---- auth stuff ----

// CheckIfEmailExists returns true if a user record with the given email exists.
// It returns (false, nil) if not found, or (false, err) on error.
CheckIfEmailExists(ctx context.Context, email string) (bool, error)
CreateUser(ctx context.Context, email, passHash string) (int64, error) //returns userid
// inserts a new user with the given email and password hash and returns the new user ID.
CreateUser(ctx context.Context, email, passHash string) (int64, error)
// fetches a user by email.
GetUserByEmail(ctx context.Context, email string) (*model.User, error)
// UpdatePassword(userID int64, passHash string) error

// driver funcs
// ---- plant stuff ----

// CreatePlant inserts a new plant, returning its new ID.
CreatePlant(ctx context.Context, p *model.Plant) (int64, error)
// GetPlantByID fetches a single plant by user+plant ID.
GetPlantByID(ctx context.Context, userID, plantID int64) (*model.Plant, error)
// ListPlants returns a paginated list of a user’s plants.
ListPlants(ctx context.Context, userID int64, limit, offset int) ([]*model.Plant, error)
// UpdatePlant updates all mutable fields on the given plant.
UpdatePlant(ctx context.Context, p *model.Plant) error
// DeletePlant removes a plant by userplant ID.
DeletePlant(ctx context.Context, userID, plantID int64) error
// SearchSpecies searches for species by common or scientific name.
SearchSpecies(ctx context.Context, query string, limit, offset int) ([]*model.Species, error)
// get species with a ID
GetSpeciesByID(ctx context.Context, speciesID int64) (*model.Species, error)

// ---- other stuff -----

// Database operations
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
// establishes a connection to the database.
Connect(dsn string) error
// Close closes the database connection.
Close() error
// Migrate runs database migrations using go-migrate and embedded sql files to ensure the schema is up-to-date.
Migrate() error
}
195 changes: 195 additions & 0 deletions backend/db/sqlite3/sqlite3.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,198 @@ func (s *SQLiteDB) GetUserByEmail(ctx context.Context, email string) (*model.Use

return &user, nil
}

func (s *SQLiteDB) SearchSpecies(ctx context.Context, query string, limit, offset int) ([]*model.Species, error) {
pattern := "%" + query + "%"
rows, err := s.db.QueryContext(ctx, `
SELECT id, common_name, scientific_name,
default_watering_frequency_days, created_at, updated_at
FROM species
WHERE common_name LIKE ?
OR scientific_name LIKE ?
ORDER BY common_name
LIMIT ? OFFSET ?`,
pattern, pattern, limit, offset,
)
if err != nil {
return nil, fmt.Errorf("error searching species query: %w", err)
}

defer rows.Close()

var res []*model.Species
for rows.Next() {
var sp model.Species
var created, updated string
if err := rows.Scan(&sp.ID,
&sp.CommonName,
&sp.ScientificName,
&sp.DefaultWateringFrequency,
&created,
&updated); err != nil {
return nil, fmt.Errorf("scan species: %w", err)
}

//parse time
sp.CreatedAt, err = time.Parse(time.RFC3339, created)
if err != nil {
return nil, fmt.Errorf("parse created_at: %w", err)
}
sp.UpdatedAt, err = time.Parse(time.RFC3339, updated)
if err != nil {
return nil, fmt.Errorf("parse updated_at: %w", err)
}
res = append(res, &sp)

}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate species rows: %w", err)
}
return res, nil
}

func (s *SQLiteDB) GetSpeciesByID(ctx context.Context, speciesID int64) (*model.Species, error) {
row := s.db.QueryRowContext(ctx, `
SELECT id, common_name, scientific_name, default_watering_frequency_days, created_at, updated_at
FROM species
WHERE id = ?`, speciesID)

var sp model.Species
var created, updated sql.NullString
if err := row.Scan(
&sp.ID, &sp.CommonName, &sp.ScientificName, &sp.DefaultWateringFrequency,
&created, &updated,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("GetSpeciesByID: %w", err)
}

sp.CreatedAt, _ = time.Parse(time.RFC3339, created.String)
sp.UpdatedAt, _ = time.Parse(time.RFC3339, updated.String)
return &sp, nil
}

// ExecContext executes a query with context that doesn't return rows, like insert, delete etc
func (s *SQLiteDB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
result, err := s.db.ExecContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("failed to execute query: %w", err)
}
return result, nil
}

// CreatePlant inserts a new plant and returns its ID.
func (s *SQLiteDB) CreatePlant(ctx context.Context, p *model.Plant) (int64, error) {
res, err := s.db.ExecContext(ctx, `
INSERT INTO plants (
user_id, species_id, nickname, image_url,
watering_frequency_days, last_watered_at, note
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
p.UserID, p.SpeciesID, p.Nickname, p.ImageURL,
p.WateringFrequencyDays, p.LastWateredAt, p.Note,
)
if err != nil {
return 0, fmt.Errorf("CreatePlant: %w", err)
}
return res.LastInsertId()
}

// GetPlantByID fetches one plant, ensuring it belongs to the user.
func (s *SQLiteDB) GetPlantByID(ctx context.Context, userID, plantID int64) (*model.Plant, error) {
row := s.db.QueryRowContext(ctx, `
SELECT id, user_id, species_id, nickname, image_url,
watering_frequency_days, last_watered_at, note,
created_at, updated_at
FROM plants
WHERE id = ? AND user_id = ?`, plantID, userID)

var p model.Plant
var lw, created, updated sql.NullString
if err := row.Scan(
&p.ID, &p.UserID, &p.SpeciesID, &p.Nickname, &p.ImageURL,
&p.WateringFrequencyDays, &lw, &p.Note,
&created, &updated,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, fmt.Errorf("GetPlantByID: %w", err)
}
if lw.Valid {
t, err := time.Parse(time.RFC3339, lw.String)
if err == nil {
p.LastWateredAt = &t
}
}
p.CreatedAt, _ = time.Parse(time.RFC3339, created.String)
p.UpdatedAt, _ = time.Parse(time.RFC3339, updated.String)
return &p, nil
}

// ListPlants returns a page of plants for a user.
func (s *SQLiteDB) ListPlants(ctx context.Context, userID int64, limit, offset int) ([]*model.Plant, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT id, user_id, species_id, nickname, image_url,
watering_frequency_days, last_watered_at, note,
created_at, updated_at
FROM plants
WHERE user_id = ?
ORDER BY id
LIMIT ? OFFSET ?`, userID, limit, offset)
if err != nil {
return nil, fmt.Errorf("ListPlants: %w", err)
}
defer rows.Close()

var list []*model.Plant
for rows.Next() {
var p model.Plant
var lw, created, updated sql.NullString
if err := rows.Scan(
&p.ID, &p.UserID, &p.SpeciesID, &p.Nickname, &p.ImageURL,
&p.WateringFrequencyDays, &lw, &p.Note,
&created, &updated,
); err != nil {
return nil, fmt.Errorf("ListPlants scan: %w", err)
}
if lw.Valid {
t, err := time.Parse(time.RFC3339, lw.String)
if err == nil {
p.LastWateredAt = &t
}
}
p.CreatedAt, _ = time.Parse(time.RFC3339, created.String)
p.UpdatedAt, _ = time.Parse(time.RFC3339, updated.String)
list = append(list, &p)
}
return list, rows.Err()
}

// UpdatePlant updates an existing plant’s mutable fields.
func (s *SQLiteDB) UpdatePlant(ctx context.Context, p *model.Plant) error {
_, err := s.db.ExecContext(ctx, `
UPDATE plants
SET species_id = ?, nickname = ?, image_url = ?,
watering_frequency_days = ?, last_watered_at = ?, note = ?
WHERE id = ? AND user_id = ?`,
p.SpeciesID, p.Nickname, p.ImageURL,
p.WateringFrequencyDays, p.LastWateredAt, p.Note,
p.ID, p.UserID,
)
if err != nil {
return fmt.Errorf("UpdatePlant: %w", err)
}
return nil
}

// DeletePlant removes a plant by its ID and owner.
func (s *SQLiteDB) DeletePlant(ctx context.Context, userID, plantID int64) error {
_, err := s.db.ExecContext(ctx,
`DELETE FROM plants WHERE id = ? AND user_id = ?`, plantID, userID)
if err != nil {
return fmt.Errorf("DeletePlant: %w", err)
}
return nil
}
Loading
Loading