Browse Source

Rewrite from the ground; SQL database

main
Unbewohnte 3 years ago
parent
commit
77fb229583
  1. 3
      .gitignore
  2. 15
      README.md
  3. 23
      database/database.json
  4. 49
      dbHandle/create.go
  5. 9
      dbHandle/db.go
  6. 29
      dbHandle/handle.go
  7. 5
      go.mod
  8. 0
      go.sum
  9. 26
      logs/logs.go
  10. 484
      main.go
  11. 35
      randomData/randomData.go

3
.gitignore vendored

@ -0,0 +1,3 @@
logs.log
crud-api
crud-api.exe

15
README.md

@ -1,16 +1,9 @@
# CRUD api # CRUD api
## A simple CRUD api written in Go`s standart library ## A simple CRUD api written in Go
API has implementation of "GET", "POST", "PUT", "DELETE" http methods, allowing to Create, Read, Update and Delete json objects in database. API has implementation of "GET", "POST", "PUT", "DELETE" http methods, allowing to Create, Read, Update and Delete objects in database via json input.
The structure of a basic json object represents Go struct "RandomData" with exported fields "title" (string), "text" (string) and unexported fields "DateCreated" (time.Time), "LastUpdated" (time.Time) and ID (int64) ---
Example of a single object stored in a json database : { Just practicing things
"ID": 1618064651615612586,
"DateCreated": "2021-04-10T14:24:11.615612068Z",
"LastUpdated": "2021-04-10T14:24:11.615612068Z",
"title": "Title",
"text": "text"
}
This project was my first take on such thing. The goal was to create a basic working example and it looks like that I`ve achieved that goal.

23
database/database.json

@ -1,23 +0,0 @@
[
{
"ID": 1618064636424100339,
"DateCreated": "2021-04-10T14:23:56.424100065Z",
"LastUpdated": "2021-04-10T14:23:56.424100065Z",
"title": "FUMO?",
"text": "FUMO"
},
{
"ID": 1618064651615612586,
"DateCreated": "2021-04-10T14:24:11.615612068Z",
"LastUpdated": "2021-04-10T14:24:11.615612068Z",
"title": "Title",
"text": "text"
},
{
"ID": 1618065099475731301,
"DateCreated": "2021-04-10T14:31:39.47573104Z",
"LastUpdated": "2021-04-10T14:31:39.47573104Z",
"title": "link",
"text": "https://youtu.be/dQw4w9WgXcQ"
}
]

49
dbHandle/create.go

@ -0,0 +1,49 @@
package dbhandle
import (
"database/sql"
"os"
"path/filepath"
_ "github.com/mattn/go-sqlite3"
)
// full path to the local database that has been created by `CreateLocalDB`
var dbpath string
// Creates a local database file in the same directory
// as executable if does not exist already. Creates a table "randomdata" with
// such structure: (id PRIMARY KEY, title TEXT, text TEXT), which
// represents underlying fields of `RandomData`
func CreateLocalDB(dbName string) (*DB, error) {
// double check if dbName is actually just a name, not a path
dbName = filepath.Base(dbName)
executablePath, err := os.Executable()
if err != nil {
return nil, err
}
exeDir := filepath.Dir(executablePath)
dbpath = filepath.Join(exeDir, dbName)
// create db if does not exist
dbfile, err := os.OpenFile(dbpath, os.O_CREATE, os.ModePerm)
if err != nil {
return nil, err
}
dbfile.Close()
// create table that suits `RandomData` struct
db, err := sql.Open(drivername, dbpath)
if err != nil {
return nil, err
}
_, err = db.Exec("CREATE TABLE IF NOT EXISTS randomdata (id INTEGER PRIMARY KEY, title TEXT, text TEXT)")
if err != nil {
return nil, err
}
return &DB{db}, nil
}

9
dbHandle/db.go

@ -0,0 +1,9 @@
package dbhandle
import "database/sql"
type DB struct {
*sql.DB
}
const drivername string = "sqlite3"

29
dbHandle/handle.go

@ -0,0 +1,29 @@
package dbhandle
import (
"net/http"
randomdata "github.com/Unbewohnte/crud-api/randomData"
_ "github.com/mattn/go-sqlite3"
)
func (db *DB) GetEverything() ([]*randomdata.RandomData, error)
func (db *DB) GetSpecific() (*randomdata.RandomData, error)
func (db *DB) DeleteSpecific() error
func (db *DB) PatchSpecific() error
func (db *DB) Create(rd randomdata.RandomData) error {
_, err := db.Exec("INSERT INTO randomdata (title, text) VALUES (?, ?)", rd.Title, rd.Text)
if err != nil {
return err
}
return nil
}
func (db *DB) HandleSpecificWeb(w http.ResponseWriter, r *http.Request)
func (db *DB) HandleGlobalWeb(w http.ResponseWriter, r *http.Request)

5
go.mod

@ -0,0 +1,5 @@
module github.com/Unbewohnte/crud-api
go 1.17
require github.com/mattn/go-sqlite3 v1.14.8

26
logs/logs.go

@ -0,0 +1,26 @@
package logs
import (
"log"
"os"
"path/filepath"
)
// Create a logfile in the same directory as executable and set output
// of `log` package to it
func SetUp() error {
executablePath, err := os.Executable()
if err != nil {
return err
}
exeDir := filepath.Dir(executablePath)
logfile, err := os.Create(filepath.Join(exeDir, "logs.log"))
if err != nil {
return err
}
log.SetOutput(logfile)
return nil
}

484
main.go

@ -1,53 +1,62 @@
package main package main
import ( import (
"encoding/json" "flag"
"fmt" "fmt"
"io"
"log" "log"
"net/http" "net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time" "time"
)
type RandomData struct {
// unexported for json
ID int64
DateCreated time.Time
LastUpdated time.Time
// exported for json
Title string `json:"title"`
Text string `json:"text"`
}
type randomDataHandler struct { dbhandle "github.com/Unbewohnte/crud-api/dbHandle"
dbFilepath string "github.com/Unbewohnte/crud-api/logs"
} )
func InitLogs() { var (
var logsDir string = filepath.Join(".", "logs") port *uint = flag.Uint("port", 8080, "Specifies a port on which the helping page will be served")
dbname string = "database.db"
)
err := os.MkdirAll(logsDir, os.ModePerm) func init() {
// set up logs, parse flags
err := logs.SetUp()
if err != nil { if err != nil {
panic(err) panic(err)
} }
logfile, err := os.Create(filepath.Join(logsDir, "logs.log"))
flag.Parse()
}
func main() {
// create a local db file
db, err := dbhandle.CreateLocalDB(dbname)
if err != nil { if err != nil {
panic(err) log.Fatalf("error setting up a database: %s", err)
}
mux := http.NewServeMux()
mux.HandleFunc("/", helpPage)
mux.HandleFunc("/randomdata", db.HandleGlobalWeb)
mux.HandleFunc("/randomdata/", db.HandleSpecificWeb)
server := &http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
} }
log.SetOutput(logfile) log.Printf("Starting on port %d\n", *port)
fmt.Printf("Starting on port %d\n", *port)
log.Fatal(server.ListenAndServe())
} }
func homepage(w http.ResponseWriter, r *http.Request) { func helpPage(w http.ResponseWriter, r *http.Request) {
helpMessage := ` helpMessage := `
<h1>CRUD api in Go's standart library</h1> <h1>CRUD api</h1>
<ul> <ul>
<li> (GET) <a href="/randomdata">/randomdata</a> - to get all database </li> <li> (GET) <a href="/randomdata">/randomdata</a> - to get all info from database (obviously a bad idea in serious projects ᗜˬᗜ) </li>
<li> (GET) /randomdata/{id} - to get specific random data under corresponding id </li> <li> (GET) /randomdata/{id} - to get specific random data under corresponding id </li>
<li> (POST) /randomdata - to create random data</li> <li> (POST) /randomdata - to create random data </li>
<li> (DELETE) /randomdata/{id} - to delete specified random data </li> <li> (DELETE) /randomdata/{id} - to delete specified random data </li>
<li> (PUT) /randomdata/{id} - to update random data with given id </li> <li> (PUT) /randomdata/{id} - to update random data with given id </li>
</ul> </ul>
@ -55,316 +64,105 @@ func homepage(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, helpMessage) fmt.Fprint(w, helpMessage)
} }
func newDatabaseHandler() *randomDataHandler { // func (dbHandler *randomDataHandler) updateSpecificRandomData(w http.ResponseWriter, r *http.Request) {
dbDirpath := filepath.Join(".", "database") // if r.Header.Get("content-type") != "application/json" {
err := os.MkdirAll(dbDirpath, os.ModePerm) // w.WriteHeader(http.StatusUnsupportedMediaType)
if err != nil { // w.Write([]byte(fmt.Sprintf("Got `%s` instead of `application/json`", r.Header.Get("content-type"))))
panic(err) // return
} // }
dbFilepath := filepath.Join(dbDirpath, "database.json")
// requestBody, err := io.ReadAll(r.Body)
dbFile, err := os.OpenFile(dbFilepath, os.O_CREATE, os.ModePerm) // if err != nil {
if err != nil { // log.Println("Error reading http request (create) : ", err)
panic(err) // w.WriteHeader(http.StatusBadRequest)
} // r.Body.Close()
defer dbFile.Close() // return
// }
log.Println("Successfully created new database handler") // defer r.Body.Close()
return &randomDataHandler{ // var givenUpdatedRandomData RandomData
dbFilepath: dbFilepath, // err = json.Unmarshal(requestBody, &givenUpdatedRandomData)
} // if err != nil {
} // log.Println("Error unmarshalling request body (updateSpecificRandomData) : ", err)
// return
func (dbHandler *randomDataHandler) writeRandomData(newData RandomData) error { // }
dbBytes, err := dbHandler.readDatabase()
if err != nil { // givenID := strings.Split(r.URL.String(), "/")[2]
log.Println("Error reading db (writeRandomData) : ", err)
} // dbBytes, err := dbHandler.readDatabase()
// if err != nil {
var db []RandomData // log.Println("Error reading db (updateSpecificRandomData) : ", err)
// w.WriteHeader(http.StatusInternalServerError)
err = json.Unmarshal(dbBytes, &db) // return
if err != nil { // }
log.Println("Error unmarshalling db (writeRandomData) : ", err) // var db []RandomData
}
// err = json.Unmarshal(dbBytes, &db)
db = append(db, newData) // if err != nil {
// log.Println("Error unmarshalling database (update) : ", err)
dbFile, err := os.OpenFile(dbHandler.dbFilepath, os.O_WRONLY, 0644) // return
if err != nil { // }
log.Println("Error opening db file (writeRandomData) : ", err)
} // int64GivenID, _ := strconv.ParseInt(givenID, 10, 64)
defer dbFile.Close() // var counter int64
// for _, randomData := range db {
jsonBytes, err := json.MarshalIndent(db, "", " ") // if int64GivenID == randomData.ID {
if err != nil { // var updatedRandomData RandomData
log.Println("Error marshalling db (writeRandomData) : ", err)
} // updatedRandomData = givenUpdatedRandomData
// updatedRandomData.ID = randomData.ID
dbFile.Write(jsonBytes) // updatedRandomData.DateCreated = randomData.DateCreated
// updatedRandomData.LastUpdated = time.Now().UTC()
return nil
} // dbHandler.removeRandomData(int64GivenID)
// dbHandler.writeRandomData(updatedRandomData)
func (dbHandler *randomDataHandler) readDatabase() ([]byte, error) {
dbBytes, err := os.ReadFile(dbHandler.dbFilepath) // log.Printf("Successfully updated RandomData with id %v \n", updatedRandomData.ID)
if err != nil { // return
log.Println("Error reading db (readDatabase) : ", err) // }
return nil, err // counter++
} // }
return dbBytes, nil
} // w.WriteHeader(http.StatusNotFound)
// }
func (dbHandler *randomDataHandler) removeRandomData(id int64) error {
dbBytes, err := dbHandler.readDatabase() // func (dbHandler *randomDataHandler) deleteSpecificRandomData(w http.ResponseWriter, r *http.Request) {
if err != nil { // givenID := strings.Split(r.URL.String(), "/")[2]
return err
} // int64GivenID, _ := strconv.ParseInt(givenID, 10, 64)
var db []RandomData // err := dbHandler.removeRandomData(int64GivenID)
err = json.Unmarshal(dbBytes, &db) // if err != nil {
if err != nil { // log.Println("Error removing RandomData (deleteSpecificRandomData) : ", err)
return err // w.WriteHeader(http.StatusInternalServerError)
} // return
// }
var counter int64 = 0
for _, randomData := range db { // w.WriteHeader(http.StatusOK)
if id == randomData.ID {
db = append(db[:counter], db[counter+1:]...) // log.Printf("Successfully deleted RandomData with id %v \n", int64GivenID)
err = dbHandler.writeDB(db) // }
if err != nil {
return err // func (dbHandler *randomDataHandler) handle(w http.ResponseWriter, r *http.Request) {
} // switch r.Method {
} // case "GET":
counter++ // dbHandler.get(w, r)
} // case "POST":
return nil // dbHandler.create(w, r)
} // default:
// w.WriteHeader(http.StatusMethodNotAllowed)
func (dbHandler *randomDataHandler) writeDB(db []RandomData) error { // }
jsonEncodedDB, err := json.MarshalIndent(db, "", " ") // }
if err != nil {
return err // func (dbHandler *randomDataHandler) handleSpecific(w http.ResponseWriter, r *http.Request) {
} // switch r.Method {
// case "GET":
dbFile, err := os.OpenFile(dbHandler.dbFilepath, os.O_WRONLY|os.O_TRUNC, 0644) // dbHandler.getSpecificRandomData(w, r)
if err != nil { // case "PUT":
return err // dbHandler.updateSpecificRandomData(w, r)
} // case "DELETE":
defer dbFile.Close() // dbHandler.deleteSpecificRandomData(w, r)
// default:
dbFile.Write(jsonEncodedDB) // w.WriteHeader(http.StatusMethodNotAllowed)
// }
return nil // }
}
func (dbHandler *randomDataHandler) get(w http.ResponseWriter, r *http.Request) {
dbBytes, err := dbHandler.readDatabase()
if err != nil {
log.Println("Error reading db (get) : ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(dbBytes)
}
func (dbHandler *randomDataHandler) create(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("content-type") != "application/json" {
w.WriteHeader(http.StatusUnsupportedMediaType)
w.Write([]byte(fmt.Sprintf("Got `%s` instead of `application/json`", r.Header.Get("content-type"))))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
log.Println("Error reading http request (create) : ", err)
w.WriteHeader(http.StatusBadRequest)
return
}
defer r.Body.Close()
var newRandomData RandomData
err = json.Unmarshal(requestBody, &newRandomData)
if err != nil {
log.Printf("Error unmarshalling http request (create) : %q \n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
newRandomData.DateCreated = time.Now().UTC()
newRandomData.LastUpdated = newRandomData.DateCreated
newRandomData.ID = time.Now().UTC().UnixNano()
err = dbHandler.writeRandomData(newRandomData)
if err != nil {
log.Println("Error writing RandomData (create): ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
log.Println("Successfuly added to db : ", newRandomData)
}
func (dbHandler *randomDataHandler) getSpecificRandomData(w http.ResponseWriter, r *http.Request) {
givenID := strings.Split(r.URL.String(), "/")[2]
dbBytes, err := dbHandler.readDatabase()
if err != nil {
log.Println("Error reading db (getSpecificRandomData) : ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
var db []RandomData
err = json.Unmarshal(dbBytes, &db)
if err != nil {
log.Println("Error unmarshalling database (getSpecificRandomData) : ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
int64GivenID, _ := strconv.ParseInt(givenID, 10, 64)
for _, randomData := range db {
if int64GivenID == randomData.ID {
response, err := json.MarshalIndent(randomData, "", " ")
if err != nil {
log.Println("Error marshaling response(getSpecificRandomData) : ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Write(response)
return
}
}
w.WriteHeader(http.StatusNotFound)
}
func (dbHandler *randomDataHandler) updateSpecificRandomData(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("content-type") != "application/json" {
w.WriteHeader(http.StatusUnsupportedMediaType)
w.Write([]byte(fmt.Sprintf("Got `%s` instead of `application/json`", r.Header.Get("content-type"))))
return
}
requestBody, err := io.ReadAll(r.Body)
if err != nil {
log.Println("Error reading http request (create) : ", err)
w.WriteHeader(http.StatusBadRequest)
r.Body.Close()
return
}
defer r.Body.Close()
var givenUpdatedRandomData RandomData
err = json.Unmarshal(requestBody, &givenUpdatedRandomData)
if err != nil {
log.Println("Error unmarshalling request body (updateSpecificRandomData) : ", err)
return
}
givenID := strings.Split(r.URL.String(), "/")[2]
dbBytes, err := dbHandler.readDatabase()
if err != nil {
log.Println("Error reading db (updateSpecificRandomData) : ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
var db []RandomData
err = json.Unmarshal(dbBytes, &db)
if err != nil {
log.Println("Error unmarshalling database (update) : ", err)
return
}
int64GivenID, _ := strconv.ParseInt(givenID, 10, 64)
var counter int64
for _, randomData := range db {
if int64GivenID == randomData.ID {
var updatedRandomData RandomData
updatedRandomData = givenUpdatedRandomData
updatedRandomData.ID = randomData.ID
updatedRandomData.DateCreated = randomData.DateCreated
updatedRandomData.LastUpdated = time.Now().UTC()
dbHandler.removeRandomData(int64GivenID)
dbHandler.writeRandomData(updatedRandomData)
log.Printf("Successfully updated RandomData with id %v \n", updatedRandomData.ID)
return
}
counter++
}
w.WriteHeader(http.StatusNotFound)
}
func (dbHandler *randomDataHandler) deleteSpecificRandomData(w http.ResponseWriter, r *http.Request) {
givenID := strings.Split(r.URL.String(), "/")[2]
int64GivenID, _ := strconv.ParseInt(givenID, 10, 64)
err := dbHandler.removeRandomData(int64GivenID)
if err != nil {
log.Println("Error removing RandomData (deleteSpecificRandomData) : ", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
log.Printf("Successfully deleted RandomData with id %v \n", int64GivenID)
}
func (dbHandler *randomDataHandler) handle(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
dbHandler.get(w, r)
case "POST":
dbHandler.create(w, r)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (dbHandler *randomDataHandler) handleSpecific(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
dbHandler.getSpecificRandomData(w, r)
case "PUT":
dbHandler.updateSpecificRandomData(w, r)
case "DELETE":
dbHandler.deleteSpecificRandomData(w, r)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func init() {
InitLogs()
}
func main() {
databaseHandler := newDatabaseHandler()
servemux := http.NewServeMux()
servemux.HandleFunc("/", homepage)
servemux.HandleFunc("/randomdata", databaseHandler.handle)
servemux.HandleFunc("/randomdata/", databaseHandler.handleSpecific)
server := &http.Server{
Addr: ":8000",
Handler: servemux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
}
log.Fatal(server.ListenAndServe())
}

35
randomData/randomData.go

@ -0,0 +1,35 @@
package randomdata
import (
"encoding/json"
"time"
)
// The `bridge` between input values and a record in db
type RandomData struct {
DateCreated int64
LastUpdated int64
Title string `json:"title"`
Text string `json:"text"`
}
// Create a new `RandomData`
func New(title string, text string) *RandomData {
return &RandomData{
DateCreated: time.Now().UTC().Unix(),
LastUpdated: time.Now().UTC().Unix(),
Title: title,
Text: text,
}
}
// Unmarshal `RandomData` from Json encoded bytes
func FromJson(jsonBytes []byte) (*RandomData, error) {
var randomData RandomData
err := json.Unmarshal(jsonBytes, &randomData)
if err != nil {
return nil, err
}
return &randomData, nil
}
Loading…
Cancel
Save