package storage import ( "context" "errors" ) // ErrInUse — запись нельзя удалить, пока на неё ссылаются (например, специальность с врачами). var ErrInUse = errors.New("storage: in use") type AdminSpecialty struct { ID int64 `json:"id"` Slug string `json:"slug"` Name string `json:"name"` NamePlural string `json:"name_plural"` Sort int `json:"sort"` Published bool `json:"published"` Doctors int `json:"doctors"` // все врачи раздела, включая скрытых } type SpecialtyInput struct { Slug string Name string NamePlural string Sort int } func (s *Storage) AdminSpecialties(ctx context.Context) ([]AdminSpecialty, error) { rows, err := s.pool.Query(ctx, ` SELECT sp.id, sp.slug, sp.name, sp.name_plural, sp.sort, sp.published, (SELECT count(*) FROM doctor_specialties ds JOIN doctors d ON d.id = ds.doctor_id AND d.deleted_at IS NULL WHERE ds.specialty_id = sp.id) FROM specialties sp ORDER BY sp.sort, sp.name_plural, sp.id`) if err != nil { return nil, err } defer rows.Close() var out []AdminSpecialty for rows.Next() { var sp AdminSpecialty if err := rows.Scan(&sp.ID, &sp.Slug, &sp.Name, &sp.NamePlural, &sp.Sort, &sp.Published, &sp.Doctors); err != nil { return nil, err } out = append(out, sp) } return out, rows.Err() } func (s *Storage) AdminCreateSpecialty(ctx context.Context, in SpecialtyInput) (int64, error) { var id int64 err := s.pool.QueryRow(ctx, ` INSERT INTO specialties (slug, name, name_plural, sort) VALUES ($1, $2, $3, $4) RETURNING id`, in.Slug, in.Name, in.NamePlural, in.Sort).Scan(&id) return id, wrapConflict(err) } // AdminUpdateSpecialty меняет название и порядок. Адрес раздела не меняется: // он входит в адреса страниц всех врачей раздела. func (s *Storage) AdminUpdateSpecialty(ctx context.Context, id int64, in SpecialtyInput) error { tag, err := s.pool.Exec(ctx, `UPDATE specialties SET name=$2, name_plural=$3, sort=$4 WHERE id=$1`, id, in.Name, in.NamePlural, in.Sort) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } // AdminDeleteSpecialty удаляет только пустой раздел: иначе у врачей пропала бы специальность // (связи удаляются каскадом) и сменились бы адреса их страниц. func (s *Storage) AdminDeleteSpecialty(ctx context.Context, id int64) error { tag, err := s.pool.Exec(ctx, ` DELETE FROM specialties sp WHERE sp.id = $1 AND NOT EXISTS (SELECT 1 FROM doctor_specialties ds WHERE ds.specialty_id = sp.id)`, id) if err != nil { return err } if tag.RowsAffected() == 0 { var exists bool if err := s.pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM specialties WHERE id=$1)`, id).Scan(&exists); err != nil { return err } if exists { return ErrInUse } return ErrNotFound } return nil } type NewDoctor struct { Slug string LastName string FirstName string MiddleName string Position string SpecialtyID int64 } // AdminCreateDoctor заводит врача скрытым: на сайт он попадёт, когда карточку заполнят и включат. func (s *Storage) AdminCreateDoctor(ctx context.Context, in NewDoctor) (int64, error) { tx, err := s.pool.Begin(ctx) if err != nil { return 0, err } defer tx.Rollback(ctx) //nolint:errcheck // после Commit откат ничего не делает var id int64 if err := tx.QueryRow(ctx, ` INSERT INTO doctors (slug, last_name, first_name, middle_name, position, published) VALUES ($1, $2, $3, $4, $5, false) RETURNING id`, in.Slug, in.LastName, in.FirstName, in.MiddleName, in.Position).Scan(&id); err != nil { return 0, wrapConflict(err) } if _, err := tx.Exec(ctx, ` INSERT INTO doctor_specialties (doctor_id, specialty_id, is_primary) VALUES ($1, $2, true)`, id, in.SpecialtyID); err != nil { return 0, wrapConflict(err) } return id, tx.Commit(ctx) }