removed 'internal' directory

This commit is contained in:
Bronku 2025-06-19 21:48:06 +02:00
parent 8c200a5a6c
commit 65709049d7
42 changed files with 35 additions and 44 deletions

105
store/cake.go Normal file
View file

@ -0,0 +1,105 @@
package store
import (
"errors"
"fmt"
"github.com/Bronku/iroon/models"
)
func (s *Store) cakeCount() int {
out := 0
rows, err := s.db.Query("select count(*) from cake;")
if err != nil {
return out
}
defer rows.Close()
_ = rows.Next()
_ = rows.Scan(&out)
return out
}
func (s *Store) loadCakes() ([]models.Cake, error) {
out := make([]models.Cake, 0, s.cakeCount())
rows, err := s.db.Query("select id, name, price, category, availability from cake order by id;")
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var cake models.Cake
err = rows.Scan(&cake.ID, &cake.Name, &cake.Price, &cake.Category, &cake.Availability)
if err != nil {
continue
}
out = append(out, cake)
}
return out, nil
}
func (s *Store) searchCakes(id int) (int, error) {
for i, e := range s.cakes {
if e.ID != id {
continue
}
return i, nil
}
return 0, errors.New("cake not found")
}
func (s *Store) GetCake(id int) (models.Cake, error) {
if id <= 0 {
return models.Cake{}, errors.New("invalid cake id")
}
i, err := s.searchCakes(id)
if err != nil {
return models.Cake{}, err
}
return s.cakes[i], err
}
func (s *Store) GetCakes() ([]models.Cake, error) {
result := make([]models.Cake, len(s.cakes))
copy(result, s.cakes)
return result, nil
}
func (s *Store) updateCake(newCake models.Cake) error {
query := "update cake set name = ? , price = ?, category = ?, availability = ? where id = ?"
_, err := s.db.Exec(query, newCake.Name, newCake.Price, newCake.Category, newCake.Availability, newCake.ID)
if err != nil {
return err
}
i, err := s.searchCakes(newCake.ID)
if err != nil {
return err
}
s.cakes[i] = newCake
return nil
}
func (s *Store) SaveCake(newCake models.Cake) (int, error) {
if newCake.ID != 0 {
return newCake.ID, s.updateCake(newCake)
}
fmt.Println("adding a new cake", newCake)
query := "insert into cake(name, price, category, availability) values (?, ?, ?, ?);"
result, err := s.db.Exec(query, newCake.Name, newCake.Price, newCake.Category, newCake.Availability)
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
newCake.ID = int(id)
s.cakes = append(s.cakes, newCake)
return newCake.ID, nil
}

49
store/migrations.go Normal file
View file

@ -0,0 +1,49 @@
package store
import (
"embed"
_ "embed"
"log"
"strconv"
"strings"
)
//go:embed migrations/*.sql
var migrations embed.FS
func (s *Store) loadFile(file string) {
filename := strings.Split(file, ".")
version, err := strconv.Atoi(filename[0])
if err != nil {
log.Fatal("only allowed files in migrations directory are <version>.txt")
}
if version <= s.version() {
return
}
query, _ := migrations.ReadFile("migrations/" + file)
_, err = s.db.Exec(string(query))
if err != nil {
log.Fatal("error executing migration: ", file, err)
}
}
func (s *Store) loadMigrations() {
migrationFiles, _ := migrations.ReadDir("migrations")
for _, e := range migrationFiles {
s.loadFile(e.Name())
}
}
func (s *Store) version() int {
out := -1
row, err := s.db.Query("PRAGMA user_version;")
if err != nil {
return out
}
defer row.Close()
row.Next()
_ = row.Scan(&out)
return out
}

107
store/migrations/1.sql Normal file
View file

