Database prototype
parent
03f976a8f9
commit
2ce1ac9bf1
@ -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) {
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
);
|
@ -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"`
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue