feat: create session. (also print the x-rid into the log)
This commit is contained in:
parent
be7f57d5a1
commit
a3c2ade9fb
62
README.md
62
README.md
@ -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; etcd’s consistency is crucial for correctly scheduling
|
> cluster management; etcd’s 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 etcd’s watch API to monitor the cluster and roll out
|
> into etcd. It uses etcd’s 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!
|
||||||
|
@ -28,4 +28,6 @@ type AppController struct {
|
|||||||
User interface{ User }
|
User interface{ User }
|
||||||
|
|
||||||
Admin interface{ Admin }
|
Admin interface{ Admin }
|
||||||
|
|
||||||
|
Session interface{ Session }
|
||||||
}
|
}
|
||||||
|
20
internal/howmuch/adapter/controller/session.go
Normal file
20
internal/howmuch/adapter/controller/session.go
Normal 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")
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
internal/howmuch/registry/session.go
Normal file
9
internal/howmuch/registry/session.go
Normal 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()
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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...)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user