feat: Added chapter 5

This commit is contained in:
syrell 2024-10-23 22:16:51 +02:00
parent 141e1df0cb
commit 9603509fcd
16 changed files with 471 additions and 8 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.env
out

105
chirp.go
View File

@ -1,15 +1,30 @@
package main
import (
"database/sql"
"encoding/json"
"errors"
"log"
"net/http"
"strings"
"time"
"github.com/finchrelia/chirpy-server/internal/database"
"github.com/google/uuid"
)
func decode(w http.ResponseWriter, r *http.Request) {
type Chirp struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
UserID uuid.UUID `json:"user_id"`
Body string `json:"body"`
}
func (cfg *apiConfig) chirpsCreate(w http.ResponseWriter, r *http.Request) {
type parameters struct {
Content string `json:"body"`
Content string `json:"body"`
UserID uuid.UUID `json:"user_id"`
}
type returnVals struct {
Data string `json:"cleaned_body"`
@ -41,17 +56,32 @@ func decode(w http.ResponseWriter, r *http.Request) {
w.Write(dat)
} else {
cleanedData := cleanText(params.Content)
respBody := returnVals{
Data: cleanedData,
// respBody := returnVals{
// Data: cleanedData,
// }
chirp, err := cfg.DB.CreateChirp(r.Context(), database.CreateChirpParams{
Body: cleanedData,
UserID: params.UserID,
})
if err != nil {
log.Printf("Error creating chirp: %s", err)
w.WriteHeader(500)
return
}
dat, err := json.Marshal(respBody)
dat, err := json.Marshal(Chirp{
ID: chirp.ID,
CreatedAt: chirp.CreatedAt,
UpdatedAt: chirp.UpdatedAt,
Body: chirp.Body,
UserID: chirp.UserID,
})
if err != nil {
log.Printf("Error marshalling JSON: %s", err)
w.WriteHeader(500)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.WriteHeader(201)
w.Write(dat)
}
}
@ -69,3 +99,66 @@ func cleanText(s string) string {
}
return strings.Join(splittedString, " ")
}
func (cfg *apiConfig) getChirps(w http.ResponseWriter, r *http.Request) {
chirps, err := cfg.DB.GetChirps(r.Context())
if err != nil {
log.Printf("Error getting chirps: %s", err)
w.WriteHeader(500)
return
}
newChirps := []Chirp{}
for _, chirp := range chirps {
newChirps = append(newChirps, Chirp{
ID: chirp.ID,
CreatedAt: chirp.CreatedAt,
UpdatedAt: chirp.UpdatedAt,
Body: chirp.Body,
UserID: chirp.UserID,
})
}
dat, err := json.Marshal(newChirps)
if err != nil {
log.Printf("Error marshalling JSON: %s", err)
w.WriteHeader(500)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write(dat)
}
func (cfg *apiConfig) getChirp(w http.ResponseWriter, r *http.Request) {
idFromQuery := r.PathValue("chirpID")
id, err := uuid.Parse(idFromQuery)
if err != nil {
log.Printf("Not a valid ID: %s", err)
w.WriteHeader(500)
return
}
chirp, err := cfg.DB.GetChirp(r.Context(), id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
w.WriteHeader(http.StatusNotFound)
return
}
log.Printf("Error getting chirp: %s", err)
w.WriteHeader(500)
return
}
dat, err := json.Marshal(Chirp{
ID: chirp.ID,
CreatedAt: chirp.CreatedAt,
UpdatedAt: chirp.UpdatedAt,
Body: chirp.Body,
UserID: chirp.UserID,
})
if err != nil {
log.Printf("Error marshalling JSON: %s", err)
w.WriteHeader(500)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
w.Write(dat)
}

6
go.mod
View File

@ -1,3 +1,9 @@
module github.com/finchrelia/chirpy-server
go 1.22.5
require (
github.com/google/uuid v1.6.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/lib/pq v1.10.9 // indirect
)

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=

View File

@ -0,0 +1,94 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: chirps.sql
package database
import (
"context"
"github.com/google/uuid"
)
const createChirp = `-- name: CreateChirp :one
INSERT INTO chirps (id, created_at, updated_at, body, user_id)
VALUES (
gen_random_uuid(),
NOW(),
NOW(),
$1,
$2
)
RETURNING id, created_at, updated_at, body, user_id
`
type CreateChirpParams struct {
Body string
UserID uuid.UUID
}
func (q *Queries) CreateChirp(ctx context.Context, arg CreateChirpParams) (Chirp, error) {
row := q.db.QueryRowContext(ctx, createChirp, arg.Body, arg.UserID)
var i Chirp
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Body,
&i.UserID,
)
return i, err
}
const getChirp = `-- name: GetChirp :one
SELECT id, created_at, updated_at, body, user_id FROM chirps
WHERE chirps.id = $1
`
func (q *Queries) GetChirp(ctx context.Context, id uuid.UUID) (Chirp, error) {
row := q.db.QueryRowContext(ctx, getChirp, id)
var i Chirp
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Body,
&i.UserID,
)
return i, err
}
const getChirps = `-- name: GetChirps :many
SELECT id, created_at, updated_at, body, user_id FROM chirps
ORDER BY created_at ASC
`
func (q *Queries) GetChirps(ctx context.Context) ([]Chirp, error) {
rows, err := q.db.QueryContext(ctx, getChirps)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Chirp
for rows.Next() {
var i Chirp
if err := rows.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Body,
&i.UserID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

31
internal/database/db.go Normal file
View File

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package database
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -0,0 +1,26 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package database
import (
"time"
"github.com/google/uuid"
)
type Chirp struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
Body string
UserID uuid.UUID
}
type User struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
Email string
}

View File

@ -0,0 +1,50 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: users.sql
package database
import (
"context"
)
const createUser = `-- name: CreateUser :one
INSERT INTO users (id, created_at, updated_at, email)
VALUES (
gen_random_uuid(),
NOW(),
NOW(),
$1
)
RETURNING id, created_at, updated_at, email
`
func (q *Queries) CreateUser(ctx context.Context, email string) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, email)
var i User
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Email,
)
return i, err
}
const deleteUser = `-- name: DeleteUser :one
DELETE FROM users
RETURNING id, created_at, updated_at, email
`
func (q *Queries) DeleteUser(ctx context.Context) (User, error) {
row := q.db.QueryRowContext(ctx, deleteUser)
var i User
err := row.Scan(
&i.ID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Email,
)
return i, err
}

