From 6d98375adb95673f18c98fe8df88589e92440862 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 6 Jun 2024 01:40:56 -0400 Subject: [PATCH 01/33] =?UTF-8?q?d=C3=A9but=20de=20r=C3=A9=C3=A9criture=20?= =?UTF-8?q?pour=20v7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 15 +-- cmd/api.go | 141 ++++++++++++++++-------- cmd/root.go | 2 +- cmd/web.go | 135 +++++++++++++++-------- config.go | 99 +++++++++++++++++ data/apiclient.go | 78 ------------- data/data.go => db.go | 48 +++++--- db_test.go | 45 ++++++++ docker-compose.yaml | 2 +- models/models.go => entity.go | 21 +--- go.mod | 16 +-- go.sum | 40 +++---- handlers/handlers.go | 11 -- handlers/health.go | 36 ------ handlers/insert.go | 129 ---------------------- handlers/read.go | 61 ---------- handlers/seed.go | 24 ---- handlers/update.go | 42 ------- main.go | 16 ++- responses/post.go => responses.go | 13 ++- responses/health.go | 7 -- responses/list.go | 13 --- sql/schema.sql | 12 ++ {web/templates => templates}/index.html | 0 v4/LICENSE | 9 -- v4/README.md | 1 - v4/go.mod | 10 -- web/embed.go | 10 -- web/webhandlers/handlers.go | 46 -------- 29 files changed, 421 insertions(+), 661 deletions(-) create mode 100644 config.go delete mode 100644 data/apiclient.go rename data/data.go => db.go (78%) create mode 100644 db_test.go rename models/models.go => entity.go (61%) delete mode 100644 handlers/handlers.go delete mode 100644 handlers/health.go delete mode 100644 handlers/insert.go delete mode 100644 handlers/read.go delete mode 100644 handlers/seed.go delete mode 100644 handlers/update.go rename responses/post.go => responses.go (59%) delete mode 100644 responses/health.go delete mode 100644 responses/list.go create mode 100644 sql/schema.sql rename {web/templates => templates}/index.html (100%) delete mode 100644 v4/LICENSE delete mode 100644 v4/README.md delete mode 100644 v4/go.mod delete mode 100644 web/embed.go delete mode 100644 web/webhandlers/handlers.go diff --git a/Dockerfile b/Dockerfile index 52a9a73..7c94ff6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,20 @@ -FROM golang:1.22.0 as build +FROM golang:1.22.3 as build LABEL author="vlbeaudoin" WORKDIR /go/src/app -COPY go.mod go.sum main.go ./ +COPY go.mod go.sum db.go entity.go main.go responses.go ./ ADD cmd/ cmd/ -ADD data/ data/ -ADD handlers/ handlers/ -ADD models/ models/ -ADD responses/ responses/ -ADD web/ web/ +ADD templates/ templates/ +ADD sql/ sql/ -RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o bottin . +RUN CGO_ENABLED=0 go build -a -o bottin . # Alpine -FROM alpine:3.19 +FROM alpine:3.20 WORKDIR /app diff --git a/cmd/api.go b/cmd/api.go index b03be9a..1e9f6d8 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -5,9 +5,6 @@ import ( "fmt" "log" - "codeberg.org/vlbeaudoin/serpents" - "git.agecem.com/agecem/bottin/v6/data" - "git.agecem.com/agecem/bottin/v6/handlers" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/spf13/cobra" @@ -19,14 +16,51 @@ var ( apiKey string ) +const ( + ViperAPIPort string = "api.port" + FlagAPIPort string = "api-port" + DefaultAPIPort int = 1312 + DescriptionAPIPort string = "API server port" + + ViperAPIKey string = "api.key" + FlagAPIKey string = "api-key" + DefaultAPIKey string = "bottin" + DescriptionAPIKey string = "API server key. Leave empty for no key auth (not recommended)" + + ViperDBDatabase string = "db.database" + FlagDBDatabase string = "db-database" + DefaultDBDatabase string = "bottin" + DescriptionDBDatabase string = "Postgres database" + + ViperDBHost string = "db.host" + FlagDBHost string = "db-host" + DefaultDBHost string = "db" + DescriptionDBHost string = "Postgres host" + + ViperDBPassword string = "db.password" + FlagDBPassword string = "db-password" + DefaultDBPassword string = "bottin" + DescriptionDBPassword string = "Postgres password" + + ViperDBPort string = "db.port" + FlagDBPort string = "db-port" + DefaultDBPort int = 5432 + DescriptionDBPort string = "Postgres port" + + ViperDBUser string = "db.user" + FlagDBUser string = "db-user" + DefaultDBUser string = "bottin" + DescriptionDBUser string = "Postgres user" +) + // apiCmd represents the api command var apiCmd = &cobra.Command{ Use: "api", Short: "Démarrer le serveur API", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - apiKey = viper.GetString("api.key") - apiPort = viper.GetInt("api.port") + apiKey = viper.GetString(ViperAPIKey) + apiPort = viper.GetInt(ViperAPIPort) e := echo.New() @@ -41,40 +75,42 @@ var apiCmd = &cobra.Command{ } // DataClient + /* + client, err := data.NewDataClientFromViper() + if err != nil { + log.Fatalf("Could not establish database connection.\n Error: %s\n", err) + } + defer client.DB.Close() - client, err := data.NewDataClientFromViper() - if err != nil { - log.Fatalf("Could not establish database connection.\n Error: %s\n", err) - } - defer client.DB.Close() + err = client.DB.Ping() + if err != nil { + log.Fatalf("Database was supposed to be ready but Ping() failed.\n Error: %s\n", err) + } - err = client.DB.Ping() - if err != nil { - log.Fatalf("Database was supposed to be ready but Ping() failed.\n Error: %s\n", err) - } - - _, err = client.Seed() - if err != nil { - log.Fatalf("Error during client.Seed(): %s", err) - } - - h := handlers.New(client) + _, err = client.Seed() + if err != nil { + log.Fatalf("Error during client.Seed(): %s", err) + } + */ // Routes + /* + h := handlers.New(client) - e.GET("/v6/health/", h.GetHealth) + e.GET("/v7/health/", h.GetHealth) - e.POST("/v6/membres/", h.PostMembres) + e.POST("/v7/membres/", h.PostMembres) - e.GET("/v6/membres/", h.ListMembres) + e.GET("/v7/membres/", h.ListMembres) - e.GET("/v6/membres/:membre_id/", h.ReadMembre) + e.GET("/v7/membres/:membre_id/", h.ReadMembre) - e.PUT("/v6/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) + e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) - e.POST("/v6/programmes/", h.PostProgrammes) + e.POST("/v7/programmes/", h.PostProgrammes) - e.POST("/v6/seed/", h.PostSeed) + e.POST("/v7/seed/", h.PostSeed) + */ // Execution @@ -86,37 +122,44 @@ func init() { rootCmd.AddCommand(apiCmd) // api.key - serpents.String(apiCmd.Flags(), - "api.key", "api-key", "bottin", - "API server key. Leave empty for no key auth") + apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey) + if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil { + log.Fatal(err) + } // api.port - serpents.Int(apiCmd.Flags(), - "api.port", "api-port", 1312, - "API server port") + apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort) + if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil { + log.Fatal(err) + } // db.database - serpents.String(apiCmd.Flags(), - "db.database", "db-database", "bottin", - "Postgres database") + apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase) + if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil { + log.Fatal(err) + } // db.host - serpents.String(apiCmd.Flags(), - "db.host", "db-host", "db", - "Postgres host") + apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost) + if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil { + log.Fatal(err) + } // db.password - serpents.String(apiCmd.Flags(), - "db.password", "db-password", "bottin", - "Postgres password") + apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword) + if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil { + log.Fatal(err) + } // db.port - serpents.Int(apiCmd.Flags(), - "db.port", "db-port", 5432, - "Postgres port") + apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort) + if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil { + log.Fatal(err) + } // db.user - serpents.String(apiCmd.Flags(), - "db.user", "db-user", "bottin", - "Postgres user") + apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser) + if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil { + log.Fatal(err) + } } diff --git a/cmd/root.go b/cmd/root.go index 0b7562e..26e0636 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -14,7 +14,7 @@ var cfgFile string // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "bottin", - Short: "Application de gestion de distribution d'agendas", + Short: "Bottin étudiant de l'AGECEM", } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cmd/web.go b/cmd/web.go index 4d1a556..2ce2fad 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -7,12 +7,7 @@ import ( "html/template" "io" "log" - "net/http" - "codeberg.org/vlbeaudoin/serpents" - "git.agecem.com/agecem/bottin/v6/data" - "git.agecem.com/agecem/bottin/v6/web" - "git.agecem.com/agecem/bottin/v6/web/webhandlers" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/spf13/cobra" @@ -29,6 +24,43 @@ var ( webApiProtocol string ) +const ( + viperWebUser string = "web.user" + flagWebUser string = "web-user" + defaultWebUser string = "bottin" + descriptionWebUser string = "Web client basic auth user" + + viperWebPassword string = "web.password" + flagWebPassword string = "web-password" + defaultWebPassword string = "bottin" + descriptionWebPassword string = "Web client basic auth password" + + viperWebPort string = "web.port" + flagWebPort string = "web-port" + defaultWebPort int = 2312 + descriptionWebPort string = "Web client port" + + viperWebAPIHost string = "api.host" + flagWebAPIHost string = "api-host" + defaultWebAPIHost string = "api" + descriptionWebAPIHost string = "Target API server host" + + viperWebAPIKey string = "api.key" + flagWebAPIKey string = "api-key" + defaultWebAPIKey string = "bottin" + descriptionWebAPIKey string = "Target API server key" + + viperWebAPIPort string = "api.port" + flagWebAPIPort string = "api-port" + defaultWebAPIPort int = 1312 + descriptionWebAPIPort string = "Target API server port" + + viperWebAPIProtocol string = "api.protocol" + flagWebAPIProtocol string = "api-protocol" + defaultWebAPIProtocol string = "http" + descriptionWebAPIProtocol string = "Target API server protocol (http/https)" +) + var templatesFS embed.FS type Template struct { @@ -45,27 +77,28 @@ var webCmd = &cobra.Command{ Short: "Démarrer le client web", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - webApiHost = viper.GetString("web.api.host") - webApiKey = viper.GetString("web.api.key") - webApiPort = viper.GetInt("web.api.port") - webApiProtocol = viper.GetString("web.api.protocol") - webPassword = viper.GetString("web.password") - webPort = viper.GetInt("web.port") - webUser = viper.GetString("web.user") + webApiHost = viper.GetString(viperWebAPIHost) + webApiKey = viper.GetString(viperWebAPIKey) + webApiPort = viper.GetInt(viperWebAPIPort) + webApiProtocol = viper.GetString(viperWebAPIProtocol) + webPassword = viper.GetString(viperWebPassword) + webPort = viper.GetInt(viperWebPort) + webUser = viper.GetString(viperWebUser) // Ping API server + /* + client := http.DefaultClient + defer client.CloseIdleConnections() - client := http.DefaultClient - defer client.CloseIdleConnections() + apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort) - apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort) + pingResult, err := apiClient.GetHealth() + if err != nil { + log.Fatal(err) + } - pingResult, err := apiClient.GetHealth() - if err != nil { - log.Fatal(err) - } - - log.Println(pingResult) + log.Println(pingResult) + */ e := echo.New() @@ -88,11 +121,12 @@ var webCmd = &cobra.Command{ e.Renderer = t // Routes + /* + handler := webhandlers.Handler{APIClient: apiClient} - handler := webhandlers.Handler{APIClient: apiClient} - - e.GET("/", handler.GetIndex) - e.GET("/membre/", handler.GetMembre) + e.GET("/", handler.GetIndex) + e.GET("/membre/", handler.GetMembre) + */ // Execution @@ -103,40 +137,47 @@ var webCmd = &cobra.Command{ func init() { rootCmd.AddCommand(webCmd) - templatesFS = web.GetTemplates() + //templatesFS = web.GetTemplates() // web.api.host - serpents.String(webCmd.Flags(), - "web.api.host", "web-api-host", "api", - "Remote API server host") + webCmd.Flags().String(flagWebAPIHost, defaultWebAPIHost, descriptionWebAPIHost) + if err := viper.BindPFlag(viperWebAPIHost, webCmd.Flags().Lookup(flagWebAPIHost)); err != nil { + log.Fatal(err) + } // web.api.key - serpents.String(webCmd.Flags(), - "web.api.key", "web-api-key", "bottin", - "Remote API server key") + webCmd.Flags().String(flagWebAPIKey, defaultWebAPIKey, descriptionWebAPIKey) + if err := viper.BindPFlag(viperWebAPIKey, webCmd.Flags().Lookup(flagWebAPIKey)); err != nil { + log.Fatal(err) + } // web.api.protocol - serpents.String(webCmd.Flags(), - "web.api.protocol", "web-api-protocol", "http", - "Remote API server protocol") + webCmd.Flags().String(flagWebAPIProtocol, defaultWebAPIProtocol, descriptionWebAPIProtocol) + if err := viper.BindPFlag(viperWebAPIProtocol, webCmd.Flags().Lookup(flagWebAPIProtocol)); err != nil { + log.Fatal(err) + } // web.api.port - serpents.Int(webCmd.Flags(), - "web.api.port", "web-api-port", 1312, - "Remote API server port") + webCmd.Flags().Int(flagWebAPIPort, defaultWebAPIPort, descriptionWebAPIPort) + if err := viper.BindPFlag(viperWebAPIPort, webCmd.Flags().Lookup(flagWebAPIPort)); err != nil { + log.Fatal(err) + } // web.password - serpents.String(webCmd.Flags(), - "web.password", "web-password", "bottin", - "Web client password") + webCmd.Flags().String(flagWebPassword, defaultWebPassword, descriptionWebPassword) + if err := viper.BindPFlag(viperWebPassword, webCmd.Flags().Lookup(flagWebPassword)); err != nil { + log.Fatal(err) + } // web.port - serpents.Int(webCmd.Flags(), - "web.port", "web-port", 2312, - "Web client port") + webCmd.Flags().Int(flagWebPort, defaultWebPort, descriptionWebPort) + if err := viper.BindPFlag(viperWebPort, webCmd.Flags().Lookup(flagWebPort)); err != nil { + log.Fatal(err) + } // web.user - serpents.String(webCmd.Flags(), - "web.user", "web-user", "bottin", - "Web client user") + webCmd.Flags().String(flagWebUser, defaultWebUser, descriptionWebUser) + if err := viper.BindPFlag(viperWebUser, webCmd.Flags().Lookup(flagWebUser)); err != nil { + log.Fatal(err) + } } diff --git a/config.go b/config.go new file mode 100644 index 0000000..c0108b4 --- /dev/null +++ b/config.go @@ -0,0 +1,99 @@ +package main + +const ( + ViperAPIPort string = "api.port" + FlagAPIPort string = "api-port" + DefaultAPIPort int = 1312 + DescriptionAPIPort string = "API server port" + + ViperAPIKey string = "api.key" + FlagAPIKey string = "api-key" + DefaultAPIKey string = "bottin" + DescriptionAPIKey string = "API server key. Leave empty for no key auth (not recommended)" + + ViperDBDatabase string = "db.database" + FlagDBDatabase string = "db-database" + DefaultDBDatabase string = "bottin" + DescriptionDBDatabase string = "Postgres database" + + ViperDBHost string = "db.host" + FlagDBHost string = "db-host" + DefaultDBHost string = "db" + DescriptionDBHost string = "Postgres host" + + ViperDBPassword string = "db.password" + FlagDBPassword string = "db-password" + DefaultDBPassword string = "bottin" + DescriptionDBPassword string = "Postgres password" + + ViperDBPort string = "db.port" + FlagDBPort string = "db-port" + DefaultDBPort int = 5432 + DescriptionDBPort string = "Postgres port" + + ViperDBUser string = "db.user" + FlagDBUser string = "db-user" + DefaultDBUser string = "bottin" + DescriptionDBUser string = "Postgres user" + + viperWebUser string = "web.user" + flagWebUser string = "web-user" + defaultWebUser string = "bottin" + descriptionWebUser string = "Web client basic auth user" + + viperWebPassword string = "web.password" + flagWebPassword string = "web-password" + defaultWebPassword string = "bottin" + descriptionWebPassword string = "Web client basic auth password" + + viperWebPort string = "web.port" + flagWebPort string = "web-port" + defaultWebPort int = 2312 + descriptionWebPort string = "Web client port" + + viperWebAPIHost string = "api.host" + flagWebAPIHost string = "api-host" + defaultWebAPIHost string = "api" + descriptionWebAPIHost string = "Target API server host" + + viperWebAPIKey string = "api.key" + flagWebAPIKey string = "api-key" + defaultWebAPIKey string = "bottin" + descriptionWebAPIKey string = "Target API server key" + + viperWebAPIPort string = "api.port" + flagWebAPIPort string = "api-port" + defaultWebAPIPort int = 1312 + descriptionWebAPIPort string = "Target API server port" + + viperWebAPIProtocol string = "api.protocol" + flagWebAPIProtocol string = "api-protocol" + defaultWebAPIProtocol string = "http" + descriptionWebAPIProtocol string = "Target API server protocol (http/https)" +) + +type Config struct { + API struct { + Port int `yaml:"port"` + Key string `yaml:"key"` + } `yaml:"api"` + DB struct { + Database string `yaml:"database"` + Host string `yaml:"host"` + SSLMode string `yaml:"sslmode"` + Password string `yaml:"password"` + Port int `yaml:"port"` + User string `yaml:"user"` + } `yaml:"db"` + Web struct { + User string `yaml:"user"` + Password string `yaml:"password"` + Port int `yaml:"port"` + API struct { + Host string `yaml:"host"` + Key string `yaml:"key"` + Port int `yaml:"port"` + Protocol string `yaml:"protocol"` + } `yaml:"api"` + } `yaml:"web"` +} diff --git a/data/apiclient.go b/data/apiclient.go deleted file mode 100644 index da70a18..0000000 --- a/data/apiclient.go +++ /dev/null @@ -1,78 +0,0 @@ -package data - -import ( - "errors" - "fmt" - "net/http" - - "codeberg.org/vlbeaudoin/voki/v2" - "git.agecem.com/agecem/bottin/v6/models" - "git.agecem.com/agecem/bottin/v6/responses" - "github.com/spf13/viper" -) - -type ApiClient struct { - Voki *voki.Voki -} - -func NewApiClientFromViper(client *http.Client) *ApiClient { - apiClientKey := viper.GetString("web.api.key") - apiClientHost := viper.GetString("web.api.host") - apiClientProtocol := viper.GetString("web.api.protocol") - apiClientPort := viper.GetInt("web.api.port") - - return NewApiClient(client, apiClientKey, apiClientHost, apiClientProtocol, apiClientPort) -} - -func NewApiClient(client *http.Client, key, host, protocol string, port int) *ApiClient { - return &ApiClient{ - Voki: voki.New(client, host, key, port, protocol), - } -} - -// GetHealth allows checking for API server health -func (a *ApiClient) GetHealth() (string, error) { - var getHealthResponse responses.GetHealthResponse - err := a.Voki.Unmarshal(http.MethodGet, "/v6/health", nil, true, &getHealthResponse) - if err != nil { - return getHealthResponse.Message, err - } - - if getHealthResponse.Message == "" { - return getHealthResponse.Message, errors.New("Could not confirm that API server is up, no response message") - } - - return getHealthResponse.Message, nil -} - -func (a *ApiClient) GetMembre(membreID string) (models.Membre, error) { - var getMembreResponse struct { - Message string `json:"message"` - Data struct { - Membre models.Membre `json:"membre"` - } `json:"data"` - } - - if membreID == "" { - return getMembreResponse.Data.Membre, errors.New("Veuillez fournir un numéro étudiant à rechercher") - } - - err := a.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v6/membres/%s", membreID), nil, true, &getMembreResponse) - if err != nil { - return getMembreResponse.Data.Membre, err - } - - if getMembreResponse.Data.Membre == *new(models.Membre) { - if getMembreResponse.Message != "" { - return getMembreResponse.Data.Membre, fmt.Errorf(getMembreResponse.Message) - } - - return getMembreResponse.Data.Membre, fmt.Errorf("Ce numéro étudiant ne correspond à aucunE membre") - } - - return getMembreResponse.Data.Membre, nil -} - -func (a *ApiClient) ListMembres() (r responses.ListMembresResponse, err error) { - return r, a.Voki.Unmarshal(http.MethodGet, "/v6/membres", nil, true, &r) -} diff --git a/data/data.go b/db.go similarity index 78% rename from data/data.go rename to db.go index fa8678c..fff8885 100644 --- a/data/data.go +++ b/db.go @@ -1,15 +1,26 @@ -package data +package main import ( - "errors" - "fmt" + "context" + _ "embed" - "git.agecem.com/agecem/bottin/v6/models" - _ "github.com/jackc/pgx/stdlib" - "github.com/jmoiron/sqlx" - "github.com/spf13/viper" + "github.com/jackc/pgx/v5/pgxpool" ) +//go:embed sql/schema.sql +var sqlSchema string + +type PostgresClient struct { + Ctx context.Context + Pool *pgxpool.Pool +} + +func (db *PostgresClient) CreateOrReplaceSchema() error { + _, err := db.Pool.Exec(db.Ctx, sqlSchema) + return err +} + +/* // DataClient is a postgres client based on sqlx type DataClient struct { PostgresConnection PostgresConnection @@ -28,11 +39,11 @@ type PostgresConnection struct { func NewDataClientFromViper() (*DataClient, error) { client, err := NewDataClient( PostgresConnection{ - User: viper.GetString("db.user"), - Password: viper.GetString("db.password"), - Host: viper.GetString("db.host"), - Database: viper.GetString("db.database"), - Port: viper.GetInt("db.port"), + User: viper.GetString(cmd.ViperDBHost), + Password: viper.GetString(cmd.ViperDBPassword), + Host: viper.GetString(cmd.ViperDBHost), + Database: viper.GetString(cmd.ViperDBDatabase), + Port: viper.GetInt(cmd.ViperDBPort), }) return client, err @@ -60,7 +71,7 @@ func NewDataClient(connection PostgresConnection) (*DataClient, error) { } func (d *DataClient) Seed() (int64, error) { - result, err := d.DB.Exec(models.Schema) + result, err := d.DB.Exec(sqlSchema) if err != nil { return 0, err } @@ -74,7 +85,7 @@ func (d *DataClient) Seed() (int64, error) { } // InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered -func (d *DataClient) InsertMembres(membres []models.Membre) (int64, error) { +func (d *DataClient) InsertMembres(membres []Membre) (int64, error) { var rowsInserted int64 tx, err := d.DB.Beginx() if err != nil { @@ -107,7 +118,7 @@ func (d *DataClient) InsertMembres(membres []models.Membre) (int64, error) { return rowsInserted, nil } -func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, error) { +func (d *DataClient) InsertProgrammes(programmes []Programme) (int64, error) { var rowsInserted int64 tx, err := d.DB.Beginx() if err != nil { @@ -141,8 +152,8 @@ func (d *DataClient) InsertProgrammes(programmes []models.Programme) (int64, err return rowsInserted, nil } -func (d *DataClient) GetMembre(membreID string) (models.Membre, error) { - var membre models.Membre +func (d *DataClient) GetMembre(membreID string) (Membre, error) { + var membre Membre rows, err := d.DB.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID) if err != nil { @@ -177,6 +188,7 @@ func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) { return rows, nil } -func (d *DataClient) GetMembres() (membres []models.Membre, err error) { +func (d *DataClient) GetMembres() (membres []Membre, err error) { return membres, d.DB.Select(&membres, "SELECT * FROM membres;") } +*/ diff --git a/db_test.go b/db_test.go new file mode 100644 index 0000000..d9d235a --- /dev/null +++ b/db_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "fmt" + "testing" + + "github.com/jackc/pgx/v5/pgxpool" +) + +func TestDB(t *testing.T) { + ctx := context.Background() + + //prep + pool, err := pgxpool.New( + ctx, + fmt.Sprintf( + "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", + "bottin", + "bottin", + "bottin", + "localhost", + 5432, + "prefer", //TODO change to "require" + )) + if err != nil { + t.Error(err) + return + } + defer pool.Close() + + db := &PostgresClient{ + Ctx: ctx, + Pool: pool, + } + + //exec + + t.Run("create or replace schema", + func(t *testing.T) { + if err := db.CreateOrReplaceSchema(); err != nil { + t.Error(err) + } + }) +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 1f739d8..9f2e604 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ services: db: - image: 'docker.io/library/postgres:16.1' + image: 'docker.io/library/postgres:16' environment: POSTGRES_DATABASE: "${BOTTIN_POSTGRES_DATABASE}" POSTGRES_PASSWORD: "${BOTTIN_POSTGRES_PASSWORD}" diff --git a/models/models.go b/entity.go similarity index 61% rename from models/models.go rename to entity.go index 845d28d..52e54d0 100644 --- a/models/models.go +++ b/entity.go @@ -1,19 +1,4 @@ -package models - -const Schema = ` -CREATE TABLE IF NOT EXISTS programmes ( - id TEXT PRIMARY KEY, - titre TEXT -); - -CREATE TABLE IF NOT EXISTS membres ( - id VARCHAR(7) PRIMARY KEY, - last_name TEXT, - first_name TEXT, - prefered_name TEXT, - programme_id TEXT REFERENCES programmes(id) -); -` +package main type Programme struct { ID string `db:"id" json:"programme_id" csv:"programme_id"` @@ -27,7 +12,3 @@ type Membre struct { PreferedName string `db:"prefered_name" json:"prefered_name" csv:"prefered_name"` ProgrammeID string `db:"programme_id" json:"programme_id" csv:"programme_id"` } - -type Entry interface { - Programme | Membre -} diff --git a/go.mod b/go.mod index bd60d6f..1952064 100644 --- a/go.mod +++ b/go.mod @@ -1,36 +1,31 @@ -module git.agecem.com/agecem/bottin/v6 +module git.agecem.com/agecem/bottin/v7 go 1.22.0 require ( - codeberg.org/vlbeaudoin/serpents v1.1.0 codeberg.org/vlbeaudoin/voki/v2 v2.0.3 - github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a - github.com/jackc/pgx v3.6.2+incompatible - github.com/jmoiron/sqlx v1.3.5 + github.com/jackc/pgx/v5 v5.6.0 github.com/labstack/echo/v4 v4.11.4 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 ) require ( - github.com/cockroachdb/apd v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/shopspring/decimal v1.3.1 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect @@ -42,6 +37,7 @@ require ( golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect golang.org/x/net v0.21.0 // indirect + golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect diff --git a/go.sum b/go.sum index b8fe100..5c5e2b0 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ -codeberg.org/vlbeaudoin/serpents v1.1.0 h1:U9f2+2D1yUVHx90yePi2ZOLRLG/Wkoob4JXDIVyoBwA= -codeberg.org/vlbeaudoin/serpents v1.1.0/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ= codeberg.org/vlbeaudoin/voki/v2 v2.0.3 h1:H3j7yk8uBiDK19OUWAKbYKmw0tsSw4t0LA5lyAfyT3E= codeberg.org/vlbeaudoin/voki/v2 v2.0.3/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,12 +9,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a h1:RYfmiM0zluBJOiPDJseKLEN4BapJ42uSi9SZBQ2YyiA= -github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= -github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= -github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -27,12 +17,14 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= -github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= -github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o= -github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -41,8 +33,6 @@ github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zG github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= -github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -50,14 +40,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -68,8 +54,6 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -85,6 +69,8 @@ github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMV github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= @@ -103,6 +89,8 @@ golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5C golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= @@ -112,8 +100,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/handlers.go b/handlers/handlers.go deleted file mode 100644 index 0b35b23..0000000 --- a/handlers/handlers.go +++ /dev/null @@ -1,11 +0,0 @@ -package handlers - -import "git.agecem.com/agecem/bottin/v6/data" - -type Handler struct { - DataClient *data.DataClient -} - -func New(dataClient *data.DataClient) *Handler { - return &Handler{DataClient: dataClient} -} diff --git a/handlers/health.go b/handlers/health.go deleted file mode 100644 index 4923493..0000000 --- a/handlers/health.go +++ /dev/null @@ -1,36 +0,0 @@ -package handlers - -import ( - "net/http" - - "git.agecem.com/agecem/bottin/v6/data" - "git.agecem.com/agecem/bottin/v6/responses" - "github.com/labstack/echo/v4" -) - -func (h *Handler) GetHealth(c echo.Context) error { - var response responses.GetHealthResponse - - dataClient, err := data.NewDataClientFromViper() - if err != nil { - response.StatusCode = http.StatusInternalServerError - response.Message = "Error during data.NewDataClientFromViper()" - response.Error = err.Error() - - return c.JSON(response.StatusCode, response) - } - defer dataClient.DB.Close() - - if err = dataClient.DB.Ping(); err != nil { - response.StatusCode = http.StatusInternalServerError - response.Message = "Error during dataClient.DB.Ping()" - response.Error = err.Error() - - return c.JSON(response.StatusCode, response) - } - - response.StatusCode = http.StatusOK - response.Message = "Bottin API v6 is ready" - - return c.JSON(response.StatusCode, response) -} diff --git a/handlers/insert.go b/handlers/insert.go deleted file mode 100644 index 8039505..0000000 --- a/handlers/insert.go +++ /dev/null @@ -1,129 +0,0 @@ -package handlers - -import ( - "encoding/csv" - "io" - "net/http" - - "git.agecem.com/agecem/bottin/v6/models" - "git.agecem.com/agecem/bottin/v6/responses" - "github.com/labstack/echo/v4" - - "github.com/gocarina/gocsv" -) - -func (h *Handler) PostMembres(c echo.Context) error { - var response responses.PostMembresResponse - - var membres []models.Membre - - switch c.Request().Header.Get("Content-Type") { - case "application/json": - if err := c.Bind(&membres); err != nil { - response.StatusCode = http.StatusBadRequest - response.Message = "Could not bind membres" - response.Error = err.Error() - return c.JSON(response.StatusCode, response) - } - case "text/csv": - body := c.Request().Body - if body == nil { - response.StatusCode = http.StatusBadRequest - response.Message = "Request body is empty" - return c.JSON(response.StatusCode, response) - } - defer body.Close() - - // Parse the CSV data from the request body using gocsv. - if err := gocsv.Unmarshal(body, &membres); err != nil { - response.StatusCode = http.StatusBadRequest - response.Message = "Could not unmarshal into membres" - response.Error = err.Error() - return c.JSON(response.StatusCode, response) - } - default: - response.StatusCode = http.StatusBadRequest - response.Message = "Invalid Content-Type: Please use application/json or text/csv" - return c.JSON(response.StatusCode, response) - } - - if len(membres) == 0 { - response.StatusCode = http.StatusOK - response.Message = "Nothing to do" - return c.JSON(response.StatusCode, response) - } - - newMembres, err := h.DataClient.InsertMembres(membres) - if err != nil { - response.StatusCode = http.StatusInternalServerError - response.Message = "Could not insert membres" - response.Error = err.Error() - return c.JSON(response.StatusCode, response) - } - - response.StatusCode = http.StatusCreated - response.Message = "Insert successful" - response.Data.MembresInserted = newMembres - return c.JSON(response.StatusCode, response) -} - -func (h *Handler) PostProgrammes(c echo.Context) error { - var response responses.PostProgrammesResponse - - var programmes []models.Programme - - switch c.Request().Header.Get("Content-Type") { - case "application/json": - if err := c.Bind(&programmes); err != nil { - response.StatusCode = http.StatusBadRequest - response.Message = "Could not bind programmes" - response.Error = err.Error() - return c.JSON(response.StatusCode, response) - } - case "text/csv": - body := c.Request().Body - if body == nil { - response.StatusCode = http.StatusBadRequest - response.Message = "Request body is empty" - 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 the CSV data from the request body using gocsv. - if err := gocsv.Unmarshal(body, &programmes); err != nil { - response.StatusCode = http.StatusBadRequest - response.Message = "Could not unmarshal into programmes" - response.Error = err.Error() - return c.JSON(response.StatusCode, response) - } - default: - response.StatusCode = http.StatusBadRequest - response.Message = "Invalid Content-Type" - return c.JSON(response.StatusCode, response) - } - - if len(programmes) == 0 { - response.StatusCode = http.StatusOK - response.Message = "Nothing to do" - return c.JSON(response.StatusCode, response) - } - - newProgrammes, err := h.DataClient.InsertProgrammes(programmes) - if err != nil { - response.StatusCode = http.StatusInternalServerError - response.Message = "Could not insert programmes" - response.Error = err.Error() - return c.JSON(response.StatusCode, response) - } - - response.StatusCode = http.StatusCreated - response.Message = "Insert successful" - response.Data.ProgrammesInserted = newProgrammes - return c.JSON(response.StatusCode, response) -} diff --git a/handlers/read.go b/handlers/read.go deleted file mode 100644 index 0a5b1fd..0000000 --- a/handlers/read.go +++ /dev/null @@ -1,61 +0,0 @@ -package handlers - -import ( - "fmt" - "net/http" - - "git.agecem.com/agecem/bottin/v6/responses" - "github.com/labstack/echo/v4" -) - -func (h *Handler) ReadMembre(c echo.Context) error { - membreID := c.Param("membre_id") - - membre, err := h.DataClient.GetMembre(membreID) - if err != nil { - if err.Error() == "No membre by that id was found" { - return c.JSON(http.StatusNotFound, map[string]string{ - "message": "Not Found", - }) - } - return c.JSON(http.StatusInternalServerError, map[string]string{ - "message": "Unknown error during GetMembre", - "error": err.Error(), - }) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "message": "Read successful", - "data": map[string]interface{}{ - "membre": &membre, - }, - }) -} - -func (h *Handler) ListMembres(c echo.Context) error { - var r responses.ListMembresResponse - - membres, err := h.DataClient.GetMembres() - if err != nil { - r.StatusCode = http.StatusInternalServerError - r.Error = err.Error() - r.Message = "Error during (*handlers.Handler).DataClient.GetMembres" - - return c.JSON(r.StatusCode, r) - } - - r.StatusCode = http.StatusOK - - switch membres := len(membres); membres { - case 0: - r.Message = "No membres returned from database" - case 1: - r.Message = "Membre returned from database" - default: - r.Message = fmt.Sprintf("%d membres returned from database", membres) - } - - r.Data.Membres = membres - - return c.JSON(r.StatusCode, r) -} diff --git a/handlers/seed.go b/handlers/seed.go deleted file mode 100644 index 27f5958..0000000 --- a/handlers/seed.go +++ /dev/null @@ -1,24 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/labstack/echo/v4" -) - -func (h *Handler) PostSeed(c echo.Context) error { - rows, err := h.DataClient.Seed() - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{ - "message": "Seed failed", - "error": err.Error(), - }) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "message": "Seed successful", - "data": map[string]interface{}{ - "rows": rows, - }, - }) -} diff --git a/handlers/update.go b/handlers/update.go deleted file mode 100644 index 8b4bd04..0000000 --- a/handlers/update.go +++ /dev/null @@ -1,42 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/labstack/echo/v4" -) - -func (h *Handler) PutMembrePreferedName(c echo.Context) error { - membreID := c.Param("membre_id") - - var newName string - - err := c.Bind(&newName) - if err != nil { - return c.JSON(http.StatusBadRequest, map[string]string{ - "message": "Could not bind newName", - "error": err.Error(), - }) - } - - rows, err := h.DataClient.UpdateMembreName(membreID, newName) - if err != nil { - return c.JSON(http.StatusInternalServerError, map[string]string{ - "message": "Could not update membre name", - "error": err.Error(), - }) - } - - if rows == 0 { - return c.JSON(http.StatusBadRequest, map[string]string{ - "message": "No update was done, probably no membre by that id", - }) - } - - return c.JSON(http.StatusOK, map[string]interface{}{ - "message": "Update successful", - "data": map[string]interface{}{ - "rows": rows, - }, - }) -} diff --git a/main.go b/main.go index f0b7d52..cb9123b 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,19 @@ package main -import "git.agecem.com/agecem/bottin/v6/cmd" +import ( + "context" + "fmt" + "io" + "log" + "os" +) func main() { - cmd.Execute() + if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { + log.Fatal(err) + } +} + +func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error { + return fmt.Errorf("not implemented") } diff --git a/responses/post.go b/responses.go similarity index 59% rename from responses/post.go rename to responses.go index 7f1b7b4..d8af880 100644 --- a/responses/post.go +++ b/responses.go @@ -1,7 +1,18 @@ -package responses +package main import "codeberg.org/vlbeaudoin/voki/v2" +type GetHealthResponse struct { + voki.ResponseWithError +} + +type ListMembresResponse struct { + voki.ResponseWithError + Data struct { + Membres []Membre + } +} + type PostMembresResponse struct { voki.ResponseWithError Data struct { diff --git a/responses/health.go b/responses/health.go deleted file mode 100644 index 59c6fe5..0000000 --- a/responses/health.go +++ /dev/null @@ -1,7 +0,0 @@ -package responses - -import "codeberg.org/vlbeaudoin/voki/v2" - -type GetHealthResponse struct { - voki.ResponseWithError -} diff --git a/responses/list.go b/responses/list.go deleted file mode 100644 index 414883b..0000000 --- a/responses/list.go +++ /dev/null @@ -1,13 +0,0 @@ -package responses - -import ( - "codeberg.org/vlbeaudoin/voki/v2" - "git.agecem.com/agecem/bottin/v6/models" -) - -type ListMembresResponse struct { - voki.ResponseWithError - Data struct { - Membres []models.Membre - } -} diff --git a/sql/schema.sql b/sql/schema.sql new file mode 100644 index 0000000..154b983 --- /dev/null +++ b/sql/schema.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS programmes ( + id TEXT PRIMARY KEY, + titre TEXT +); + +CREATE TABLE IF NOT EXISTS membres ( + id VARCHAR(7) PRIMARY KEY, + last_name TEXT, + first_name TEXT, + prefered_name TEXT, + programme_id TEXT REFERENCES programmes(id) +); diff --git a/web/templates/index.html b/templates/index.html similarity index 100% rename from web/templates/index.html rename to templates/index.html diff --git a/v4/LICENSE b/v4/LICENSE deleted file mode 100644 index b5cc18d..0000000 --- a/v4/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -MIT License - -Copyright (c) 2021-2023 AGECEM - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/v4/README.md b/v4/README.md deleted file mode 100644 index 4da14f8..0000000 --- a/v4/README.md +++ /dev/null @@ -1 +0,0 @@ -deprecated, see git.agecem.com/agecem/bottin or git.agecem.com/agecem/bottin/v5 diff --git a/v4/go.mod b/v4/go.mod deleted file mode 100644 index 2d01404..0000000 --- a/v4/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module git.agecem.com/agecem/bottin/v4 - -go 1.20 - -//retract ( -// v4.1.0 -// v4.0.3 -// v4.0.2 -// v4.0.1 -//) diff --git a/web/embed.go b/web/embed.go deleted file mode 100644 index 465e5ec..0000000 --- a/web/embed.go +++ /dev/null @@ -1,10 +0,0 @@ -package web - -import "embed" - -//go:embed templates/* -var templatesFS embed.FS - -func GetTemplates() embed.FS { - return templatesFS -} diff --git a/web/webhandlers/handlers.go b/web/webhandlers/handlers.go deleted file mode 100644 index 348e43a..0000000 --- a/web/webhandlers/handlers.go +++ /dev/null @@ -1,46 +0,0 @@ -package webhandlers - -import ( - "fmt" - "net/http" - - "git.agecem.com/agecem/bottin/v6/data" - "github.com/labstack/echo/v4" -) - -type Handler struct { - APIClient *data.ApiClient -} - -func (h *Handler) GetIndex(c echo.Context) error { - return c.Render(http.StatusOK, "index-html", nil) -} - -func (h *Handler) GetMembre(c echo.Context) error { - - membreID := c.QueryParam("membre_id") - - membre, err := h.APIClient.GetMembre(membreID) - if err != nil { - return c.Render(http.StatusBadRequest, "index-html", struct { - Result string - }{ - Result: fmt.Sprintln("👎", err.Error()), - }) - } - - membreResult := fmt.Sprintf(`👍 - Membre trouvéE: [%s]`, membre.ID) - - if membre.PreferedName != "" { - membreResult = fmt.Sprintf("%s -> %s", membreResult, membre.PreferedName) - } else { - membreResult = fmt.Sprintf("%s -> %s, %s", membreResult, membre.LastName, membre.FirstName) - } - - return c.Render(http.StatusOK, "index-html", struct { - Result string - }{ - Result: membreResult, - }) -} From b67955ab283bc119aa64aea82ddb460273de65cd Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 6 Jun 2024 16:28:14 -0400 Subject: [PATCH 02/33] wip: merge cmd package into main package --- Dockerfile | 2 +- cmd.go | 314 ++++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/api.go | 165 --------------------------- cmd/root.go | 59 ---------- cmd/web.go | 183 ------------------------------ db_test.go | 9 +- main.go | 14 ++- 7 files changed, 330 insertions(+), 416 deletions(-) create mode 100644 cmd.go delete mode 100644 cmd/api.go delete mode 100644 cmd/root.go delete mode 100644 cmd/web.go diff --git a/Dockerfile b/Dockerfile index 7c94ff6..361fe34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,8 @@ WORKDIR /go/src/app COPY go.mod go.sum db.go entity.go main.go responses.go ./ ADD cmd/ cmd/ -ADD templates/ templates/ ADD sql/ sql/ +ADD templates/ templates/ RUN CGO_ENABLED=0 go build -a -o bottin . diff --git a/cmd.go b/cmd.go new file mode 100644 index 0000000..413e1a2 --- /dev/null +++ b/cmd.go @@ -0,0 +1,314 @@ +package main + +import ( + "crypto/subtle" + "embed" + "fmt" + "html/template" + "io" + "log" + "os" + "strings" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var ( + apiPort int + apiKey string +) + +// apiCmd represents the api command +var apiCmd = &cobra.Command{ + Use: "api", + Short: "Démarrer le serveur API", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + apiKey = viper.GetString(ViperAPIKey) + apiPort = viper.GetInt(ViperAPIPort) + + e := echo.New() + + // Middlewares + + e.Pre(middleware.AddTrailingSlash()) + + if apiKey != "" { + e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { + return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil + })) + } + + // DataClient + /* + client, err := data.NewDataClientFromViper() + if err != nil { + log.Fatalf("Could not establish database connection.\n Error: %s\n", err) + } + defer client.DB.Close() + + err = client.DB.Ping() + if err != nil { + log.Fatalf("Database was supposed to be ready but Ping() failed.\n Error: %s\n", err) + } + + _, err = client.Seed() + if err != nil { + log.Fatalf("Error during client.Seed(): %s", err) + } + */ + + // Routes + /* + h := handlers.New(client) + + e.GET("/v7/health/", h.GetHealth) + + e.POST("/v7/membres/", h.PostMembres) + + e.GET("/v7/membres/", h.ListMembres) + + e.GET("/v7/membres/:membre_id/", h.ReadMembre) + + e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) + + e.POST("/v7/programmes/", h.PostProgrammes) + + e.POST("/v7/seed/", h.PostSeed) + */ + + // Execution + + e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort))) + }, +} + +func init() { + rootCmd.AddCommand(apiCmd) + + // api.key + apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey) + if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil { + log.Fatal(err) + } + + // api.port + apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort) + if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil { + log.Fatal(err) + } + + // db.database + apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase) + if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil { + log.Fatal(err) + } + + // db.host + apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost) + if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil { + log.Fatal(err) + } + + // db.password + apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword) + if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil { + log.Fatal(err) + } + + // db.port + apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort) + if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil { + log.Fatal(err) + } + + // db.user + apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser) + if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil { + log.Fatal(err) + } +} + +var cfgFile string + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "bottin", + Short: "Bottin étudiant de l'AGECEM", +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)") +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".bottin" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".bottin") + } + + viper.SetEnvPrefix("BOTTIN") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } +} + +var ( + webUser string + webPassword string + webPort int + webApiHost string + webApiKey string + webApiPort int + webApiProtocol string +) + +//go:embed templates/* +var templatesFS embed.FS + +type Template struct { + templates *template.Template +} + +func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} + +// webCmd represents the web command +var webCmd = &cobra.Command{ + Use: "web", + Short: "Démarrer le client web", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + webApiHost = viper.GetString(viperWebAPIHost) + webApiKey = viper.GetString(viperWebAPIKey) + webApiPort = viper.GetInt(viperWebAPIPort) + webApiProtocol = viper.GetString(viperWebAPIProtocol) + webPassword = viper.GetString(viperWebPassword) + webPort = viper.GetInt(viperWebPort) + webUser = viper.GetString(viperWebUser) + + // Ping API server + /* + client := http.DefaultClient + defer client.CloseIdleConnections() + + apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort) + + pingResult, err := apiClient.GetHealth() + if err != nil { + log.Fatal(err) + } + + log.Println(pingResult) + */ + + e := echo.New() + + // Middlewares + + e.Pre(middleware.AddTrailingSlash()) + + e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { + usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(webUser)) == 1 + passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(webPassword)) == 1 + return usersMatch && passwordsMatch, nil + })) + + // Template + + t := &Template{ + templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")), + } + + e.Renderer = t + + // Routes + /* + handler := webhandlers.Handler{APIClient: apiClient} + + e.GET("/", handler.GetIndex) + e.GET("/membre/", handler.GetMembre) + */ + + // Execution + + e.Logger.Fatal(e.Start( + fmt.Sprintf(":%d", webPort))) + }, +} + +func init() { + rootCmd.AddCommand(webCmd) + //templatesFS = web.GetTemplates() + + // web.api.host + webCmd.Flags().String(flagWebAPIHost, defaultWebAPIHost, descriptionWebAPIHost) + if err := viper.BindPFlag(viperWebAPIHost, webCmd.Flags().Lookup(flagWebAPIHost)); err != nil { + log.Fatal(err) + } + + // web.api.key + webCmd.Flags().String(flagWebAPIKey, defaultWebAPIKey, descriptionWebAPIKey) + if err := viper.BindPFlag(viperWebAPIKey, webCmd.Flags().Lookup(flagWebAPIKey)); err != nil { + log.Fatal(err) + } + + // web.api.protocol + webCmd.Flags().String(flagWebAPIProtocol, defaultWebAPIProtocol, descriptionWebAPIProtocol) + if err := viper.BindPFlag(viperWebAPIProtocol, webCmd.Flags().Lookup(flagWebAPIProtocol)); err != nil { + log.Fatal(err) + } + + // web.api.port + webCmd.Flags().Int(flagWebAPIPort, defaultWebAPIPort, descriptionWebAPIPort) + if err := viper.BindPFlag(viperWebAPIPort, webCmd.Flags().Lookup(flagWebAPIPort)); err != nil { + log.Fatal(err) + } + + // web.password + webCmd.Flags().String(flagWebPassword, defaultWebPassword, descriptionWebPassword) + if err := viper.BindPFlag(viperWebPassword, webCmd.Flags().Lookup(flagWebPassword)); err != nil { + log.Fatal(err) + } + + // web.port + webCmd.Flags().Int(flagWebPort, defaultWebPort, descriptionWebPort) + if err := viper.BindPFlag(viperWebPort, webCmd.Flags().Lookup(flagWebPort)); err != nil { + log.Fatal(err) + } + + // web.user + webCmd.Flags().String(flagWebUser, defaultWebUser, descriptionWebUser) + if err := viper.BindPFlag(viperWebUser, webCmd.Flags().Lookup(flagWebUser)); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/api.go b/cmd/api.go deleted file mode 100644 index 1e9f6d8..0000000 --- a/cmd/api.go +++ /dev/null @@ -1,165 +0,0 @@ -package cmd - -import ( - "crypto/subtle" - "fmt" - "log" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var ( - apiPort int - apiKey string -) - -const ( - ViperAPIPort string = "api.port" - FlagAPIPort string = "api-port" - DefaultAPIPort int = 1312 - DescriptionAPIPort string = "API server port" - - ViperAPIKey string = "api.key" - FlagAPIKey string = "api-key" - DefaultAPIKey string = "bottin" - DescriptionAPIKey string = "API server key. Leave empty for no key auth (not recommended)" - - ViperDBDatabase string = "db.database" - FlagDBDatabase string = "db-database" - DefaultDBDatabase string = "bottin" - DescriptionDBDatabase string = "Postgres database" - - ViperDBHost string = "db.host" - FlagDBHost string = "db-host" - DefaultDBHost string = "db" - DescriptionDBHost string = "Postgres host" - - ViperDBPassword string = "db.password" - FlagDBPassword string = "db-password" - DefaultDBPassword string = "bottin" - DescriptionDBPassword string = "Postgres password" - - ViperDBPort string = "db.port" - FlagDBPort string = "db-port" - DefaultDBPort int = 5432 - DescriptionDBPort string = "Postgres port" - - ViperDBUser string = "db.user" - FlagDBUser string = "db-user" - DefaultDBUser string = "bottin" - DescriptionDBUser string = "Postgres user" -) - -// apiCmd represents the api command -var apiCmd = &cobra.Command{ - Use: "api", - Short: "Démarrer le serveur API", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - apiKey = viper.GetString(ViperAPIKey) - apiPort = viper.GetInt(ViperAPIPort) - - e := echo.New() - - // Middlewares - - e.Pre(middleware.AddTrailingSlash()) - - if apiKey != "" { - e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { - return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil - })) - } - - // DataClient - /* - client, err := data.NewDataClientFromViper() - if err != nil { - log.Fatalf("Could not establish database connection.\n Error: %s\n", err) - } - defer client.DB.Close() - - err = client.DB.Ping() - if err != nil { - log.Fatalf("Database was supposed to be ready but Ping() failed.\n Error: %s\n", err) - } - - _, err = client.Seed() - if err != nil { - log.Fatalf("Error during client.Seed(): %s", err) - } - */ - - // Routes - /* - h := handlers.New(client) - - e.GET("/v7/health/", h.GetHealth) - - e.POST("/v7/membres/", h.PostMembres) - - e.GET("/v7/membres/", h.ListMembres) - - e.GET("/v7/membres/:membre_id/", h.ReadMembre) - - e.PUT("/v7/membres/:membre_id/prefered_name/", h.PutMembrePreferedName) - - e.POST("/v7/programmes/", h.PostProgrammes) - - e.POST("/v7/seed/", h.PostSeed) - */ - - // Execution - - e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort))) - }, -} - -func init() { - rootCmd.AddCommand(apiCmd) - - // api.key - apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey) - if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil { - log.Fatal(err) - } - - // api.port - apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort) - if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil { - log.Fatal(err) - } - - // db.database - apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase) - if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil { - log.Fatal(err) - } - - // db.host - apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost) - if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil { - log.Fatal(err) - } - - // db.password - apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword) - if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil { - log.Fatal(err) - } - - // db.port - apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort) - if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil { - log.Fatal(err) - } - - // db.user - apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser) - if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil { - log.Fatal(err) - } -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 26e0636..0000000 --- a/cmd/root.go +++ /dev/null @@ -1,59 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var cfgFile string - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "bottin", - Short: "Bottin étudiant de l'AGECEM", -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - cobra.OnInitialize(initConfig) - - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)") -} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := os.UserHomeDir() - cobra.CheckErr(err) - - // Search config in home directory with name ".bottin" (without extension). - viper.AddConfigPath(home) - viper.SetConfigType("yaml") - viper.SetConfigName(".bottin") - } - - viper.SetEnvPrefix("BOTTIN") - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.AutomaticEnv() // read in environment variables that match - - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) - } -} diff --git a/cmd/web.go b/cmd/web.go deleted file mode 100644 index 2ce2fad..0000000 --- a/cmd/web.go +++ /dev/null @@ -1,183 +0,0 @@ -package cmd - -import ( - "crypto/subtle" - "embed" - "fmt" - "html/template" - "io" - "log" - - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -var ( - webUser string - webPassword string - webPort int - webApiHost string - webApiKey string - webApiPort int - webApiProtocol string -) - -const ( - viperWebUser string = "web.user" - flagWebUser string = "web-user" - defaultWebUser string = "bottin" - descriptionWebUser string = "Web client basic auth user" - - viperWebPassword string = "web.password" - flagWebPassword string = "web-password" - defaultWebPassword string = "bottin" - descriptionWebPassword string = "Web client basic auth password" - - viperWebPort string = "web.port" - flagWebPort string = "web-port" - defaultWebPort int = 2312 - descriptionWebPort string = "Web client port" - - viperWebAPIHost string = "api.host" - flagWebAPIHost string = "api-host" - defaultWebAPIHost string = "api" - descriptionWebAPIHost string = "Target API server host" - - viperWebAPIKey string = "api.key" - flagWebAPIKey string = "api-key" - defaultWebAPIKey string = "bottin" - descriptionWebAPIKey string = "Target API server key" - - viperWebAPIPort string = "api.port" - flagWebAPIPort string = "api-port" - defaultWebAPIPort int = 1312 - descriptionWebAPIPort string = "Target API server port" - - viperWebAPIProtocol string = "api.protocol" - flagWebAPIProtocol string = "api-protocol" - defaultWebAPIProtocol string = "http" - descriptionWebAPIProtocol string = "Target API server protocol (http/https)" -) - -var templatesFS embed.FS - -type Template struct { - templates *template.Template -} - -func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { - return t.templates.ExecuteTemplate(w, name, data) -} - -// webCmd represents the web command -var webCmd = &cobra.Command{ - Use: "web", - Short: "Démarrer le client web", - Args: cobra.ExactArgs(0), - Run: func(cmd *cobra.Command, args []string) { - webApiHost = viper.GetString(viperWebAPIHost) - webApiKey = viper.GetString(viperWebAPIKey) - webApiPort = viper.GetInt(viperWebAPIPort) - webApiProtocol = viper.GetString(viperWebAPIProtocol) - webPassword = viper.GetString(viperWebPassword) - webPort = viper.GetInt(viperWebPort) - webUser = viper.GetString(viperWebUser) - - // Ping API server - /* - client := http.DefaultClient - defer client.CloseIdleConnections() - - apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort) - - pingResult, err := apiClient.GetHealth() - if err != nil { - log.Fatal(err) - } - - log.Println(pingResult) - */ - - e := echo.New() - - // Middlewares - - e.Pre(middleware.AddTrailingSlash()) - - e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { - usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(webUser)) == 1 - passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(webPassword)) == 1 - return usersMatch && passwordsMatch, nil - })) - - // Template - - t := &Template{ - templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")), - } - - e.Renderer = t - - // Routes - /* - handler := webhandlers.Handler{APIClient: apiClient} - - e.GET("/", handler.GetIndex) - e.GET("/membre/", handler.GetMembre) - */ - - // Execution - - e.Logger.Fatal(e.Start( - fmt.Sprintf(":%d", webPort))) - }, -} - -func init() { - rootCmd.AddCommand(webCmd) - //templatesFS = web.GetTemplates() - - // web.api.host - webCmd.Flags().String(flagWebAPIHost, defaultWebAPIHost, descriptionWebAPIHost) - if err := viper.BindPFlag(viperWebAPIHost, webCmd.Flags().Lookup(flagWebAPIHost)); err != nil { - log.Fatal(err) - } - - // web.api.key - webCmd.Flags().String(flagWebAPIKey, defaultWebAPIKey, descriptionWebAPIKey) - if err := viper.BindPFlag(viperWebAPIKey, webCmd.Flags().Lookup(flagWebAPIKey)); err != nil { - log.Fatal(err) - } - - // web.api.protocol - webCmd.Flags().String(flagWebAPIProtocol, defaultWebAPIProtocol, descriptionWebAPIProtocol) - if err := viper.BindPFlag(viperWebAPIProtocol, webCmd.Flags().Lookup(flagWebAPIProtocol)); err != nil { - log.Fatal(err) - } - - // web.api.port - webCmd.Flags().Int(flagWebAPIPort, defaultWebAPIPort, descriptionWebAPIPort) - if err := viper.BindPFlag(viperWebAPIPort, webCmd.Flags().Lookup(flagWebAPIPort)); err != nil { - log.Fatal(err) - } - - // web.password - webCmd.Flags().String(flagWebPassword, defaultWebPassword, descriptionWebPassword) - if err := viper.BindPFlag(viperWebPassword, webCmd.Flags().Lookup(flagWebPassword)); err != nil { - log.Fatal(err) - } - - // web.port - webCmd.Flags().Int(flagWebPort, defaultWebPort, descriptionWebPort) - if err := viper.BindPFlag(viperWebPort, webCmd.Flags().Lookup(flagWebPort)); err != nil { - log.Fatal(err) - } - - // web.user - webCmd.Flags().String(flagWebUser, defaultWebUser, descriptionWebUser) - if err := viper.BindPFlag(viperWebUser, webCmd.Flags().Lookup(flagWebUser)); err != nil { - log.Fatal(err) - } -} diff --git a/db_test.go b/db_test.go index d9d235a..0f508a5 100644 --- a/db_test.go +++ b/db_test.go @@ -8,6 +8,9 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) +// path to a file containing the db password +var passfile string + func TestDB(t *testing.T) { ctx := context.Background() @@ -17,11 +20,11 @@ func TestDB(t *testing.T) { fmt.Sprintf( "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", "bottin", + dbPassword, "bottin", - "bottin", - "localhost", + "postgres.agecem.com", 5432, - "prefer", //TODO change to "require" + "require", //TODO change to "require" )) if err != nil { t.Error(err) diff --git a/main.go b/main.go index cb9123b..58be335 100644 --- a/main.go +++ b/main.go @@ -4,14 +4,18 @@ import ( "context" "fmt" "io" - "log" - "os" ) func main() { - if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { - log.Fatal(err) - } + //TODO + /* + if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { + log.Fatal(err) + } + */ + + // Handle the command-line + Execute() } func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error { From 0123d9d37c8ceb356fbe059241b53d2812248203 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 6 Jun 2024 17:01:16 -0400 Subject: [PATCH 03/33] wip: integration between cmd.go and config.go --- cmd.go | 48 +++++++++++++++++-------------- config.go | 84 ++++++++++++++++++++++++++++++++++++------------------ db_test.go | 19 ++++++------ 3 files changed, 93 insertions(+), 58 deletions(-) diff --git a/cmd.go b/cmd.go index 413e1a2..8800c3d 100644 --- a/cmd.go +++ b/cmd.go @@ -107,6 +107,12 @@ func init() { log.Fatal(err) } + // db.sslmode + apiCmd.Flags().String(FlagDBSSLMode, DefaultDBSSLMode, DescriptionDBSSLMode) + if err := viper.BindPFlag(ViperDBSSLMode, apiCmd.Flags().Lookup(FlagDBSSLMode)); err != nil { + log.Fatal(err) + } + // db.host apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost) if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil { @@ -208,13 +214,13 @@ var webCmd = &cobra.Command{ Short: "Démarrer le client web", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - webApiHost = viper.GetString(viperWebAPIHost) - webApiKey = viper.GetString(viperWebAPIKey) - webApiPort = viper.GetInt(viperWebAPIPort) - webApiProtocol = viper.GetString(viperWebAPIProtocol) - webPassword = viper.GetString(viperWebPassword) - webPort = viper.GetInt(viperWebPort) - webUser = viper.GetString(viperWebUser) + webApiHost = viper.GetString(ViperWebAPIHost) + webApiKey = viper.GetString(ViperWebAPIKey) + webApiPort = viper.GetInt(ViperWebAPIPort) + webApiProtocol = viper.GetString(ViperWebAPIProtocol) + webPassword = viper.GetString(ViperWebPassword) + webPort = viper.GetInt(ViperWebPort) + webUser = viper.GetString(ViperWebUser) // Ping API server /* @@ -271,44 +277,44 @@ func init() { //templatesFS = web.GetTemplates() // web.api.host - webCmd.Flags().String(flagWebAPIHost, defaultWebAPIHost, descriptionWebAPIHost) - if err := viper.BindPFlag(viperWebAPIHost, webCmd.Flags().Lookup(flagWebAPIHost)); err != nil { + webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost) + if err := viper.BindPFlag(ViperWebAPIHost, webCmd.Flags().Lookup(FlagWebAPIHost)); err != nil { log.Fatal(err) } // web.api.key - webCmd.Flags().String(flagWebAPIKey, defaultWebAPIKey, descriptionWebAPIKey) - if err := viper.BindPFlag(viperWebAPIKey, webCmd.Flags().Lookup(flagWebAPIKey)); err != nil { + webCmd.Flags().String(FlagWebAPIKey, DefaultWebAPIKey, DescriptionWebAPIKey) + if err := viper.BindPFlag(ViperWebAPIKey, webCmd.Flags().Lookup(FlagWebAPIKey)); err != nil { log.Fatal(err) } // web.api.protocol - webCmd.Flags().String(flagWebAPIProtocol, defaultWebAPIProtocol, descriptionWebAPIProtocol) - if err := viper.BindPFlag(viperWebAPIProtocol, webCmd.Flags().Lookup(flagWebAPIProtocol)); err != nil { + webCmd.Flags().String(FlagWebAPIProtocol, DefaultWebAPIProtocol, DescriptionWebAPIProtocol) + if err := viper.BindPFlag(ViperWebAPIProtocol, webCmd.Flags().Lookup(FlagWebAPIProtocol)); err != nil { log.Fatal(err) } // web.api.port - webCmd.Flags().Int(flagWebAPIPort, defaultWebAPIPort, descriptionWebAPIPort) - if err := viper.BindPFlag(viperWebAPIPort, webCmd.Flags().Lookup(flagWebAPIPort)); err != nil { + webCmd.Flags().Int(FlagWebAPIPort, DefaultWebAPIPort, DescriptionWebAPIPort) + if err := viper.BindPFlag(ViperWebAPIPort, webCmd.Flags().Lookup(FlagWebAPIPort)); err != nil { log.Fatal(err) } // web.password - webCmd.Flags().String(flagWebPassword, defaultWebPassword, descriptionWebPassword) - if err := viper.BindPFlag(viperWebPassword, webCmd.Flags().Lookup(flagWebPassword)); err != nil { + webCmd.Flags().String(FlagWebPassword, DefaultWebPassword, DescriptionWebPassword) + if err := viper.BindPFlag(ViperWebPassword, webCmd.Flags().Lookup(FlagWebPassword)); err != nil { log.Fatal(err) } // web.port - webCmd.Flags().Int(flagWebPort, defaultWebPort, descriptionWebPort) - if err := viper.BindPFlag(viperWebPort, webCmd.Flags().Lookup(flagWebPort)); err != nil { + webCmd.Flags().Int(FlagWebPort, DefaultWebPort, DescriptionWebPort) + if err := viper.BindPFlag(ViperWebPort, webCmd.Flags().Lookup(FlagWebPort)); err != nil { log.Fatal(err) } // web.user - webCmd.Flags().String(flagWebUser, defaultWebUser, descriptionWebUser) - if err := viper.BindPFlag(viperWebUser, webCmd.Flags().Lookup(flagWebUser)); err != nil { + webCmd.Flags().String(FlagWebUser, DefaultWebUser, DescriptionWebUser) + if err := viper.BindPFlag(ViperWebUser, webCmd.Flags().Lookup(FlagWebUser)); err != nil { log.Fatal(err) } } diff --git a/config.go b/config.go index c0108b4..73d6007 100644 --- a/config.go +++ b/config.go @@ -1,5 +1,7 @@ package main +//TODO move flag declarations here + const ( ViperAPIPort string = "api.port" FlagAPIPort string = "api-port" @@ -16,6 +18,11 @@ const ( DefaultDBDatabase string = "bottin" DescriptionDBDatabase string = "Postgres database" + ViperDBSSLMode string = "db.sslmode" + FlagDBSSLMode string = "db-sslmode" + DefaultDBSSLMode string = "prefer" + DescriptionDBSSLMode string = "Postgres sslmode" + ViperDBHost string = "db.host" FlagDBHost string = "db-host" DefaultDBHost string = "db" @@ -36,40 +43,40 @@ const ( DefaultDBUser string = "bottin" DescriptionDBUser string = "Postgres user" - viperWebUser string = "web.user" - flagWebUser string = "web-user" - defaultWebUser string = "bottin" - descriptionWebUser string = "Web client basic auth user" + ViperWebUser string = "web.user" + FlagWebUser string = "web-user" + DefaultWebUser string = "bottin" + DescriptionWebUser string = "Web client basic auth user" - viperWebPassword string = "web.password" - flagWebPassword string = "web-password" - defaultWebPassword string = "bottin" - descriptionWebPassword string = "Web client basic auth password" + ViperWebPassword string = "web.password" + FlagWebPassword string = "web-password" + DefaultWebPassword string = "bottin" + DescriptionWebPassword string = "Web client basic auth password" - viperWebPort string = "web.port" - flagWebPort string = "web-port" - defaultWebPort int = 2312 - descriptionWebPort string = "Web client port" + ViperWebPort string = "web.port" + FlagWebPort string = "web-port" + DefaultWebPort int = 2312 + DescriptionWebPort string = "Web client port" - viperWebAPIHost string = "api.host" - flagWebAPIHost string = "api-host" - defaultWebAPIHost string = "api" - descriptionWebAPIHost string = "Target API server host" + ViperWebAPIHost string = "api.host" + FlagWebAPIHost string = "api-host" + DefaultWebAPIHost string = "api" + DescriptionWebAPIHost string = "Target API server host" - viperWebAPIKey string = "api.key" - flagWebAPIKey string = "api-key" - defaultWebAPIKey string = "bottin" - descriptionWebAPIKey string = "Target API server key" + ViperWebAPIKey string = "api.key" + FlagWebAPIKey string = "api-key" + DefaultWebAPIKey string = "bottin" + DescriptionWebAPIKey string = "Target API server key" - viperWebAPIPort string = "api.port" - flagWebAPIPort string = "api-port" - defaultWebAPIPort int = 1312 - descriptionWebAPIPort string = "Target API server port" + ViperWebAPIPort string = "api.port" + FlagWebAPIPort string = "api-port" + DefaultWebAPIPort int = 1312 + DescriptionWebAPIPort string = "Target API server port" - viperWebAPIProtocol string = "api.protocol" - flagWebAPIProtocol string = "api-protocol" - defaultWebAPIProtocol string = "http" - descriptionWebAPIProtocol string = "Target API server protocol (http/https)" + ViperWebAPIProtocol string = "api.protocol" + FlagWebAPIProtocol string = "api-protocol" + DefaultWebAPIProtocol string = "http" + DescriptionWebAPIProtocol string = "Target API server protocol (http/https)" ) type Config struct { @@ -97,3 +104,24 @@ type Config struct { } `yaml:"api"` } `yaml:"web"` } + +// DefaultConfig returns a Config filled with the default values from the +// `Default*` constants defined in this file. +func DefaultConfig() (cfg Config) { + cfg.API.Port = DefaultAPIPort + cfg.API.Key = DefaultAPIKey + cfg.DB.Database = DefaultDBDatabase + cfg.DB.Host = DefaultDBHost + cfg.DB.SSLMode = DefaultDBSSLMode + cfg.DB.Password = DefaultDBPassword + cfg.DB.Port = DefaultDBPort + cfg.DB.User = DefaultDBUser + cfg.Web.User = DefaultWebUser + cfg.Web.Password = DefaultWebPassword + cfg.Web.Port = DefaultWebPort + cfg.Web.API.Host = DefaultWebAPIHost + cfg.Web.API.Key = DefaultWebAPIKey + cfg.Web.API.Port = DefaultWebAPIPort + cfg.Web.API.Protocol = DefaultWebAPIProtocol + return +} diff --git a/db_test.go b/db_test.go index 0f508a5..4c3881a 100644 --- a/db_test.go +++ b/db_test.go @@ -6,12 +6,13 @@ import ( "testing" "github.com/jackc/pgx/v5/pgxpool" + "github.com/spf13/viper" ) -// path to a file containing the db password -var passfile string - func TestDB(t *testing.T) { + cfg := DefaultConfig() + cfg.DB.Password = viper.GetString(ViperDBPassword) + ctx := context.Background() //prep @@ -19,12 +20,12 @@ func TestDB(t *testing.T) { ctx, fmt.Sprintf( "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", - "bottin", - dbPassword, - "bottin", - "postgres.agecem.com", - 5432, - "require", //TODO change to "require" + cfg.DB.User, + cfg.DB.Password, + cfg.DB.Database, + cfg.DB.Host, + cfg.DB.Port, + cfg.DB.SSLMode, )) if err != nil { t.Error(err) From cdd526a6f3e17cbd1fd366ce9cb95ecfd40202ec Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 6 Jun 2024 17:59:58 -0400 Subject: [PATCH 04/33] wip: make apiCmd run and remove db test --- client.go | 1 + client_test.go | 1 + cmd.go | 90 ++++++++++++++++++++++++-------------------------- db_test.go | 49 --------------------------- 4 files changed, 46 insertions(+), 95 deletions(-) create mode 100644 client.go create mode 100644 client_test.go delete mode 100644 db_test.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/client.go @@ -0,0 +1 @@ +package main diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..0fee6f5 --- /dev/null +++ b/client_test.go @@ -0,0 +1 @@ +package main_test diff --git a/cmd.go b/cmd.go index 8800c3d..3e37028 100644 --- a/cmd.go +++ b/cmd.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/subtle" "embed" "fmt" @@ -10,25 +11,23 @@ import ( "os" "strings" + "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var ( - apiPort int - apiKey string -) - // apiCmd represents the api command var apiCmd = &cobra.Command{ Use: "api", Short: "Démarrer le serveur API", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - apiKey = viper.GetString(ViperAPIKey) - apiPort = viper.GetInt(ViperAPIPort) + var cfg Config + if err := viper.Unmarshal(&cfg); err != nil { + log.Fatal("parse config:", err) + } e := echo.New() @@ -36,30 +35,43 @@ var apiCmd = &cobra.Command{ e.Pre(middleware.AddTrailingSlash()) - if apiKey != "" { + if cfg.API.Key != "" { e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) { - return subtle.ConstantTimeCompare([]byte(key), []byte(apiKey)) == 1, nil + return subtle.ConstantTimeCompare([]byte(key), []byte(cfg.API.Key)) == 1, nil })) } // DataClient - /* - client, err := data.NewDataClientFromViper() - if err != nil { - log.Fatalf("Could not establish database connection.\n Error: %s\n", err) - } - defer client.DB.Close() + ctx := context.Background() - err = client.DB.Ping() - if err != nil { - log.Fatalf("Database was supposed to be ready but Ping() failed.\n Error: %s\n", err) - } + //prep + pool, err := pgxpool.New( + ctx, + fmt.Sprintf( + "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", + cfg.DB.User, + cfg.DB.Password, + cfg.DB.Database, + cfg.DB.Host, + cfg.DB.Port, + cfg.DB.SSLMode, + )) + if err != nil { + log.Fatal("init pgx pool:", err) + } + defer pool.Close() - _, err = client.Seed() - if err != nil { - log.Fatalf("Error during client.Seed(): %s", err) - } - */ + db := &PostgresClient{ + Ctx: ctx, + Pool: pool, + } + if err := db.Pool.Ping(ctx); err != nil { + log.Fatal("ping db:", err) + } + + if err := db.CreateOrReplaceSchema(); err != nil { + log.Fatal("create or replace schema:", err) + } // Routes /* @@ -82,7 +94,7 @@ var apiCmd = &cobra.Command{ // Execution - e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", apiPort))) + e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.API.Port))) }, } @@ -187,16 +199,6 @@ func initConfig() { } } -var ( - webUser string - webPassword string - webPort int - webApiHost string - webApiKey string - webApiPort int - webApiProtocol string -) - //go:embed templates/* var templatesFS embed.FS @@ -214,13 +216,10 @@ var webCmd = &cobra.Command{ Short: "Démarrer le client web", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { - webApiHost = viper.GetString(ViperWebAPIHost) - webApiKey = viper.GetString(ViperWebAPIKey) - webApiPort = viper.GetInt(ViperWebAPIPort) - webApiProtocol = viper.GetString(ViperWebAPIProtocol) - webPassword = viper.GetString(ViperWebPassword) - webPort = viper.GetInt(ViperWebPort) - webUser = viper.GetString(ViperWebUser) + var cfg Config + if err := viper.Unmarshal(&cfg); err != nil { + log.Fatal("init config:", err) + } // Ping API server /* @@ -244,8 +243,8 @@ var webCmd = &cobra.Command{ e.Pre(middleware.AddTrailingSlash()) e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { - usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(webUser)) == 1 - passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(webPassword)) == 1 + usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Web.User)) == 1 + passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Web.Password)) == 1 return usersMatch && passwordsMatch, nil })) @@ -268,13 +267,12 @@ var webCmd = &cobra.Command{ // Execution e.Logger.Fatal(e.Start( - fmt.Sprintf(":%d", webPort))) + fmt.Sprintf(":%d", cfg.Web.Port))) }, } func init() { rootCmd.AddCommand(webCmd) - //templatesFS = web.GetTemplates() // web.api.host webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost) diff --git a/db_test.go b/db_test.go deleted file mode 100644 index 4c3881a..0000000 --- a/db_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "context" - "fmt" - "testing" - - "github.com/jackc/pgx/v5/pgxpool" - "github.com/spf13/viper" -) - -func TestDB(t *testing.T) { - cfg := DefaultConfig() - cfg.DB.Password = viper.GetString(ViperDBPassword) - - ctx := context.Background() - - //prep - pool, err := pgxpool.New( - ctx, - fmt.Sprintf( - "user=%s password=%s database=%s host=%s port=%d sslmode=%s ", - cfg.DB.User, - cfg.DB.Password, - cfg.DB.Database, - cfg.DB.Host, - cfg.DB.Port, - cfg.DB.SSLMode, - )) - if err != nil { - t.Error(err) - return - } - defer pool.Close() - - db := &PostgresClient{ - Ctx: ctx, - Pool: pool, - } - - //exec - - t.Run("create or replace schema", - func(t *testing.T) { - if err := db.CreateOrReplaceSchema(); err != nil { - t.Error(err) - } - }) -} From 780d493dc1e5ac431239771a7b7f92f9e115cfa7 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 6 Jun 2024 18:07:30 -0400 Subject: [PATCH 05/33] split cmd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmd.go contient maintenant juste les actual commandes. Les fonctionalités liées à la configuration sont dans config.go, et les fonctionalités liées au templating est dans template.go. --- cmd.go | 176 +++++----------------------------------------------- config.go | 143 +++++++++++++++++++++++++++++++++++++++++- main.go | 21 +++---- template.go | 20 ++++++ 4 files changed, 185 insertions(+), 175 deletions(-) create mode 100644 template.go diff --git a/cmd.go b/cmd.go index 3e37028..859f49a 100644 --- a/cmd.go +++ b/cmd.go @@ -3,13 +3,10 @@ package main import ( "context" "crypto/subtle" - "embed" "fmt" "html/template" - "io" "log" "os" - "strings" "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" @@ -18,6 +15,21 @@ import ( "github.com/spf13/viper" ) +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "bottin", + Short: "Bottin étudiant de l'AGECEM", +} + +// execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + // apiCmd represents the api command var apiCmd = &cobra.Command{ Use: "api", @@ -98,118 +110,6 @@ var apiCmd = &cobra.Command{ }, } -func init() { - rootCmd.AddCommand(apiCmd) - - // api.key - apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey) - if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil { - log.Fatal(err) - } - - // api.port - apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort) - if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil { - log.Fatal(err) - } - - // db.database - apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase) - if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil { - log.Fatal(err) - } - - // db.sslmode - apiCmd.Flags().String(FlagDBSSLMode, DefaultDBSSLMode, DescriptionDBSSLMode) - if err := viper.BindPFlag(ViperDBSSLMode, apiCmd.Flags().Lookup(FlagDBSSLMode)); err != nil { - log.Fatal(err) - } - - // db.host - apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost) - if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil { - log.Fatal(err) - } - - // db.password - apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword) - if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil { - log.Fatal(err) - } - - // db.port - apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort) - if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil { - log.Fatal(err) - } - - // db.user - apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser) - if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil { - log.Fatal(err) - } -} - -var cfgFile string - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "bottin", - Short: "Bottin étudiant de l'AGECEM", -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - -func init() { - cobra.OnInitialize(initConfig) - - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)") -} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := os.UserHomeDir() - cobra.CheckErr(err) - - // Search config in home directory with name ".bottin" (without extension). - viper.AddConfigPath(home) - viper.SetConfigType("yaml") - viper.SetConfigName(".bottin") - } - - viper.SetEnvPrefix("BOTTIN") - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.AutomaticEnv() // read in environment variables that match - - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) - } -} - -//go:embed templates/* -var templatesFS embed.FS - -type Template struct { - templates *template.Template -} - -func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { - return t.templates.ExecuteTemplate(w, name, data) -} - // webCmd represents the web command var webCmd = &cobra.Command{ Use: "web", @@ -270,49 +170,3 @@ var webCmd = &cobra.Command{ fmt.Sprintf(":%d", cfg.Web.Port))) }, } - -func init() { - rootCmd.AddCommand(webCmd) - - // web.api.host - webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost) - if err := viper.BindPFlag(ViperWebAPIHost, webCmd.Flags().Lookup(FlagWebAPIHost)); err != nil { - log.Fatal(err) - } - - // web.api.key - webCmd.Flags().String(FlagWebAPIKey, DefaultWebAPIKey, DescriptionWebAPIKey) - if err := viper.BindPFlag(ViperWebAPIKey, webCmd.Flags().Lookup(FlagWebAPIKey)); err != nil { - log.Fatal(err) - } - - // web.api.protocol - webCmd.Flags().String(FlagWebAPIProtocol, DefaultWebAPIProtocol, DescriptionWebAPIProtocol) - if err := viper.BindPFlag(ViperWebAPIProtocol, webCmd.Flags().Lookup(FlagWebAPIProtocol)); err != nil { - log.Fatal(err) - } - - // web.api.port - webCmd.Flags().Int(FlagWebAPIPort, DefaultWebAPIPort, DescriptionWebAPIPort) - if err := viper.BindPFlag(ViperWebAPIPort, webCmd.Flags().Lookup(FlagWebAPIPort)); err != nil { - log.Fatal(err) - } - - // web.password - webCmd.Flags().String(FlagWebPassword, DefaultWebPassword, DescriptionWebPassword) - if err := viper.BindPFlag(ViperWebPassword, webCmd.Flags().Lookup(FlagWebPassword)); err != nil { - log.Fatal(err) - } - - // web.port - webCmd.Flags().Int(FlagWebPort, DefaultWebPort, DescriptionWebPort) - if err := viper.BindPFlag(ViperWebPort, webCmd.Flags().Lookup(FlagWebPort)); err != nil { - log.Fatal(err) - } - - // web.user - webCmd.Flags().String(FlagWebUser, DefaultWebUser, DescriptionWebUser) - if err := viper.BindPFlag(ViperWebUser, webCmd.Flags().Lookup(FlagWebUser)); err != nil { - log.Fatal(err) - } -} diff --git a/config.go b/config.go index 73d6007..d50e241 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,14 @@ package main -//TODO move flag declarations here +import ( + "fmt" + "log" + "os" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) const ( ViperAPIPort string = "api.port" @@ -125,3 +133,136 @@ func DefaultConfig() (cfg Config) { cfg.Web.API.Protocol = DefaultWebAPIProtocol return } + +func init() { + // rootCmd + + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.bottin.yaml)") + + // apiCmd + + rootCmd.AddCommand(apiCmd) + + // api.key + apiCmd.Flags().String(FlagAPIKey, DefaultAPIKey, DescriptionAPIKey) + if err := viper.BindPFlag(ViperAPIKey, apiCmd.Flags().Lookup(FlagAPIKey)); err != nil { + log.Fatal(err) + } + + // api.port + apiCmd.Flags().Int(FlagAPIPort, DefaultAPIPort, DescriptionAPIPort) + if err := viper.BindPFlag(ViperAPIPort, apiCmd.Flags().Lookup(FlagAPIPort)); err != nil { + log.Fatal(err) + } + + // db.database + apiCmd.Flags().String(FlagDBDatabase, DefaultDBDatabase, DescriptionDBDatabase) + if err := viper.BindPFlag(ViperDBDatabase, apiCmd.Flags().Lookup(FlagDBDatabase)); err != nil { + log.Fatal(err) + } + + // db.sslmode + apiCmd.Flags().String(FlagDBSSLMode, DefaultDBSSLMode, DescriptionDBSSLMode) + if err := viper.BindPFlag(ViperDBSSLMode, apiCmd.Flags().Lookup(FlagDBSSLMode)); err != nil { + log.Fatal(err) + } + + // db.host + apiCmd.Flags().String(FlagDBHost, DefaultDBHost, DescriptionDBHost) + if err := viper.BindPFlag(ViperDBHost, apiCmd.Flags().Lookup(FlagDBHost)); err != nil { + log.Fatal(err) + } + + // db.password + apiCmd.Flags().String(FlagDBPassword, DefaultDBPassword, DescriptionDBPassword) + if err := viper.BindPFlag(ViperDBPassword, apiCmd.Flags().Lookup(FlagDBPassword)); err != nil { + log.Fatal(err) + } + + // db.port + apiCmd.Flags().Int(FlagDBPort, DefaultDBPort, DescriptionDBPort) + if err := viper.BindPFlag(ViperDBPort, apiCmd.Flags().Lookup(FlagDBPort)); err != nil { + log.Fatal(err) + } + + // db.user + apiCmd.Flags().String(FlagDBUser, DefaultDBUser, DescriptionDBUser) + if err := viper.BindPFlag(ViperDBUser, apiCmd.Flags().Lookup(FlagDBUser)); err != nil { + log.Fatal(err) + } + + // WebCmd + rootCmd.AddCommand(webCmd) + + // web.api.host + webCmd.Flags().String(FlagWebAPIHost, DefaultWebAPIHost, DescriptionWebAPIHost) + if err := viper.BindPFlag(ViperWebAPIHost, webCmd.Flags().Lookup(FlagWebAPIHost)); err != nil { + log.Fatal(err) + } + + // web.api.key + webCmd.Flags().String(FlagWebAPIKey, DefaultWebAPIKey, DescriptionWebAPIKey) + if err := viper.BindPFlag(ViperWebAPIKey, webCmd.Flags().Lookup(FlagWebAPIKey)); err != nil { + log.Fatal(err) + } + + // web.api.protocol + webCmd.Flags().String(FlagWebAPIProtocol, DefaultWebAPIProtocol, DescriptionWebAPIProtocol) + if err := viper.BindPFlag(ViperWebAPIProtocol, webCmd.Flags().Lookup(FlagWebAPIProtocol)); err != nil { + log.Fatal(err) + } + + // web.api.port + webCmd.Flags().Int(FlagWebAPIPort, DefaultWebAPIPort, DescriptionWebAPIPort) + if err := viper.BindPFlag(ViperWebAPIPort, webCmd.Flags().Lookup(FlagWebAPIPort)); err != nil { + log.Fatal(err) + } + + // web.password + webCmd.Flags().String(FlagWebPassword, DefaultWebPassword, DescriptionWebPassword) + if err := viper.BindPFlag(ViperWebPassword, webCmd.Flags().Lookup(FlagWebPassword)); err != nil { + log.Fatal(err) + } + + // web.port + webCmd.Flags().Int(FlagWebPort, DefaultWebPort, DescriptionWebPort) + if err := viper.BindPFlag(ViperWebPort, webCmd.Flags().Lookup(FlagWebPort)); err != nil { + log.Fatal(err) + } + + // web.user + webCmd.Flags().String(FlagWebUser, DefaultWebUser, DescriptionWebUser) + if err := viper.BindPFlag(ViperWebUser, webCmd.Flags().Lookup(FlagWebUser)); err != nil { + log.Fatal(err) + } +} + +var cfgFile string + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Find home directory. + home, err := os.UserHomeDir() + cobra.CheckErr(err) + + // Search config in home directory with name ".bottin" (without extension). + viper.AddConfigPath(home) + viper.SetConfigType("yaml") + viper.SetConfigName(".bottin") + } + + viper.SetEnvPrefix("BOTTIN") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() // read in environment variables that match + + // If a config file is found, read it in. + if err := viper.ReadInConfig(); err == nil { + fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed()) + } +} diff --git a/main.go b/main.go index 58be335..6f81738 100644 --- a/main.go +++ b/main.go @@ -1,23 +1,18 @@ package main -import ( - "context" - "fmt" - "io" -) - func main() { - //TODO - /* - if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { - log.Fatal(err) - } + /* TODO + if err := Run(context.Background(), Config{}, nil, os.Stdout); err != nil { + log.Fatal(err) + } */ - // Handle the command-line - Execute() + // Handle the command-line via cobra and viper + execute() } +/* TODO func Run(ctx context.Context, config Config, args []string, stdout io.Writer) error { return fmt.Errorf("not implemented") } +*/ diff --git a/template.go b/template.go new file mode 100644 index 0000000..a5b4980 --- /dev/null +++ b/template.go @@ -0,0 +1,20 @@ +package main + +import ( + "embed" + "html/template" + "io" + + "github.com/labstack/echo/v4" +) + +//go:embed templates/* +var templatesFS embed.FS + +type Template struct { + templates *template.Template +} + +func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} From 11251042808e44bfa463e8beb893234edd31db0f Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Fri, 7 Jun 2024 14:59:49 -0400 Subject: [PATCH 06/33] chores: `go get -u` --- go.mod | 24 ++++++++++++------------ go.sum | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 1952064..a7833f1 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module git.agecem.com/agecem/bottin/v7 go 1.22.0 require ( - codeberg.org/vlbeaudoin/voki/v2 v2.0.3 + codeberg.org/vlbeaudoin/voki/v2 v2.1.0 github.com/jackc/pgx/v5 v5.6.0 - github.com/labstack/echo/v4 v4.11.4 + github.com/labstack/echo/v4 v4.12.0 github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.18.2 + github.com/spf13/viper v1.19.0 ) require ( @@ -16,15 +16,15 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect @@ -34,12 +34,12 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.19.0 // indirect - golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect - golang.org/x/net v0.21.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 5c5e2b0..f022429 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ codeberg.org/vlbeaudoin/voki/v2 v2.0.3 h1:H3j7yk8uBiDK19OUWAKbYKmw0tsSw4t0LA5lyAfyT3E= codeberg.org/vlbeaudoin/voki/v2 v2.0.3/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U= +codeberg.org/vlbeaudoin/voki/v2 v2.1.0 h1:pXav77QGMHvMF1RyvkEwK3VKBdQh3ATmgh48TXX0tlU= +codeberg.org/vlbeaudoin/voki/v2 v2.1.0/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -21,6 +23,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= @@ -31,6 +35,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -44,6 +50,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -52,6 +60,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -66,15 +76,19 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -85,18 +99,30 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 1b04237c96f29cd9373631b07ec79e49e9b9edb4 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Fri, 7 Jun 2024 15:18:22 -0400 Subject: [PATCH 07/33] =?UTF-8?q?ajouter=20fichiers=20manquants=20=C3=A0?= =?UTF-8?q?=20Dockerfile=20build=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 361fe34..7c0ad26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,8 @@ LABEL author="vlbeaudoin" WORKDIR /go/src/app -COPY go.mod go.sum db.go entity.go main.go responses.go ./ +COPY go.mod go.sum cmd.go config.go db.go entity.go main.go responses.go template.go ./ -ADD cmd/ cmd/ ADD sql/ sql/ ADD templates/ templates/ From eca5ffa7fb061af5e320f397fc1cc1cfb71fd743 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 10 Jun 2024 17:25:01 -0400 Subject: [PATCH 08/33] feature(db): Ajouter InsertMembres, InsertProgrammes et GetMembres --- db.go | 244 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 120 insertions(+), 124 deletions(-) diff --git a/db.go b/db.go index fff8885..ff715d8 100644 --- a/db.go +++ b/db.go @@ -3,6 +3,7 @@ package main import ( "context" _ "embed" + "fmt" "github.com/jackc/pgx/v5/pgxpool" ) @@ -11,6 +12,7 @@ import ( var sqlSchema string type PostgresClient struct { + //TODO move context out of client Ctx context.Context Pool *pgxpool.Pool } @@ -20,142 +22,92 @@ func (db *PostgresClient) CreateOrReplaceSchema() error { return err } -/* -// DataClient is a postgres client based on sqlx -type DataClient struct { - PostgresConnection PostgresConnection - DB sqlx.DB -} - -type PostgresConnection struct { - User string - Password string - Database string - Host string - Port int - SSL bool -} - -func NewDataClientFromViper() (*DataClient, error) { - client, err := NewDataClient( - PostgresConnection{ - User: viper.GetString(cmd.ViperDBHost), - Password: viper.GetString(cmd.ViperDBPassword), - Host: viper.GetString(cmd.ViperDBHost), - Database: viper.GetString(cmd.ViperDBDatabase), - Port: viper.GetInt(cmd.ViperDBPort), - }) - - return client, err -} - -func NewDataClient(connection PostgresConnection) (*DataClient, error) { - client := &DataClient{PostgresConnection: connection} - - connectionString := fmt.Sprintf("postgres://%s:%s@%s:%d/%s", - client.PostgresConnection.User, - client.PostgresConnection.Password, - client.PostgresConnection.Host, - client.PostgresConnection.Port, - client.PostgresConnection.Database, - ) - - db, err := sqlx.Connect("pgx", connectionString) - if err != nil { - return nil, err - } - - client.DB = *db - - return client, nil -} - -func (d *DataClient) Seed() (int64, error) { - result, err := d.DB.Exec(sqlSchema) - if err != nil { - return 0, err - } - - rows, err := result.RowsAffected() - if err != nil { - return rows, err - } - - return rows, nil -} - // InsertMembres inserts a slice of Membre into a database, returning the amount inserted and any error encountered -func (d *DataClient) InsertMembres(membres []Membre) (int64, error) { - var rowsInserted int64 - tx, err := d.DB.Beginx() - if err != nil { - return rowsInserted, err - } - defer tx.Rollback() - - for _, membre := range membres { - if membre.ID == "" { - return 0, errors.New("Cannot insert membre with no membre_id") - } - result, err := tx.NamedExec("INSERT INTO membres (id, last_name, first_name, prefered_name, programme_id) VALUES (:id, :last_name, :first_name, :prefered_name, :programme_id) ON CONFLICT (id) DO NOTHING;", &membre) +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 } - rows, err := result.RowsAffected() - if err != nil { - return 0, err - } - - rowsInserted += rows + return inserted, err } - - err = tx.Commit() - if err != nil { - return rowsInserted, err - } - - return rowsInserted, nil } -func (d *DataClient) InsertProgrammes(programmes []Programme) (int64, error) { - var rowsInserted int64 - tx, err := d.DB.Beginx() - if err != nil { - return rowsInserted, err - } - defer tx.Rollback() - - for _, programme := range programmes { - if programme.ID == "" { - return 0, errors.New("Cannot insert programme with no programme_id") - } - - result, err := tx.NamedExec("INSERT INTO programmes (id, titre) VALUES (:id, :titre) ON CONFLICT DO NOTHING;", &programme) +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 0, err + 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, titre) +VALUES ($1, $2) ON CONFLICT DO NOTHING;`, + programme.ID, + programme.Titre) + if err != nil { + return 0, err + } + + inserted += result.RowsAffected() } - rows, err := result.RowsAffected() - if err != nil { - return 0, err + if err := tx.Commit(d.Ctx); err != nil { + return inserted, err } - rowsInserted += rows + return inserted, err } - - err = tx.Commit() - if err != nil { - return rowsInserted, err - } - - return rowsInserted, nil } -func (d *DataClient) GetMembre(membreID string) (Membre, error) { +/* +func (d *PostgresClient) GetMembre(membreID string) (Membre, error) { var membre Membre - rows, err := d.DB.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID) + rows, err := d.Pool.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID) if err != nil { return membre, err } @@ -173,9 +125,11 @@ func (d *DataClient) GetMembre(membreID string) (Membre, error) { return membre, nil } +*/ -func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) { - result, err := d.DB.Exec("UPDATE membres SET prefered_name = $1 WHERE id = $2;", newName, membreID) +/* +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 } @@ -187,8 +141,50 @@ func (d *DataClient) UpdateMembreName(membreID, newName string) (int64, error) { return rows, nil } - -func (d *DataClient) GetMembres() (membres []Membre, err error) { - return membres, d.DB.Select(&membres, "SELECT * FROM membres;") -} */ + +func (d *PostgresClient) GetMembres() (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 +LIMIT + 10000 +ORDER BY + membres.id;`) + 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 + } +} From be766f593db1f1af072d99f607248967756433f7 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 11 Jun 2024 17:28:20 -0400 Subject: [PATCH 09/33] ajouter API client et tester /api/health --- client.go | 25 +++++++++++++++++++++ client_test.go | 39 +++++++++++++++++++++++++++++++- cmd.go | 4 +++- go.mod | 3 ++- go.sum | 36 ++++++----------------------- request.go | 40 +++++++++++++++++++++++++++++++++ response.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ responses.go | 28 ----------------------- routes.go | 45 +++++++++++++++++++++++++++++++++++++ 9 files changed, 221 insertions(+), 60 deletions(-) create mode 100644 request.go create mode 100644 response.go delete mode 100644 responses.go create mode 100644 routes.go diff --git a/client.go b/client.go index 06ab7d0..918fbee 100644 --- a/client.go +++ b/client.go @@ -1 +1,26 @@ package main + +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 +} diff --git a/client_test.go b/client_test.go index 0fee6f5..2c66470 100644 --- a/client_test.go +++ b/client_test.go @@ -1 +1,38 @@ -package main_test +package main + +import ( + "net/http" + "testing" + + "codeberg.org/vlbeaudoin/voki/v3" + "github.com/spf13/viper" +) + +func TestAPI(t *testing.T) { + var cfg Config + if err := viper.Unmarshal(&cfg); err != nil { + t.Error(err) + return + } + + httpClient := http.DefaultClient + defer httpClient.CloseIdleConnections() + + vokiClient := voki.New(httpClient, "localhost", cfg.API.Key, cfg.API.Port, "http") + apiClient := APIClient{vokiClient} + + t.Run("get API health", func(t *testing.T) { + health, err := apiClient.GetHealth() + if err != nil { + t.Error(err) + } + + want := "ok" + got := health + + if want != got { + t.Errorf("want=%s got=%s", want, got) + } + }) + +} diff --git a/cmd.go b/cmd.go index 859f49a..4cd12fb 100644 --- a/cmd.go +++ b/cmd.go @@ -86,6 +86,9 @@ var apiCmd = &cobra.Command{ } // Routes + if err := addRoutes(e, db); err != nil { + log.Fatal("add routes:", err) + } /* h := handlers.New(client) @@ -105,7 +108,6 @@ var apiCmd = &cobra.Command{ */ // Execution - e.Logger.Fatal(e.Start(fmt.Sprintf(":%d", cfg.API.Port))) }, } diff --git a/go.mod b/go.mod index a7833f1..b6ce20d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module git.agecem.com/agecem/bottin/v7 go 1.22.0 require ( - codeberg.org/vlbeaudoin/voki/v2 v2.1.0 + codeberg.org/vlbeaudoin/pave/v2 v2.0.0 + codeberg.org/vlbeaudoin/voki/v3 v3.0.0 github.com/jackc/pgx/v5 v5.6.0 github.com/labstack/echo/v4 v4.12.0 github.com/spf13/cobra v1.8.0 diff --git a/go.sum b/go.sum index f022429..73ace91 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -codeberg.org/vlbeaudoin/voki/v2 v2.0.3 h1:H3j7yk8uBiDK19OUWAKbYKmw0tsSw4t0LA5lyAfyT3E= -codeberg.org/vlbeaudoin/voki/v2 v2.0.3/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U= -codeberg.org/vlbeaudoin/voki/v2 v2.1.0 h1:pXav77QGMHvMF1RyvkEwK3VKBdQh3ATmgh48TXX0tlU= -codeberg.org/vlbeaudoin/voki/v2 v2.1.0/go.mod h1:TVdOLAxB94EJkylt5dleJlTkBzuxau8Xwd4TANQIR7U= +codeberg.org/vlbeaudoin/pave/v2 v2.0.0 h1:hfB5KnqMMu17g5QBWgLvWOsqidrYaohRfu2LflmTrb0= +codeberg.org/vlbeaudoin/pave/v2 v2.0.0/go.mod h1:TsTfP6IA+3Ph33vLZigeJWS5vgBPgkW1tfs3zFPfycU= +codeberg.org/vlbeaudoin/voki/v3 v3.0.0 h1:XdF/UTe9YUNj3hYrAyEvdmIMDYLL8SkqTwPkqw1yJ2c= +codeberg.org/vlbeaudoin/voki/v3 v3.0.0/go.mod h1:+6LMXosAu2ijNKV04sMwkeujpH+cghZU1fydqj2y95g= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,16 +13,14 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= @@ -33,8 +31,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= -github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -48,8 +44,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -58,8 +52,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -74,8 +66,6 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -86,8 +76,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= @@ -97,30 +87,18 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= -golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= diff --git a/request.go b/request.go new file mode 100644 index 0000000..ecb4346 --- /dev/null +++ b/request.go @@ -0,0 +1,40 @@ +package main + +import ( + "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/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 +} diff --git a/response.go b/response.go new file mode 100644 index 0000000..55a700d --- /dev/null +++ b/response.go @@ -0,0 +1,61 @@ +package main + +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 MembresGETResponse struct { + APIResponse + Data MembresGETResponseData `json:"data"` +} + +type MembresGETResponseData struct { + Membres []Membre `json:"membres"` +} + +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"` +} diff --git a/responses.go b/responses.go deleted file mode 100644 index d8af880..0000000 --- a/responses.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import "codeberg.org/vlbeaudoin/voki/v2" - -type GetHealthResponse struct { - voki.ResponseWithError -} - -type ListMembresResponse struct { - voki.ResponseWithError - Data struct { - Membres []Membre - } -} - -type PostMembresResponse struct { - voki.ResponseWithError - Data struct { - MembresInserted int64 - } -} - -type PostProgrammesResponse struct { - voki.ResponseWithError - Data struct { - ProgrammesInserted int64 - } -} diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..ee07763 --- /dev/null +++ b/routes.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "net/http" + + "codeberg.org/vlbeaudoin/pave/v2" + "codeberg.org/vlbeaudoin/voki/v3" + "github.com/labstack/echo/v4" +) + +func addRoutes(e *echo.Echo, db *PostgresClient) error { + _ = db + + apiPath := "/api" + 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 + } + return nil +} From c5339bd45b76a0458e916368421bc36cd241e33d Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 17 Jun 2024 14:06:43 -0400 Subject: [PATCH 10/33] fix(Dockerfile): copier fichiers go manquants vers image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7c0ad26..6479581 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ LABEL author="vlbeaudoin" WORKDIR /go/src/app -COPY go.mod go.sum cmd.go config.go db.go entity.go main.go responses.go template.go ./ +COPY go.mod go.sum client.go client_test.go cmd.go config.go db.go entity.go main.go request.go response.go routes.go template.go ./ ADD sql/ sql/ ADD templates/ templates/ From e1bce94d18e78788ae3be1f3fd0061ef8eb6d103 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 17 Jun 2024 14:07:49 -0400 Subject: [PATCH 11/33] feature: add and test ProgrammesPOST --- client.go | 17 +++++++++++++++++ client_test.go | 23 +++++++++++++++++++++++ db.go | 2 +- request.go | 41 +++++++++++++++++++++++++++++++++++++++++ routes.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index 918fbee..dd7e1ea 100644 --- a/client.go +++ b/client.go @@ -24,3 +24,20 @@ func (c APIClient) GetHealth() (health string, err error) { 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 +} diff --git a/client_test.go b/client_test.go index 2c66470..1f902d2 100644 --- a/client_test.go +++ b/client_test.go @@ -8,6 +8,10 @@ import ( "github.com/spf13/viper" ) +func init() { + initConfig() +} + func TestAPI(t *testing.T) { var cfg Config if err := viper.Unmarshal(&cfg); err != nil { @@ -35,4 +39,23 @@ func TestAPI(t *testing.T) { } }) + //TODO create or replace schema + //TODO insert programmes + t.Run("insert programmes", + func(t *testing.T) { + programmes := []Programme{ + {"404.42", "Cool programme"}, + {"200.10", "Autre programme"}, + } + t.Log("programmes:", programmes) + _, err := apiClient.InsertProgrammes(programmes...) + if err != nil { + t.Error(err) + } + }) + //TODO insert membres + //TODO get membre + //TODO update membre prefered name + //TODO get membres + } diff --git a/db.go b/db.go index ff715d8..eda594c 100644 --- a/db.go +++ b/db.go @@ -66,7 +66,7 @@ ON CONFLICT (id) DO NOTHING;`, } } -func (d *PostgresClient) InsertProgrammes(programmes []Programme) (inserted int64, err error) { +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()) diff --git a/request.go b/request.go index ecb4346..3ff29d7 100644 --- a/request.go +++ b/request.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/json" "fmt" "net/http" @@ -38,3 +39,43 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons 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/programmes/", + &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 ee07763..6041358 100644 --- a/routes.go +++ b/routes.go @@ -41,5 +41,48 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { }); err != nil { return err } + + if err := pave.EchoRegister[ProgrammesPOSTRequest]( + apiGroup, + &p, + apiPath, + http.MethodPost, + "/programmes/", + "Get registered programmes", + "ProgrammesPOST", func(c echo.Context) error { + var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{} + + 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 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 + } return nil } From e847f693e057a2652ff019f5fc15d7cdacce9d6f Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 17 Jun 2024 17:25:53 -0400 Subject: [PATCH 12/33] rework: renommer champs dans entities et ajouter MembresPOST - ajouter et tester InsertMembres - ajouter sql/views.sql - ajouter view `membres_for_display` -> concat names ou prefered name - rendre plusieurs champs NOT NULL dans schema --- client.go | 17 +++++++++++++++++ client_test.go | 23 ++++++++++++++++++++++- cmd.go | 4 ++++ db.go | 14 +++++++++++--- entity.go | 4 ++-- request.go | 41 +++++++++++++++++++++++++++++++++++++++++ routes.go | 45 ++++++++++++++++++++++++++++++++++++++++++++- sql/schema.sql | 8 ++++---- sql/views.sql | 23 +++++++++++++++++++++++ 9 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 sql/views.sql 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 +); From c7c64674c771a49c58233c6d608d14355a508c0b Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 18 Jun 2024 19:44:20 -0400 Subject: [PATCH 13/33] rework: change api prefix to /api/v7/ - add and test GetMembre - add `IsMembreID(string) bool` function BREAKING: Rename routes to `/api/v7/...` scheme --- client.go | 17 +++++++++++ client_test.go | 81 +++++++++++++++++++++++++++++++++++++------------- db.go | 51 +++++++++++++++++++------------ entity.go | 16 ++++++++++ request.go | 47 +++++++++++++++++++++++++++-- routes.go | 45 ++++++++++++++++++++++++++-- 6 files changed, 212 insertions(+), 45 deletions(-) diff --git a/client.go b/client.go index 6221ce0..d47a723 100644 --- a/client.go +++ b/client.go @@ -58,3 +58,20 @@ func (c APIClient) InsertMembres(membres ...Membre) (amountInserted int64, err e 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 +} diff --git a/client_test.go b/client_test.go index 9d1212d..7a847ad 100644 --- a/client_test.go +++ b/client_test.go @@ -40,6 +40,7 @@ func TestAPI(t *testing.T) { }) //TODO create or replace schema + t.Run("insert programmes", func(t *testing.T) { programmes := []Programme{ @@ -52,31 +53,71 @@ func TestAPI(t *testing.T) { t.Error(err) } }) - //TODO insert membres + + testMembres := []Membre{ + { + ID: "0000000", + FirstName: "Test", + LastName: "User", + ProgrammeID: "404.42", + }, + { + ID: "1234567", + FirstName: "Deadname", + LastName: "User", + PreferedName: "User, Test-Name", + ProgrammeID: "200.10", + }, + } + 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...) + _, err := apiClient.InsertMembres(testMembres...) if err != nil { t.Error(err) } }) - //TODO get membre - //TODO update membre prefered name - //TODO get membres + t.Run("get membre", + func(t *testing.T) { + membre, err := apiClient.GetMembre(testMembres[0].ID) + if err != nil { + t.Error(err) + } + + want := testMembres[0].LastName + got := membre.LastName + + if want != got { + t.Errorf("want=%s got=%s", want, got) + } + }) + + t.Run("get invalid membre", + func(t *testing.T) { + _, err := apiClient.GetMembre("invalid") + if err == nil { + t.Error("`invalid` should not have been accepted as value to GetMembre, but did") + } + }) + + //TODO update membre prefered name + /* + t.Run("", + func(t *testing.T) { + if err := apiClient.UpdateMembrePreferedName(testMembres[0].ID, "User, Galaxy"); err != nil { + t.Error(err) + } + }) + */ + //TODO get membres + /* + t.Run("get membres, max 50", + func(t *testing.T) { + membres, err := apiClient.GetMembres(50) + if err != nil { + t.Error(err) + } + }) + */ } diff --git a/db.go b/db.go index c2394e4..bb401ae 100644 --- a/db.go +++ b/db.go @@ -111,29 +111,42 @@ VALUES ($1, $2) ON CONFLICT DO NOTHING;`, } } -/* -func (d *PostgresClient) GetMembre(membreID string) (Membre, error) { - var membre Membre - - rows, err := d.Pool.Queryx("SELECT * FROM membres WHERE id = $1 LIMIT 1;", membreID) - if err != nil { - return membre, err - } - - for rows.Next() { - err := rows.StructScan(&membre) - if err != nil { - return membre, 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("No membre by that id was found") - } + if membre.ID == "" { + return membre, fmt.Errorf("Aucun membre trouvé avec numéro '%s'", membre.ID) + } - return membre, nil + return membre, nil + } } -*/ /* func (d *PostgresClient) UpdateMembreName(membreID, newName string) (int64, error) { diff --git a/entity.go b/entity.go index 385acde..b919c29 100644 --- a/entity.go +++ b/entity.go @@ -1,5 +1,7 @@ package main +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"` @@ -12,3 +14,17 @@ type Membre struct { PreferedName string `db:"prefered_name" json:"prefered_name" csv:"prefered_name"` ProgrammeID string `db:"programme_id" json:"programme_id" csv:"programme_id"` } + +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 +} diff --git a/request.go b/request.go index 720561b..277e6f4 100644 --- a/request.go +++ b/request.go @@ -23,7 +23,7 @@ func (request HealthGETRequest) Request(v *voki.Voki) (response HealthGETRespons statusCode, body, err := v.CallAndParse( http.MethodGet, - "/api/health/", + "/api/v7/health/", nil, true, ) @@ -64,7 +64,7 @@ func (request ProgrammesPOSTRequest) Request(v *voki.Voki) (response ProgrammesP statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/programmes/", + "/api/v7/programme/", &buf, true, ) @@ -105,7 +105,7 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes statusCode, body, err := v.CallAndParse( http.MethodPost, - "/api/membres/", + "/api/v7/membre/", &buf, true, ) @@ -120,3 +120,44 @@ func (request MembresPOSTRequest) Request(v *voki.Voki) (response MembresPOSTRes 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/v7/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 +} diff --git a/routes.go b/routes.go index 05c62b5..c5b3083 100644 --- a/routes.go +++ b/routes.go @@ -12,7 +12,7 @@ import ( func addRoutes(e *echo.Echo, db *PostgresClient) error { _ = db - apiPath := "/api" + apiPath := "/api/v7" apiGroup := e.Group(apiPath) p := pave.New() if err := pave.EchoRegister[HealthGETRequest]( @@ -47,7 +47,7 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { &p, apiPath, http.MethodPost, - "/programmes/", + "/programme/", "Insert programmes", "ProgrammesPOST", func(c echo.Context) error { var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{} @@ -90,7 +90,7 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { &p, apiPath, http.MethodPost, - "/membres/", + "/membre/", "Insert membres", "MembresPOST", func(c echo.Context) error { var request, response = MembresPOSTRequest{}, MembresPOSTResponse{} @@ -127,5 +127,44 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { }); 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 + } return nil } From 00aebc2ae317e49d1cf4517c61619de19e485835 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 18 Jun 2024 19:47:28 -0400 Subject: [PATCH 14/33] feature: add basic Makefile for integration testing --- Makefile | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31a6c23 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +## This Makefile uses the help target explained in the following blogpost: +## +## https://victoria.dev/blog/how-to-create-a-self-documenting-makefile/ + +.DEFAULT_GOAL := help + +.PHONY: help +help: ## Show this help + @egrep -h '\s##\s' $(MAKEFILE_LIST) | \ + sort | \ + awk \ + 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +.PHONY: test-integration +test-integration: ## run integration tests through API client. Config is read from `~/.bottin.yaml`. WARNING: affects data in the database, do not run on production server + docker-compose down && docker-compose up -d --build && sleep 2 && go test From f8b5c720036b5d37bd1415ed4628ef002e08cc90 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 18 Jun 2024 21:21:30 -0400 Subject: [PATCH 15/33] feature: add and test GetMembres --- client.go | 18 ++++++++++++++++++ client_test.go | 19 ++++++++++--------- db.go | 21 +++++++++++---------- request.go | 34 ++++++++++++++++++++++++++++++++++ routes.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 19 deletions(-) diff --git a/client.go b/client.go index d47a723..386dbdd 100644 --- a/client.go +++ b/client.go @@ -75,3 +75,21 @@ func (c APIClient) GetMembre(membreID string) (membre Membre, err error) { 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 +} diff --git a/client_test.go b/client_test.go index 7a847ad..7a7a62d 100644 --- a/client_test.go +++ b/client_test.go @@ -111,13 +111,14 @@ func TestAPI(t *testing.T) { }) */ //TODO get membres - /* - t.Run("get membres, max 50", - func(t *testing.T) { - membres, err := apiClient.GetMembres(50) - if err != nil { - t.Error(err) - } - }) - */ + t.Run("get membres, max 50", + func(t *testing.T) { + membres, err := apiClient.GetMembres(50) + if err != nil { + t.Error(err) + } + t.Log(membres) + }) + + //TODO remove test membres and programmes } diff --git a/db.go b/db.go index bb401ae..90d24f7 100644 --- a/db.go +++ b/db.go @@ -164,24 +164,25 @@ func (d *PostgresClient) UpdateMembreName(membreID, newName string) (int64, erro } */ -func (d *PostgresClient) GetMembres() (membres []Membre, err error) { +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 + "membres".id, + "membres".last_name, + "membres".first_name, + "membres".prefered_name, + "membres".programme_id FROM - membres -LIMIT - 10000 + "membres" ORDER BY - membres.id;`) + "membres".id +LIMIT + $1; +`, limit) if err != nil { return nil, err } diff --git a/request.go b/request.go index 277e6f4..df538b3 100644 --- a/request.go +++ b/request.go @@ -161,3 +161,37 @@ func (request MembreGETRequest) Request(v *voki.Voki) (response MembreGETRespons 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/v7/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 +} diff --git a/routes.go b/routes.go index c5b3083..412141a 100644 --- a/routes.go +++ b/routes.go @@ -3,6 +3,7 @@ package main import ( "fmt" "net/http" + "strconv" "codeberg.org/vlbeaudoin/pave/v2" "codeberg.org/vlbeaudoin/voki/v3" @@ -166,5 +167,48 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { }); 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{} + + request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit")) + if err != nil { + var response voki.ResponseBadRequest + response.Message = fmt.Sprintf("parsing limit: %s", err) + return c.JSON(response.StatusCode(), response) + } + + 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 + } return nil } From f6ffa0337969028d566f92cfa915276296f4b8c6 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 18 Jun 2024 22:51:20 -0400 Subject: [PATCH 16/33] feature: ajouter MembrePreferedNamePUTResponse --- response.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/response.go b/response.go index 55a700d..cf53a80 100644 --- a/response.go +++ b/response.go @@ -33,6 +33,10 @@ type MembreGETResponseData struct { Membre Membre `json:"membre"` } +type MembrePreferedNamePUTResponse struct { + APIResponse +} + type MembresGETResponse struct { APIResponse Data MembresGETResponseData `json:"data"` From 4d338f2b03318605e4c9a0c18256b2c82f75eafa Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 18 Jun 2024 22:51:32 -0400 Subject: [PATCH 17/33] feature: ajouter ProgrammesGETResponse et data --- response.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/response.go b/response.go index cf53a80..9f0d076 100644 --- a/response.go +++ b/response.go @@ -63,3 +63,12 @@ type ProgrammesPOSTResponse struct { type ProgrammesPOSTResponseData struct { ProgrammesInserted int64 `json:"programmes_inserted"` } + +type ProgrammesGETResponse struct { + APIResponse + Data ProgrammesGETResponseData `json:"data"` +} + +type ProgrammesGETResponseData struct { + Programmes []Programme `json:"programmes"` +} From 26b3134861d36e8a71ed515acdae9612fd5720f7 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Tue, 18 Jun 2024 23:55:55 -0400 Subject: [PATCH 18/33] feature(request): ajouter MembrePreferedNamePUT et ProgrammesGET --- request.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/request.go b/request.go index df538b3..ab4642d 100644 --- a/request.go +++ b/request.go @@ -195,3 +195,81 @@ func (request MembresGETRequest) Request(v *voki.Voki) (response MembresGETRespo 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, + "/api/v7/membre/%s/", + &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/v7/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 +} From 78aafe0ce9953154fd94f4ff38124da8d737081e Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Wed, 19 Jun 2024 00:04:19 -0400 Subject: [PATCH 19/33] feature(api): add and test ProgrammesGET --- client.go | 18 ++++++++++++++++++ client_test.go | 14 +++++++++----- db.go | 41 +++++++++++++++++++++++++++++++++++++++++ routes.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 5 deletions(-) diff --git a/client.go b/client.go index 386dbdd..28ac08c 100644 --- a/client.go +++ b/client.go @@ -93,3 +93,21 @@ func (c APIClient) GetMembres(limit int) (membres []Membre, err error) { 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 +} diff --git a/client_test.go b/client_test.go index 7a7a62d..7d3ad78 100644 --- a/client_test.go +++ b/client_test.go @@ -39,8 +39,6 @@ func TestAPI(t *testing.T) { } }) - //TODO create or replace schema - t.Run("insert programmes", func(t *testing.T) { programmes := []Programme{ @@ -70,6 +68,15 @@ func TestAPI(t *testing.T) { }, } + t.Run("get programmes, max 50", + func(t *testing.T) { + programmes, err := apiClient.GetProgrammes(50) + if err != nil { + t.Error(err) + } + t.Log(programmes) + }) + t.Run("insert membres", func(t *testing.T) { _, err := apiClient.InsertMembres(testMembres...) @@ -110,7 +117,6 @@ func TestAPI(t *testing.T) { } }) */ - //TODO get membres t.Run("get membres, max 50", func(t *testing.T) { membres, err := apiClient.GetMembres(50) @@ -119,6 +125,4 @@ func TestAPI(t *testing.T) { } t.Log(membres) }) - - //TODO remove test membres and programmes } diff --git a/db.go b/db.go index 90d24f7..3ae7ee0 100644 --- a/db.go +++ b/db.go @@ -210,3 +210,44 @@ LIMIT 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 + } +} diff --git a/routes.go b/routes.go index 412141a..1ec580e 100644 --- a/routes.go +++ b/routes.go @@ -210,5 +210,48 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { }); 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{} + + request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit")) + if err != nil { + var response voki.ResponseBadRequest + response.Message = fmt.Sprintf("parsing limit: %s", err) + return c.JSON(response.StatusCode(), response) + } + + 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 + } return nil } From e6103c6e6e461605edc2472658daabf95d477e4d Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Wed, 19 Jun 2024 00:28:26 -0400 Subject: [PATCH 20/33] feature(api): add and test UpdateMembrePreferedName --- client.go | 22 ++++++++++++++++++++++ client_test.go | 15 ++++++--------- db.go | 21 +++++++++++++++++++++ request.go | 2 +- routes.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/client.go b/client.go index 28ac08c..91cdbe7 100644 --- a/client.go +++ b/client.go @@ -111,3 +111,25 @@ func (c APIClient) GetProgrammes(limit int) (programmes []Programme, err error) 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 +} diff --git a/client_test.go b/client_test.go index 7d3ad78..1a02a15 100644 --- a/client_test.go +++ b/client_test.go @@ -108,15 +108,12 @@ func TestAPI(t *testing.T) { } }) - //TODO update membre prefered name - /* - t.Run("", - func(t *testing.T) { - if err := apiClient.UpdateMembrePreferedName(testMembres[0].ID, "User, Galaxy"); err != nil { - t.Error(err) - } - }) - */ + t.Run("update membre prefered name", + func(t *testing.T) { + if err := apiClient.UpdateMembrePreferedName(testMembres[0].ID, "User, Galaxy"); err != nil { + t.Error(err) + } + }) t.Run("get membres, max 50", func(t *testing.T) { membres, err := apiClient.GetMembres(50) diff --git a/db.go b/db.go index 3ae7ee0..2608940 100644 --- a/db.go +++ b/db.go @@ -251,3 +251,24 @@ LIMIT 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 +} diff --git a/request.go b/request.go index ab4642d..97f51de 100644 --- a/request.go +++ b/request.go @@ -224,7 +224,7 @@ func (request MembrePreferedNamePUTRequest) Request(v *voki.Voki) (response Memb statusCode, body, err := v.CallAndParse( http.MethodPut, - "/api/v7/membre/%s/", + fmt.Sprintf("/api/v7/membre/%s/prefered_name/", request.Param.MembreID), &buf, true, ) diff --git a/routes.go b/routes.go index 1ec580e..355dd8f 100644 --- a/routes.go +++ b/routes.go @@ -253,5 +253,49 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { }); 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 + } + return nil } From e4ff1013d04288a8fbb3710506cebfb3b9b7cd8a Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 20 Jun 2024 18:51:38 -0400 Subject: [PATCH 21/33] feature: ajouter et tester GetMembre[s]ForDisplay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Priorisent le prefered_name ("nom d'usage") et devraient être utilisés aux endroits où l'affichage est important. --- client.go | 35 +++++++++++++++++++++ client_test.go | 18 +++++++++++ db.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++ entity.go | 8 +++++ request.go | 75 +++++++++++++++++++++++++++++++++++++++++++++ response.go | 17 +++++++++++ routes.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 315 insertions(+) diff --git a/client.go b/client.go index 91cdbe7..e4958a6 100644 --- a/client.go +++ b/client.go @@ -133,3 +133,38 @@ func (c APIClient) UpdateMembrePreferedName(membreID string, name string) (err e 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 +} diff --git a/client_test.go b/client_test.go index 1a02a15..a2aea93 100644 --- a/client_test.go +++ b/client_test.go @@ -122,4 +122,22 @@ func TestAPI(t *testing.T) { } t.Log(membres) }) + + t.Run("get membre for display", + func(t *testing.T) { + membre, err := apiClient.GetMembreForDisplay(testMembres[0].ID) + if err != nil { + t.Error(err) + } + t.Log(membre) + }) + + t.Run("get membres for display, max 5", + func(t *testing.T) { + membres, err := apiClient.GetMembresForDisplay(5) + if err != nil { + t.Error(err) + } + t.Log(membres) + }) } diff --git a/db.go b/db.go index 2608940..915ddab 100644 --- a/db.go +++ b/db.go @@ -272,3 +272,83 @@ WHERE } 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 { + 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 + } +} diff --git a/entity.go b/entity.go index b919c29..672f54c 100644 --- a/entity.go +++ b/entity.go @@ -15,6 +15,14 @@ type Membre struct { 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 diff --git a/request.go b/request.go index 97f51de..8f47f10 100644 --- a/request.go +++ b/request.go @@ -273,3 +273,78 @@ func (request ProgrammesGETRequest) Request(v *voki.Voki) (response ProgrammesGE 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/v7/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/v7/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 +} diff --git a/response.go b/response.go index 9f0d076..19818df 100644 --- a/response.go +++ b/response.go @@ -46,6 +46,23 @@ 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"` diff --git a/routes.go b/routes.go index 355dd8f..a5aaed6 100644 --- a/routes.go +++ b/routes.go @@ -297,5 +297,87 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { 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{} + + request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit")) + if err != nil { + var response voki.ResponseBadRequest + response.Message = fmt.Sprintf("parsing limit: %s", err) + return c.JSON(response.StatusCode(), response) + } + + 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 } From 929704c6ff25c9fc0bd4a16f9fbf36f79674dff5 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 20 Jun 2024 19:32:26 -0400 Subject: [PATCH 22/33] =?UTF-8?q?fix(config):=20ajouter=20pr=C3=A9fixe=20w?= =?UTF-8?q?eb[.-]=20aux=20options=20config=20web?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config.go b/config.go index d50e241..3096960 100644 --- a/config.go +++ b/config.go @@ -66,23 +66,23 @@ const ( DefaultWebPort int = 2312 DescriptionWebPort string = "Web client port" - ViperWebAPIHost string = "api.host" - FlagWebAPIHost string = "api-host" + ViperWebAPIHost string = "web.api.host" + FlagWebAPIHost string = "web-api-host" DefaultWebAPIHost string = "api" DescriptionWebAPIHost string = "Target API server host" - ViperWebAPIKey string = "api.key" - FlagWebAPIKey string = "api-key" + ViperWebAPIKey string = "web.api.key" + FlagWebAPIKey string = "web-api-key" DefaultWebAPIKey string = "bottin" DescriptionWebAPIKey string = "Target API server key" - ViperWebAPIPort string = "api.port" - FlagWebAPIPort string = "api-port" + ViperWebAPIPort string = "web.api.port" + FlagWebAPIPort string = "web-api-port" DefaultWebAPIPort int = 1312 DescriptionWebAPIPort string = "Target API server port" - ViperWebAPIProtocol string = "api.protocol" - FlagWebAPIProtocol string = "api-protocol" + ViperWebAPIProtocol string = "web.api.protocol" + FlagWebAPIProtocol string = "web-api-protocol" DefaultWebAPIProtocol string = "http" DescriptionWebAPIProtocol string = "Target API server protocol (http/https)" ) From 8cb2014f3bafddb56c0dea3051988a3043a98a8a Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 20 Jun 2024 19:34:27 -0400 Subject: [PATCH 23/33] fix(template): expect voki.MessageResponse in input object --- templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/index.html b/templates/index.html index 700d9bb..85c857d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -100,7 +100,7 @@ button { -

