From 2ce1ac9bf1b6929460802d58ec61a72d865e48b7 Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Tue, 13 Sep 2022 02:12:00 +0200 Subject: [PATCH] Database prototype --- appunti/appunti.go | 17 +++ database/cmd/main.go | 14 +++ database/database.go | 91 ++++++++++++++ database/migrations/0001-2022-08-31.sql | 48 +++++++ database/model.go | 44 +++++++ database/sqlite.go | 161 ++++++++++++++++++++++++ go.mod | 2 + go.sum | 7 ++ model/appunti.go | 44 +++++++ 9 files changed, 428 insertions(+) create mode 100644 appunti/appunti.go create mode 100644 database/cmd/main.go create mode 100644 database/database.go create mode 100644 database/migrations/0001-2022-08-31.sql create mode 100644 database/model.go create mode 100644 database/sqlite.go create mode 100644 model/appunti.go diff --git a/appunti/appunti.go b/appunti/appunti.go new file mode 100644 index 0000000..4ee6c47 --- /dev/null +++ b/appunti/appunti.go @@ -0,0 +1,17 @@ +package appunti + +import ( + "git.phc.dm.unipi.it/phc/website/database" +) + +type Service interface { + ViewDispense() error +} + +type DefaultService struct { + DB database.DB +} + +func (s *DefaultService) CreateDispensa(user string, template database.Dispensa) { + +} diff --git a/database/cmd/main.go b/database/cmd/main.go new file mode 100644 index 0000000..2b6da1a --- /dev/null +++ b/database/cmd/main.go @@ -0,0 +1,14 @@ +package main + +import "git.phc.dm.unipi.it/phc/website/database" + +func main() { + db, err := database.NewSqlite3Database("phc-server.local.db") + if err != nil { + panic(err) + } + + if err := db.Migrate("database/migrations"); err != nil { + panic(err) + } +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..ce076a2 --- /dev/null +++ b/database/database.go @@ -0,0 +1,91 @@ +package database + +import "time" + +// Tools + +type DBMigrate interface { + Migrate(migrationDir string) error +} + +// Entities + +type DBDispense interface { + CreateDispensa(template Dispensa) (string, error) + GetDispensa(id string) (Dispensa, error) + AllDispense() ([]Dispensa, error) + UpdateDispensa(d Dispensa) error + DeleteDispensa(id string) error +} + +type DBUpload interface { + CreateUpload(template UploadedContent) (string, error) + DeleteUpload(id UploadedContent) (UploadedContent, error) +} + +type DBHashApprovals interface { + CreateApprovedHash(a HashApproval) error + GetApprovedHash(hash string) (HashApproval, error) + AllApprovedHash(a HashApproval) error +} + +type DBHashRejections interface { + CreateRejectedHash(a HashRejection) error + GetRejectedHash(hash string) (HashRejection, error) + AllRejectedHash(a HashRejection) error +} + +// Relations + +type DBTags interface { + SetTags(dispensaId string, tag []string) error + GetTags(dispensaId string) ([]string, error) +} + +type DBOwners interface { + CreateOwner(owner, owned string) error + GetOwner(ownedId string) (string, error) +} + +type DBAuthors interface { + CreateAuthor(userId, dispensaId string) error + GetAuthorId(dispensaId string) (string, error) + GetUserDispenseIds(user string) ([]string, error) +} + +type DBCreationTimes interface { + CreateCreationTime(entityId string, createdAt time.Time) error + GetCreationTime(entityId string) (time.Time, error) +} + +type DBOther interface { + // AllDispenseOfUser ritorna tutte le dispense di un certo utente + AllDispenseOfUser(username string) ([]Dispensa, error) + // AllLatestsApprovedDispenseUploads ritorna una lista contenente tutte le dispense con almeno un upload approvato e ritorna la coppia della (dispensa, upload piĆ¹ recente) + AllLatestsApprovedDispenseUploads() ([]struct { + Dispensa + UploadedContent + }, error) + // AllDispenseWithState ... + AllDispenseWithState(username string) ([]struct { + Dispensa + State 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. +type DB struct { + DBMigrate + + DBDispense + DBUpload + DBHashApprovals + DBHashRejections + + DBTags + DBOwners + DBAuthors + DBCreationTimes + + DBOther +} diff --git a/database/migrations/0001-2022-08-31.sql b/database/migrations/0001-2022-08-31.sql new file mode 100644 index 0000000..fe6db1a --- /dev/null +++ b/database/migrations/0001-2022-08-31.sql @@ -0,0 +1,48 @@ + +-- +-- Entities +-- + +-- Dispense +CREATE TABLE IF NOT EXISTS "dispense"( + "id" TEXT NOT NULL PRIMARY KEY, + "created_at" TEXT NOT NULL, + "owner_id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL +); + +-- Contenuto caricato +CREATE TABLE IF NOT EXISTS "uploaded_contents"( + "id" TEXT NOT NULL PRIMARY KEY, + "created_at" TEXT NOT NULL, + "owner_id" TEXT NOT NULL, + "dispensa_id" TEXT NOT NULL, + "hash" TEXT NOT NULL +); + +-- Contenuti con hash approvati +CREATE TABLE IF NOT EXISTS "hash_approvals"( + "id" TEXT NOT NULL PRIMARY KEY, + "created_at" TEXT NOT NULL, + "owner_id" TEXT NOT NULL, + "hash" TEXT NOT NULL +); + +-- Contenuti con hash rifiutati +CREATE TABLE IF NOT EXISTS "hash_rejections"( + "id" TEXT NOT NULL PRIMARY KEY, + "created_at" TEXT NOT NULL, + "owner_id" TEXT NOT NULL, + "hash" TEXT NOT NULL +); + +-- +-- Relations +-- + +-- Tags per le dispense +CREATE TABLE IF NOT EXISTS "tags"( + "dispensa_id" TEXT NOT NULL, + "tags" TEXT NOT NULL +); diff --git a/database/model.go b/database/model.go new file mode 100644 index 0000000..ca83b5f --- /dev/null +++ b/database/model.go @@ -0,0 +1,44 @@ +package database + +type Dispensa struct { + Id string `db:"id"` + Title string `db:"title"` + Description string `db:"description"` +} + +type Tag struct { + DispensaId string `db:"dispensa_id"` + Tag string `db:"tag"` +} + +type UploadedContent struct { + Id string `db:"id"` + Hash string `db:"hash"` +} + +type HashApproval struct { + Id string `db:"id"` + Hash string `db:"hash"` +} + +type HashRejection struct { + Id string `db:"id"` + Hash string `db:"hash"` +} + +// Relations + +type Owner struct { + OwnerId string `db:"owner_id"` + OwnedId string `db:"owned_id"` +} + +type Author struct { + UserId string `db:"user_id"` + DispensaId string `db:"dispensa_id"` +} + +type CreationTime struct { + EntityId string `db:"entity_id"` + CreatedAt string `db:"created_at"` +} diff --git a/database/sqlite.go b/database/sqlite.go new file mode 100644 index 0000000..69de180 --- /dev/null +++ b/database/sqlite.go @@ -0,0 +1,161 @@ +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 { + if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS migrations(timestamp TEXT, filename TEXT)`); err != nil { + return err + } + + 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(15) + + if _, err := db.NamedExec(` + INSERT INTO + dispense(id, title, description) + VALUES + (: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 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 NewSqlite3Database(filename string) (*DB, error) { + sqlDB, err := sqlx.Open("sqlite3", filename) + 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, + // DBUpload: db, + // DBApprovedHashes: db, + // DBRejectedHashes: db, + // DBOther: db, + }, nil +} diff --git a/go.mod b/go.mod index 51891f2..c40ae7f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,9 @@ require ( github.com/andybalholm/brotli v1.0.4 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/gorilla/feeds v1.1.1 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect github.com/klauspost/compress v1.15.6 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.37.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect diff --git a/go.sum b/go.sum index c9c310c..916b6ac 100644 --- a/go.sum +++ b/go.sum @@ -15,21 +15,28 @@ github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55k github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gofiber/fiber/v2 v2.34.0 h1:96BJMw6uaxQhJsHY54SFGOtGgp9pgombK5Hbi4JSEQA= github.com/gofiber/fiber/v2 v2.34.0/go.mod h1:ozRQfS+D7EL1+hMH+gutku0kfx1wLX4hAxDCtDzpj4U= github.com/gofiber/redirect/v2 v2.1.23 h1:MqRyyeKyGqkF4GIFgTB4SuqIeeXviUglgRL2HCOFofM= github.com/gofiber/redirect/v2 v2.1.23/go.mod h1:IYF5pPLDLYrrHMcxajDyWV+nHMbyPd6agCXkCnfLxS0= github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY= github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.6 h1:6D9PcO8QWu0JyaQ2zUMmu16T1T+zjjEpP91guRsvDfY= github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs= github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/model/appunti.go b/model/appunti.go new file mode 100644 index 0000000..b8166ad --- /dev/null +++ b/model/appunti.go @@ -0,0 +1,44 @@ +package model + +import "time" + +// Tables + +type Dispensa struct { + // Id is a unique identifier of this object + Id string + // CreatedAt is the time of creation of this object + CreatedAt time.Time + // Title of this dispensa + Title string + // Description of this dispensa + Description string + // Tags for this dispensa, used by search to easily categorize dispense + Tags string +} + +// Upload represents a file identified by its hash and stored in the appunti content store +type Upload struct { + // Id is a unique identifier of this object + Id string + // CreatedAt is the time of creation of this object + CreatedAt time.Time + // Hash of this file + Hash string +} + +// ApprovedHash represents an approved hash +type ApprovedHash struct { + // Hash being approved + Hash string + // CreatedAt is the creation time of this object + CreatedAt time.Time +} + +// RejectedHash represents a rejected hash +type RejectedHash struct { + // Hash being rejected + Hash string + // CreatedAt is the creation time of this object + CreatedAt time.Time +}