diff --git a/client.go b/client.go index dd7e1ea..6221ce0 100644 --- a/client.go +++ b/client.go @@ -41,3 +41,20 @@ func (c APIClient) InsertProgrammes(programmes ...Programme) (amountInserted int 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 +} diff --git a/client_test.go b/client_test.go index 1f902d2..9d1212d 100644 --- a/client_test.go +++ b/client_test.go @@ -40,7 +40,6 @@ func TestAPI(t *testing.T) { }) //TODO create or replace schema - //TODO insert programmes t.Run("insert programmes", func(t *testing.T) { programmes := []Programme{ @@ -54,6 +53,28 @@ func TestAPI(t *testing.T) { } }) //TODO insert membres + t.Run("insert membres", + func(t *testing.T) { + membres := []Membre{ + { + ID: "0000000", + FirstName: "Test", + LastName: "User", + ProgrammeID: "404.42", + }, + { + ID: "1234567", + FirstName: "Deadname", + LastName: "User", + PreferedName: "User, Test-Name", + ProgrammeID: "200.10", + }, + } + _, err := apiClient.InsertMembres(membres...) + if err != nil { + t.Error(err) + } + }) //TODO get membre //TODO update membre prefered name //TODO get membres diff --git a/cmd.go b/cmd.go index 4cd12fb..6067795 100644 --- a/cmd.go +++ b/cmd.go @@ -85,6 +85,10 @@ var apiCmd = &cobra.Command{ log.Fatal("create or replace schema:", err) } + if err := db.CreateOrReplaceViews(); err != nil { + log.Fatal("create or replace views:", err) + } + // Routes if err := addRoutes(e, db); err != nil { log.Fatal("add routes:", err) diff --git a/db.go b/db.go index eda594c..c2394e4 100644 --- a/db.go +++ b/db.go @@ -11,6 +11,9 @@ import ( //go:embed sql/schema.sql var sqlSchema string +//go:embed sql/views.sql +var sqlViews string + type PostgresClient struct { //TODO move context out of client Ctx context.Context @@ -22,8 +25,13 @@ func (db *PostgresClient) CreateOrReplaceSchema() error { return err } +func (db *PostgresClient) CreateOrReplaceViews() error { + _, err := db.Pool.Exec(db.Ctx, 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) { +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()) @@ -84,10 +92,10 @@ func (d *PostgresClient) InsertProgrammes(programmes ...Programme) (inserted int result, err := tx.Exec(d.Ctx, ` INSERT INTO programmes -(id, titre) +(id, name) VALUES ($1, $2) ON CONFLICT DO NOTHING;`, programme.ID, - programme.Titre) + programme.Name) if err != nil { return 0, err } diff --git a/entity.go b/entity.go index 52e54d0..385acde 100644 --- a/entity.go +++ b/entity.go @@ -1,8 +1,8 @@ package main type Programme struct { - ID string `db:"id" json:"programme_id" csv:"programme_id"` - Titre string `db:"titre" json:"nom_programme" csv:"nom_programme"` + ID string `db:"id" json:"programme_id" csv:"programme_id"` + Name string `db:"name" json:"nom_programme" csv:"nom_programme"` } type Membre struct { diff --git a/request.go b/request.go index 3ff29d7..720561b 100644 --- a/request.go +++ b/request.go @@ -79,3 +79,44 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP 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/membres/", + &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 +} diff --git a/routes.go b/routes.go index 6041358..05c62b5 100644 --- a/routes.go +++ b/routes.go @@ -48,7 +48,7 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { apiPath, http.MethodPost, "/programmes/", - "Get registered programmes", + "Insert programmes", "ProgrammesPOST", func(c echo.Context) error { var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{} @@ -84,5 +84,48 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { }); err != nil { return err } + + if err := pave.EchoRegister[MembresPOSTRequest]( + apiGroup, + &p, + apiPath, + http.MethodPost, + "/membres/", + "Insert membres", + "MembresPOST", func(c echo.Context) error { + var request, response = MembresPOSTRequest{}, MembresPOSTResponse{} + + 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 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 + } return nil } diff --git a/sql/schema.sql b/sql/schema.sql index 154b983..6253131 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -1,12 +1,12 @@ CREATE TABLE IF NOT EXISTS programmes ( id TEXT PRIMARY KEY, - titre TEXT + name TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS membres ( id VARCHAR(7) PRIMARY KEY, - last_name TEXT, - first_name TEXT, + last_name TEXT NOT NULL, + first_name TEXT NOT NULL, prefered_name TEXT, - programme_id TEXT REFERENCES programmes(id) + programme_id TEXT REFERENCES programmes(id) NOT NULL ); diff --git a/sql/views.sql b/sql/views.sql new file mode 100644 index 0000000..ecdde1a --- /dev/null +++ b/sql/views.sql @@ -0,0 +1,23 @@ +-- membres_for_display affiche le numéro étudiant, nom complet OU prefered_name, et titre du programme. +-- +-- Utilisé par l'application web pour rechercher et afficher les informations des membres +CREATE OR REPLACE VIEW + "membres_for_display" +AS ( + SELECT + "membres".id, + CASE + WHEN + "membres".prefered_name != '' AND "membres".prefered_name IS NOT NULL + THEN + "membres".prefered_name + ELSE + CONCAT("membres".last_name, ', ', "membres".first_name) + END AS name, + "programmes".id AS programme_id, + "programmes".name AS programme_name + FROM + "membres" + INNER JOIN + "programmes" ON "programmes".id = "membres".programme_id +);