{{ .Result }}

+

{{ .Message }}

From 7484bafc84252ae468dc094a3ed084640c0ad2eb Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 20 Jun 2024 19:35:07 -0400 Subject: [PATCH 24/33] =?UTF-8?q?fix(web):=20neutraliser=20texte=20avec=20?= =?UTF-8?q?middle=20dot=20(=C2=B7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/index.html b/templates/index.html index 85c857d..2822147 100644 --- a/templates/index.html +++ b/templates/index.html @@ -83,7 +83,7 @@ button {

- Scannez la carte étudiante d'unE membre
+ Scannez la carte étudiante d'un·e membre
-ou-
Entrez manuellement le code à 7 chiffres

From 244276905b94253aad868032d388847ef1ffeab4 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 20 Jun 2024 19:36:38 -0400 Subject: [PATCH 25/33] =?UTF-8?q?feature(cmd):=20impl=C3=A9menter=20webCmd?= =?UTF-8?q?=20de=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit manque encore le processus de scan mais sinon c'est presque fini --- cmd.go | 52 +++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/cmd.go b/cmd.go index 6067795..dd41f0b 100644 --- a/cmd.go +++ b/cmd.go @@ -6,8 +6,10 @@ import ( "fmt" "html/template" "log" + "net/http" "os" + "codeberg.org/vlbeaudoin/voki/v3" "github.com/jackc/pgx/v5/pgxpool" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" @@ -122,56 +124,56 @@ var webCmd = &cobra.Command{ Short: "Démarrer le client web", Args: cobra.ExactArgs(0), Run: func(cmd *cobra.Command, args []string) { + // Parse config var cfg Config if err := viper.Unmarshal(&cfg); err != nil { log.Fatal("init config:", err) } - // Ping API server - /* - client := http.DefaultClient - defer client.CloseIdleConnections() - - apiClient := data.NewApiClient(client, webApiKey, webApiHost, webApiProtocol, webApiPort) - - pingResult, err := apiClient.GetHealth() - if err != nil { - log.Fatal(err) - } - - log.Println(pingResult) - */ - e := echo.New() // Middlewares + // Trailing slash e.Pre(middleware.AddTrailingSlash()) + // Auth e.Use(middleware.BasicAuth(func(user, password string, c echo.Context) (bool, error) { usersMatch := subtle.ConstantTimeCompare([]byte(user), []byte(cfg.Web.User)) == 1 passwordsMatch := subtle.ConstantTimeCompare([]byte(password), []byte(cfg.Web.Password)) == 1 return usersMatch && passwordsMatch, nil })) - // Template - - t := &Template{ + // Templating + e.Renderer = &Template{ templates: template.Must(template.ParseFS(templatesFS, "templates/*.html")), } - e.Renderer = t + // API Client + apiClient := APIClient{voki.New( + http.DefaultClient, + cfg.Web.API.Host, + cfg.Web.API.Key, + cfg.Web.API.Port, + cfg.Web.API.Protocol, + )} + defer apiClient.Voki.CloseIdleConnections() // Routes - /* - handler := webhandlers.Handler{APIClient: apiClient} + e.GET("/", func(c echo.Context) error { + pingResult, err := apiClient.GetHealth() + if err != nil { + log.Fatal(err) + } - e.GET("/", handler.GetIndex) - e.GET("/membre/", handler.GetMembre) - */ + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: pingResult}, + ) + }) // Execution - e.Logger.Fatal(e.Start( fmt.Sprintf(":%d", cfg.Web.Port))) }, From 0321b1b2a0c9bf2b91e617c95aee61b61dec0781 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 20 Jun 2024 19:54:41 -0400 Subject: [PATCH 26/33] =?UTF-8?q?fix(web):=20correctement=20render=20erreu?= =?UTF-8?q?r=20d'acc=C3=A8s=20au=20serveur=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd.go b/cmd.go index dd41f0b..88f36a5 100644 --- a/cmd.go +++ b/cmd.go @@ -163,7 +163,11 @@ var webCmd = &cobra.Command{ e.GET("/", func(c echo.Context) error { pingResult, err := apiClient.GetHealth() if err != nil { - log.Fatal(err) + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf("impossible d'accéder au serveur API: %s", err)}, + ) } return c.Render( From 6cc90b1a29cb56e88519689de66c38586702f4dc Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 20 Jun 2024 19:55:12 -0400 Subject: [PATCH 27/33] feature(web): ajouter route /membre/ permet la recherche de membre --- cmd.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/cmd.go b/cmd.go index 88f36a5..3bbd59c 100644 --- a/cmd.go +++ b/cmd.go @@ -177,6 +177,48 @@ var webCmd = &cobra.Command{ ) }) + e.GET("/membre/", func(c echo.Context) error { + membreID := c.QueryParam("membre_id") + switch { + case membreID == "": + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: "Veuillez entrer un numéro étudiant à rechercher"}, + ) + case !IsMembreID(membreID): + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf("Numéro étudiant '%s' invalide", membreID)}, + ) + } + + membre, err := apiClient.GetMembreForDisplay(membreID) + if err != nil { + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf("erreur: %s", err)}, + ) + } + + return c.Render( + http.StatusOK, + "index-html", + voki.MessageResponse{Message: fmt.Sprintf(` +Numéro étudiant: %s +Nom d'usage: %s +Programme: [%s] %s +`, + membre.ID, + membre.Name, + membre.ProgrammeID, + membre.ProgrammeName, + )}, + ) + }) + // Execution e.Logger.Fatal(e.Start( fmt.Sprintf(":%d", cfg.Web.Port))) From 8af11615dda87c67d01090c8eaaea87aef285f50 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 20 Jun 2024 20:16:33 -0400 Subject: [PATCH 28/33] =?UTF-8?q?adjust:=20ajouter=20emojis=20=C3=A0=20cer?= =?UTF-8?q?taines=20web=20responses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd.go b/cmd.go index 3bbd59c..591bdc8 100644 --- a/cmd.go +++ b/cmd.go @@ -184,13 +184,13 @@ var webCmd = &cobra.Command{ return c.Render( http.StatusOK, "index-html", - voki.MessageResponse{Message: "Veuillez entrer un numéro étudiant à rechercher"}, + voki.MessageResponse{Message: "❗Veuillez entrer un numéro étudiant à rechercher"}, ) case !IsMembreID(membreID): return c.Render( http.StatusOK, "index-html", - voki.MessageResponse{Message: fmt.Sprintf("Numéro étudiant '%s' invalide", membreID)}, + voki.MessageResponse{Message: fmt.Sprintf("❗Numéro étudiant '%s' invalide", membreID)}, ) } @@ -199,7 +199,7 @@ var webCmd = &cobra.Command{ return c.Render( http.StatusOK, "index-html", - voki.MessageResponse{Message: fmt.Sprintf("erreur: %s", err)}, + voki.MessageResponse{Message: fmt.Sprintf("❗erreur: %s", err)}, ) } From 64ddfa96d69820ce8bf2829f63486f14538cbf46 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Thu, 20 Jun 2024 20:20:30 -0400 Subject: [PATCH 29/33] =?UTF-8?q?fix:=20franciser=20erreur=20de=20membre?= =?UTF-8?q?=20non=20trouv=C3=A9=C2=B7e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/db.go b/db.go index 915ddab..f0b2f4e 100644 --- a/db.go +++ b/db.go @@ -5,6 +5,7 @@ import ( _ "embed" "fmt" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) @@ -297,6 +298,9 @@ LIMIT &membre.ProgrammeID, &membre.ProgrammeName, ); err != nil { + if err == pgx.ErrNoRows { + err = fmt.Errorf("Numéro étudiant valide mais aucun·e membre trouvé·e") + } return } From d0de8115473509ff8122a51d7153c8f97bbb3563 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Fri, 21 Jun 2024 18:46:45 -0400 Subject: [PATCH 30/33] chores: update dependencies --- go.mod | 4 ++-- go.sum | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index b6ce20d..a573bf5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( codeberg.org/vlbeaudoin/voki/v3 v3.0.0 github.com/jackc/pgx/v5 v5.6.0 github.com/labstack/echo/v4 v4.12.0 - github.com/spf13/cobra v1.8.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 ) @@ -36,7 +36,7 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.24.0 // indirect - golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect diff --git a/go.sum b/go.sum index 73ace91..7d7ac8f 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,7 @@ codeberg.org/vlbeaudoin/pave/v2 v2.0.0 h1:hfB5KnqMMu17g5QBWgLvWOsqidrYaohRfu2Lfl codeberg.org/vlbeaudoin/pave/v2 v2.0.0/go.mod h1:TsTfP6IA+3Ph33vLZigeJWS5vgBPgkW1tfs3zFPfycU= codeberg.org/vlbeaudoin/voki/v3 v3.0.0 h1:XdF/UTe9YUNj3hYrAyEvdmIMDYLL8SkqTwPkqw1yJ2c= codeberg.org/vlbeaudoin/voki/v3 v3.0.0/go.mod h1:+6LMXosAu2ijNKV04sMwkeujpH+cghZU1fydqj2y95g= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -62,8 +62,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= @@ -89,8 +89,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= From 14eb6c5d02c80ec50888824558f9ae41194365bb Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Wed, 3 Jul 2024 17:33:56 -0400 Subject: [PATCH 31/33] ajouter examples/example.csv --- examples/example.csv | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/example.csv diff --git a/examples/example.csv b/examples/example.csv new file mode 100644 index 0000000..fce75ff --- /dev/null +++ b/examples/example.csv @@ -0,0 +1,3 @@ +programme_id;nom_programme; +000.00;test programme; +111.11;autre test programme; From 1f2ba0576a16fd84c0191643092025ce17131102 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Wed, 3 Jul 2024 17:34:18 -0400 Subject: [PATCH 32/33] feature: permettre insert par csv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ajouter parameter cfg à addRoutes() Fix empty et default limit sur get requests (set default limit à 1000 hardcoded, todo move to config) --- cmd.go | 2 +- go.mod | 1 + go.sum | 2 + routes.go | 120 +++++++++++++++++++++++++++++++++++++++++++++--------- 4 files changed, 104 insertions(+), 21 deletions(-) diff --git a/cmd.go b/cmd.go index 591bdc8..8c5571d 100644 --- a/cmd.go +++ b/cmd.go @@ -92,7 +92,7 @@ var apiCmd = &cobra.Command{ } // Routes - if err := addRoutes(e, db); err != nil { + if err := addRoutes(e, db, cfg); err != nil { log.Fatal("add routes:", err) } /* diff --git a/go.mod b/go.mod index a573bf5..2c52fee 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.0 require ( codeberg.org/vlbeaudoin/pave/v2 v2.0.0 codeberg.org/vlbeaudoin/voki/v3 v3.0.0 + github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 github.com/jackc/pgx/v5 v5.6.0 github.com/labstack/echo/v4 v4.12.0 github.com/spf13/cobra v1.8.1 diff --git a/go.sum b/go.sum index 7d7ac8f..e1ad5b4 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= diff --git a/routes.go b/routes.go index a5aaed6..f7eb61c 100644 --- a/routes.go +++ b/routes.go @@ -1,16 +1,19 @@ package main 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) error { +func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error { _ = db apiPath := "/api/v7" @@ -53,9 +56,37 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { "ProgrammesPOST", func(c echo.Context) error { var request, response = ProgrammesPOSTRequest{}, ProgrammesPOSTResponse{} - if err := c.Bind(&request.Data); err != nil { + 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("parse request body: %s", err) + response.Message = fmt.Sprintf("cannot parse body with content-type: %s", contentType) return c.JSON(response.StatusCode(), response) } @@ -96,9 +127,37 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { "MembresPOST", func(c echo.Context) error { var request, response = MembresPOSTRequest{}, MembresPOSTResponse{} - if err := c.Bind(&request.Data); err != nil { + 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("parse request body: %s", err) + response.Message = fmt.Sprintf("cannot parse body with content-type: %s", contentType) return c.JSON(response.StatusCode(), response) } @@ -178,11 +237,18 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { "MembresGET", func(c echo.Context) (err error) { var request, response = MembresGETRequest{}, MembresGETResponse{} - request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit")) - if err != nil { - var response voki.ResponseBadRequest - response.Message = fmt.Sprintf("parsing limit: %s", err) - return c.JSON(response.StatusCode(), response) + 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() { @@ -221,11 +287,18 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { "ProgrammesGET", func(c echo.Context) (err error) { var request, response = ProgrammesGETRequest{}, ProgrammesGETResponse{} - request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit")) - if err != nil { - var response voki.ResponseBadRequest - response.Message = fmt.Sprintf("parsing limit: %s", err) - return c.JSON(response.StatusCode(), response) + 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() { @@ -307,11 +380,18 @@ func addRoutes(e *echo.Echo, db *PostgresClient) error { "MembresDisplayGET", func(c echo.Context) (err error) { var request, response = MembresDisplayGETRequest{}, MembresDisplayGETResponse{} - request.Query.Limit, err = strconv.Atoi(c.QueryParam("limit")) - if err != nil { - var response voki.ResponseBadRequest - response.Message = fmt.Sprintf("parsing limit: %s", err) - return c.JSON(response.StatusCode(), response) + 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() { From d80c7675f9ed40d1aaadd844fd69502ca6249fb9 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Wed, 3 Jul 2024 17:37:29 -0400 Subject: [PATCH 33/33] fix(routes): unused param cfg --- routes.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routes.go b/routes.go index f7eb61c..ca7dcb2 100644 --- a/routes.go +++ b/routes.go @@ -15,6 +15,7 @@ import ( func addRoutes(e *echo.Echo, db *PostgresClient, cfg Config) error { _ = db + _ = cfg apiPath := "/api/v7" apiGroup := e.Group(apiPath)