From c7e33c8e72b1d7af22af44e6c7a7eb8d4ed459ef Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Mon, 26 Sep 2022 20:14:54 +0200 Subject: [PATCH] Initial db controllers --- ARCHITECTURE.md | 23 ++++ database/cmd/main.go | 6 +- database/database.go | 42 +++--- database/model.go | 11 +- database/sqlite.go | 187 --------------------------- database/sqlite/000_sqlite.go | 31 +++++ database/sqlite/001_migration.go | 82 ++++++++++++ database/sqlite/002_dispense.go | 68 ++++++++++ database/sqlite/003_uploads.go | 35 +++++ database/sqlite/004_fileapprovals.go | 28 ++++ database/sqlite/005_downloads.go | 23 ++++ 11 files changed, 326 insertions(+), 210 deletions(-) create mode 100644 ARCHITECTURE.md delete mode 100644 database/sqlite.go create mode 100644 database/sqlite/000_sqlite.go create mode 100644 database/sqlite/001_migration.go create mode 100644 database/sqlite/002_dispense.go create mode 100644 database/sqlite/003_uploads.go create mode 100644 database/sqlite/004_fileapprovals.go create mode 100644 database/sqlite/005_downloads.go diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..881b389 --- /dev/null +++ b/ARCHITECTURE.md @@ -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. \ No newline at end of file diff --git a/database/cmd/main.go b/database/cmd/main.go index 2b6da1a..25eb408 100644 --- a/database/cmd/main.go +++ b/database/cmd/main.go @@ -1,9 +1,11 @@ package main -import "git.phc.dm.unipi.it/phc/website/database" +import ( + "git.phc.dm.unipi.it/phc/website/database/sqlite" +) func main() { - db, err := database.NewSqlite3Database("phc-server.local.db") + db, err := sqlite.New("phc-server.local.db") if err != nil { panic(err) } diff --git a/database/database.go b/database/database.go index 5ebeec9..08f501a 100644 --- a/database/database.go +++ b/database/database.go @@ -1,39 +1,41 @@ package database -// Tools - type DBMigrate interface { Migrate(migrationDir string) error } +type DBQueryAppunti interface { + AllDispenseFile() ([]DispensaFile, error) +} + type DBDispense interface { - CreateDispensa(template Dispensa) (string, error) - GetDispensa(id string) (Dispensa, error) - AllDispense() ([]Dispensa, error) - UpdateDispensa(d Dispensa) error - DeleteDispensa(id string) error + Create(template Dispensa) (string, error) + Get(id string) (Dispensa, error) + All() ([]Dispensa, error) + Update(d Dispensa) error + Delete(id string) error } type DBUploads interface { - CreateUpload(template Upload) (string, error) - GetUpload(id string) (Upload, error) + Create(template Upload) (string, error) + Get(id string) (Upload, error) } type DBFileApprovals interface { - CreateFileApproval(template FileApproval) (string, error) - GetFileApproval(id string) (FileApproval, error) - AllFileApprovals() ([]FileApproval, error) - UpdateFileApproval(d FileApproval) error - DeleteFileApproval(id string) error + Create(template FileApproval) (string, error) + Get(id string) (FileApproval, error) + All() ([]FileApproval, error) + Update(d FileApproval) error + Delete(id string) error } -type DBDownloads interface { - CreateDownload(template Download) (string, error) +type DBDispensaTags interface { + Set(dispensaId string, tags []string) error + Get(dispensaId string) ([]string, error) } -type DBTags interface { - SetTags(dispensaId string, tags []string) error - GetTags(dispensaId string) ([]string, error) +type DBDownloads interface { + Create(template Download) error } // 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 DBUploads DBFileApprovals + DBDispensaTags DBDownloads - DBTags } diff --git a/database/model.go b/database/model.go index a5557fc..9004269 100644 --- a/database/model.go +++ b/database/model.go @@ -8,6 +8,15 @@ type Dispensa struct { 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 { Id string `db:"id"` CreatedAt string `db:"created_at"` @@ -21,7 +30,7 @@ type FileApproval struct { CreatedAt string `db:"created_at"` OwnerId string `db:"owner_id"` UploadId string `db:"upload_id"` - Status string `db:"status"` + Status string `db:"status"` // "approved" | "rejected" } type Download struct { diff --git a/database/sqlite.go b/database/sqlite.go deleted file mode 100644 index cae6a2e..0000000 --- a/database/sqlite.go +++ /dev/null @@ -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 -} diff --git a/database/sqlite/000_sqlite.go b/database/sqlite/000_sqlite.go new file mode 100644 index 0000000..87d8deb --- /dev/null +++ b/database/sqlite/000_sqlite.go @@ -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 +} diff --git a/database/sqlite/001_migration.go b/database/sqlite/001_migration.go new file mode 100644 index 0000000..6abfa5a --- /dev/null +++ b/database/sqlite/001_migration.go @@ -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 +} diff --git a/database/sqlite/002_dispense.go b/database/sqlite/002_dispense.go new file mode 100644 index 0000000..707c6a3 --- /dev/null +++ b/database/sqlite/002_dispense.go @@ -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") +} diff --git a/database/sqlite/003_uploads.go b/database/sqlite/003_uploads.go new file mode 100644 index 0000000..b8641f1 --- /dev/null +++ b/database/sqlite/003_uploads.go @@ -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 +} diff --git a/database/sqlite/004_fileapprovals.go b/database/sqlite/004_fileapprovals.go new file mode 100644 index 0000000..05bcc77 --- /dev/null +++ b/database/sqlite/004_fileapprovals.go @@ -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 +} diff --git a/database/sqlite/005_downloads.go b/database/sqlite/005_downloads.go new file mode 100644 index 0000000..2e95316 --- /dev/null +++ b/database/sqlite/005_downloads.go @@ -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 +}