Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
*.json
*.db
data/
.DS_Store
.env
.vscode
Empty file added LICENSE
Empty file.
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
APP="My daily control questions"

.PHONY: run_linter
.PHONY: run_linter run_app

run_linter:
golangci-lint run

run_app:
set -a; . .env; set +a; go run cmd/main.go ask

# Запуск приложения с нуля для проверки работы
start_from_scratch:
rm -rf data
mkdir data/
set -a; . .env; set +a; go run cmd/main.go ask
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
- разбит по неделям
- каждая неделя - один лист
- ~~Создать makefile и прикрутить линтер~~ ([MR](https://github.com/tulashvili/MyDailyControlQuestions/commit/b381af895f580a186e9eb7a07be5a0b26dc70f4f))
- Добавить возможность настройки бэкапа данных по расписанию
38 changes: 13 additions & 25 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,26 @@
package main

import (
"fmt"
"log"
"log/slog"
"os"

"github.com/tulashvili/MyDailyControlQuestions/internal/storage"
"github.com/tulashvili/MyDailyControlQuestions/internal/ui"
)

const (
sqliteStoragePath = "data/sqlite3.db"
app "github.com/tulashvili/MyDailyControlQuestions/internal"
"github.com/tulashvili/MyDailyControlQuestions/internal/config"
"github.com/tulashvili/MyDailyControlQuestions/internal/ui/cli"
)

func main() {
// questions := service.GetQuestions()
// answers := ui.AskQuestion(questions)

conn, err := storage.InitDB(sqliteStoragePath)
conf, err := config.NewConfig(true)
if err != nil {
log.Fatal(err)
slog.Error("failed to load config", slog.Any("error", err))
os.Exit(1)
}

if err := storage.CreateTable(conn); err != nil {
log.Fatal(err)
app, err := app.NewApp(*conf)
if err != nil {
slog.Error("failed to create app", slog.Any("error", err))
os.Exit(1)
}
cli.Execute(app.DB)

// for _, answer := range answers {
// if err := storage.InsertRow(conn, answer); err != nil {
// log.Fatal(err)
// }
// }
fmt.Println("✅ Данные успешно добавлены в таблицу daily_log") // change to log?

if err := ui.ShowDataOverPeriod(conn); err != nil {
log.Panic(err)
}
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ module github.com/tulashvili/MyDailyControlQuestions
go 1.25.4

require github.com/mattn/go-sqlite3 v1.14.33

require (
github.com/caarlos0/env/v11 v11.3.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
)
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
35 changes: 35 additions & 0 deletions internal/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package app

import (
"database/sql"
"log"

"github.com/tulashvili/MyDailyControlQuestions/internal/config"

"github.com/tulashvili/MyDailyControlQuestions/pkg/db/sqlitedb"
)

type App struct {
DB *sql.DB
Config config.Config
}

func NewApp(conf config.Config) (App, error) {
app := &App{
Config: conf,
}

// DB
var err error
app.DB, err = sqlitedb.InitDB(conf.DbPath)
if err != nil {
log.Fatal(err)
}

if err := sqlitedb.CreateTable(app.DB); err != nil {
log.Fatal(err)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а где все остальное? слой репозитория, сервисный слой и т.д

return App{
DB: app.DB,
}, err
}
36 changes: 36 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package config

import (
"fmt"
"log/slog"
"os"

"github.com/joho/godotenv"
"github.com/tulashvili/MyDailyControlQuestions/pkg/db"
)

type Config struct {
DbPath db.DataSource
}

func NewConfig(loadDotEnv bool) (*Config, error) {
if err := os.Setenv("TZ", "UTC"); err != nil {
return nil, err
}

if loadDotEnv {
if err := godotenv.Load(".env"); err == nil {
slog.Info("loaded .env file")
}
}

c := &Config{
DbPath: db.LoadDatasource(),
}

if c.DbPath.SqlitePath == "" {
return nil, fmt.Errorf("DB_PATH is empty")
}

return c, nil
}
23 changes: 23 additions & 0 deletions internal/domain/answer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package domain

import "time"

type UserAnswer struct {
ID int
QuestionCategory string
QuestionText string
Answer int
AnsweredAt *time.Time
}
Comment on lines +5 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

нужно тегирование полей db: "id" это нужно для сопоставления полей с названиями колонок в таблице (для lib sqlx gorm и т.д)


// Init user answer
func CreateUserAnswer(question Question, answer int) UserAnswer {
now := time.Now()

return UserAnswer{
QuestionCategory: string(question.Category),
QuestionText: question.Text,
Answer: answer,
AnsweredAt: &now,
}
}
50 changes: 10 additions & 40 deletions internal/service/service.go → internal/domain/question.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package service
package domain

import (
"fmt"
"time"
type Question struct {
Category
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Category: Category

Text string
}

"github.com/tulashvili/MyDailyControlQuestions/internal/models"
)
type Category string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В отдельный файл


const (
Biology models.Category = "Биология"
Psychology models.Category = "Психология"
Biology Category = "Биология"
Psychology Category = "Психология"
)

var QUESTIONS = []models.Question{
var QUESTIONS = []Question{
{
Category: Psychology,
Text: "Насколько высокий уровень стресса сегодня? (1 - очень стрессовый день, 5 - стресса почти не было)",
Expand All @@ -33,34 +33,4 @@ var QUESTIONS = []models.Question{
Category: Biology,
Text: "В рационе было достаточно белка и клетчатки? (1 - не было, 5 - да)",
},
}

// Return question list with category
func GetQuestions() []models.Question {
return QUESTIONS
}

// Validate user answer
func ValidateAnswer(answer int) error {
if answer < 1 || answer > 5 {
return fmt.Errorf("answer must be between 1 and 5")
}
return nil
}

// Init user answer
func CreateUserAnswer(question models.Question, answer int) models.UserAnswer {
now := time.Now()


return models.UserAnswer{
QuestionCategory: string(question.Category),
QuestionText: question.Text,
Answer: answer,
AnsweredAt: &now,
}
}




}
11 changes: 11 additions & 0 deletions internal/domain/rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package domain

import "errors"

// Validate user answer
func ValidateAnswer(answer int) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сделать его методом для UserAnswer, вынести его в файл answer.go

if answer < 1 || answer > 5 {
return errors.New("answer must be between 1 and 5")
}
return nil
}
20 changes: 0 additions & 20 deletions internal/models/models.go

This file was deleted.

8 changes: 8 additions & 0 deletions internal/repo/answer_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package repo

import "github.com/tulashvili/MyDailyControlQuestions/internal/domain"

type AnswerRepository interface {
SaveAnswer(answer domain.UserAnswer) error
GetAnswers(period int) ([]domain.UserAnswer, error)
}
Comment on lines +1 to +8
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут это не нужно, за хорошую практику считается использовать интерфейсы в месте использования

77 changes: 77 additions & 0 deletions internal/repo/sqlite_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package repo

import (
"database/sql"
"fmt"
"time"

"github.com/tulashvili/MyDailyControlQuestions/internal/domain"
)

type SQLiteAnswerRepository struct {
db *sql.DB
}

func NewSQLiteAnswerRepository(db *sql.DB) *SQLiteAnswerRepository {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

убери из названия SQLite

return &SQLiteAnswerRepository{db: db}
}

// Save answers to questions
func (r *SQLiteAnswerRepository) SaveAnswer(answer domain.UserAnswer) error {
query := `
INSERT INTO daily_log (category, question, answer, answeredAt)
VALUES (?, ?, ?, ?)
`
Comment on lines +21 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

переписать под sqlx и почитай про sql инъекции

_, err := r.db.Exec(
query,
answer.QuestionCategory,
answer.QuestionText,
answer.Answer,
answer.AnsweredAt.UTC().Format(time.RFC3339Nano),
)
return err
}

// Get data over some period
func (r *SQLiteAnswerRepository) GetAnswers(period int) ([]domain.UserAnswer, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

так же переписать весь метод под sqlx

periodDay := fmt.Sprintf("-%d days", period)
query := `
SELECT category, question, answer, answeredAt
FROM daily_log
WHERE answeredAt >= datetime('now', ?);
`

rows, err := r.db.Query(query, periodDay)
if err != nil {
return nil, err
}
defer rows.Close()

var result []domain.UserAnswer
var answeredAtStr sql.NullString

for rows.Next() {
var ua domain.UserAnswer

if err := rows.Scan(
&ua.QuestionCategory,
&ua.QuestionText,
&ua.Answer,
&answeredAtStr,
); err != nil {
return nil, err
}

if answeredAtStr.Valid {
t, err := time.Parse(time.RFC3339Nano, answeredAtStr.String)
if err != nil {
return nil, err
}
ua.AnsweredAt = &t
}

result = append(result, ua)
}

return result, nil
}
Loading