diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe4217f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +out \ No newline at end of file diff --git a/chirp.go b/chirp.go index d4e61d2..cb8f11f 100644 --- a/chirp.go +++ b/chirp.go @@ -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) +} diff --git a/go.mod b/go.mod index 8365c87..d0428d8 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..63c8b5f --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/database/chirps.sql.go b/internal/database/chirps.sql.go new file mode 100644 index 0000000..b4348f4 --- /dev/null +++ b/internal/database/chirps.sql.go @@ -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 +} diff --git a/internal/database/db.go b/internal/database/db.go new file mode 100644 index 0000000..dacb52e --- /dev/null +++ b/internal/database/db.go @@ -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, + } +} diff --git a/internal/database/models.go b/internal/database/models.go new file mode 100644 index 0000000..b7a9ca1 --- /dev/null +++ b/internal/database/models.go @@ -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 +} diff --git a/internal/database/users.sql.go b/internal/database/users.sql.go new file mode 100644 index 0000000..e9f3c23 --- /dev/null +++ b/internal/database/users.sql.go @@ -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 +} diff --git a/main.go b/main.go index 19989d0..f204ccd 100644 --- a/main.go +++ b/main.go @@ -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", diff --git a/reset.go b/reset.go index 4513334..11aade0 100644 --- a/reset.go +++ b/reset.go @@ -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) + } } diff --git a/sql/queries/chirps.sql b/sql/queries/chirps.sql new file mode 100644 index 0000000..01e58fa --- /dev/null +++ b/sql/queries/chirps.sql @@ -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; \ No newline at end of file diff --git a/sql/queries/users.sql b/sql/queries/users.sql new file mode 100644 index 0000000..b2d478c --- /dev/null +++ b/sql/queries/users.sql @@ -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 *; \ No newline at end of file diff --git a/sql/schema/001_users.sql b/sql/schema/001_users.sql new file mode 100644 index 0000000..6d89961 --- /dev/null +++ b/sql/schema/001_users.sql @@ -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; \ No newline at end of file diff --git a/sql/schema/002_chirps.sql b/sql/schema/002_chirps.sql new file mode 100644 index 0000000..1ec77b1 --- /dev/null +++ b/sql/schema/002_chirps.sql @@ -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; diff --git a/sqlc.yml b/sqlc.yml new file mode 100644 index 0000000..c20ea46 --- /dev/null +++ b/sqlc.yml @@ -0,0 +1,8 @@ +version: "2" +sql: + - schema: "sql/schema" + queries: "sql/queries" + engine: "postgresql" + gen: + go: + out: "internal/database" \ No newline at end of file diff --git a/users.go b/users.go new file mode 100644 index 0000000..2d8199d --- /dev/null +++ b/users.go @@ -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(¶ms) + 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) +}