package model import ( "database/sql" "encoding/json" "errors" "fmt" ) type Recipe struct { Id string `json:"id"` Title string `json:"title"` Portions string `json:"portions"` Url string `json:"url"` Notes string `json:"notes"` Created string `json:"created"` LastChanged string `json:"last_changed"` Steps []Step `json:"steps"` IsFavorite bool `json:"is_favorite"` } func (r Recipe) String() string { b, _ := json.MarshalIndent(r, "", " ") return string(b) } func (r *Recipe) Validate() error { var err error if r.Id != "" { err = isPositiveOrZeroInt(r.Id) if err != nil { return fmt.Errorf("Invalid recipe ID: %w", err) } } if r.Title == "" { return fmt.Errorf("Recipes must have a title") } if r.Portions != "" { err = isPositiveOrZeroInt(r.Portions) if err != nil { return fmt.Errorf("Invalid recipe portions: %w", err) } } err = isInt(r.Created) if err != nil { return fmt.Errorf("Invalid creation time stamp: %w", err) } err = isInt(r.LastChanged) if err != nil { return fmt.Errorf("Invalid last changed time stamp: %w", err) } return nil } func (r *Recipe) Create(tx *sql.Tx) error { if r.Id != "" { return fmt.Errorf("Cannot create recipe if ID is given") } err := r.Validate() if err != nil { return err } cmd := ` INSERT INTO recipes (title, portions, url, notes, created, last_changed, is_favorite) VALUES (?, ?, ?, ?, ?, ?, ?) ` result, err := tx.Exec(cmd, r.Title, r.Portions, r.Url, r.Notes, r.Created, r.LastChanged, r.IsFavorite) if err != nil { return err } id, err := result.LastInsertId() if err != nil { return err } r.Id = fmt.Sprint(id) for i := range r.Steps { r.Steps[i].Recipe = r.Id r.Steps[i].Index = fmt.Sprint(i) err = r.Steps[i].Create(tx) if err != nil { return err } } return nil } func (r *Recipe) getStepIds(tx *sql.Tx) ([]Step, error) { retval := make([]Step, 0) cmd := ` SELECT id FROM steps WHERE recipe = ? ORDER BY 'index' ASC ` rows, err := tx.Query(cmd, r.Id) if err != nil { return retval, err } defer rows.Close() for rows.Next() { s := Step{} err = rows.Scan(&s.Id) if err != nil { return retval, err } retval = append(retval, s) } return retval, nil } func (r *Recipe) Read(tx *sql.Tx) error { cmd := ` SELECT title, portions, url, notes, created, last_changed, is_favorite FROM recipes WHERE id = ? ` rows, err := tx.Query(cmd, r.Id) if err != nil { return err } defer rows.Close() if !rows.Next() { return sql.ErrNoRows } err = rows.Scan( &r.Title, &r.Portions, &r.Url, &r.Notes, &r.Created, &r.LastChanged, &r.IsFavorite, ) if err != nil { return err } r.Steps, err = r.getStepIds(tx) if err != nil { return err } for i := range r.Steps { err = r.Steps[i].Read(tx) if err != nil { return err } } return r.Validate() } func (r *Recipe) Update(tx *sql.Tx) error { err := r.Validate() if err != nil { return err } oldSteps, err := r.getStepIds(tx) if err != nil { return err } for i := range oldSteps { err = oldSteps[i].Delete(tx) if err != nil { return err } } for i := range r.Steps { r.Steps[i].Index = fmt.Sprint(i) r.Steps[i].Recipe = r.Id err = r.Steps[i].Create(tx) if err != nil { return err } } cmd := ` UPDATE recipes SET title = ?, portions = ?, url = ?, notes = ?, created = ?, last_changed = ?, is_favorite = ? WHERE id = ?` res, err := tx.Exec(cmd, r.Title, r.Portions, r.Url, r.Notes, r.Created, r.LastChanged, r.IsFavorite, r.Id) if err != nil { return err } affected, err := res.RowsAffected() if err != nil { return err } if affected != 1 { return fmt.Errorf("Recipe update affected %d rows instead of 1", affected) } return nil } func (r *Recipe) Delete(tx *sql.Tx) error { oldSteps, err := r.getStepIds(tx) if err != nil { return err } for i := range oldSteps { err = oldSteps[i].Delete(tx) if err != nil { return err } } cmd := ` DELETE FROM recipes WHERE id = ? ` result, err := tx.Exec(cmd, r.Id) if err != nil { return err } rows, err := result.RowsAffected() if rows != 1 { return errors.New("Recipe deletion did not affect exactly one row") } return nil } func RecipeTestData() []Recipe { return []Recipe{ { Title: "Pancakes", Portions: "4", Url: "https://example.org", Notes: "Very fluffy", Created: "1715957069", LastChanged: "1715958069", Steps: []Step{ { Text: "Stir the dough", Ingredients: []Ingredient{ {Amount: "4", Unit: "pieces", Type: "egg"}, {Amount: "800", Unit: "g", Type: "special stuff"}, {Amount: "0.5", Unit: "l", Type: "milk"}, }, }, {Text: "Heat up pan", Ingredients: []Ingredient{}}, { Text: "Make pancakes!", Ingredients: []Ingredient{ {Amount: "", Unit: "", Type: "sugar"}, }, }, }, IsFavorite: true, }, { Title: "Burger", Portions: "2", Url: "https://xengineering.eu/git/ceres", Notes: "Delicious!", Created: "1715658069", LastChanged: "1715958070", Steps: []Step{}, IsFavorite: false, }, } }