major: séparer commande de librairie importable

Bump major version à 9

package main déplacé vers cmd/bottin/ pour garder `go install` qui nomme
l'exécutable `bottin`, sans empêcher d'importer le code à l'extérieur du
projet avec pkg/bottin/.

Déplacer fichiers SQL vers queries/

Déplacer fichiers html vers templates/

Ajouter scripts/ avec génération et injection de certificats x509
(https) et les ajouter au Makefile

Ajouter début d'exemple de manifests dans deployments/kubernetes/
This commit is contained in:
Victor Lacasse-Beaudoin 2024-09-18 19:06:33 -04:00
parent a17d6bf06c
commit b419a5b260
25 changed files with 513 additions and 451 deletions

170
pkg/bottin/client.go Normal file
View file

@ -0,0 +1,170 @@
package bottin
import (
"fmt"
"codeberg.org/vlbeaudoin/voki/v3"
)
type APIClient struct {
Voki *voki.Voki
}
func (c APIClient) GetHealth() (health string, err error) {
var request HealthGETRequest
response, err := request.Request(c.Voki)
if err != nil {
return "", err
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Message, nil
}
func (c APIClient) InsertProgrammes(programmes ...Programme) (amountInserted int64, err error) {
var request ProgrammesPOSTRequest
request.Data.Programmes = programmes
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.ProgrammesInserted, nil
}
func (c APIClient) InsertMembres(membres ...Membre) (amountInserted int64, err error) {
var request MembresPOSTRequest
request.Data.Membres = membres
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.MembresInserted, nil
}
func (c APIClient) GetMembre(membreID string) (membre Membre, err error) {
var request MembreGETRequest
request.Param.MembreID = membreID
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Membre, nil
}
func (c APIClient) GetMembres(limit int) (membres []Membre, err error) {
var request MembresGETRequest
request.Query.Limit = limit
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Membres, nil
}
func (c APIClient) GetProgrammes(limit int) (programmes []Programme, err error) {
var request ProgrammesGETRequest
request.Query.Limit = limit
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Programmes, nil
}
func (c APIClient) UpdateMembrePreferedName(membreID string, name string) (err error) {
var request MembrePreferedNamePUTRequest
if !IsMembreID(membreID) {
return fmt.Errorf("Numéro étudiant '%s' invalide", membreID)
}
request.Param.MembreID = membreID
request.Data.PreferedName = name
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return nil
}
func (c APIClient) GetMembreForDisplay(membreID string) (membre MembreForDisplay, err error) {
var request MembreDisplayGETRequest
request.Param.MembreID = membreID
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Membre, nil
}
func (c APIClient) GetMembresForDisplay(limit int) (membres []MembreForDisplay, err error) {
var request MembresDisplayGETRequest
request.Query.Limit = limit
response, err := request.Request(c.Voki)
if err != nil {
return
}
if code, message := response.StatusCode(), response.Message; code >= 400 {
err = fmt.Errorf("%d: %s", code, message)
return
}
return response.Data.Membres, nil
}

53
pkg/bottin/config.go Normal file
View file

@ -0,0 +1,53 @@
package bottin
type Config struct {
Client struct {
API struct {
Host string `yaml:"host"`
Key string `yaml:"key"`
Port int `yaml:"port"`
Protocol string `yaml:"protocol"`
} `yaml:"api"`
} `yaml:"client"`
Server struct {
API struct {
DB struct {
Database string `yaml:"database"`
Host string `yaml:"host"`
Password string `yaml:"password"`
Port int `yaml:"port"`
SSLMode string `yaml:"sslmode"`
User string `yaml:"user"`
} `yaml:"db"`
Host string `yaml:"host"`
Key string `yaml:"key"`
Port int `yaml:"port"`
TLS struct {
Enabled bool `yaml:"enabled"`
Certfile string `yaml:"certfile"`
Keyfile string `yaml:"keyfile"`
} `yaml:"tls"`
} `yaml:"api"`
UI struct {
API struct {
Host string `yaml:"host"`
Key string `yaml:"key"`
Port int `yaml:"port"`
Protocol string `yaml:"protocol"`
TLS struct {
SkipVerify bool `yaml:"skipverify"`
} `yaml:"tls"`
} `yaml:"api"`
Host string `yaml:"host"`
Password string `yaml:"password"`
Port int `yaml:"port"`
TLS struct {
Enabled bool `yaml:"enabled"`
Certfile string `yaml:"certfile"`
Keyfile string `yaml:"keyfile"`
} `yaml:"tls"`
User string `yaml:"user"`
} `yaml:"ui"`
} `yaml:"server"`
}

353
pkg/bottin/db.go Normal file
View file

@ -0,0 +1,353 @@
package bottin
import (
"context"
_ "embed"
"fmt"
"git.agecem.com/agecem/bottin/v9/queries"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type PostgresClient struct {
//TODO move context out of client
Ctx context.Context
Pool *pgxpool.Pool
}
func (db *PostgresClient) CreateOrReplaceSchema() error {
_, err := db.Pool.Exec(db.Ctx, queries.SQLSchema())
return err
}
func (db *PostgresClient) CreateOrReplaceViews() error {
_, err := db.Pool.Exec(db.Ctx, queries.SQLViews())
return err
}
// InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered
func (d *PostgresClient) InsertMembres(membres ...Membre) (inserted int64, err error) {
select {
case <-d.Ctx.Done():
return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
tx, err := d.Pool.Begin(d.Ctx)
if err != nil {
return inserted, err
}
defer tx.Rollback(d.Ctx)
for i, membre := range membres {
if membre.ID == "" {
return inserted, fmt.Errorf("insertion ligne %d: membre requiert numéro étudiant valide", i)
}
result, err := tx.Exec(d.Ctx, `
INSERT INTO membres
(id, last_name, first_name, prefered_name, programme_id)
VALUES
($1, $2, $3, $4, $5)
ON CONFLICT (id) DO NOTHING;`,
membre.ID,
membre.LastName,
membre.FirstName,
membre.PreferedName,
membre.ProgrammeID,
)
if err != nil {
return 0, err
}
inserted += result.RowsAffected()
}
if err = tx.Commit(d.Ctx); err != nil {
return 0, err
}
return inserted, err
}
}
func (d *PostgresClient) InsertProgrammes(programmes ...Programme) (inserted int64, err error) {
select {
case <-d.Ctx.Done():
return inserted, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
tx, err := d.Pool.Begin(d.Ctx)
if err != nil {
return inserted, err
}
defer tx.Rollback(d.Ctx)
for _, programme := range programmes {
if programme.ID == "" {
return 0, fmt.Errorf("Cannot insert programme with no programme_id")
}
result, err := tx.Exec(d.Ctx, `
INSERT INTO programmes
(id, name)
VALUES ($1, $2) ON CONFLICT DO NOTHING;`,
programme.ID,
programme.Name)
if err != nil {
return 0, err
}
inserted += result.RowsAffected()
}
if err := tx.Commit(d.Ctx); err != nil {
return inserted, err
}
return inserted, err
}
}
func (d *PostgresClient) GetMembre(membreID string) (membre Membre, err error) {
select {
case <-d.Ctx.Done():
err = fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
return
default:
if err = d.Pool.QueryRow(d.Ctx, `
SELECT
"membres".id,
"membres".last_name,
"membres".first_name,
"membres".prefered_name,
"membres".programme_id
FROM
"membres"
WHERE
"membres".id = $1
LIMIT
1;
`, membreID).Scan(
&membre.ID,
&membre.LastName,
&membre.FirstName,
&membre.PreferedName,
&membre.ProgrammeID,
); err != nil {
return
}
if membre.ID == "" {
return membre, fmt.Errorf("Aucun membre trouvé avec numéro '%s'", membre.ID)
}
return membre, nil
}
}
/*
func (d *PostgresClient) UpdateMembreName(membreID, newName string) (int64, error) {
result, err := d.Pool.Exec("UPDATE membres SET prefered_name = $1 WHERE id = $2;", newName, membreID)
if err != nil {
return 0, err
}
rows, err := result.RowsAffected()
if err != nil {
return rows, err
}
return rows, nil
}
*/
func (d *PostgresClient) GetMembres(limit int) (membres []Membre, err error) {
select {
case <-d.Ctx.Done():
return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
rows, err := d.Pool.Query(d.Ctx, `
SELECT
"membres".id,
"membres".last_name,
"membres".first_name,
"membres".prefered_name,
"membres".programme_id
FROM
"membres"
ORDER BY
"membres".id
LIMIT
$1;
`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var membre Membre
if err = rows.Scan(
&membre.ID,
&membre.LastName,
&membre.FirstName,
&membre.PreferedName,
&membre.ProgrammeID,
); err != nil {
return nil, err
}
membres = append(membres, membre)
}
if rows.Err() != nil {
return membres, rows.Err()
}
return membres, nil
}
}
func (d *PostgresClient) GetProgrammes(limit int) (programmes []Programme, err error) {
select {
case <-d.Ctx.Done():
return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
rows, err := d.Pool.Query(d.Ctx, `
SELECT
"programmes".id,
"programmes".name
FROM
"programmes"
ORDER BY
"programmes".id
LIMIT
$1;
`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var programme Programme
if err = rows.Scan(
&programme.ID,
&programme.Name,
); err != nil {
return nil, err
}
programmes = append(programmes, programme)
}
if rows.Err() != nil {
return programmes, rows.Err()
}
return programmes, nil
}
}
func (d *PostgresClient) UpdateMembrePreferedName(membreID string, name string) (err error) {
select {
case <-d.Ctx.Done():
return fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
if !IsMembreID(membreID) {
return fmt.Errorf("Numéro étudiant '%s' invalide", membreID)
}
_, err = d.Pool.Exec(d.Ctx, `
UPDATE
"membres"
SET
prefered_name = $1
WHERE
"membres".id = $2;
`, name, membreID)
}
return
}
func (d *PostgresClient) GetMembreForDisplay(membreID string) (membre MembreForDisplay, err error) {
select {
case <-d.Ctx.Done():
err = fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
return
default:
if err = d.Pool.QueryRow(d.Ctx, `
SELECT
"membres_for_display".id,
"membres_for_display".name,
"membres_for_display".programme_id,
"membres_for_display".programme_name
FROM
"membres_for_display"
WHERE
"membres_for_display".id = $1
LIMIT
1;
`, membreID).Scan(
&membre.ID,
&membre.Name,
&membre.ProgrammeID,
&membre.ProgrammeName,
); err != nil {
if err == pgx.ErrNoRows {
err = fmt.Errorf("Numéro étudiant valide mais aucun·e membre trouvé·e")
}
return
}
if membre.ID == "" {
return membre, fmt.Errorf("Aucun membre trouvé avec numéro '%s'", membre.ID)
}
return membre, nil
}
}
func (d *PostgresClient) GetMembresForDisplay(limit int) (membres []MembreForDisplay, err error) {
select {
case <-d.Ctx.Done():
return nil, fmt.Errorf("PostgresClient.Ctx closed: %s", d.Ctx.Err())
default:
rows, err := d.Pool.Query(d.Ctx, `
SELECT
"membres_for_display".id,
"membres_for_display".name,
"membres_for_display".programme_id,
"membres_for_display".programme_name
FROM
"membres_for_display"
ORDER BY
"membres_for_display".id
LIMIT
$1;
`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var membre MembreForDisplay
if err = rows.Scan(
&membre.ID,
&membre.Name,
&membre.ProgrammeID,
&membre.ProgrammeName,
); err != nil {
return nil, err
}
membres = append(membres, membre)
}
if rows.Err() != nil {
return membres, rows.Err()
}
return membres, nil
}
}

38
pkg/bottin/entity.go Normal file
View file

@ -0,0 +1,38 @@
package bottin
import "unicode"
type Programme struct {
ID string `db:"id" json:"programme_id" csv:"programme_id"`
Name string `db:"name" json:"nom_programme" csv:"nom_programme"`
}
type Membre struct {
ID string `db:"id" json:"membre_id" csv:"membre_id"`
LastName string `db:"last_name" json:"last_name" csv:"last_name"`
FirstName string `db:"first_name" json:"first_name" csv:"first_name"`
PreferedName string `db:"prefered_name" json:"prefered_name" csv:"prefered_name"`
ProgrammeID string `db:"programme_id" json:"programme_id" csv:"programme_id"`
}
// MembreForDisplay maps to the `membres_for_display` view declared in `sql/views.sql`
type MembreForDisplay struct {
ID string `db:"id" json:"membre_id" csv:"membre_id"`
Name string `db:"name" json:"name" csv:"name"`
ProgrammeID string `db:"programme_id" json:"programme_id" csv:"programme_id"`
ProgrammeName string `db:"programme_name" json:"programme_name" csv:"programme_name"`
}
func IsMembreID(membre_id string) bool {
if len(membre_id) != 7 {
return false
}
for _, character := range membre_id {
if !unicode.IsDigit(character) {
return false
}
}
return true
}

350
pkg/bottin/request.go Normal file
View file

@ -0,0 +1,350 @@
package bottin
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"codeberg.org/vlbeaudoin/voki/v3"
)
var _ voki.Requester[HealthGETResponse] = HealthGETRequest{}
type HealthGETRequest struct{}
func (request HealthGETRequest) Complete() bool { return true }
func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete HealthGET request")
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
"/api/v9/health/",
nil,
true,
)
if err != nil {
err = fmt.Errorf("%d: %s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[ProgrammesPOSTResponse] = ProgrammesPOSTRequest{}
type ProgrammesPOSTRequest struct {
Data struct {
Programmes []Programme
}
}
func (request ProgrammesPOSTRequest) Complete() bool {
return len(request.Data.Programmes) != 0
}
func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesPOSTResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete ProgrammesPOSTRequest")
return
}
var buf bytes.Buffer
if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
return
}
statusCode, body, err := v.CallAndParse(
http.MethodPost,
"/api/v9/programme/",
&buf,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembresPOSTResponse] = MembresPOSTRequest{}
type MembresPOSTRequest struct {
Data struct {
Membres []Membre
}
}
func (request MembresPOSTRequest) Complete() bool {
return len(request.Data.Membres) != 0
}
func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembresPOSTRequest")
return
}
var buf bytes.Buffer
if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
return
}
statusCode, body, err := v.CallAndParse(
http.MethodPost,
"/api/v9/membre/",
&buf,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembreGETResponse] = MembreGETRequest{}
type MembreGETRequest struct {
Param struct {
MembreID string `json:"membre_id" param:"membre_id"`
}
}
func (request MembreGETRequest) Complete() bool {
return request.Param.MembreID != ""
}
func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembreGETRequest")
return
}
if id := request.Param.MembreID; !IsMembreID(id) {
err = fmt.Errorf("MembreID '%s' invalide", id)
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/membre/%s/", request.Param.MembreID),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembresGETResponse] = MembresGETRequest{}
type MembresGETRequest struct {
Query struct {
Limit int `json:"limit" query:"limit"`
}
}
func (request MembresGETRequest) Complete() bool { return true }
func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembresGETRequest")
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/membre/?limit=%d", request.Query.Limit),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembrePreferedNamePUTResponse] = MembrePreferedNamePUTRequest{}
type MembrePreferedNamePUTRequest struct {
Data struct {
PreferedName string `json:"prefered_name"`
} `json:"data"`
Param struct {
MembreID string `json:"membre_id" param:"membre_id"`
} `json:"param"`
}
func (request MembrePreferedNamePUTRequest) Complete() bool {
return IsMembreID(request.Param.MembreID) && len(request.Data.PreferedName) != 0
}
func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response MembrePreferedNamePUTResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembrePreferedNamePUTRequest")
return
}
var buf bytes.Buffer
if err = json.NewEncoder(&buf).Encode(request.Data); err != nil {
return
}
statusCode, body, err := v.CallAndParse(
http.MethodPut,
fmt.Sprintf("/api/v9/membre/%s/prefered_name/", request.Param.MembreID),
&buf,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[ProgrammesGETResponse] = ProgrammesGETRequest{}
type ProgrammesGETRequest struct {
Query struct {
Limit int `json:"limit" query:"limit"`
}
}
func (request ProgrammesGETRequest) Complete() bool { return true }
func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete ProgrammesGETRequest")
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/programme/?limit=%d", request.Query.Limit),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembresDisplayGETResponse] = MembresDisplayGETRequest{}
type MembresDisplayGETRequest struct {
Query struct {
Limit int `json:"limit" query:"limit"`
}
}
func (request MembresDisplayGETRequest) Complete() bool { return true }
func (request MembresDisplayGETRequest) Request(v *voki.Voki) (response MembresDisplayGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembresDisplayGETRequest")
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/membre/display/?limit=%d", request.Query.Limit),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}
var _ voki.Requester[MembreDisplayGETResponse] = MembreDisplayGETRequest{}
type MembreDisplayGETRequest struct {
Param struct {
MembreID string `json:"membre_id" param:"membre_id"`
}
}
func (request MembreDisplayGETRequest) Complete() bool {
return request.Param.MembreID != ""
}
func (request MembreDisplayGETRequest) Request(v *voki.Voki) (response MembreDisplayGETResponse, err error) {
if !request.Complete() {
err = fmt.Errorf("Incomplete MembreDisplayGETRequest")
return
}
if id := request.Param.MembreID; !IsMembreID(id) {
err = fmt.Errorf("MembreID '%s' invalide", id)
return
}
statusCode, body, err := v.CallAndParse(
http.MethodGet,
fmt.Sprintf("/api/v9/membre/%s/display/", request.Param.MembreID),
nil,
true,
)
if err != nil {
err = fmt.Errorf("code=%d err=%s", statusCode, err)
return
}
response.SetStatusCode(statusCode)
if err = json.Unmarshal(body, &response); err != nil {
return
}
return
}

91
pkg/bottin/response.go Normal file
View file

@ -0,0 +1,91 @@
package bottin
import (
"fmt"
"codeberg.org/vlbeaudoin/voki/v3"
)
type APIResponse struct {
voki.MessageResponse
statusCode int
}
func (R APIResponse) StatusCode() int { return R.statusCode }
func (R *APIResponse) SetStatusCode(code int) error {
if code <= 0 {
return fmt.Errorf("Cannot set status code to %d", code)
}
R.statusCode = code
return nil
}
type HealthGETResponse struct {
APIResponse
}
type MembreGETResponse struct {
APIResponse
Data MembreGETResponseData `json:"data"`
}
type MembreGETResponseData struct {
Membre Membre `json:"membre"`
}
type MembrePreferedNamePUTResponse struct {
APIResponse
}
type MembresGETResponse struct {
APIResponse
Data MembresGETResponseData `json:"data"`
}
type MembresGETResponseData struct {
Membres []Membre `json:"membres"`
}
type MembreDisplayGETResponse struct {
APIResponse
Data MembreDisplayGETResponseData `json:"data"`
}
type MembreDisplayGETResponseData struct {
Membre MembreForDisplay `json:"membre_for_display"`
}
type MembresDisplayGETResponse struct {
APIResponse
Data MembresDisplayGETResponseData `json:"data"`
}
type MembresDisplayGETResponseData struct {
Membres []MembreForDisplay `json:"membres_for_display"`
}
type MembresPOSTResponse struct {
APIResponse
Data MembresPOSTResponseData `json:"data"`
}
type MembresPOSTResponseData struct {
MembresInserted int64 `json:"membres_inserted"`
}
type ProgrammesPOSTResponse struct {
APIResponse
Data ProgrammesPOSTResponseData `json:"data"`
}
type ProgrammesPOSTResponseData struct {
ProgrammesInserted int64 `json:"programmes_inserted"`
}
type ProgrammesGETResponse struct {
APIResponse
Data ProgrammesGETResponseData `json:"data"`
}
type ProgrammesGETResponseData struct {
Programmes []Programme `json:"programmes"`
}

465
pkg/bottin/routes.go Normal file
View file

@ -0,0 +1,465 @@
package bottin
import (
"encoding/csv"
"fmt"
"io"
"net/http"
"strconv"
"codeberg.org/vlbeaudoin/pave/v2"
"codeberg.org/vlbeaudoin/voki/v3"
"github.com/gocarina/gocsv"
"github.com/labstack/echo/v4"
)
func AddRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error {
_ = db
_ = cfg
apiPath := "/api/v9"
apiGroup := e.Group(apiPath)
p := pave.New()
if err := pave.EchoRegister[HealthGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/health/",
"Get API server health",
"HealthGET", func(c echo.Context) error {
var request, response = HealthGETRequest{}, HealthGETResponse{}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete HealthGET request received"
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[ProgrammesPOSTRequest](
apiGroup,
&p,
apiPath,
http.MethodPost,
"/programme/",
"Insert programmes",
"ProgrammesPOST", func(c echo.Context) error {
var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{}
switch contentType := c.Request().Header.Get("Content-Type"); contentType {
case "application/json":
if err := c.Bind(&request.Data); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse request body: %s", err)
return c.JSON(response.StatusCode(), response)
}
case "text/csv":
body := c.Request().Body
if body == nil {
var response voki.ResponseBadRequest
response.Message = "empty request body cannot be parsed"
return c.JSON(response.StatusCode(), response)
}
defer body.Close()
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
r := csv.NewReader(in)
r.Comma = ';'
return r // Allows use ; as delimiter
})
// Parse CSV data using gocsv
if err := gocsv.Unmarshal(body, &request.Data.Programmes); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse programmes from csv: %s", err)
return c.JSON(response.StatusCode(), response)
}
default:
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("cannot parse body with content-type: %s", contentType)
return c.JSON(response.StatusCode(), response)
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete ProgrammesPOST request received"
return c.JSON(response.StatusCode(), response)
}
amountInserted, err := db.InsertProgrammes(request.Data.Programmes...)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Data.ProgrammesInserted = amountInserted
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembresPOSTRequest](
apiGroup,
&p,
apiPath,
http.MethodPost,
"/membre/",
"Insert membres",
"MembresPOST", func(c echo.Context) error {
var request, response = MembresPOSTRequest{}, MembresPOSTResponse{}
switch contentType := c.Request().Header.Get("Content-Type"); contentType {
case "application/json":
if err := c.Bind(&request.Data); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse request body: %s", err)
return c.JSON(response.StatusCode(), response)
}
case "text/csv":
body := c.Request().Body
if body == nil {
var response voki.ResponseBadRequest
response.Message = "empty request body cannot be parsed"
return c.JSON(response.StatusCode(), response)
}
defer body.Close()
gocsv.SetCSVReader(func(in io.Reader) gocsv.CSVReader {
r := csv.NewReader(in)
r.Comma = ';'
return r // Allows use ; as delimiter
})
// Parse CSV data using gocsv
if err := gocsv.Unmarshal(body, &request.Data.Membres); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse membres from csv: %s", err)
return c.JSON(response.StatusCode(), response)
}
default:
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("cannot parse body with content-type: %s", contentType)
return c.JSON(response.StatusCode(), response)
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembresPOST request received"
return c.JSON(response.StatusCode(), response)
}
amountInserted, err := db.InsertMembres(request.Data.Membres...)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Data.MembresInserted = amountInserted
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembreGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/membre/:membre_id/",
"Get membre",
"MembreGET", func(c echo.Context) error {
var request, response = MembreGETRequest{}, MembreGETResponse{}
request.Param.MembreID = c.Param("membre_id")
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembreGET request received"
return c.JSON(response.StatusCode(), response)
}
membre, err := db.GetMembre(request.Param.MembreID)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Data.Membre = membre
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembresGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/membre/",
"Get membres",
"MembresGET", func(c echo.Context) (err error) {
var request, response = MembresGETRequest{}, MembresGETResponse{}
queryLimit := c.QueryParam("limit")
if queryLimit != "" {
request.Query.Limit, err = strconv.Atoi(queryLimit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parsing limit: %s", err)
return c.JSON(response.StatusCode(), response)
}
} else {
//TODO cfg.Server.API.DefaultLimit
//TODO cfg.Client.API.Limit
request.Query.Limit = 1000
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembresGET request received"
return c.JSON(response.StatusCode(), response)
}
response.Data.Membres, err = db.GetMembres(request.Query.Limit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[ProgrammesGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/programme/",
"Get programmes",
"ProgrammesGET", func(c echo.Context) (err error) {
var request, response = ProgrammesGETRequest{}, ProgrammesGETResponse{}
queryLimit := c.QueryParam("limit")
if queryLimit != "" {
request.Query.Limit, err = strconv.Atoi(queryLimit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parsing limit: %s", err)
return c.JSON(response.StatusCode(), response)
}
} else {
//TODO cfg.API.DefaultLimit
request.Query.Limit = 1000
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete ProgrammesGET request received"
return c.JSON(response.StatusCode(), response)
}
response.Data.Programmes, err = db.GetProgrammes(request.Query.Limit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembrePreferedNamePUTRequest](
apiGroup,
&p,
apiPath,
http.MethodPut,
"/membre/:membre_id/prefered_name/",
"Update membre prefered name, which is prioritized in the membres_for_display view",
"MembrePreferedNamePUT", func(c echo.Context) error {
var request, response = MembrePreferedNamePUTRequest{}, MembrePreferedNamePUTResponse{}
request.Param.MembreID = c.Param("membre_id")
if err := c.Bind(&request.Data); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parse request body: %s", err)
return c.JSON(response.StatusCode(), response)
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembrePreferedNamePUT request received"
return c.JSON(response.StatusCode(), response)
}
if err := db.UpdateMembrePreferedName(request.Param.MembreID, request.Data.PreferedName); err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = fmt.Sprintf("Updated membre %s name to %s", request.Param.MembreID, request.Data.PreferedName)
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembresDisplayGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/membre/display/",
"Get membres",
"MembresDisplayGET", func(c echo.Context) (err error) {
var request, response = MembresDisplayGETRequest{}, MembresDisplayGETResponse{}
queryLimit := c.QueryParam("limit")
if queryLimit != "" {
request.Query.Limit, err = strconv.Atoi(queryLimit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("parsing limit: %s", err)
return c.JSON(response.StatusCode(), response)
}
} else {
//TODO cfg.API.DefaultLimit
request.Query.Limit = 1000
}
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembresDisplayGET request received"
return c.JSON(response.StatusCode(), response)
}
response.Data.Membres, err = db.GetMembresForDisplay(request.Query.Limit)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
if err := pave.EchoRegister[MembreDisplayGETRequest](
apiGroup,
&p,
apiPath,
http.MethodGet,
"/membre/:membre_id/display/",
"Get membre",
"MembreDisplayGET", func(c echo.Context) error {
var request, response = MembreDisplayGETRequest{}, MembreDisplayGETResponse{}
request.Param.MembreID = c.Param("membre_id")
if !request.Complete() {
var response voki.ResponseBadRequest
response.Message = "Incomplete MembreDisplayGET request received"
return c.JSON(response.StatusCode(), response)
}
membre, err := db.GetMembreForDisplay(request.Param.MembreID)
if err != nil {
var response voki.ResponseBadRequest
response.Message = fmt.Sprintf("db: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Data.Membre = membre
if err := response.SetStatusCode(http.StatusOK); err != nil {
var response voki.ResponseInternalServerError
response.Message = fmt.Sprintf("handler: %s", err)
return c.JSON(response.StatusCode(), response)
}
response.Message = "ok"
return c.JSON(response.StatusCode(), response)
}); err != nil {
return err
}
return nil
}