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