feat: Initialize project with MySQL database and API for managing projects and technologies

- Created SQL script to set up database tables for projects and technologies.
- Added Docker Compose configuration for application and MySQL service.
- Implemented API endpoints for CRUD operations on technologies.
- Defined data types for projects and technologies.
- Established routing for API endpoints.
This commit is contained in:
2026-02-10 09:45:25 +01:00
parent 4335fd5cae
commit 4b5069d179
16 changed files with 615 additions and 178 deletions
+4
View File
@@ -0,0 +1,4 @@
DB_USER=""
DB_PASS=""
DB_HOST=""
DB_NAME=""
+1
View File
@@ -1 +1,2 @@
.env .env
/vendor
+6
View File
@@ -0,0 +1,6 @@
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go mod tidy && go build -o main .
EXPOSE 3000
CMD ["./main"]
Binary file not shown.
+1 -2
View File
@@ -4,7 +4,6 @@ import (
"database/sql" "database/sql"
"log" "log"
"os" "os"
"github.com/go-sql-driver/mysql" "github.com/go-sql-driver/mysql"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
@@ -16,7 +15,7 @@ func GetDB() *sql.DB {
if db == nil { if db == nil {
errenv := godotenv.Load() errenv := godotenv.Load()
if errenv != nil { if errenv != nil {
log.Fatal("Error loading .env file") log.Println("No .env file found; using environment variables")
} }
// Capture connection properties. // Capture connection properties.
cfg := mysql.NewConfig() cfg := mysql.NewConfig()
+49
View File
@@ -0,0 +1,49 @@
DROP TABLE IF EXISTS projet;
DROP TABLE IF EXISTS technologie;
DROP TABLE IF EXISTS projet_technologie;
create table
projet (
id int primary key auto_increment,
name varchar(255) not null,
shortdescriptionfr text not null,
longdescriptionfr text not null,
shortdescriptionen text not null,
longdescriptionen text not null,
linkGithub varchar(255),
linkWeb varchar(255),
datec date not null
);
create table
technologie (
id int primary key auto_increment,
name varchar(255) not null
);
create table
projet_technologie (
projet_id int,
technologie_id int,
primary key (projet_id, technologie_id),
foreign key (projet_id) references projet (id) on delete cascade,
foreign key (technologie_id) references technologie (id) on delete cascade
);
insert into projet (name, shortdescriptionfr, longdescriptionfr, shortdescriptionen, longdescriptionen, linkGithub, linkWeb, datec) values
('Mon Portefolio', 'Un site web pour présenter mes projets et compétences.', 'Ce site web a été développé pour présenter mes projets personnels et professionnels, ainsi que mes compétences en développement web. Il utilise Go pour le backend et React pour le frontend.', 'A website to showcase my projects and skills.', 'This website was developed to showcase my personal and professional projects, as well as my web development skills. It uses Go for the backend and React for the frontend.', 'https://github.com/sylvain/portfolio', 'https://sylvain-portfolio.com', '2023-01-15');
insert into technologie (name) values
('Go'),
('React'),
('JavaScript'),
('HTML'),
('CSS');
insert into projet_technologie (projet_id, technologie_id) values
(1, 1),
(1, 2),
(1, 3),
(1, 4),
(1, 5);
+30
View File
@@ -0,0 +1,30 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
env_file:
- .env
depends_on:
- db
command: ["sh", "-c", "sleep 6 && ./main"]
restart: unless-stopped
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql
- ./create_table.sql:/docker-entrypoint-initdb.d/create_table.sql:ro
ports:
- "3306:3306"
restart: unless-stopped
volumes:
db_data:
+1 -1
View File
@@ -1,6 +1,6 @@
module api_serveurless_go module api_serveurless_go
go 1.25.1 go 1.21.0
require ( require (
github.com/go-sql-driver/mysql v1.9.3 github.com/go-sql-driver/mysql v1.9.3
+89 -19
View File
@@ -2,35 +2,53 @@ package requette
import ( import (
"api_serveurless_go/bd" "api_serveurless_go/bd"
"api_serveurless_go/types"
"database/sql" "database/sql"
"log" "log"
"strings"
) )
type Projet struct { func GetAllProjets(nb int,page int) types.Projets {
Id int `json:"id"`
Name string `json:"name"`
ShortDescription string `json:"shortdescription"`
}
func GetAllProjets() []Projet {
db := bd.GetDB() db := bd.GetDB()
stmt, err := db.Prepare("SELECT * FROM projet") offset := (page - 1) * nb
stmt, err := db.Prepare("SELECT projet.id, projet.name, projet.shortdescriptionfr, GROUP_CONCAT(technologie.name SEPARATOR ',') as technologies FROM projet LEFT JOIN projet_technologie on projet.id = projet_technologie.projet_id LEFT JOIN technologie on projet_technologie.technologie_id = technologie.id GROUP BY projet.id, projet.name, projet.shortdescriptionfr LIMIT ? OFFSET ?")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
row, err := stmt.Query() row, err := stmt.Query(nb, offset)
defer stmt.Close() defer stmt.Close()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return hydrateProjets(row) stmt2, err := db.Prepare("SELECT count(*) FROM projet")
if err != nil {
log.Fatal(err)
}
row2, err := stmt2.Query()
defer stmt2.Close()
if err != nil {
log.Fatal(err)
}
var count int
for row2.Next() {
err := row2.Scan(&count)
if err != nil {
log.Fatal(err)
}
}
var projets types.Projets
projets.Projets = hydrateProjets(row)
projets.Count = count
return projets
} }
func GetProjetByID(id int) Projet { func GetProjetByID(id int) types.ProjetFull {
db := bd.GetDB() db := bd.GetDB()
stmt, err := db.Prepare("SELECT * FROM projet WHERE id = ?") stmt, err := db.Prepare("SELECT id, name, shortdescriptionfr, longdescriptionfr, shortdescriptionen, longdescriptionen, linkGithub, linkWeb FROM projet WHERE id = ?")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -39,24 +57,76 @@ func GetProjetByID(id int) Projet {
return hydrateProjet(row) return hydrateProjet(row)
} }
func hydrateProjet(row *sql.Row) Projet { func hydrateProjet(row *sql.Row) types.ProjetFull {
var projet Projet var projet types.ProjetFull
err := row.Scan(&projet.Id, &projet.Name, &projet.ShortDescription) err := row.Scan(&projet.Id, &projet.Name, &projet.ShortDescriptionFr, &projet.LongDescriptionFr, &projet.ShortDescriptionEn, &projet.LongDescriptionEn, &projet.LinkGithub, &projet.LinkWeb)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
return projet return projet
} }
func hydrateProjets(rows *sql.Rows) []Projet { func hydrateProjets(rows *sql.Rows) []types.ProjetShort {
var projets []Projet var projets []types.ProjetShort
for rows.Next() { for rows.Next() {
var projet Projet var projet types.ProjetShort
err := rows.Scan(&projet.Id, &projet.Name, &projet.ShortDescription) var techs sql.NullString
err := rows.Scan(&projet.Id, &projet.Name, &projet.ShortDescriptionFr, &techs)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if techs.Valid && techs.String != "" {
projet.Technologies = strings.Split(techs.String, ",")
} else {
projet.Technologies = []string{}
}
projets = append(projets, projet) projets = append(projets, projet)
} }
return projets return projets
} }
func CreateProjet(projet types.Projetcreate) *types.Projetcreate {
db := bd.GetDB()
stmt, err := db.Prepare("INSERT INTO projet (name, shortdescriptionfr, longdescriptionfr, shortdescriptionen, longdescriptionen, linkGithub, linkWeb, datec) VALUES (?, ?, '', '', '', '', '', CURDATE())")
if err != nil {
log.Fatal(err)
}
_, err = stmt.Exec(projet.Name, projet.ShortDescription)
if err != nil {
log.Fatal(err)
return nil
}
defer stmt.Close()
return &projet
}
func UpdateProjet(projet types.ProjetFull) *types.ProjetFull {
db := bd.GetDB()
stmt, err := db.Prepare("UPDATE projet SET name = ?, shortdescriptionfr = ?, longdescriptionfr = ?, shortdescriptionen = ?, longdescriptionen = ?, linkGithub = ?, linkWeb = ? WHERE id = ?")
if err != nil {
log.Fatal(err)
}
_, err = stmt.Exec(projet.Name, projet.ShortDescriptionFr, projet.LongDescriptionFr, projet.ShortDescriptionEn, projet.LongDescriptionEn, projet.LinkGithub, projet.LinkWeb, projet.Id)
if err != nil {
log.Fatal(err)
return nil
}
defer stmt.Close()
return &projet
}
func DeleteProjet(id int) bool {
db := bd.GetDB()
stmt, err := db.Prepare("DELETE FROM projet WHERE id = ?")
if err != nil {
log.Fatal(err)
return false
}
_, err = stmt.Exec(id)
if err != nil {
log.Fatal(err)
return false
}
defer stmt.Close()
return true
}
+107
View File
@@ -0,0 +1,107 @@
package requette
import (
"api_serveurless_go/bd"
"api_serveurless_go/types"
"database/sql"
"log"
)
func GetAllTechnologies() types.Technologies {
db := bd.GetDB()
stmt, err := db.Prepare("SELECT * FROM technologie")
if err != nil {
log.Fatal(err)
}
row, err := stmt.Query()
defer stmt.Close()
if err != nil {
log.Fatal(err)
}
stmt2, err := db.Prepare("SELECT count(*) FROM technologie")
if err != nil {
log.Fatal(err)
}
row2, err := stmt2.Query()
defer stmt2.Close()
if err != nil {
log.Fatal(err)
}
var count int
for row2.Next() {
err := row2.Scan(&count)
if err != nil {
log.Fatal(err)
}
}
var technologies types.Technologies
technologies.Technologies = hydrateTechnologies(row)
technologies.Count = count
return technologies
}
func hydrateTechnologies(rows *sql.Rows) []types.TechnologieShort {
var technologies []types.TechnologieShort
for rows.Next() {
var technologie types.TechnologieShort
err := rows.Scan(&technologie.Id, &technologie.Name)
if err != nil {
log.Fatal(err)
}
technologies = append(technologies, technologie)
}
return technologies
}
func CreateTechnologie(projet types.TechnologieCreate) *types.TechnologieCreate {
db := bd.GetDB()
stmt, err := db.Prepare("INSERT INTO technologie (name) VALUES (?)")
if err != nil {
log.Fatal(err)
}
_, err = stmt.Exec(projet.Name)
if err != nil {
log.Fatal(err)
return nil
}
defer stmt.Close()
return &projet
}
func UpdateTechnologie(technologie types.TechnologieFull) *types.TechnologieFull {
db := bd.GetDB()
stmt, err := db.Prepare("UPDATE technologie SET name = ? WHERE id = ?")
if err != nil {
log.Fatal(err)
}
_, err = stmt.Exec(technologie.Name, technologie.Id)
if err != nil {
log.Fatal(err)
return nil
}
defer stmt.Close()
return &technologie
}
func DeleteTechnologie(id int) bool {
db := bd.GetDB()
stmt, err := db.Prepare("DELETE FROM technologie WHERE id = ?")
if err != nil {
log.Fatal(err)
return false
}
_, err = stmt.Exec(id)
if err != nil {
log.Fatal(err)
return false
}
defer stmt.Close()
return true
}
+74 -14
View File
@@ -2,34 +2,74 @@ package routes
import ( import (
"api_serveurless_go/requette" "api_serveurless_go/requette"
"api_serveurless_go/types"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
) )
// SetupRoutes configure toutes les routes de l'application
func SetupRoutes() {
http.HandleFunc("/", homeHandler)
http.HandleFunc("/api/projets", projetsHandler)
http.HandleFunc("/api/projet/", projetHandler)
}
// homeHandler gère la route racine
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Welcome to my website!")
}
// projetsHandler gère la liste des projets // projetsHandler gère la liste des projets
func projetsHandler(w http.ResponseWriter, r *http.Request) { func projetsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", os.Getenv("FRONTEND_URL"))
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
switch r.Method { switch r.Method {
case "GET": case "GET":
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(requette.GetAllProjets()) pageStr := r.URL.Query().Get("page")
nbStr := r.URL.Query().Get("nb")
// defaults
page := 1
nb := 10
if pageStr != "" {
p, err := strconv.Atoi(pageStr)
if err != nil {
http.Error(w, "Paramètre 'page' invalide", http.StatusBadRequest)
return
}
page = p
}
if nbStr != "" {
n, err := strconv.Atoi(nbStr)
if err != nil {
http.Error(w, "Paramètre 'nb' invalide", http.StatusBadRequest)
return
}
nb = n
}
json.NewEncoder(w).Encode(requette.GetAllProjets(nb, page))
case "POST": case "POST":
fmt.Fprintf(w, "Créer un nouveau projet")
var projet types.Projetcreate
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&projet)
if err != nil {
http.Error(w, "Erreur de décodage JSON: "+err.Error(), http.StatusBadRequest)
return
}
fmt.Println(projet.Name, projet.ShortDescription)
if requette.CreateProjet(projet) == nil {
http.Error(w, "Erreur lors de la création du projet", http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusCreated)
}
case "OPTIONS":
w.WriteHeader(http.StatusOK)
return
default: default:
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed) http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
} }
@@ -37,6 +77,9 @@ func projetsHandler(w http.ResponseWriter, r *http.Request) {
// projetHandler gère les actions sur un projet spécifique // projetHandler gère les actions sur un projet spécifique
func projetHandler(w http.ResponseWriter, r *http.Request) { func projetHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", os.Getenv("FRONTEND_URL"))
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
id := strings.TrimPrefix(r.URL.Path, "/api/projet/") id := strings.TrimPrefix(r.URL.Path, "/api/projet/")
if id == "" { if id == "" {
http.Error(w, "ID manquant", http.StatusBadRequest) http.Error(w, "ID manquant", http.StatusBadRequest)
@@ -53,9 +96,26 @@ func projetHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(requette.GetProjetByID(idInt)) json.NewEncoder(w).Encode(requette.GetProjetByID(idInt))
case "PUT": case "PUT":
fmt.Fprintf(w, "Modifier le projet %d", idInt) var projet types.ProjetFull
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&projet)
if err != nil {
http.Error(w, "Erreur de décodage JSON: "+err.Error(), http.StatusBadRequest)
return
}
fmt.Println(projet.Name, projet.ShortDescriptionFr)
if requette.UpdateProjet(projet) == nil {
http.Error(w, "Erreur lors de la mise à jour du projet", http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusOK)
}
case "DELETE": case "DELETE":
fmt.Fprintf(w, "Supprimer le projet %d", idInt) if requette.DeleteProjet(idInt) {
w.WriteHeader(http.StatusNoContent)
} else {
http.Error(w, "Erreur lors de la suppression du projet", http.StatusInternalServerError)
}
default: default:
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed) http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
} }
+11
View File
@@ -0,0 +1,11 @@
package routes
import (
"net/http"
)
func SetupRoutes() {
http.HandleFunc("/api/projets", projetsHandler)
http.HandleFunc("/api/projet/", projetHandler)
http.HandleFunc("/api/technologies", technologiesHandler)
}
+48
View File
@@ -0,0 +1,48 @@
package routes
import (
"api_serveurless_go/requette"
"api_serveurless_go/types"
"encoding/json"
"net/http"
"os"
)
// technologiesHandler gère la liste des technologies
func technologiesHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", os.Getenv("FRONTEND_URL")) // <-- Remplace avec le port de ton front-end
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(requette.GetAllTechnologies())
case "POST":
var technologie types.TechnologieCreate
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
err := decoder.Decode(&technologie)
if err != nil {
http.Error(w, "Erreur de décodage JSON: "+err.Error(), http.StatusBadRequest)
return
}
if requette.CreateTechnologie(technologie) == nil {
http.Error(w, "Erreur lors de la création de la technologie", http.StatusInternalServerError)
} else {
w.WriteHeader(http.StatusCreated)
}
case "OPTIONS":
w.WriteHeader(http.StatusOK)
return
default:
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
}
}
+31
View File
@@ -0,0 +1,31 @@
package types
type ProjetShort struct {
Id int `json:"id"`
Name string `json:"name"`
ShortDescriptionFr string `json:"shortdescriptionfr"`
Technologies []string `json:"technologies"`
}
type Projetcreate struct {
Name string `json:"name"`
ShortDescription string `json:"shortdescriptionfr"`
}
type ProjetFull struct {
Id int `json:"id"`
Name string `json:"name"`
ShortDescriptionFr string `json:"shortdescriptionfr"`
LongDescriptionFr string `json:"longdescriptionfr"`
ShortDescriptionEn string `json:"shortdescriptionen"`
LongDescriptionEn string `json:"longdescriptionen"`
LinkGithub string `json:"linkGithub"`
LinkWeb string `json:"linkWeb"`
Technologies []string `json:"technologies"`
}
type Projets struct {
Projets []ProjetShort `json:"projets"`
Count int `json:"count"`
}
+21
View File
@@ -0,0 +1,21 @@
package types
type TechnologieShort struct {
Id int `json:"id"`
Name string `json:"name"`
}
type TechnologieCreate struct {
Name string `json:"name"`
}
type TechnologieFull struct {
Id int `json:"id"`
Name string `json:"name"`
}
type Technologies struct {
Technologies []TechnologieShort `json:"technologies"`
Count int `json:"count"`
}