@ -0,0 +1,107 @@
create table cake (
id integer primary key autoincrement,
name text not null unique,
price integer not null,
category text,
availability text
);
create table customer_order (
id integer primary key autoincrement,
name text,
surname text,
phone text,
location text,
order_date string not null,
delivery_date string not null,
status text,
paid integer
);
create table ordered_cake (
customer_order integer references customer_order (id) not null,
cake integer references cake (id) not null,
amount integer not null,
primary key (customer_order, cake)
);
create table user (
login text primary key,
password text,
role text,
salt text
);
create table session (
token text primary key,
user text,
expiration text
);
create virtual table order_fts using fts5 (
id,
name,
surname,
phone,
location,
order_date,
delivery_date,
status,
paid,
content = 'customer_order'
);
create trigger order_fts_insert after insert on customer_order begin
insert into
order_fts (
id,
name,
surname,
phone,
location,
order_date,
delivery_date,
status,
paid
)
values
(
new.id,
new.name,
new.surname,
new.phone,
new.location,
new.order_date,
new.delivery_date,
new.status,
new.paid
);
end;
create trigger order_fts_delete after delete on customer_order begin
delete from order_fts
where
id = old.id;
end;
create trigger order_fts_update after
update on customer_order begin
update customer_order
set
id = new.id,
name = new.name,
surname = new.surname,
phone = new.phone,
location = new.location,
order_date = new.order_date,
delivery_date = new.delivery_date,
status = new.status,
paid = new.paid
where
id = old.id;
end;
pragma user_version = 1;

176
store/order.go Normal file
View file

@ -0,0 +1,176 @@
package store
import (
"database/sql"
"errors"
"fmt"
"time"
"github.com/Bronku/iroon/models"
)
// #todo update labels in templates
func (s *Store) parseOrderRow(row *sql.Rows) (models.Order, error) {
var out models.Order
var orderDate, deliveryDate string
err := row.Scan(&out.ID, &out.Name, &out.Surname, &out.Phone, &out.Location, &orderDate, &deliveryDate, &out.Status, &out.Paid)
if err != nil {
return out, err
}
out.Accepted, _ = time.Parse("2006-01-02 15:04", orderDate)
out.Date, _ = time.Parse("2006-01-02 15:04", deliveryDate)
out.Cakes = make([]models.Cake, 0)
rows, err := s.db.Query("select cake, amount from ordered_cake where customer_order = ?;", out.ID)
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var newCake models.Cake
err = rows.Scan(&newCake.ID, &newCake.Amount)
if err != nil {
return out, err
}
cakeData, err := s.GetCake(newCake.ID)
if err != nil {
continue
}
newCake.Name = cakeData.Name
newCake.Price = cakeData.Price
out.Cakes = append(out.Cakes, newCake)
}
return out, nil
}
func (s *Store) getOrdersFromQuery(query string, args ...any) ([]models.Order, error) {
var out []models.Order
rows, err := s.db.Query(query, args...)
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
order, err := s.parseOrderRow(rows)
if err != nil {
fmt.Println(err)
continue
}
out = append(out, order)
}
return out, nil
}
func (s *Store) GetOrder(id int) (models.Order, error) {
query := "select * from customer_order where id = ?;"
out, err := s.getOrdersFromQuery(query, id)
if err != nil {
return models.Order{}, err
}
if len(out) < 1 {
return models.Order{}, errors.New("no output order")
}
return out[0], nil
}
func (s *Store) GetFilteredOrder(filter string, from, to time.Time) ([]models.Order, error) {
start := from.Format("2006-01-02") + " 00:00"
end := to.Format("2006-01-02") + " 99:99"
if to.IsZero() {
end = "9999-99-99 99:99"
}
if filter == "" {
query := "select * from customer_order where status != 'done' and delivery_date >= ? and delivery_date <= ? ;"
return s.getOrdersFromQuery(query, start, end)
}
query := "select id, name, surname, phone, location, order_date, delivery_date, status, paid from order_fts(?) where delivery_date >= ? and delivery_date <= ? order by rank;"
return s.getOrdersFromQuery(query, filter, start, end)
}
func (s *Store) UpdateOrderContents(tx *sql.Tx, newOrder models.Order) error {
query := "delete from ordered_cake where customer_order = ?;"
_, err := tx.Exec(query, newOrder.ID)
if err != nil {
return err
}
query = "insert into ordered_cake(customer_order, cake, amount) values (?,?,?);"
for _, e := range newOrder.Cakes {
_, err := tx.Exec(query, newOrder.ID, e.ID, e.Amount)
if err != nil {
return err
}
}
return nil
}
func (s *Store) UpdateOrder(newOrder models.Order) error {
_, err := s.GetOrder(newOrder.ID)
if err != nil {
return err
}
query := "update customer_order set name = ?, surname = ?, phone = ?, location = ?, order_date = ?, delivery_date = ?, status = ?, paid = ? where id = ?;"
accepted := newOrder.Accepted.Format("2006-01-02 15:04")
date := newOrder.Date.Format("2006-01-02 15:04")
tx, err := s.db.Begin()
if err != nil {
return err
}
_, err = tx.Exec(query, newOrder.Name, newOrder.Surname, newOrder.Phone, newOrder.Location, accepted, date, newOrder.Status, newOrder.Paid, newOrder.ID)
if err != nil {
_ = tx.Rollback()
return err
}
err = s.UpdateOrderContents(tx, newOrder)
if err != nil {
_ = tx.Rollback()
return err
}
err = tx.Commit()
return nil
}
func (s *Store) SaveOrder(newOrder models.Order) (int, error) {
if newOrder.ID != 0 {
return newOrder.ID, s.UpdateOrder(newOrder)
}
query := "insert into customer_order(name, surname, phone, location, order_date, delivery_date, status, paid) values (?, ?, ?, ?, ?, ?, ?, ?);"
tx, err := s.db.Begin()
if err != nil {
return 0, err
}
accepted := newOrder.Accepted.Format("2006-01-02 15:04")
date := newOrder.Date.Format("2006-01-02 15:04")
result, err := tx.Exec(query, newOrder.Name, newOrder.Surname, newOrder.Phone, newOrder.Location, accepted, date, newOrder.Status, newOrder.Paid)
if err != nil {
_ = tx.Rollback()
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
_ = tx.Rollback()
return newOrder.ID, err
}
newOrder.ID = int(id)
err = s.UpdateOrderContents(tx, newOrder)
if err != nil {
_ = tx.Rollback()
return newOrder.ID, err
}
err = tx.Commit()
return newOrder.ID, err
}

