Initial db controllers

feat/db
Antonio De Lucreziis 2 years ago
parent 9df45cf83a
commit c7e33c8e72

@ -0,0 +1,23 @@
# Architecture
## Server, Database and File Uploads
- **server**
The server module defines all routes and extracts data from HTTP request and passes data to the **handler** module.
- **handler**
This is the main controller module that dispatches requests to various services and constructs back responses to send to the client.
### Services
...
- **appunti**
This module manages all things relating the _appunti condivisi_ pages, it talks with the **database** and **auth** modules.
- **database**
This is one of the "leaf" modules that doesn't depend on anything, its purpose is to talk to the database and return "low level data" stored there. For example instances of `time.Time` are represented as `string`s at this layer.

@ -1,9 +1,11 @@
package main package main
import "git.phc.dm.unipi.it/phc/website/database" import (
"git.phc.dm.unipi.it/phc/website/database/sqlite"
)
func main() { func main() {
db, err := database.NewSqlite3Database("phc-server.local.db") db, err := sqlite.New("phc-server.local.db")
if err != nil { if err != nil {
panic(err) panic(err)
} }

@ -1,39 +1,41 @@
package database package database
// Tools
type DBMigrate interface { type DBMigrate interface {
Migrate(migrationDir string) error Migrate(migrationDir string) error
} }
type DBQueryAppunti interface {
AllDispenseFile() ([]DispensaFile, error)
}
type DBDispense interface { type DBDispense interface {
CreateDispensa(template Dispensa) (string, error) Create(template Dispensa) (string, error)
GetDispensa(id string) (Dispensa, error) Get(id string) (Dispensa, error)
AllDispense() ([]Dispensa, error) All() ([]Dispensa, error)
UpdateDispensa(d Dispensa) error Update(d Dispensa) error
DeleteDispensa(id string) error Delete(id string) error
} }
type DBUploads interface { type DBUploads interface {
CreateUpload(template Upload) (string, error) Create(template Upload) (string, error)
GetUpload(id string) (Upload, error) Get(id string) (Upload, error)
} }
type DBFileApprovals interface { type DBFileApprovals interface {
CreateFileApproval(template FileApproval) (string, error) Create(template FileApproval) (string, error)
GetFileApproval(id string) (FileApproval, error) Get(id string) (FileApproval, error)
AllFileApprovals() ([]FileApproval, error) All() ([]FileApproval, error)
UpdateFileApproval(d FileApproval) error Update(d FileApproval) error
DeleteFileApproval(id string) error Delete(id string) error
} }
type DBDownloads interface { type DBDispensaTags interface {
CreateDownload(template Download) (string, error) Set(dispensaId string, tags []string) error
Get(dispensaId string) ([]string, error)
} }
type DBTags interface { type DBDownloads interface {
SetTags(dispensaId string, tags []string) error Create(template Download) error
GetTags(dispensaId string) ([]string, error)
} }
// DB main "interface group" for interacting with the database, with this technique we can test each "table" api of the DB in isolation. // DB main "interface group" for interacting with the database, with this technique we can test each "table" api of the DB in isolation.
@ -42,6 +44,6 @@ type DB struct {
DBDispense DBDispense
DBUploads DBUploads
DBFileApprovals DBFileApprovals
DBDispensaTags
DBDownloads DBDownloads
DBTags
} }

@ -8,6 +8,15 @@ type Dispensa struct {
Description string `db:"description"` Description string `db:"description"`
} }
type DispensaFile struct {
DispensaId string `db:"dispensa_id"`
UploadId string `db:"upload_id"`
OwnerId string `db:"owner_id"`
Title string `db:"title"`
Description string `db:"description"`
File string `db:"file"`
}
type Upload struct { type Upload struct {
Id string `db:"id"` Id string `db:"id"`
CreatedAt string `db:"created_at"` CreatedAt string `db:"created_at"`
@ -21,7 +30,7 @@ type FileApproval struct {
CreatedAt string `db:"created_at"` CreatedAt string `db:"created_at"`
OwnerId string `db:"owner_id"` OwnerId string `db:"owner_id"`
UploadId string `db:"upload_id"` UploadId string `db:"upload_id"`
Status string `db:"status"` Status string `db:"status"` // "approved" | "rejected"
} }
type Download struct { type Download struct {

@ -1,187 +0,0 @@
package database
import (
"fmt"
"log"
"os"
"path"
"time"
"git.phc.dm.unipi.it/phc/website/util"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
type migration struct {
Timestamp string
Filename string
}
type sqliteDB struct {
*sqlx.DB
}
func (db *sqliteDB) Migrate(migrationFolder string) error {
log.Printf(`Creating migrations table`)
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS migrations(timestamp TEXT, filename TEXT)`); err != nil {
return err
}
log.Printf(`Loading applied migrations`)
appliedMigrations := []migration{}
if err := db.Select(&appliedMigrations, `SELECT * FROM migrations`); err != nil {
return err
}
entries, err := os.ReadDir(migrationFolder)
if err != nil {
return err
}
for i, entry := range entries {
if entry.IsDir() {
return fmt.Errorf("no dirs in migrations folder")
}
if i < len(appliedMigrations) {
if appliedMigrations[i].Filename != entry.Name() {
return fmt.Errorf("misapplied migration %q with %q", appliedMigrations[i].Filename, entry.Name())
}
log.Printf("Found applied migration %q", entry.Name())
continue
}
log.Printf("Applying new migration %q", entry.Name())
migrationPath := path.Join(migrationFolder, entry.Name())
sqlStmts, err := os.ReadFile(migrationPath)
if err != nil {
return err
}
tx, err := db.Beginx()
if err != nil {
return err
}
if _, err := tx.Exec(string(sqlStmts)); err != nil {
return err
}
if _, err := tx.Exec(`INSERT INTO migrations VALUES (?, ?)`,
time.Now().Format(time.RFC3339),
entry.Name(),
); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
}
log.Printf("All migrations applied successfully, database up to date")
return nil
}
func (db *sqliteDB) CreateDispensa(template Dispensa) (string, error) {
template.Id = "dispensa/" + util.GenerateRandomString(8)
template.CreatedAt = time.Now().Format(time.RFC3339)
if _, err := db.NamedExec(`
INSERT INTO
dispense(id, created_at, owner_id, title, description)
VALUES
(:id, :created_at, :owner_id, :title, :description)
`, &template); err != nil {
return "", err
}
return template.Id, nil
}
func (db *sqliteDB) GetDispensa(id string) (Dispensa, error) {
var dispensa Dispensa
if err := db.Get(&dispensa, `SELECT * FROM dispense WHERE id = ?`, id); err != nil {
return Dispensa{}, err
}
return dispensa, nil
}
func (db *sqliteDB) AllDispense() ([]Dispensa, error) {
var dispense []Dispensa
if err := db.Select(&dispense, `SELECT * FROM dispense`); err != nil {
return nil, err
}
return dispense, nil
}
func (db *sqliteDB) UpdateDispensa(d Dispensa) error {
if _, err := db.NamedExec(`
UPDATE
dispense
SET
owner_id = :owner_id
title = :title,
description = :description
WHERE
id = :id
`, &d); err != nil {
return err
}
return nil
}
func (db *sqliteDB) DeleteDispensa(id string) error {
panic("TODO: Not implemented")
}
func (db *sqliteDB) CreateUpload(template Upload) (string, error) {
template.Id = "upload/" + util.GenerateRandomString(8)
if _, err := db.NamedExec(`
INSERT INTO
uploads(id, created_at, owner_id, dispensa_id, file)
VALUES
(:id, :created_at, :owner_id, :dispensa_id, :file)
`, &template); err != nil {
return "", err
}
return template.Id, nil
}
func (db *sqliteDB) GetUpload(id string) (Upload, error) {
var upload Upload
if err := db.Select(`SELECT * FROM uploads WHERE id = ?`, id); err != nil {
return Upload{}, err
}
return upload, nil
}
func NewSqlite3Database(filename string) (*DB, error) {
sqlDB, err := sqlx.Open("sqlite3", filename+"?_fk=1")
if err != nil {
panic(err)
}
if err := sqlDB.Ping(); err != nil {
return nil, err
}
db := &sqliteDB{sqlDB}
// in this case the type "sqliteDB" implements all the interfaces declared in "database.DB" so we can pass "db" to all the fields.
return &DB{
DBMigrate: db,
DBDispense: db,
DBUploads: db,
}, nil
}

@ -0,0 +1,31 @@
package sqlite
import (
"git.phc.dm.unipi.it/phc/website/database"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
type migration struct {
Timestamp string
Filename string
}
func New(filename string) (*database.DB, error) {
db, err := sqlx.Open("sqlite3", filename+"?_fk=1")
if err != nil {
panic(err)
}
if err := db.Ping(); err != nil {
return nil, err
}
return &database.DB{
DBMigrate: &sqliteDBMigrate{db},
DBDispense: &sqliteDBDispense{db},
DBUploads: &sqliteDBUploads{db},
DBFileApprovals: &sqliteDBFileApprovals{db},
DBDownloads: &sqliteDBDownloads{db},
}, nil
}

@ -0,0 +1,82 @@
package sqlite
import (
"fmt"
"log"
"os"
"path"
"time"
"git.phc.dm.unipi.it/phc/website/database"
"github.com/jmoiron/sqlx"
)
type sqliteDBMigrate struct{ *sqlx.DB }
var _ database.DBMigrate = sqliteDBMigrate{}
func (db sqliteDBMigrate) Migrate(migrationFolder string) error {
log.Printf(`Creating migrations table`)
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS migrations(timestamp TEXT, filename TEXT)`); err != nil {
return err
}
log.Printf(`Loading applied migrations`)
appliedMigrations := []migration{}
if err := db.Select(&appliedMigrations, `SELECT * FROM migrations`); err != nil {
return err
}
entries, err := os.ReadDir(migrationFolder)
if err != nil {
return err
}
for i, entry := range entries {
if entry.IsDir() {
return fmt.Errorf("no dirs in migrations folder")
}
if i < len(appliedMigrations) {
if appliedMigrations[i].Filename != entry.Name() {
return fmt.Errorf("misapplied migration %q with %q", appliedMigrations[i].Filename, entry.Name())
}
log.Printf("Found applied migration %q", entry.Name())
continue
}
log.Printf("Applying new migration %q", entry.Name())
migrationPath := path.Join(migrationFolder, entry.Name())
sqlStmts, err := os.ReadFile(migrationPath)
if err != nil {
return err
}
tx, err := db.Beginx()
if err != nil {
return err
}
if _, err := tx.Exec(string(sqlStmts)); err != nil {
return err
}
if _, err := tx.Exec(`INSERT INTO migrations VALUES (?, ?)`,
time.Now().Format(time.RFC3339),
entry.Name(),
); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
}
log.Printf("All migrations applied successfully, database up to date")
return nil
}

@ -0,0 +1,68 @@
package sqlite
import (
"time"
"git.phc.dm.unipi.it/phc/website/database"
"git.phc.dm.unipi.it/phc/website/util"
"github.com/jmoiron/sqlx"
)
type sqliteDBDispense struct{ *sqlx.DB }
var _ database.DBDispense = sqliteDBDispense{}
func (d sqliteDBDispense) Create(template database.Dispensa) (string, error) {
template.Id = "dispensa/" + util.GenerateRandomString(8)
template.CreatedAt = time.Now().Format(time.RFC3339)
if _, err := d.DB.NamedExec(`
INSERT INTO
dispense(id, created_at, owner_id, title, description)
VALUES
(:id, :created_at, :owner_id, :title, :description)
`, &template); err != nil {
return "", err
}
return template.Id, nil
}
func (d sqliteDBDispense) Get(id string) (database.Dispensa, error) {
var dispensa database.Dispensa
if err := d.DB.Get(&dispensa, `SELECT * FROM dispense WHERE id = ?`, id); err != nil {
return database.Dispensa{}, err
}
return dispensa, nil
}
func (d sqliteDBDispense) All() ([]database.Dispensa, error) {
var dispense []database.Dispensa
if err := d.DB.Select(&dispense, `SELECT * FROM dispense`); err != nil {
return nil, err
}
return dispense, nil
}
func (d sqliteDBDispense) Update(dispensa database.Dispensa) error {
if _, err := d.DB.NamedExec(`
UPDATE
dispense
SET
owner_id = :owner_id
title = :title,
description = :description
WHERE
id = :id
`, &dispensa); err != nil {
return err
}
return nil
}
func (d sqliteDBDispense) Delete(id string) error {
panic("TODO: Not implemented")
}

@ -0,0 +1,35 @@
package sqlite
import (
"git.phc.dm.unipi.it/phc/website/database"
"git.phc.dm.unipi.it/phc/website/util"
"github.com/jmoiron/sqlx"
)
type sqliteDBUploads struct{ *sqlx.DB }
var _ database.DBUploads = sqliteDBUploads{}
func (u sqliteDBUploads) Create(template database.Upload) (string, error) {
template.Id = "upload/" + util.GenerateRandomString(8)
if _, err := u.DB.NamedExec(`
INSERT INTO
uploads(id, created_at, owner_id, dispensa_id, file)
VALUES
(:id, :created_at, :owner_id, :dispensa_id, :file)
`, &template); err != nil {
return "", err
}
return template.Id, nil
}
func (u sqliteDBUploads) Get(id string) (database.Upload, error) {
var upload database.Upload
if err := u.DB.Select(`SELECT * FROM uploads WHERE id = ?`, id); err != nil {
return database.Upload{}, err
}
return upload, nil
}

@ -0,0 +1,28 @@
package sqlite
import (
"git.phc.dm.unipi.it/phc/website/database"
"github.com/jmoiron/sqlx"
)
type sqliteDBFileApprovals struct{ *sqlx.DB }
func (f sqliteDBFileApprovals) Create(template database.FileApproval) (string, error) {
panic("not implemented") // TODO: Implement
}
func (f sqliteDBFileApprovals) Get(id string) (database.FileApproval, error) {
panic("not implemented") // TODO: Implement
}
func (f sqliteDBFileApprovals) All() ([]database.FileApproval, error) {
panic("not implemented") // TODO: Implement
}
func (f sqliteDBFileApprovals) Update(d database.FileApproval) error {
panic("not implemented") // TODO: Implement
}
func (f sqliteDBFileApprovals) Delete(id string) error {
panic("not implemented") // TODO: Implement
}

@ -0,0 +1,23 @@
package sqlite
import (
"git.phc.dm.unipi.it/phc/website/database"
"github.com/jmoiron/sqlx"
)
type sqliteDBDownloads struct{ *sqlx.DB }
var _ database.DBDownloads = sqliteDBDownloads{}
func (d sqliteDBDownloads) Create(template database.Download) error {
if _, err := d.DB.NamedExec(`
INSERT INTO
downloads(dispensa_id, timestamp)
VALUES
(:dispensa_id, :timestamp)
`, &template); err != nil {
return err
}
return nil
}
Loading…
Cancel
Save