feat: create session. (also print the x-rid into the log)

This commit is contained in:
Muyao CHEN 2024-10-11 23:24:29 +02:00
parent be7f57d5a1
commit a3c2ade9fb
10 changed files with 105 additions and 36 deletions

View File

@ -54,11 +54,11 @@ Next I need to design the API.
- add other users to that event - add other users to that event
- A user can only view their own events, but not the events of other users' - A user can only view their own events, but not the events of other users'
- A user can add an expense to the event (reason, date, who payed how much, - A user can add an expense to the event (reason, date, who payed how much,
who benefited how much) who benefited how much)
- Users in the event can edit or delete one entry - Users in the event can edit or delete one entry
- changes are sent to friends in the event - changes are sent to friends in the event
- User can get the money they spent themselves and the money they must pay - User can get the money they spent themselves and the money they must pay
to each other to each other
- User can also get the total amount or the histories. - User can also get the total amount or the histories.
That is what I thought of for now. That is what I thought of for now.
@ -102,10 +102,10 @@ The execution of the program is then just a command like `howmuch run`.
Moreover, in a distributed system, configs can be stored on `etcd`. Moreover, in a distributed system, configs can be stored on `etcd`.
> [Kubernetes stores configuration data into etcd for service discovery and > [Kubernetes stores configuration data into etcd for service discovery and
cluster management; etcds consistency is crucial for correctly scheduling > cluster management; etcds consistency is crucial for correctly scheduling
and operating services. The Kubernetes API server persists cluster state > and operating services. The Kubernetes API server persists cluster state
into etcd. It uses etcds watch API to monitor the cluster and roll out > into etcd. It uses etcds watch API to monitor the cluster and roll out
critical configuration changes.](https://etcd.io/docs/v3.5/learning/why/) > critical configuration changes.](https://etcd.io/docs/v3.5/learning/why/)
#### Business logic #### Business logic
@ -113,8 +113,8 @@ critical configuration changes.](https://etcd.io/docs/v3.5/learning/why/)
- init DBs (Redis, SQL, Kafka, etc.) - init DBs (Redis, SQL, Kafka, etc.)
- init web service (http, https, gRPC, etc.) - init web service (http, https, gRPC, etc.)
- start async tasks like `watch kube-apiserver`; pull data from third-party - start async tasks like `watch kube-apiserver`; pull data from third-party
services; store, register `/metrics` and listen on some port; start kafka services; store, register `/metrics` and listen on some port; start kafka
consumer queue, etc. consumer queue, etc.
- Run specific business logic - Run specific business logic
- Stop the program - Stop the program
- others... - others...
@ -166,26 +166,26 @@ that has several layers:
- Entities: the models of the product - Entities: the models of the product
- Use cases: the core business rule - Use cases: the core business rule
- Interface Adapters: convert data-in to entities and convert data-out to - Interface Adapters: convert data-in to entities and convert data-out to
output ports. output ports.
- Frameworks and drivers: Web server, DB. - Frameworks and drivers: Web server, DB.
Based on this logic, we create the following directories: Based on this logic, we create the following directories:
- `model`: entities - `model`: entities
- `infra`: Provides the necessary functions to setup the infrastructure, - `infra`: Provides the necessary functions to setup the infrastructure,
especially the DB (output-port), but also the router (input-port). Once especially the DB (output-port), but also the router (input-port). Once
setup, we don't touch them anymore. setup, we don't touch them anymore.
- `registry`: Provides a register function for the main to register a service. - `registry`: Provides a register function for the main to register a service.
It takes the pass to the output-port (ex.DBs) and gives back a pass It takes the pass to the output-port (ex.DBs) and gives back a pass
(controller) to the input-port (controller) to the input-port
- `adapter`: Controllers are one of the adapters, when they are called, - `adapter`: Controllers are one of the adapters, when they are called,
they parse the user input and parse them into models and run the usecase they parse the user input and parse them into models and run the usecase
rules. Then they send back the response(input-port). For the output-port rules. Then they send back the response(input-port). For the output-port
part, the `repo` is the implementation of interfaces defined in `usecase/repo`. part, the `repo` is the implementation of interfaces defined in `usecase/repo`.
- `usecase`: with the input of adapter, do what have to be done, and answer - `usecase`: with the input of adapter, do what have to be done, and answer
with the result. In the meantime, we may have to store things into DBs. with the result. In the meantime, we may have to store things into DBs.
Here we use the Repository model to decouple the implementation of the repo Here we use the Repository model to decouple the implementation of the repo
with the interface. Thus in `usecase/repo` we only define interfaces. with the interface. Thus in `usecase/repo` we only define interfaces.
Then it comes the real design for the app. Then it comes the real design for the app.
@ -219,17 +219,17 @@ For the test-driven part,
- infra: routes and db connections, it works when it works. Nothing to test. - infra: routes and db connections, it works when it works. Nothing to test.
- registry: Just return some structs, no logic. **Not worth testing** - registry: Just return some structs, no logic. **Not worth testing**
- adapter: - adapter:
- input-port (controller) test: it is about testing parsing the input - input-port (controller) test: it is about testing parsing the input
value, and the output results writing. The unit test of controller is to value, and the output results writing. The unit test of controller is to
**make sure that they behave as defined in the API documentation**. To **make sure that they behave as defined in the API documentation**. To
test, we have to mock the **business service**. test, we have to mock the **business service**.
- output-port (repo) test: it is about testing converting business model - output-port (repo) test: it is about testing converting business model
to database model and the interaction with the database. If we are going to database model and the interaction with the database. If we are going
to test them, it's about simulating different type of database behaviour to test them, it's about simulating different type of database behaviour
(success, timeout, etc.). To test, we have to mock the (success, timeout, etc.). To test, we have to mock the
**database connection**. **database connection**.
- usecase: This is the core part to test, it's about the core business. - usecase: This is the core part to test, it's about the core business.
We provide the data input and we check the data output in a fake repository. We provide the data input and we check the data output in a fake repository.
With this design, although it may seem overkill for this little project, fits With this design, although it may seem overkill for this little project, fits
perfectly well with the TDD method. perfectly well with the TDD method.
@ -253,7 +253,7 @@ integration test part (the 2nd point).
I rethought about the whole API design (even though I have only one yet). I I rethought about the whole API design (even though I have only one yet). I
have created `/signup` and `/login` without thinking too much, but in fact have created `/signup` and `/login` without thinking too much, but in fact
it is not quite *RESTful*. it is not quite _RESTful_.
**REST** is all about resources. While `/signup` and `/login` is quite **REST** is all about resources. While `/signup` and `/login` is quite
comprehensible, thus service-oriented, they don't follow the REST philosophy, comprehensible, thus service-oriented, they don't follow the REST philosophy,
@ -292,8 +292,8 @@ choose server-side-rendering with `templ + htmx`, or even `template+vanilla
javascript`. javascript`.
I can still write a rather static Go-frontend-server to serve HTMLs and call I can still write a rather static Go-frontend-server to serve HTMLs and call
my Go backend. *And it might be a good idea if they communicate on Go native my Go backend. _And it might be a good idea if they communicate on Go native
rpc.* It worth a try. rpc._ It worth a try.
And I have moved on to `Svelte` which seems very simple by design and the And I have moved on to `Svelte` which seems very simple by design and the
whole compile thing makes it really charm. But this is mainly a Go project, whole compile thing makes it really charm. But this is mainly a Go project,
@ -330,3 +330,15 @@ the database model design.
![Core user story part 1](./docs/howmuch_us1.drawio.png) ![Core user story part 1](./docs/howmuch_us1.drawio.png)
![Database model](./docs/howmuch.drawio.png) ![Database model](./docs/howmuch.drawio.png)
### 2024/10/11
I spent 2 days learning some basic of Vue. Learning Vue takes time. There
are a lot of concepts and it needs a lot of practice. Even though I may not
need a professional level web page, I don't want to copy one module from this
blog and another one from another tutorial. I might just put aside the
front-end for now and concentrate on my backend Go app.
For now, I will just test my backend with `curl`.
And today's job is to get the login part done!

View File

@ -28,4 +28,6 @@ type AppController struct {
User interface{ User } User interface{ User }
Admin interface{ Admin } Admin interface{ Admin }
Session interface{ Session }
} }

View File

@ -0,0 +1,20 @@
package controller
import (
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
"github.com/gin-gonic/gin"
)
type Session interface {
Create(*gin.Context)
}
type SessionController struct{}
func NewSessionController() Session {
return &SessionController{}
}
func (sc *SessionController) Create(ctx *gin.Context) {
log.CtxLog(ctx).DebugLog("session create")
}

View File

@ -54,6 +54,8 @@ func Routes(engine *gin.Engine, c controller.AppController) *gin.Engine {
{ {
userV1.POST("/create", func(ctx *gin.Context) { c.User.Create(ctx) }) userV1.POST("/create", func(ctx *gin.Context) { c.User.Create(ctx) })
} }
v1.POST("/session/create", func(ctx *gin.Context) { c.Session.Create(ctx) })
} }
return engine return engine

View File

@ -51,7 +51,8 @@ func NewRegistry(db *pgx.Conn) Registry {
// each domain. // each domain.
func (r *registry) NewAppController() controller.AppController { func (r *registry) NewAppController() controller.AppController {
return controller.AppController{ return controller.AppController{
User: r.NewUserController(), User: r.NewUserController(),
Admin: r.NewAdminController(), Admin: r.NewAdminController(),
Session: r.NewSessionController(),
} }
} }

View File

@ -0,0 +1,9 @@
package registry
import "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller"
// NewSessionController returns a session controller's implementation
func (r *registry) NewSessionController() controller.Session {
// u := usecase.NewSessionUsecase(repo.NewSessionRepository(r.db), repo.NewDBRepository(r.db))
return controller.NewSessionController()
}

View File

@ -33,6 +33,7 @@ type Context interface {
// Request // Request
Bind(obj any) error Bind(obj any) error
GetHeader(key string) string
// Response // Response
JSON(code int, obj any) JSON(code int, obj any)

View File

@ -26,6 +26,8 @@ import (
"os" "os"
"sync" "sync"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
@ -100,6 +102,26 @@ func NewLogger(opts *Options) *zapLogger {
return &zapLogger{z: z} return &zapLogger{z: z}
} }
// CtxLog writes context's information into the log
func CtxLog(ctx core.Context) *zapLogger {
return std.CtxLog(ctx)
}
func (z *zapLogger) CtxLog(ctx core.Context) *zapLogger {
zz := z.clone()
if rid := ctx.GetHeader(middleware.XRequestID); rid != "" {
zz.z = zz.z.With(zap.Any(middleware.XRequestID, rid))
}
return zz
}
func (z *zapLogger) clone() *zapLogger {
zz := *z
return &zz
}
func (z *zapLogger) FatalLog(msg string, keyValues ...interface{}) { func (z *zapLogger) FatalLog(msg string, keyValues ...interface{}) {
z.z.Sugar().Fatalw(msg, keyValues...) z.z.Sugar().Fatalw(msg, keyValues...)
} }

View File

@ -27,20 +27,20 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
) )
const requestID = "X-Request-Id" const XRequestID = "X-Request-Id"
func RequestID() gin.HandlerFunc { func RequestID() gin.HandlerFunc {
return func(ctx *gin.Context) { return func(ctx *gin.Context) {
var rid string var rid string
if rid = ctx.GetHeader(requestID); rid != "" { if rid = ctx.GetHeader(XRequestID); rid != "" {
ctx.Request.Header.Add(requestID, rid) ctx.Request.Header.Add(XRequestID, rid)
ctx.Next() ctx.Next()
} }
rid = uuid.NewString() rid = uuid.NewString()
ctx.Request.Header.Add(requestID, rid) ctx.Request.Header.Add(XRequestID, rid)
ctx.Header(requestID, rid) ctx.Header(XRequestID, rid)
ctx.Next() ctx.Next()
} }
} }

View File

@ -58,21 +58,21 @@ func TestRequestID(t *testing.T) {
wanted := "123" wanted := "123"
r.GET("/example", func(c *gin.Context) { r.GET("/example", func(c *gin.Context) {
got = c.GetHeader(requestID) got = c.GetHeader(XRequestID)
c.Status(http.StatusOK) c.Status(http.StatusOK)
}) })
r.POST("/example", func(c *gin.Context) { r.POST("/example", func(c *gin.Context) {
got = c.GetHeader(requestID) got = c.GetHeader(XRequestID)
c.String(http.StatusAccepted, "ok") c.String(http.StatusAccepted, "ok")
}) })
// Test with Request ID // Test with Request ID
_ = performRequest(r, "GET", "/example?a=100", header{requestID, wanted}) _ = performRequest(r, "GET", "/example?a=100", header{XRequestID, wanted})
assert.Equal(t, "123", got) assert.Equal(t, "123", got)
res := performRequest(r, "GET", "/example?a=100") res := performRequest(r, "GET", "/example?a=100")
assert.NotEqual(t, "", got) assert.NotEqual(t, "", got)
assert.NoError(t, uuid.Validate(got)) assert.NoError(t, uuid.Validate(got))
assert.Equal(t, res.Header()[requestID][0], got) assert.Equal(t, res.Header()[XRequestID][0], got)
} }