58
store/session.go Normal file
View file

@ -0,0 +1,58 @@
package store
import (
"time"
"github.com/Bronku/iroon/models"
)
func (s *Store) AddSession(token, userName string, expiration time.Time) error {
query := "insert into session (token, user, expiration) values(?, ?, ?)"
_, err := s.db.Exec(query, token, userName, expiration.Format("2006-01-02 15:04"))
return err
}
func (s *Store) RevokeSession(token string) error {
query := "delete from session where token = ?;"
_, err := s.db.Exec(query, token)
return err
}
func (s *Store) CleanSessions() error {
now := time.Now().Format("2006-01-02 15:04")
query := "delete from session where expiration < ?;"
_, err := s.db.Exec(query, now)
return err
}
func (s *Store) GetSessions() (map[string]models.Token, error) {
out := make(map[string]models.Token)
err := s.CleanSessions()
if err != nil {
return out, err
}
query := "select token, user, expiration from session;"
rows, err := s.db.Query(query)
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var current models.Token
var token string
var expiration string
err = rows.Scan(&token, &current.User, &expiration)
if err != nil {
continue
}
current.Expiration, err = time.Parse("2006-01-02 15:04", expiration)
if err != nil {
continue
}
if token == "" {
continue
}
out[token] = current
}
return out, nil
}

43
store/store.go Normal file
View file

