package model import ( "database/sql" "encoding/json" "errors" "fmt" ) type Step struct { Id string `json:"id"` Index string `json:"index"` Text string `json:"text"` Recipe string `json:"recipe"` Ingredients []Ingredient `json:"ingredients"` } func (s Step) String() string { b, _ := json.MarshalIndent(s, "", " ") return string(b) } func (s *Step) Validate() error { var err error if s.Id != "" { err = isPositiveOrZeroInt(s.Id) if err != nil { return fmt.Errorf("Invalid step ID: %w", err) } } if s.Index != "" { err = isPositiveOrZeroInt(s.Index) if err != nil { return fmt.Errorf("Invalid step index: %w", err) } } if s.Text == "" { return fmt.Errorf("Step text must not be empty") } err = isPositiveOrZeroInt(s.Recipe) if err != nil { return fmt.Errorf("Step does not reference a valid recipe ID: %w", err) } return nil } func (s *Step) Create(tx *sql.Tx) error { if s.Id != "" { return fmt.Errorf("Cannot create step if ID is given") } err := s.Validate() if err != nil { return err } cmd := ` INSERT INTO steps ('index', text, recipe) VALUES (?, ?, ?) ` result, err := tx.Exec(cmd, s.Index, s.Text, s.Recipe) if err != nil { return err } id, err := result.LastInsertId() if err != nil { return err } s.Id = fmt.Sprint(id) for i := range s.Ingredients { s.Ingredients[i].Step = s.Id s.Ingredients[i].Index = fmt.Sprint(i) err = s.Ingredients[i].Create(tx) if err != nil { return err } } return nil } func (s *Step) getIngredientIds(tx *sql.Tx) ([]Ingredient, error) { retval := make([]Ingredient, 0) cmd := ` SELECT id FROM ingredients WHERE step = ? ORDER BY 'index' ASC ` rows, err := tx.Query(cmd, s.Id) if err != nil { return retval, err } defer rows.Close() for rows.Next() { i := Ingredient{} err = rows.Scan(&i.Id) if err != nil { return retval, err } retval = append(retval, i) } return retval, nil } func (s *Step) Read(tx *sql.Tx) error { cmd := ` SELECT "index", text, recipe FROM steps WHERE id = ? ` rows, err := tx.Query(cmd, s.Id) if err != nil { return err } defer rows.Close() if !rows.Next() { return sql.ErrNoRows } err = rows.Scan(&s.Index, &s.Text, &s.Recipe) if err != nil { return err } s.Ingredients, err = s.getIngredientIds(tx) if err != nil { return err } for i := range s.Ingredients { err = s.Ingredients[i].Read(tx) if err != nil { return err } } return s.Validate() } func (s *Step) Update(tx *sql.Tx) error { err := s.Validate() if err != nil { return err } oldIngredients, err := s.getIngredientIds(tx) if err != nil { return err } for i := range oldIngredients { err = oldIngredients[i].Delete(tx) if err != nil { return err } } for i := range s.Ingredients { s.Ingredients[i].Index = fmt.Sprint(i) s.Ingredients[i].Step = s.Id err = s.Ingredients[i].Create(tx) if err != nil { return err } } cmd := ` UPDATE steps SET index = ?, text = ?, recipe = ? WHERE id = ?` res, err := tx.Exec(cmd, s.Index, s.Text, s.Recipe, s.Id) if err != nil { return err } affected, err := res.RowsAffected() if err != nil { return err } if affected != 1 { return fmt.Errorf("Step update affected %d rows instead of 1", affected) } return nil } func (s *Step) Delete(tx *sql.Tx) error { oldIngredients, err := s.getIngredientIds(tx) if err != nil { return err } for i := range oldIngredients { err = oldIngredients[i].Delete(tx) if err != nil { return err } } cmd := ` DELETE FROM steps WHERE id = ? ` result, err := tx.Exec(cmd, s.Id) if err != nil { return err } rows, err := result.RowsAffected() if rows != 1 { return errors.New("Step deletion did not affect exactly one row") } return nil }