33
main.go
View File

@ -1,16 +1,42 @@
package main
import (
"database/sql"
"log"
"net/http"
"os"
"sync/atomic"
"github.com/finchrelia/chirpy-server/internal/database"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
)
type apiConfig struct {
fileserverHits atomic.Int32
DB *database.Queries
Platform string
}
func main() {
apiCfg := &apiConfig{}
godotenv.Load()
dbURL := os.Getenv("DB_URL")
if dbURL == "" {
log.Fatalf("Empty dbURL !")
}
platform := os.Getenv("PLATFORM")
if platform == "" {
log.Fatalf("Empty PLATFORM env var!")
}
db, err := sql.Open("postgres", dbURL)
if err != nil {
log.Fatalf("Unable to connect to db: %s", err)
}
apiCfg := &apiConfig{
fileserverHits: atomic.Int32{},
DB: database.New(db),
Platform: platform,
}
mux := http.NewServeMux()
fsHandler := apiCfg.middlewareMetricsInc(http.StripPrefix("/app", http.FileServer(http.Dir("."))))
mux.Handle("/app/", fsHandler)
@ -21,7 +47,10 @@ func main() {
})
mux.Handle("GET /admin/metrics", http.HandlerFunc(apiCfg.serveMetrics))
mux.Handle("POST /admin/reset", http.HandlerFunc(apiCfg.serveReset))
mux.HandleFunc("POST /api/validate_chirp", decode)
mux.HandleFunc("GET /api/chirps", apiCfg.getChirps)
mux.HandleFunc("POST /api/chirps", apiCfg.chirpsCreate)
mux.HandleFunc("POST /api/users", apiCfg.createUsers)
mux.HandleFunc("GET /api/chirps/{chirpID}", apiCfg.getChirp)
server := &http.Server{
Addr: ":8080",

View File

@ -1,9 +1,20 @@
package main
import (
"log"
"net/http"
)
func (cfg *apiConfig) serveReset(w http.ResponseWriter, r *http.Request) {
cfg.fileserverHits.Store(0)
if cfg.Platform != "dev" {
log.Printf("Invalid %s platform !", cfg.Platform)
w.WriteHeader(http.StatusForbidden)
return
}
_, err := cfg.DB.DeleteUser(r.Context())
if err != nil {
log.Printf("Error deleting users: %s", err)
}
}

18
sql/queries/chirps.sql Normal file
View File

@ -0,0 +1,18 @@
-- name: CreateChirp :one
INSERT INTO chirps (id, created_at, updated_at, body, user_id)
VALUES (
gen_random_uuid(),
NOW(),
NOW(),
$1,
$2
)
RETURNING *;
-- name: GetChirps :many
SELECT * FROM chirps
ORDER BY created_at ASC;
-- name: GetChirp :one
SELECT * FROM chirps
WHERE chirps.id = $1;

13
sql/queries/users.sql Normal file
View File

@ -0,0 +1,13 @@
-- name: CreateUser :one
INSERT INTO users (id, created_at, updated_at, email)
VALUES (
gen_random_uuid(),
NOW(),
NOW(),
$1
)
RETURNING *;
-- name: DeleteUser :one
DELETE FROM users
RETURNING *;

10
sql/schema/001_users.sql Normal file
View File

@ -0,0 +1,10 @@
-- +goose Up
CREATE TABLE users (
id UUID PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
email TEXT NOT NULL UNIQUE
);
-- +goose Down
DROP TABLE users;

11
sql/schema/002_chirps.sql Normal file
View File

@ -0,0 +1,11 @@
-- +goose Up
CREATE TABLE chirps (
id UUID PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
body TEXT NOT NULL,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE
);
-- +goose Down
DROP TABLE chirps;

8
sqlc.yml Normal file
View File

@ -0,0 +1,8 @@
version: "2"
sql:
- schema: "sql/schema"
queries: "sql/queries"
engine: "postgresql"
gen:
go:
out: "internal/database"

55
users.go Normal file
View File

@ -0,0 +1,55 @@
package main
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Email string `json:"email"`
}
func (cfg *apiConfig) createUsers(w http.ResponseWriter, r *http.Request) {
type parameters struct {
Email string `json:"email"`
}
decoder := json.NewDecoder(r.Body)
params := parameters{}
err := decoder.Decode(&params)
if err != nil {
log.Printf("Error decoding parameters: %s", err)
w.WriteHeader(500)
return
}
defer r.Body.Close()
newDBUser, err := cfg.DB.CreateUser(r.Context(), params.Email)
if err != nil {
log.Printf("Error creating user %s: %s", params.Email, err)
w.WriteHeader(500)
return
}
newId := newDBUser.ID
newUser := User{
ID: newId,
CreatedAt: newDBUser.CreatedAt,
UpdatedAt: newDBUser.UpdatedAt,
Email: newDBUser.Email,
}
dat, err := json.Marshal(newUser)
if err != nil {
log.Printf("Error marshalling JSON: %s", err)
w.WriteHeader(500)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(201)
w.Write(dat)
}