@ -0,0 +1,43 @@
package store
import (
"database/sql"
"log"
"github.com/Bronku/iroon/models"
_ "github.com/knaka/go-sqlite3-fts5"
_ "github.com/mattn/go-sqlite3"
)
type Store struct {
db *sql.DB
cakes []models.Cake
users map[string]models.User
}
func OpenStore(filename string) *Store {
var out Store
var err error
out.db, err = sql.Open("sqlite3", filename)
if err != nil {
log.Fatal("Can't open the database", filename, err)
}
out.loadMigrations()
out.cakes, err = out.loadCakes()
if err != nil {
log.Fatal(err)
}
out.users, err = out.loadUsers()
if err != nil {
log.Fatal(err)
}
return &out
}
func (s *Store) Close() {
if s.db != nil {
s.db.Close()
}
}

293
store/store_test.go Normal file
View file

@ -0,0 +1,293 @@
package store
import (
"testing"
"time"
"github.com/Bronku/iroon/internal/models"
)
func TestStore(t *testing.T) {
s := OpenStore("file:memdb1?mode=memory&cache=shared")
defer s.Close()
// ensure db is empty
cakes, err := s.GetCakes()
if err != nil {
t.Errorf("Error getting cakes: %v", err)
}
if len(cakes) != 0 {
t.Errorf("Expected empty cakes list, got: %v", cakes)
}
orders, err := s.GetFilteredOrder("", time.Now(), time.Now().Add(time.Hour*24))
if err != nil {
t.Errorf("Error getting orders: %v", err)
}
if len(orders) != 0 {
t.Errorf("Expected empty orders list, got: %v", orders)
}
// create new cake
newCake := models.Cake{Name: "Chocolate Cake", Price: 2500}
newCake.ID, err = s.SaveCake(newCake)
if err != nil {
t.Fatalf("Failed to save new cake: %v", err)
}
if newCake.ID <= 0 {
t.Errorf("Expected positive cake ID, got: %d", newCake.ID)
}
// create another cake
anotherCake := models.Cake{Name: "Another Cake", Price: 3000}
anotherCake.ID, err = s.SaveCake(anotherCake)
if err != nil {
t.Fatalf("Failed to save new cake: %v", err)
}
if anotherCake.ID <= 0 {
t.Errorf("Expected positive cake ID, got: %d", anotherCake.ID)
}
// Update Existing Cake
newCake.Name = "Updated Cake"
newCake.Price = 100
newID, err := s.SaveCake(newCake)
if newID != newCake.ID {
t.Error("wrong id")
}
if err != nil {
t.Error("error updating cake", err)
}
// Update Non-existing Cake
var updatedCake models.Cake
updatedCake.ID = 10
_, err = s.SaveCake(updatedCake)
if err == nil {
t.Error("did not return an error when attempted to update non existent cake")
}
// get cakes
cakes, err = s.GetCakes()
if err != nil {
t.Fatalf("Failed to get all cakes after creation: %v", err)
}
if len(cakes) != 2 {
t.Error("Expected two cakes returned, got ", len(cakes))
}
for i, c := range cakes {
if c.ID == newCake.ID {
newCake.Amount = i
}
if c.ID == anotherCake.ID {
anotherCake.Amount = i
}
}
if newCake.Amount == -1 {
t.Error("not found newCake")
}
if cakes[newCake.Amount].ID != newCake.ID || cakes[newCake.Amount].Name != newCake.Name || cakes[newCake.Amount].Price != newCake.Price {
t.Errorf("Want %v\nGot %v", newCake, cakes[newCake.Amount])
}
if anotherCake.Amount == -1 {
t.Error("not found anotherCake")
}
if cakes[anotherCake.Amount].ID != anotherCake.ID || cakes[anotherCake.Amount].Name != anotherCake.Name || cakes[anotherCake.Amount].Price != anotherCake.Price {
t.Errorf("Want %v\nGot %v", anotherCake, cakes)
}
selectedCake, err := s.GetCake(newCake.ID)
if err != nil {
t.Error("error getting a cake", err)
}
if selectedCake.ID != newCake.ID || selectedCake.Name != newCake.Name || selectedCake.Price != newCake.Price {
t.Errorf("Want %v\nGot %v", newCake, selectedCake)
}
selectedCake, err = s.GetCake(-1)
if err == nil {
t.Error("no error getting a non-existent cake")
}
// create new order
now := time.Now()
newOrder := models.Order{
Name: "John",
Surname: "Doe",
Phone: "123-456-7890",
Location: "Some Location",
Accepted: now,
Date: now.Add(time.Hour * 24),
Status: "Pending",
Paid: 1000,
Cakes: []models.Cake{{ID: newCake.ID, Amount: 2}, {ID: anotherCake.ID, Amount: 10}},
}
newOrder.ID, err = s.SaveOrder(newOrder)
if err != nil {
t.Fatalf("Failed to save new order: %v", err)
}
if newOrder.ID <= 0 {
t.Errorf("Expected positive order ID, got: %d", newOrder.ID)
}
// create another order
anotherOrder := models.Order{
Name: "Jane",
Surname: "Doe",
Phone: "123-456-7890",
Location: "New Location",
Accepted: now,
Date: now.Add(time.Hour * 192),
Status: "Accepted",
Paid: 1500,
Cakes: []models.Cake{{ID: newCake.ID, Amount: 100}},
}
anotherOrder.ID, err = s.SaveOrder(anotherOrder)
if err != nil {
t.Fatalf("Failed to save new order: %v", err)
}
if anotherOrder.ID <= 0 {
t.Errorf("Expected positive order ID, got: %d", anotherOrder.ID)
}
// update existing order
newOrder.Name = "James"
newOrder.Status = "Done"
newOrder.Cakes = newOrder.Cakes[1:]
newID, err = s.SaveOrder(newOrder)
if newID != newOrder.ID {
t.Error("wrong id")
}
if err != nil {
t.Error("error updating order", err)
}
// update non existing order
var updatedOrder models.Order
updatedOrder.ID = 10
_, err = s.SaveOrder(updatedOrder)
if err == nil {
t.Error("did not return an error when attempted to update non existent cake")
}
// get orders
orders, err = s.GetFilteredOrder("", time.Time{}, time.Time{})
if err != nil {
t.Fatalf("Failed to get all orders after creation: %v", err)
}
if len(orders) != 2 {
t.Error("Expected two orders returned, got ", len(orders))
}
newOrderPos := -1
anotherOrderPos := -1
for i, o := range orders {
if o.ID == newOrder.ID {
newOrderPos = i
}
if o.ID == anotherOrder.ID {
anotherOrderPos = i
}
}
if newOrderPos == -1 {
t.Error("not found newOrder")
}
if newOrder.Name != orders[newOrderPos].Name ||
newOrder.Surname != orders[newOrderPos].Surname ||
newOrder.Phone != orders[newOrderPos].Phone ||
newOrder.Status != orders[newOrderPos].Status ||
newOrder.Location != orders[newOrderPos].Location ||
newOrder.Accepted.Format("2006-01-02 15:04") != orders[newOrderPos].Accepted.Format("2006-01-02 15:04") ||
newOrder.Date.Format("2006-01-02 15:04") != orders[newOrderPos].Date.Format("2006-01-02 15:04") {
t.Errorf("Want %v\nGot %v", newOrder, orders[newOrderPos])
}
if !areCakeSlicesEqual(newOrder.Cakes, orders[newOrderPos].Cakes) {
t.Errorf("Want %v\nGot %v", newOrder.Cakes, orders[newOrderPos].Cakes)
}
if anotherOrderPos == -1 {
t.Error("not found anotherOrder")
}
if anotherOrder.Name != orders[anotherOrderPos].Name ||
anotherOrder.Surname != orders[anotherOrderPos].Surname ||
anotherOrder.Phone != orders[anotherOrderPos].Phone ||
anotherOrder.Status != orders[anotherOrderPos].Status ||
anotherOrder.Location != orders[anotherOrderPos].Location ||
anotherOrder.Accepted.Format("2006-01-02 15:04") != orders[anotherOrderPos].Accepted.Format("2006-01-02 15:04") ||
anotherOrder.Date.Format("2006-01-02 15:04") != orders[anotherOrderPos].Date.Format("2006-01-02 15:04") {
t.Errorf("Want %v\nGot %v", anotherOrder, orders[anotherOrderPos])
}
if !areCakeSlicesEqual(anotherOrder.Cakes, orders[anotherOrderPos].Cakes) {
t.Errorf("Want %v\nGot %v", anotherOrder.Cakes, orders[anotherOrderPos].Cakes)
}
selectedOrder, err := s.GetOrder(newOrder.ID)
if err != nil {
t.Error("error getting an order by id ", err)
}
if newOrder.Name != selectedOrder.Name ||
newOrder.Surname != selectedOrder.Surname ||
newOrder.Phone != selectedOrder.Phone ||
newOrder.Status != selectedOrder.Status ||
newOrder.Location != selectedOrder.Location ||
newOrder.Accepted.Format("2006-01-02 15:04") != selectedOrder.Accepted.Format("2006-01-02 15:04") ||
newOrder.Date.Format("2006-01-02 15:04") != selectedOrder.Date.Format("2006-01-02 15:04") {
t.Errorf("Want %v\nGot %v", newOrder, selectedOrder)
}
if !areCakeSlicesEqual(newOrder.Cakes, selectedOrder.Cakes) {
t.Errorf("Want %v\nGot %v", newOrder.Cakes, selectedOrder.Cakes)
}
_, err = s.GetOrder(-1)
if err == nil {
t.Error("no error getting invalid order")
}
}
func TestSliceComparison(t *testing.T) {
a := []models.Cake{
{Name: "ok", Price: 100, ID: 12, Amount: 10},
{Name: "ok", Price: 11, ID: 13, Amount: 1},
{Name: "ok", Price: 100, ID: 10, Amount: 11},
}
b := []models.Cake{
{Name: "okas", Price: 110, ID: 12, Amount: 10},
{Name: "oksa", Price: 11, ID: 13, Amount: 1},
{Name: "oasdk", Price: 101, ID: 10, Amount: 11},
}
if !areCakeSlicesEqual(a, b) {
t.Error("aren't equal")
}
}
func areCakesEqual(a, b models.Cake) bool {
return a.ID == b.ID && a.Amount == b.Amount
}
func areCakeSlicesEqual(a, b []models.Cake) bool {
if len(a) != len(b) {
return false
}
matched := make([]bool, len(a))
mainLoop:
for _, e1 := range a {
for i, e2 := range b {
if matched[i] {
continue
}
if areCakesEqual(e1, e2) {
matched[i] = true
continue mainLoop
}
}
}
for _, e := range matched {
if e == false {
return false
}
}
return true
}

48
store/user.go Normal file
View file

@ -0,0 +1,48 @@
package store
import (
"errors"
"github.com/Bronku/iroon/crypto"
"github.com/Bronku/iroon/models"
)
func (s *Store) AddUser(login, password string) error {
_, exists := s.GetUser(login)
if exists {
return errors.New("the user already exists")
}
query := "insert into user (login, password, salt) values(?, ?, ?)"
salt := crypto.GenerateKey()
hash := crypto.PasswordHash(password, salt)
_, err := s.db.Exec(query, login, hash, salt)
if err == nil {
s.users[login] = models.User{Password: hash, Salt: salt}
}
return err
}
func (s *Store) loadUsers() (map[string]models.User, error) {
out := make(map[string]models.User)
query := "select login, password, salt from user;"
rows, err := s.db.Query(query)
if err != nil {
return out, err
}
defer rows.Close()
for rows.Next() {
var current models.User
var login string
err = rows.Scan(&login, &current.Password, &current.Salt)
if err != nil {
continue
}
out[login] = current
}
return out, nil
}
func (s *Store) GetUser(login string) (models.User, bool) {
value, ok := s.users[login]
return value, ok
}