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
- 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,
who benefited how much)
who benefited how much)
- Users in the event can edit or delete one entry
- changes are sent to friends in the event
- 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.
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`.
> [Kubernetes stores configuration data into etcd for service discovery and
cluster management; etcds consistency is crucial for correctly scheduling
and operating services. The Kubernetes API server persists cluster state
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/)
> cluster management; etcds consistency is crucial for correctly scheduling
> and operating services. The Kubernetes API server persists cluster state
> 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/)
#### Business logic
@ -113,8 +113,8 @@ critical configuration changes.](https://etcd.io/docs/v3.5/learning/why/)
- init DBs (Redis, SQL, Kafka, etc.)
- init web service (http, https, gRPC, etc.)
- start async tasks like `watch kube-apiserver`; pull data from third-party
services; store, register `/metrics` and listen on some port; start kafka
consumer queue, etc.
services; store, register `/metrics` and listen on some port; start kafka
consumer queue, etc.
- Run specific business logic
- Stop the program
- others...
@ -166,26 +166,26 @@ that has several layers:
- Entities: the models of the product
- Use cases: the core business rule
- Interface Adapters: convert data-in to entities and convert data-out to
output ports.
output ports.
- Frameworks and drivers: Web server, DB.
Based on this logic, we create the following directories:
- `model`: entities
- `infra`: Provides the necessary functions to setup the infrastructure,
especially the DB (output-port), but also the router (input-port). Once
setup, we don't touch them anymore.
especially the DB (output-port), but also the router (input-port). Once
setup, we don't touch them anymore.
- `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
(controller) to the input-port
It takes the pass to the output-port (ex.DBs) and gives back a pass
(controller) to the input-port
- `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
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`.
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
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
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
with the interface. Thus in `usecase/repo` we only define interfaces.
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
with the interface. Thus in `usecase/repo` we only define interfaces.
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.
- registry: Just return some structs, no logic. **Not worth testing**
- 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
**make sure that they behave as defined in the API documentation**. To
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 test them, it's about simulating different type of database behaviour
(success, timeout, etc.). To test, we have to mock the
**database connection**.
- 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
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
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
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`.
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
rpc.* It worth a try.
my Go backend. _And it might be a good idea if they communicate on Go native
rpc._ It worth a try.
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,
@ -330,3 +330,15 @@ the database model design.
![Core user story part 1](./docs/howmuch_us1.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 }
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) })
}
v1.POST("/session/create", func(ctx *gin.Context) { c.Session.Create(ctx) })
}
return engine

View File

@ -51,7 +51,8 @@ func NewRegistry(db *pgx.Conn) Registry {
// each domain.
func (r *registry) NewAppController() controller.AppController {
return controller.AppController{
User: r.NewUserController(),
Admin: r.NewAdminController(),
User: r.NewUserController(),
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
Bind(obj any) error
GetHeader(key string) string
// Response
JSON(code int, obj any)

View File

@ -26,6 +26,8 @@ import (
"os"
"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/zapcore"
)
@ -100,6 +102,26 @@ func NewLogger(opts *Options) *zapLogger {
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{}) {
z.z.Sugar().Fatalw(msg, keyValues...)
}

View File

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

View File

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