Compare commits
11 Commits
46c14b63ea
...
main
Author | SHA1 | Date | |
---|---|---|---|
dd999b9355 | |||
14ee642aab | |||
b30a5c5c2d | |||
58cff774e6 | |||
716a58d44c | |||
de7c6f7223 | |||
0258ff6620 | |||
0da8b29507 | |||
304651e7ff | |||
74ae6b7877 | |||
b4259e9a51 |
2
Makefile
2
Makefile
@ -46,7 +46,7 @@ format: # format code.
|
|||||||
|
|
||||||
.PHONY: add-copyright
|
.PHONY: add-copyright
|
||||||
add-copyright: # add license to file headers.
|
add-copyright: # add license to file headers.
|
||||||
@addlicense -v -f $(ROOT_DIR)/LICENSE $(ROOT_DIR) --skip-files=database.yml --skip-dirs=$(OUTPUT_DIR),deployment,migrations,configs,sqlc,web
|
@addlicense -v -f $(ROOT_DIR)/LICENSE $(ROOT_DIR) --skip-files=database.yml --skip-dirs=$(OUTPUT_DIR),deployment,migrations,configs,sqlc,web,mock
|
||||||
|
|
||||||
.PHONY: swagger
|
.PHONY: swagger
|
||||||
swagger: # Run swagger.
|
swagger: # Run swagger.
|
||||||
|
61
README.md
61
README.md
@ -521,3 +521,64 @@ is on my future learning plan!
|
|||||||
_I found it quite interesting that simply with SQL, we can simulate the most
|
_I found it quite interesting that simply with SQL, we can simulate the most
|
||||||
business logic. It is a must-have competence for software design and
|
business logic. It is a must-have competence for software design and
|
||||||
development._
|
development._
|
||||||
|
|
||||||
|
### 2024/10/20
|
||||||
|
|
||||||
|
I was thinking that I should write test for `sqlc` generated code. And then
|
||||||
|
I found out `gomock` and see how it is done in the project of
|
||||||
|
`techschoo/simplebank`. It's a great tutorial project. It makes me questioning
|
||||||
|
my own project's structure. It seems overwhelmed at least at the repo level.
|
||||||
|
|
||||||
|
I don't actually use the sqlc generated object, instead I do a conversion to
|
||||||
|
my `Retrieved` objects. But with some advanced configuration we could make the
|
||||||
|
output of sqlc object directly usable. That will save a lot of code.
|
||||||
|
|
||||||
|
The problem I saw here is the dependency on `sqlc/models`, and the model
|
||||||
|
designed there has no business logic. Everything is done in the handlers
|
||||||
|
and the handlers query directly the DB.
|
||||||
|
|
||||||
|
More concretely, `sqlc` generates `RawJSON` for some fields that are embedded
|
||||||
|
structs. So I have to do the translation somewhere.
|
||||||
|
|
||||||
|
So I will just stick to the plan and keep going with the predefined structure.
|
||||||
|
|
||||||
|
I have to figure out how to use the generated mock files.
|
||||||
|
|
||||||
|
The goals for the next week is to finish the basic operations for each level
|
||||||
|
and run some integration tests with `curl`.
|
||||||
|
|
||||||
|
### 2024/10/22
|
||||||
|
|
||||||
|
I am facing come difficulties on testing of the `repo` functions.
|
||||||
|
|
||||||
|
First, I have to keep the business logic in the service layer. That means I
|
||||||
|
have to create the transaction at the service layer. I don't need to depend
|
||||||
|
on the implementation detail. So I have created a Transaction interface.
|
||||||
|
|
||||||
|
I don't care of the type of `tx` because I will pass it to repo layer and I
|
||||||
|
suppose that it knows what it is doing. Considering this, my repo `Create`
|
||||||
|
function will have to take an any and deduct the type of `tx`. So the layer
|
||||||
|
becomes untestable, because I have to pass a *sql.Tx into it and create a
|
||||||
|
querier.
|
||||||
|
|
||||||
|
Since this repo layer is just a wrapping layer between the `sqlc.models` and
|
||||||
|
my own models, I can extract the conversion part to functions and test them.
|
||||||
|
I'm not testing the whole thing but I test what I can.
|
||||||
|
|
||||||
|
### 2024/10/24
|
||||||
|
|
||||||
|
When writing the tests. I am asking myself the differences between `[]T`,
|
||||||
|
`[]*T` and `*[]T`.
|
||||||
|
|
||||||
|
`*[]T` is simple, it is a reference to the original slice. So modifying it
|
||||||
|
means modifying the original slice.
|
||||||
|
|
||||||
|
But between `[]*T` and `[]T`, the only difference that I see (pointed out by
|
||||||
|
`ChatGPT`) is how the memory is allocated. With `[]T` it might be better for
|
||||||
|
the GC to deal with the memory free. I thing for my project I will stick to
|
||||||
|
`[]T`.
|
||||||
|
|
||||||
|
### 2024/10/25
|
||||||
|
|
||||||
|
Read this [article](https://konradreiche.com/blog/two-common-go-interface-misuses/)
|
||||||
|
today, maybe I am abusing the usage of interfaces?
|
||||||
|
1
go.mod
1
go.mod
@ -16,7 +16,6 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
go.uber.org/mock v0.5.0
|
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/crypto v0.27.0
|
golang.org/x/crypto v0.27.0
|
||||||
golang.org/x/net v0.26.0
|
golang.org/x/net v0.26.0
|
||||||
|
2
go.sum
2
go.sum
@ -142,8 +142,6 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
|
|||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
@ -25,7 +25,9 @@ package repo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
||||||
)
|
)
|
||||||
@ -34,6 +36,8 @@ type dbRepository struct {
|
|||||||
db *sql.DB
|
db *sql.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryTimeout = 3 * time.Second
|
||||||
|
|
||||||
func NewDBRepository(db *sql.DB) repo.DBRepository {
|
func NewDBRepository(db *sql.DB) repo.DBRepository {
|
||||||
return &dbRepository{
|
return &dbRepository{
|
||||||
db: db,
|
db: db,
|
||||||
@ -66,3 +70,11 @@ func (dr *dbRepository) Transaction(
|
|||||||
data, err := txFunc(ctx, tx)
|
data, err := txFunc(ctx, tx)
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getQueries(queries *sqlc.Queries, tx any) *sqlc.Queries {
|
||||||
|
transaction, ok := tx.(*sql.Tx)
|
||||||
|
if ok {
|
||||||
|
return sqlc.New(transaction)
|
||||||
|
}
|
||||||
|
return queries
|
||||||
|
}
|
||||||
|
@ -1,9 +1,33 @@
|
|||||||
|
// MIT License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||||
@ -12,12 +36,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type eventRepository struct {
|
type eventRepository struct {
|
||||||
db *sql.DB
|
queries *sqlc.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEventRepository(db *sql.DB) repo.EventRepository {
|
func NewEventRepository(db *sql.DB) repo.EventRepository {
|
||||||
return &eventRepository{
|
return &eventRepository{
|
||||||
db: db,
|
queries: sqlc.New(db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,35 +49,49 @@ func NewEventRepository(db *sql.DB) repo.EventRepository {
|
|||||||
func (e *eventRepository) Create(
|
func (e *eventRepository) Create(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
evEntity *model.EventEntity,
|
evEntity *model.EventEntity,
|
||||||
|
tx any,
|
||||||
) (*model.EventEntity, error) {
|
) (*model.EventEntity, error) {
|
||||||
panic("unimplemented")
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
}
|
defer cancel()
|
||||||
|
|
||||||
// Delete implements repo.EventRepository.
|
queries := getQueries(e.queries, tx)
|
||||||
func (e *eventRepository) Delete() {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByID implements repo.EventRepository.
|
event, err := queries.InsertEvent(timeoutCtx, sqlc.InsertEventParams{
|
||||||
func (e *eventRepository) GetByID(ctx context.Context, eventID int) (*model.EventRetrieved, error) {
|
Name: evEntity.Name,
|
||||||
queries := sqlc.New(e.db)
|
Description: sql.NullString{String: evEntity.Description, Valid: true},
|
||||||
eventDTO, err := queries.GetEventByID(ctx, int32(eventID))
|
TotalAmount: sql.NullInt32{Int32: int32(evEntity.TotalAmount), Valid: true},
|
||||||
|
DefaultCurrency: evEntity.DefaultCurrency,
|
||||||
|
OwnerID: int32(evEntity.OwnerID),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.ErrorLog("query error", "err", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return &model.EventEntity{
|
||||||
|
ID: int(event.ID),
|
||||||
|
Name: event.Name,
|
||||||
|
Description: event.Description.String,
|
||||||
|
TotalAmount: int(event.TotalAmount.Int32),
|
||||||
|
DefaultCurrency: event.DefaultCurrency,
|
||||||
|
OwnerID: int(event.OwnerID),
|
||||||
|
CreatedAt: event.CreatedAt,
|
||||||
|
UpdatedAt: event.UpdatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convToEventRetrieved(eventDTO *sqlc.GetEventByIDRow) (*model.EventRetrieved, error) {
|
||||||
// marshal owner and users
|
// marshal owner and users
|
||||||
var owner *model.UserBaseRetrieved
|
var owner model.UserBaseRetrieved
|
||||||
err = json.Unmarshal(eventDTO.Owner, owner)
|
err := json.Unmarshal(eventDTO.Owner, &owner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Unexpected
|
// Unexpected
|
||||||
log.ErrorLog("json unmarshal error", "err", err)
|
log.ErrorLog("json unmarshal error", "err", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var users []*model.UserBaseRetrieved
|
var users []model.UserBaseRetrieved
|
||||||
err = json.Unmarshal(eventDTO.Owner, &users)
|
err = json.Unmarshal(eventDTO.Users, &users)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Unexpected
|
// Unexpected
|
||||||
log.ErrorLog("json unmarshal error", "err", err)
|
log.ErrorLog("json unmarshal error", "err", err)
|
||||||
@ -71,27 +109,168 @@ func (e *eventRepository) GetByID(ctx context.Context, eventID int) (*model.Even
|
|||||||
DefaultCurrency: model.Currency(eventDTO.DefaultCurrency),
|
DefaultCurrency: model.Currency(eventDTO.DefaultCurrency),
|
||||||
CreatedAt: eventDTO.CreatedAt,
|
CreatedAt: eventDTO.CreatedAt,
|
||||||
UpdatedAt: eventDTO.UpdatedAt,
|
UpdatedAt: eventDTO.UpdatedAt,
|
||||||
Owner: owner,
|
Owner: &owner,
|
||||||
Users: users,
|
Users: users,
|
||||||
}
|
}
|
||||||
|
|
||||||
return eventRetrieved, nil
|
return eventRetrieved, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetByID implements repo.EventRepository.
|
||||||
|
func (e *eventRepository) GetByID(
|
||||||
|
ctx context.Context,
|
||||||
|
eventID int,
|
||||||
|
tx any,
|
||||||
|
) (*model.EventRetrieved, error) {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
|
||||||
|
eventDTO, err := queries.GetEventByID(timeoutCtx, int32(eventID))
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorLog("query error", "err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return convToEventRetrieved(&eventDTO)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convToEventList(eventsDTO []sqlc.ListEventsByUserIDRow) ([]model.EventListRetrieved, error) {
|
||||||
|
events := make([]model.EventListRetrieved, len(eventsDTO))
|
||||||
|
|
||||||
|
for i, evDTO := range eventsDTO {
|
||||||
|
var owner model.UserBaseRetrieved
|
||||||
|
err := json.Unmarshal(evDTO.Owner, &owner)
|
||||||
|
if err != nil {
|
||||||
|
// Unexpected
|
||||||
|
log.ErrorLog("json unmarshal error", "err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ev := model.EventListRetrieved{
|
||||||
|
ID: int(evDTO.ID),
|
||||||
|
Name: evDTO.Name,
|
||||||
|
Description: evDTO.Description.String,
|
||||||
|
Owner: &owner,
|
||||||
|
CreatedAt: evDTO.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
events[i] = ev
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListEventsByUserID implements repo.EventRepository.
|
// ListEventsByUserID implements repo.EventRepository.
|
||||||
func (e *eventRepository) ListEventsByUserID(
|
func (e *eventRepository) ListEventsByUserID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID int,
|
userID int,
|
||||||
) ([]model.EventBaseItemEntity, error) {
|
tx any,
|
||||||
panic("unimplemented")
|
) ([]model.EventListRetrieved, error) {
|
||||||
}
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// ListExpensesByUserID implements repo.EventRepository.
|
queries := getQueries(e.queries, tx)
|
||||||
func (e *eventRepository) ListExpensesByUserID() {
|
|
||||||
panic("unimplemented")
|
eventsDTO, err := queries.ListEventsByUserID(timeoutCtx, int32(userID))
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorLog("query error", "err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return convToEventList(eventsDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateInfo implements repo.EventRepository.
|
// UpdateInfo implements repo.EventRepository.
|
||||||
func (e *eventRepository) UpdateInfo() {
|
func (e *eventRepository) UpdateEventByID(
|
||||||
panic("unimplemented")
|
ctx context.Context,
|
||||||
|
event *model.EventUpdateEntity,
|
||||||
|
tx any,
|
||||||
|
) error {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
|
||||||
|
err := queries.UpdateEventByID(timeoutCtx, sqlc.UpdateEventByIDParams{
|
||||||
|
ID: int32(event.ID),
|
||||||
|
Name: event.Name,
|
||||||
|
Description: sql.NullString{String: event.Description, Valid: true},
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParticipation implements repo.EventRepository.
|
||||||
|
func (e *eventRepository) GetParticipation(
|
||||||
|
ctx context.Context,
|
||||||
|
userID, eventID int,
|
||||||
|
tx any,
|
||||||
|
) (*model.ParticipationEntity, error) {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
|
||||||
|
partDTO, err := queries.GetParticipation(timeoutCtx, sqlc.GetParticipationParams{
|
||||||
|
UserID: int32(userID),
|
||||||
|
EventID: int32(eventID),
|
||||||
|
})
|
||||||
|
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
// No error, but participation not found
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.ParticipationEntity{
|
||||||
|
ID: int(partDTO.ID),
|
||||||
|
UserID: int(partDTO.UserID),
|
||||||
|
EventID: int(partDTO.EventID),
|
||||||
|
InvitedByUserID: int(partDTO.InvitedByUserID.Int32),
|
||||||
|
CreatedAt: partDTO.CreatedAt,
|
||||||
|
UpdatedAt: partDTO.UpdatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertParticipation implements repo.EventRepository.
|
||||||
|
func (e *eventRepository) InsertParticipation(
|
||||||
|
ctx context.Context,
|
||||||
|
userID int,
|
||||||
|
eventID int,
|
||||||
|
invitedByUserID int,
|
||||||
|
tx any,
|
||||||
|
) (*model.ParticipationEntity, error) {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
|
||||||
|
var invitedBy sql.NullInt32
|
||||||
|
if invitedByUserID == 0 {
|
||||||
|
invitedBy = sql.NullInt32{Int32: 0, Valid: false}
|
||||||
|
} else {
|
||||||
|
invitedBy = sql.NullInt32{Int32: int32(invitedByUserID), Valid: true}
|
||||||
|
}
|
||||||
|
participationDTO, err := queries.InsertParticipation(timeoutCtx, sqlc.InsertParticipationParams{
|
||||||
|
UserID: int32(userID),
|
||||||
|
EventID: int32(eventID),
|
||||||
|
InvitedByUserID: invitedBy,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.ParticipationEntity{
|
||||||
|
ID: int(participationDTO.ID),
|
||||||
|
UserID: int(participationDTO.UserID),
|
||||||
|
EventID: int(participationDTO.EventID),
|
||||||
|
InvitedByUserID: int(participationDTO.InvitedByUserID.Int32),
|
||||||
|
CreatedAt: participationDTO.CreatedAt,
|
||||||
|
UpdatedAt: participationDTO.UpdatedAt,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
122
internal/howmuch/adapter/repo/event_test.go
Normal file
122
internal/howmuch/adapter/repo/event_test.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
// MIT License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvToEventRetrieved(t *testing.T) {
|
||||||
|
input := &sqlc.GetEventByIDRow{
|
||||||
|
ID: 123,
|
||||||
|
Name: "event",
|
||||||
|
Description: sql.NullString{Valid: false},
|
||||||
|
TotalAmount: sql.NullInt32{Valid: false},
|
||||||
|
DefaultCurrency: "EUR",
|
||||||
|
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
Owner: json.RawMessage(`{"id":1, "first_name":"owner", "last_name":"owner"}`),
|
||||||
|
Users: json.RawMessage(`[{"id":1, "first_name":"owner", "last_name":"owner"}]`),
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &model.EventRetrieved{
|
||||||
|
ID: 123,
|
||||||
|
Name: "event",
|
||||||
|
Description: "",
|
||||||
|
TotalAmount: model.Money{Amount: 0, Currency: "EUR"},
|
||||||
|
DefaultCurrency: model.Currency("EUR"),
|
||||||
|
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
Owner: &model.UserBaseRetrieved{
|
||||||
|
ID: 1,
|
||||||
|
FirstName: "owner",
|
||||||
|
LastName: "owner",
|
||||||
|
},
|
||||||
|
Users: []model.UserBaseRetrieved{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
FirstName: "owner",
|
||||||
|
LastName: "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := convToEventRetrieved(input)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvToEventList(t *testing.T) {
|
||||||
|
input := []sqlc.ListEventsByUserIDRow{
|
||||||
|
{
|
||||||
|
ID: 123,
|
||||||
|
Name: "event",
|
||||||
|
Description: sql.NullString{Valid: false},
|
||||||
|
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
Owner: json.RawMessage(`{"id":1, "first_name":"owner", "last_name":"owner"}`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 456,
|
||||||
|
Name: "event2",
|
||||||
|
Description: sql.NullString{String: "super event", Valid: true},
|
||||||
|
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
Owner: json.RawMessage(`{"id":1, "first_name":"owner", "last_name":"owner"}`),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []model.EventListRetrieved{
|
||||||
|
{
|
||||||
|
ID: 123,
|
||||||
|
Name: "event",
|
||||||
|
Description: "",
|
||||||
|
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
Owner: &model.UserBaseRetrieved{
|
||||||
|
ID: 1,
|
||||||
|
FirstName: "owner",
|
||||||
|
LastName: "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: 456,
|
||||||
|
Name: "event2",
|
||||||
|
Description: "super event",
|
||||||
|
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
Owner: &model.UserBaseRetrieved{
|
||||||
|
ID: 1,
|
||||||
|
FirstName: "owner",
|
||||||
|
LastName: "owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := convToEventList(input)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
267
internal/howmuch/adapter/repo/expense.go
Normal file
267
internal/howmuch/adapter/repo/expense.go
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
// MIT License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type expenseRepository struct {
|
||||||
|
queries *sqlc.Queries
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExpenseRepository(db *sql.DB) repo.ExpenseRepository {
|
||||||
|
return &expenseRepository{
|
||||||
|
queries: sqlc.New(db),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteExpense implements repo.ExpenseRepository.
|
||||||
|
func (e *expenseRepository) DeleteExpense(ctx context.Context, expenseID int, tx any) error {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
return queries.DeleteExpense(timeoutCtx, int32(expenseID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteTransactionsOfExpense implements repo.ExpenseRepository.
|
||||||
|
func (e *expenseRepository) DeleteTransactionsOfExpense(
|
||||||
|
ctx context.Context,
|
||||||
|
expenseID int,
|
||||||
|
tx any,
|
||||||
|
) error {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
return queries.DeleteTransactionsOfExpenseID(timeoutCtx, int32(expenseID))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExpenseByID implements repo.ExpenseRepository.
|
||||||
|
func (e *expenseRepository) GetExpenseByID(
|
||||||
|
ctx context.Context,
|
||||||
|
expenseID int,
|
||||||
|
tx any,
|
||||||
|
) (*model.ExpenseRetrieved, error) {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
|
||||||
|
expenseDTO, err := queries.GetExpenseByID(timeoutCtx, int32(expenseID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
expense, err := convToExpenseRetrieved(&expenseDTO)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return expense, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convToPayments(raw json.RawMessage) ([]model.Payment, error) {
|
||||||
|
var paymentsRetrieved []model.PaymentRetrieved
|
||||||
|
err := json.Unmarshal(raw, &paymentsRetrieved)
|
||||||
|
if err != nil {
|
||||||
|
// Unexpected
|
||||||
|
log.ErrorLog("json unmarshal error", "err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payments := make([]model.Payment, len(paymentsRetrieved))
|
||||||
|
for i, p := range paymentsRetrieved {
|
||||||
|
payment := model.Payment{
|
||||||
|
PayerID: p.PayerID,
|
||||||
|
PayerFirstName: p.PayerFirstName,
|
||||||
|
PayerLastName: p.PayerLastName,
|
||||||
|
Amount: model.MakeMoney(p.Amount, model.Currency(p.Currency)),
|
||||||
|
}
|
||||||
|
payments[i] = payment
|
||||||
|
}
|
||||||
|
|
||||||
|
return payments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convToBenefits(raw json.RawMessage) ([]model.Benefit, error) {
|
||||||
|
var benefitsRetrieved []model.BenefitRetrieved
|
||||||
|
err := json.Unmarshal(raw, &benefitsRetrieved)
|
||||||
|
if err != nil {
|
||||||
|
// Unexpected
|
||||||
|
log.ErrorLog("json unmarshal error", "err", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
benefits := make([]model.Benefit, len(benefitsRetrieved))
|
||||||
|
for i, b := range benefitsRetrieved {
|
||||||
|
benefit := model.Benefit{
|
||||||
|
RecipientID: b.RecipientID,
|
||||||
|
RecipientFirstName: b.RecipientFirstName,
|
||||||
|
RecipientLastName: b.RecipientLastName,
|
||||||
|
Amount: model.MakeMoney(b.Amount, model.Currency(b.Currency)),
|
||||||
|
}
|
||||||
|
benefits[i] = benefit
|
||||||
|
}
|
||||||
|
|
||||||
|
return benefits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convToExpenseRetrieved(expenseDTO *sqlc.GetExpenseByIDRow) (*model.ExpenseRetrieved, error) {
|
||||||
|
payments, err := convToPayments(expenseDTO.Payments)
|
||||||
|
if err != nil {
|
||||||
|
// Unexpected
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
benefits, err := convToBenefits(expenseDTO.Benefits)
|
||||||
|
if err != nil {
|
||||||
|
// Unexpected
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseRetrieved := &model.ExpenseRetrieved{
|
||||||
|
ID: int(expenseDTO.ID),
|
||||||
|
CreatedAt: expenseDTO.CreatedAt,
|
||||||
|
UpdatedAt: expenseDTO.UpdatedAt,
|
||||||
|
Amount: model.MakeMoney(int(expenseDTO.Amount), model.Currency(expenseDTO.Currency)),
|
||||||
|
EventID: int(expenseDTO.EventID),
|
||||||
|
Detail: model.ExpenseDetail{
|
||||||
|
Name: expenseDTO.Name.String,
|
||||||
|
Place: expenseDTO.Place.String,
|
||||||
|
},
|
||||||
|
Payments: payments,
|
||||||
|
Benefits: benefits,
|
||||||
|
}
|
||||||
|
|
||||||
|
return expenseRetrieved, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertExpense implements repo.ExpenseRepository.
|
||||||
|
func (e *expenseRepository) InsertExpense(
|
||||||
|
ctx context.Context,
|
||||||
|
expenseEntity *model.ExpenseEntity,
|
||||||
|
tx any,
|
||||||
|
) (*model.ExpenseEntity, error) {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
|
||||||
|
expenseDTO, err := queries.InsertExpense(timeoutCtx, sqlc.InsertExpenseParams{
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
Amount: int32(expenseEntity.Amount),
|
||||||
|
Currency: expenseEntity.Currency,
|
||||||
|
EventID: int32(expenseEntity.EventID),
|
||||||
|
Name: sql.NullString{String: expenseEntity.Name, Valid: true},
|
||||||
|
Place: sql.NullString{String: expenseEntity.Place, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.ExpenseEntity{
|
||||||
|
ID: int(expenseDTO.ID),
|
||||||
|
CreatedAt: expenseDTO.CreatedAt,
|
||||||
|
UpdatedAt: expenseDTO.CreatedAt,
|
||||||
|
Amount: int(expenseDTO.Amount),
|
||||||
|
Currency: expenseDTO.Currency,
|
||||||
|
EventID: int(expenseDTO.EventID),
|
||||||
|
Name: expenseDTO.Name.String,
|
||||||
|
Place: expenseDTO.Place.String,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListExpensesByEventID implements repo.ExpenseRepository.
|
||||||
|
func (e *expenseRepository) ListExpensesByEventID(
|
||||||
|
ctx context.Context,
|
||||||
|
id int,
|
||||||
|
tx any,
|
||||||
|
) ([]model.ExpensesListRetrieved, error) {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
|
||||||
|
listDTO, err := queries.ListExpensesByEventID(timeoutCtx, int32(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res := make([]model.ExpensesListRetrieved, len(listDTO))
|
||||||
|
for i, dto := range listDTO {
|
||||||
|
elem := model.ExpensesListRetrieved{
|
||||||
|
ID: int(dto.ID),
|
||||||
|
CreatedAt: dto.CreatedAt,
|
||||||
|
UpdatedAt: dto.UpdatedAt,
|
||||||
|
Amount: model.MakeMoney(int(dto.Amount), model.Currency(dto.Currency)),
|
||||||
|
EventID: int(dto.EventID),
|
||||||
|
Detail: model.ExpenseDetail{
|
||||||
|
Name: dto.Name.String,
|
||||||
|
Place: dto.Place.String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
res[i] = elem
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateExpenseByID implements repo.ExpenseRepository.
|
||||||
|
func (e *expenseRepository) UpdateExpenseByID(
|
||||||
|
ctx context.Context,
|
||||||
|
expenseUpdate *model.ExpenseUpdateEntity,
|
||||||
|
tx any,
|
||||||
|
) (*model.ExpenseEntity, error) {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(e.queries, tx)
|
||||||
|
|
||||||
|
expenseDTO, err := queries.UpdateExpenseByID(timeoutCtx, sqlc.UpdateExpenseByIDParams{
|
||||||
|
ID: int32(expenseUpdate.ID),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
Amount: int32(expenseUpdate.Amount),
|
||||||
|
Currency: expenseUpdate.Currency,
|
||||||
|
Name: sql.NullString{String: expenseUpdate.Name, Valid: true},
|
||||||
|
Place: sql.NullString{String: expenseUpdate.Place, Valid: true},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &model.ExpenseEntity{
|
||||||
|
ID: int(expenseDTO.ID),
|
||||||
|
CreatedAt: expenseDTO.CreatedAt,
|
||||||
|
UpdatedAt: expenseDTO.CreatedAt,
|
||||||
|
Amount: int(expenseDTO.Amount),
|
||||||
|
Currency: expenseDTO.Currency,
|
||||||
|
EventID: int(expenseDTO.EventID),
|
||||||
|
Name: expenseDTO.Name.String,
|
||||||
|
Place: expenseDTO.Place.String,
|
||||||
|
}, nil
|
||||||
|
}
|
96
internal/howmuch/adapter/repo/expense_test.go
Normal file
96
internal/howmuch/adapter/repo/expense_test.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// MIT License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||||
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvToExpenseRetrieved(t *testing.T) {
|
||||||
|
input := &sqlc.GetExpenseByIDRow{
|
||||||
|
ID: 123,
|
||||||
|
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
Amount: 123,
|
||||||
|
Currency: "EUR",
|
||||||
|
EventID: 123,
|
||||||
|
Name: sql.NullString{Valid: false},
|
||||||
|
Place: sql.NullString{Valid: false},
|
||||||
|
Payments: json.RawMessage(
|
||||||
|
`[{"payer_id": 1, "payer_first_name": "toto", "payer_last_name": "titi", "amount": 10, "currency": "EUR"},
|
||||||
|
{"payer_id": 2, "payer_first_name": "tata", "payer_last_name": "titi", "amount": 10, "currency": "EUR"}]`,
|
||||||
|
),
|
||||||
|
Benefits: json.RawMessage(
|
||||||
|
`[{"recipient_id": 1, "recipient_first_name": "toto", "recipient_last_name": "titi", "amount": 10, "currency": "EUR"},
|
||||||
|
{"recipient_id": 2, "recipient_first_name": "tata", "recipient_last_name": "titi", "amount": 10, "currency": "EUR"}]`,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
want := &model.ExpenseRetrieved{
|
||||||
|
ID: 123,
|
||||||
|
CreatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
UpdatedAt: time.Date(2000, time.April, 11, 0, 0, 0, 0, time.UTC),
|
||||||
|
Amount: model.Money{Amount: 123, Currency: model.Currency("EUR")},
|
||||||
|
EventID: 123,
|
||||||
|
Detail: model.ExpenseDetail{},
|
||||||
|
Payments: []model.Payment{
|
||||||
|
{
|
||||||
|
PayerID: 1,
|
||||||
|
PayerFirstName: "toto",
|
||||||
|
PayerLastName: "titi",
|
||||||
|
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PayerID: 2,
|
||||||
|
PayerFirstName: "tata",
|
||||||
|
PayerLastName: "titi",
|
||||||
|
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Benefits: []model.Benefit{
|
||||||
|
{
|
||||||
|
RecipientID: 1,
|
||||||
|
RecipientFirstName: "toto",
|
||||||
|
RecipientLastName: "titi",
|
||||||
|
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
RecipientID: 2,
|
||||||
|
RecipientFirstName: "tata",
|
||||||
|
RecipientLastName: "titi",
|
||||||
|
Amount: model.Money{Amount: 10, Currency: model.Currency("EUR")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := convToExpenseRetrieved(input)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
}
|
@ -1,277 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: ./internal/howmuch/adapter/repo/sqlc/querier.go
|
|
||||||
//
|
|
||||||
// Generated by this command:
|
|
||||||
//
|
|
||||||
// mockgen -source=./internal/howmuch/adapter/repo/sqlc/querier.go -package=mock
|
|
||||||
//
|
|
||||||
|
|
||||||
// Package mock is a generated GoMock package.
|
|
||||||
package mock
|
|
||||||
|
|
||||||
import (
|
|
||||||
context "context"
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
sqlc "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
|
||||||
gomock "go.uber.org/mock/gomock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockQuerier is a mock of Querier interface.
|
|
||||||
type MockQuerier struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockQuerierMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockQuerierMockRecorder is the mock recorder for MockQuerier.
|
|
||||||
type MockQuerierMockRecorder struct {
|
|
||||||
mock *MockQuerier
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockQuerier creates a new mock instance.
|
|
||||||
func NewMockQuerier(ctrl *gomock.Controller) *MockQuerier {
|
|
||||||
mock := &MockQuerier{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockQuerierMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockQuerier) EXPECT() *MockQuerierMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExpense mocks base method.
|
|
||||||
func (m *MockQuerier) DeleteExpense(ctx context.Context, id int32) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteExpense", ctx, id)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteExpense indicates an expected call of DeleteExpense.
|
|
||||||
func (mr *MockQuerierMockRecorder) DeleteExpense(ctx, id any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExpense", reflect.TypeOf((*MockQuerier)(nil).DeleteExpense), ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTransactionsOfExpenseID mocks base method.
|
|
||||||
func (m *MockQuerier) DeleteTransactionsOfExpenseID(ctx context.Context, expenseID int32) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "DeleteTransactionsOfExpenseID", ctx, expenseID)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteTransactionsOfExpenseID indicates an expected call of DeleteTransactionsOfExpenseID.
|
|
||||||
func (mr *MockQuerierMockRecorder) DeleteTransactionsOfExpenseID(ctx, expenseID any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTransactionsOfExpenseID", reflect.TypeOf((*MockQuerier)(nil).DeleteTransactionsOfExpenseID), ctx, expenseID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEventByID mocks base method.
|
|
||||||
func (m *MockQuerier) GetEventByID(ctx context.Context, id int32) (sqlc.GetEventByIDRow, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetEventByID", ctx, id)
|
|
||||||
ret0, _ := ret[0].(sqlc.GetEventByIDRow)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEventByID indicates an expected call of GetEventByID.
|
|
||||||
func (mr *MockQuerierMockRecorder) GetEventByID(ctx, id any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEventByID", reflect.TypeOf((*MockQuerier)(nil).GetEventByID), ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExpenseByID mocks base method.
|
|
||||||
func (m *MockQuerier) GetExpenseByID(ctx context.Context, id int32) (sqlc.GetExpenseByIDRow, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetExpenseByID", ctx, id)
|
|
||||||
ret0, _ := ret[0].(sqlc.GetExpenseByIDRow)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExpenseByID indicates an expected call of GetExpenseByID.
|
|
||||||
func (mr *MockQuerierMockRecorder) GetExpenseByID(ctx, id any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetExpenseByID", reflect.TypeOf((*MockQuerier)(nil).GetExpenseByID), ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByEmail mocks base method.
|
|
||||||
func (m *MockQuerier) GetUserByEmail(ctx context.Context, email string) (sqlc.User, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetUserByEmail", ctx, email)
|
|
||||||
ret0, _ := ret[0].(sqlc.User)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByEmail indicates an expected call of GetUserByEmail.
|
|
||||||
func (mr *MockQuerierMockRecorder) GetUserByEmail(ctx, email any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByEmail", reflect.TypeOf((*MockQuerier)(nil).GetUserByEmail), ctx, email)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByID mocks base method.
|
|
||||||
func (m *MockQuerier) GetUserByID(ctx context.Context, id int32) (sqlc.User, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetUserByID", ctx, id)
|
|
||||||
ret0, _ := ret[0].(sqlc.User)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByID indicates an expected call of GetUserByID.
|
|
||||||
func (mr *MockQuerierMockRecorder) GetUserByID(ctx, id any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByID", reflect.TypeOf((*MockQuerier)(nil).GetUserByID), ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertEvent mocks base method.
|
|
||||||
func (m *MockQuerier) InsertEvent(ctx context.Context, arg sqlc.InsertEventParams) (sqlc.Event, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "InsertEvent", ctx, arg)
|
|
||||||
ret0, _ := ret[0].(sqlc.Event)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertEvent indicates an expected call of InsertEvent.
|
|
||||||
func (mr *MockQuerierMockRecorder) InsertEvent(ctx, arg any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertEvent", reflect.TypeOf((*MockQuerier)(nil).InsertEvent), ctx, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertExpense mocks base method.
|
|
||||||
func (m *MockQuerier) InsertExpense(ctx context.Context, arg sqlc.InsertExpenseParams) (sqlc.Expense, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "InsertExpense", ctx, arg)
|
|
||||||
ret0, _ := ret[0].(sqlc.Expense)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertExpense indicates an expected call of InsertExpense.
|
|
||||||
func (mr *MockQuerierMockRecorder) InsertExpense(ctx, arg any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertExpense", reflect.TypeOf((*MockQuerier)(nil).InsertExpense), ctx, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertParticipation mocks base method.
|
|
||||||
func (m *MockQuerier) InsertParticipation(ctx context.Context, arg sqlc.InsertParticipationParams) (sqlc.Participation, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "InsertParticipation", ctx, arg)
|
|
||||||
ret0, _ := ret[0].(sqlc.Participation)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertParticipation indicates an expected call of InsertParticipation.
|
|
||||||
func (mr *MockQuerierMockRecorder) InsertParticipation(ctx, arg any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertParticipation", reflect.TypeOf((*MockQuerier)(nil).InsertParticipation), ctx, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertTransaction mocks base method.
|
|
||||||
func (m *MockQuerier) InsertTransaction(ctx context.Context, arg sqlc.InsertTransactionParams) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "InsertTransaction", ctx, arg)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertTransaction indicates an expected call of InsertTransaction.
|
|
||||||
func (mr *MockQuerierMockRecorder) InsertTransaction(ctx, arg any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertTransaction", reflect.TypeOf((*MockQuerier)(nil).InsertTransaction), ctx, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertUser mocks base method.
|
|
||||||
func (m *MockQuerier) InsertUser(ctx context.Context, arg sqlc.InsertUserParams) (sqlc.User, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "InsertUser", ctx, arg)
|
|
||||||
ret0, _ := ret[0].(sqlc.User)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertUser indicates an expected call of InsertUser.
|
|
||||||
func (mr *MockQuerierMockRecorder) InsertUser(ctx, arg any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertUser", reflect.TypeOf((*MockQuerier)(nil).InsertUser), ctx, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEventsByUserID mocks base method.
|
|
||||||
func (m *MockQuerier) ListEventsByUserID(ctx context.Context, userID int32) ([]sqlc.ListEventsByUserIDRow, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ListEventsByUserID", ctx, userID)
|
|
||||||
ret0, _ := ret[0].([]sqlc.ListEventsByUserIDRow)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEventsByUserID indicates an expected call of ListEventsByUserID.
|
|
||||||
func (mr *MockQuerierMockRecorder) ListEventsByUserID(ctx, userID any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEventsByUserID", reflect.TypeOf((*MockQuerier)(nil).ListEventsByUserID), ctx, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExpensesByEventID mocks base method.
|
|
||||||
func (m *MockQuerier) ListExpensesByEventID(ctx context.Context, id int32) ([]sqlc.Expense, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ListExpensesByEventID", ctx, id)
|
|
||||||
ret0, _ := ret[0].([]sqlc.Expense)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExpensesByEventID indicates an expected call of ListExpensesByEventID.
|
|
||||||
func (mr *MockQuerierMockRecorder) ListExpensesByEventID(ctx, id any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExpensesByEventID", reflect.TypeOf((*MockQuerier)(nil).ListExpensesByEventID), ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExpensesByEventIDByUserID mocks base method.
|
|
||||||
func (m *MockQuerier) ListExpensesByEventIDByUserID(ctx context.Context, id int32) ([]sqlc.Expense, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ListExpensesByEventIDByUserID", ctx, id)
|
|
||||||
ret0, _ := ret[0].([]sqlc.Expense)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListExpensesByEventIDByUserID indicates an expected call of ListExpensesByEventIDByUserID.
|
|
||||||
func (mr *MockQuerierMockRecorder) ListExpensesByEventIDByUserID(ctx, id any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExpensesByEventIDByUserID", reflect.TypeOf((*MockQuerier)(nil).ListExpensesByEventIDByUserID), ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateEventByID mocks base method.
|
|
||||||
func (m *MockQuerier) UpdateEventByID(ctx context.Context, arg sqlc.UpdateEventByIDParams) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UpdateEventByID", ctx, arg)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateEventByID indicates an expected call of UpdateEventByID.
|
|
||||||
func (mr *MockQuerierMockRecorder) UpdateEventByID(ctx, arg any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEventByID", reflect.TypeOf((*MockQuerier)(nil).UpdateEventByID), ctx, arg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExpenseByID mocks base method.
|
|
||||||
func (m *MockQuerier) UpdateExpenseByID(ctx context.Context, arg sqlc.UpdateExpenseByIDParams) (sqlc.Expense, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UpdateExpenseByID", ctx, arg)
|
|
||||||
ret0, _ := ret[0].(sqlc.Expense)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateExpenseByID indicates an expected call of UpdateExpenseByID.
|
|
||||||
func (mr *MockQuerierMockRecorder) UpdateExpenseByID(ctx, arg any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateExpenseByID", reflect.TypeOf((*MockQuerier)(nil).UpdateExpenseByID), ctx, arg)
|
|
||||||
}
|
|
@ -24,14 +24,6 @@ FROM "expense" ex
|
|||||||
JOIN "event" ev ON ev.id = ex.event_id
|
JOIN "event" ev ON ev.id = ex.event_id
|
||||||
WHERE ev.id = $1;
|
WHERE ev.id = $1;
|
||||||
|
|
||||||
-- name: ListExpensesByEventIDByUserID :many
|
|
||||||
SELECT
|
|
||||||
ex.id, ex.created_at, ex.updated_at, ex.amount, ex.currency, ex.event_id,
|
|
||||||
ex.name, ex.place
|
|
||||||
FROM "expense" ex
|
|
||||||
JOIN "event" ev ON ev.id = ex.event_id
|
|
||||||
WHERE ev.id = $1;
|
|
||||||
|
|
||||||
-- name: GetExpenseByID :one
|
-- name: GetExpenseByID :one
|
||||||
WITH payer_transaction as (
|
WITH payer_transaction as (
|
||||||
SELECT pt.expense_id,
|
SELECT pt.expense_id,
|
||||||
|
@ -183,47 +183,6 @@ func (q *Queries) ListExpensesByEventID(ctx context.Context, id int32) ([]Expens
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const listExpensesByEventIDByUserID = `-- name: ListExpensesByEventIDByUserID :many
|
|
||||||
SELECT
|
|
||||||
ex.id, ex.created_at, ex.updated_at, ex.amount, ex.currency, ex.event_id,
|
|
||||||
ex.name, ex.place
|
|
||||||
FROM "expense" ex
|
|
||||||
JOIN "event" ev ON ev.id = ex.event_id
|
|
||||||
WHERE ev.id = $1
|
|
||||||
`
|
|
||||||
|
|
||||||
func (q *Queries) ListExpensesByEventIDByUserID(ctx context.Context, id int32) ([]Expense, error) {
|
|
||||||
rows, err := q.db.QueryContext(ctx, listExpensesByEventIDByUserID, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
var items []Expense
|
|
||||||
for rows.Next() {
|
|
||||||
var i Expense
|
|
||||||
if err := rows.Scan(
|
|
||||||
&i.ID,
|
|
||||||
&i.CreatedAt,
|
|
||||||
&i.UpdatedAt,
|
|
||||||
&i.Amount,
|
|
||||||
&i.Currency,
|
|
||||||
&i.EventID,
|
|
||||||
&i.Name,
|
|
||||||
&i.Place,
|
|
||||||
); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
items = append(items, i)
|
|
||||||
}
|
|
||||||
if err := rows.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateExpenseByID = `-- name: UpdateExpenseByID :one
|
const updateExpenseByID = `-- name: UpdateExpenseByID :one
|
||||||
UPDATE "expense"
|
UPDATE "expense"
|
||||||
SET updated_at = $2, amount = $3, currency = $4, name = $5, place = $6
|
SET updated_at = $2, amount = $3, currency = $4, name = $5, place = $6
|
||||||
|
@ -3,3 +3,8 @@ INSERT INTO participation (
|
|||||||
user_id, event_id, invited_by_user_id, created_at, updated_at
|
user_id, event_id, invited_by_user_id, created_at, updated_at
|
||||||
) VALUES ($1, $2, $3, $4, $5)
|
) VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetParticipation :one
|
||||||
|
SELECT id, user_id, event_id, invited_by_user_id, created_at, updated_at
|
||||||
|
FROM "participation"
|
||||||
|
WHERE user_id = $1 AND event_id = $2;
|
||||||
|
@ -11,6 +11,31 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getParticipation = `-- name: GetParticipation :one
|
||||||
|
SELECT id, user_id, event_id, invited_by_user_id, created_at, updated_at
|
||||||
|
FROM "participation"
|
||||||
|
WHERE user_id = $1 AND event_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetParticipationParams struct {
|
||||||
|
UserID int32
|
||||||
|
EventID int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetParticipation(ctx context.Context, arg GetParticipationParams) (Participation, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getParticipation, arg.UserID, arg.EventID)
|
||||||
|
var i Participation
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.EventID,
|
||||||
|
&i.InvitedByUserID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const insertParticipation = `-- name: InsertParticipation :one
|
const insertParticipation = `-- name: InsertParticipation :one
|
||||||
INSERT INTO participation (
|
INSERT INTO participation (
|
||||||
user_id, event_id, invited_by_user_id, created_at, updated_at
|
user_id, event_id, invited_by_user_id, created_at, updated_at
|
||||||
|
@ -13,37 +13,16 @@ type Querier interface {
|
|||||||
DeleteTransactionsOfExpenseID(ctx context.Context, expenseID int32) error
|
DeleteTransactionsOfExpenseID(ctx context.Context, expenseID int32) error
|
||||||
GetEventByID(ctx context.Context, id int32) (GetEventByIDRow, error)
|
GetEventByID(ctx context.Context, id int32) (GetEventByIDRow, error)
|
||||||
GetExpenseByID(ctx context.Context, id int32) (GetExpenseByIDRow, error)
|
GetExpenseByID(ctx context.Context, id int32) (GetExpenseByIDRow, error)
|
||||||
|
GetParticipation(ctx context.Context, arg GetParticipationParams) (Participation, error)
|
||||||
GetUserByEmail(ctx context.Context, email string) (User, error)
|
GetUserByEmail(ctx context.Context, email string) (User, error)
|
||||||
GetUserByID(ctx context.Context, id int32) (User, error)
|
GetUserByID(ctx context.Context, id int32) (User, error)
|
||||||
InsertEvent(ctx context.Context, arg InsertEventParams) (Event, error)
|
InsertEvent(ctx context.Context, arg InsertEventParams) (Event, error)
|
||||||
InsertExpense(ctx context.Context, arg InsertExpenseParams) (Expense, error)
|
InsertExpense(ctx context.Context, arg InsertExpenseParams) (Expense, error)
|
||||||
InsertParticipation(ctx context.Context, arg InsertParticipationParams) (Participation, error)
|
InsertParticipation(ctx context.Context, arg InsertParticipationParams) (Participation, error)
|
||||||
InsertTransaction(ctx context.Context, arg InsertTransactionParams) error
|
InsertTransaction(ctx context.Context, arg InsertTransactionParams) error
|
||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
|
InsertUser(ctx context.Context, arg InsertUserParams) (User, error)
|
||||||
ListEventsByUserID(ctx context.Context, userID int32) ([]ListEventsByUserIDRow, error)
|
ListEventsByUserID(ctx context.Context, userID int32) ([]ListEventsByUserIDRow, error)
|
||||||
ListExpensesByEventID(ctx context.Context, id int32) ([]Expense, error)
|
ListExpensesByEventID(ctx context.Context, id int32) ([]Expense, error)
|
||||||
ListExpensesByEventIDByUserID(ctx context.Context, id int32) ([]Expense, error)
|
|
||||||
UpdateEventByID(ctx context.Context, arg UpdateEventByIDParams) error
|
UpdateEventByID(ctx context.Context, arg UpdateEventByIDParams) error
|
||||||
UpdateExpenseByID(ctx context.Context, arg UpdateExpenseByIDParams) (Expense, error)
|
UpdateExpenseByID(ctx context.Context, arg UpdateExpenseByIDParams) (Expense, error)
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,3 @@
|
|||||||
-- MIT License
|
|
||||||
--
|
|
||||||
-- Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
--
|
|
||||||
-- 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.
|
|
||||||
|
|
||||||
-- name: InsertUser :one
|
-- name: InsertUser :one
|
||||||
INSERT INTO "user" (
|
INSERT INTO "user" (
|
||||||
email, first_name, last_name, password, created_at, updated_at
|
email, first_name, last_name, password, created_at, updated_at
|
||||||
|
@ -53,7 +53,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int32) (User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertUser = `-- name: InsertUser :one
|
const insertUser = `-- name: InsertUser :one
|
||||||
|
|
||||||
INSERT INTO "user" (
|
INSERT INTO "user" (
|
||||||
email, first_name, last_name, password, created_at, updated_at
|
email, first_name, last_name, password, created_at, updated_at
|
||||||
) VALUES ( $1, $2, $3, $4, $5, $6 )
|
) VALUES ( $1, $2, $3, $4, $5, $6 )
|
||||||
@ -69,27 +68,6 @@ type InsertUserParams struct {
|
|||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// MIT License
|
|
||||||
//
|
|
||||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
|
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (User, error) {
|
||||||
row := q.db.QueryRowContext(ctx, insertUser,
|
row := q.db.QueryRowContext(ctx, insertUser,
|
||||||
arg.Email,
|
arg.Email,
|
||||||
|
@ -31,47 +31,37 @@ import (
|
|||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo/sqlc"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model"
|
||||||
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
"git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/repo"
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type userRepository struct {
|
type userRepository struct {
|
||||||
db *sql.DB
|
queries *sqlc.Queries
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertTimeout = 1 * time.Second
|
|
||||||
|
|
||||||
func NewUserRepository(db *sql.DB) repo.UserRepository {
|
func NewUserRepository(db *sql.DB) repo.UserRepository {
|
||||||
return &userRepository{
|
return &userRepository{
|
||||||
db: db,
|
queries: sqlc.New(db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
func (ur *userRepository) Create(
|
func (u *userRepository) Create(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
transaction interface{},
|
userEntity *model.UserEntity,
|
||||||
u *model.UserEntity,
|
tx any,
|
||||||
) (*model.UserEntity, error) {
|
) (*model.UserEntity, error) {
|
||||||
timeoutCtx, cancel := context.WithTimeout(ctx, insertTimeout)
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
args := sqlc.InsertUserParams{
|
queries := getQueries(u.queries, tx)
|
||||||
Email: u.Email,
|
|
||||||
FirstName: u.FirstName,
|
userDB, err := queries.InsertUser(timeoutCtx, sqlc.InsertUserParams{
|
||||||
LastName: u.LastName,
|
Email: userEntity.Email,
|
||||||
Password: u.Password,
|
FirstName: userEntity.FirstName,
|
||||||
|
LastName: userEntity.LastName,
|
||||||
|
Password: userEntity.Password,
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
})
|
||||||
|
|
||||||
tx, ok := transaction.(*sql.Tx)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("transaction is not a pgx.Tx")
|
|
||||||
}
|
|
||||||
|
|
||||||
queries := sqlc.New(tx)
|
|
||||||
|
|
||||||
userDB, err := queries.InsertUser(timeoutCtx, args)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -88,10 +78,18 @@ func (ur *userRepository) Create(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetByEmail if not found, return nil for user but not error.
|
// GetByEmail if not found, return nil for user but not error.
|
||||||
func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.UserEntity, error) {
|
func (u *userRepository) GetByEmail(
|
||||||
queries := sqlc.New(ur.db)
|
ctx context.Context,
|
||||||
userDB, err := queries.GetUserByEmail(ctx, email)
|
email string,
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
tx any,
|
||||||
|
) (*model.UserEntity, error) {
|
||||||
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
queries := getQueries(u.queries, tx)
|
||||||
|
|
||||||
|
userDB, err := queries.GetUserByEmail(timeoutCtx, email)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
// No query error, but user not found
|
// No query error, but user not found
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@ -109,10 +107,14 @@ func (ur *userRepository) GetByEmail(ctx context.Context, email string) (*model.
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ur *userRepository) GetByID(ctx context.Context, id int) (*model.UserEntity, error) {
|
func (u *userRepository) GetByID(ctx context.Context, id int, tx any) (*model.UserEntity, error) {
|
||||||
queries := sqlc.New(ur.db)
|
timeoutCtx, cancel := context.WithTimeout(ctx, queryTimeout)
|
||||||
userDB, err := queries.GetUserByID(ctx, int32(id))
|
defer cancel()
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
|
||||||
|
queries := getQueries(u.queries, tx)
|
||||||
|
|
||||||
|
userDB, err := queries.GetUserByID(timeoutCtx, int32(id))
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
// No query error, but user not found
|
// No query error, but user not found
|
||||||
return nil, nil
|
return nil, nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -36,7 +36,7 @@ type EventCreateRequest struct {
|
|||||||
// }}}
|
// }}}
|
||||||
// {{{ Response View Object (from service to controller)
|
// {{{ Response View Object (from service to controller)
|
||||||
|
|
||||||
type EventBaseItemResponse struct {
|
type EventListResponse struct {
|
||||||
ID int
|
ID int
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
@ -57,20 +57,12 @@ type EventInfoResponse struct {
|
|||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
|
|
||||||
Users []*UserBaseResponse
|
Users []UserBaseResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Entity (DB In)
|
// {{{ Entity (DB In)
|
||||||
|
|
||||||
type EventBaseItemEntity struct {
|
|
||||||
ID int
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
OwnerID int
|
|
||||||
CreatedAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventEntity struct {
|
type EventEntity struct {
|
||||||
ID int
|
ID int
|
||||||
|
|
||||||
@ -84,6 +76,14 @@ type EventEntity struct {
|
|||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventUpdateEntity struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
CreatedAt time.Time
|
||||||
|
// TODO: maybe I can change owner too
|
||||||
|
}
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Retrieved (DB out)
|
// {{{ Retrieved (DB out)
|
||||||
|
|
||||||
@ -93,7 +93,7 @@ type EventRetrieved struct {
|
|||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
|
|
||||||
Users []*UserBaseRetrieved
|
Users []UserBaseRetrieved
|
||||||
|
|
||||||
TotalAmount Money
|
TotalAmount Money
|
||||||
DefaultCurrency Currency
|
DefaultCurrency Currency
|
||||||
@ -103,6 +103,14 @@ type EventRetrieved struct {
|
|||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventListRetrieved struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
CreatedAt time.Time
|
||||||
|
Owner *UserBaseRetrieved
|
||||||
|
}
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ DO Domain Object (Contains the domain service)
|
// {{{ DO Domain Object (Contains the domain service)
|
||||||
|
|
||||||
@ -113,9 +121,9 @@ type Event struct {
|
|||||||
Description string
|
Description string
|
||||||
|
|
||||||
// lazy get using participation join
|
// lazy get using participation join
|
||||||
Users []*UserDO
|
Users []UserDO
|
||||||
// lazy get
|
// lazy get
|
||||||
Expenses []*Expense
|
Expenses []Expense
|
||||||
|
|
||||||
TotalAmount Money
|
TotalAmount Money
|
||||||
DefaultCurrency Currency
|
DefaultCurrency Currency
|
||||||
|
@ -37,7 +37,17 @@ type ExpenseRequest struct {
|
|||||||
// }}}
|
// }}}
|
||||||
// {{{ Response
|
// {{{ Response
|
||||||
|
|
||||||
type ExpensesListResponse struct {
|
type (
|
||||||
|
ExpenseGetResponse Expense
|
||||||
|
ExpenseResponse ExpenseRetrieved
|
||||||
|
)
|
||||||
|
|
||||||
|
// }}}
|
||||||
|
// {{{ Retrieved
|
||||||
|
|
||||||
|
type ExpenseRetrieved Expense
|
||||||
|
|
||||||
|
type ExpensesListRetrieved struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@ -48,12 +58,21 @@ type ExpensesListResponse struct {
|
|||||||
Detail ExpenseDetail `json:"detail"`
|
Detail ExpenseDetail `json:"detail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseGetResponse Expense
|
type PaymentRetrieved struct {
|
||||||
|
PayerID int `json:"payer_id"`
|
||||||
|
PayerFirstName string `json:"payer_first_name"`
|
||||||
|
PayerLastName string `json:"payer_last_name"`
|
||||||
|
Amount int `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
}
|
||||||
|
|
||||||
// }}}
|
type BenefitRetrieved struct {
|
||||||
// {{{ Retrieved
|
RecipientID int `json:"recipient_id"`
|
||||||
|
RecipientFirstName string `json:"recipient_first_name"`
|
||||||
type ExpensesListRetrieved ExpensesListResponse
|
RecipientLastName string `json:"recipient_last_name"`
|
||||||
|
Amount int `json:"amount"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
}
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Entity
|
// {{{ Entity
|
||||||
@ -72,6 +91,18 @@ type ExpenseEntity struct {
|
|||||||
Place string
|
Place string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExpenseUpdateEntity struct {
|
||||||
|
ID int
|
||||||
|
UpdatedAt time.Time
|
||||||
|
|
||||||
|
Amount int
|
||||||
|
Currency string
|
||||||
|
|
||||||
|
// Expense Detail
|
||||||
|
Name string
|
||||||
|
Place string
|
||||||
|
}
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
// {{{ Domain Models
|
// {{{ Domain Models
|
||||||
|
|
||||||
|
@ -22,7 +22,9 @@
|
|||||||
|
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
type DBRepository interface {
|
type DBRepository interface {
|
||||||
Transaction(
|
Transaction(
|
||||||
|
@ -29,37 +29,46 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type EventRepository interface {
|
type EventRepository interface {
|
||||||
Create(ctx context.Context, evEntity *model.EventEntity) (*model.EventEntity, error)
|
Create(ctx context.Context, evEntity *model.EventEntity, tx any) (*model.EventEntity, error)
|
||||||
|
|
||||||
// UpdateInfo updates the event related information (name, descriptions)
|
// UpdateEventByID updates the event related information (name, descriptions)
|
||||||
UpdateInfo()
|
UpdateEventByID(ctx context.Context, event *model.EventUpdateEntity, tx any) error
|
||||||
|
|
||||||
Delete() // XXX: Pay attention to the foreign key relationships
|
GetByID(ctx context.Context, eventID int, tx any) (*model.EventRetrieved, error)
|
||||||
|
|
||||||
GetByID(ctx context.Context, eventID int) (*model.EventRetrieved, error)
|
|
||||||
|
|
||||||
ListExpensesByUserID()
|
|
||||||
|
|
||||||
// related to events of a user
|
// related to events of a user
|
||||||
ListEventsByUserID(ctx context.Context, userID int) ([]model.EventBaseItemEntity, error)
|
ListEventsByUserID(ctx context.Context, userID int, tx any) ([]model.EventListRetrieved, error)
|
||||||
|
|
||||||
|
InsertParticipation(
|
||||||
|
ctx context.Context,
|
||||||
|
userID, eventID, invitedByUserID int,
|
||||||
|
tx any,
|
||||||
|
) (*model.ParticipationEntity, error)
|
||||||
|
|
||||||
|
GetParticipation(
|
||||||
|
ctx context.Context,
|
||||||
|
userID, eventID int,
|
||||||
|
tx any,
|
||||||
|
) (*model.ParticipationEntity, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRepository interface {
|
type ExpenseRepository interface {
|
||||||
Create()
|
DeleteExpense(ctx context.Context, expenseID int, tx any) error
|
||||||
Update()
|
DeleteTransactionsOfExpense(ctx context.Context, expenseID int, tx any) error
|
||||||
Delete() // Delete also the related transactions
|
GetExpenseByID(ctx context.Context, expenseID int, tx any) (*model.ExpenseRetrieved, error)
|
||||||
|
InsertExpense(
|
||||||
GetByID()
|
ctx context.Context,
|
||||||
}
|
expenseEntity *model.ExpenseEntity,
|
||||||
|
tx any,
|
||||||
type ParticipationRepository interface {
|
) (*model.ExpenseEntity, error)
|
||||||
Create()
|
ListExpensesByEventID(
|
||||||
Delete()
|
ctx context.Context,
|
||||||
CheckParticipation(ctx context.Context, userID, eventID int) error
|
id int,
|
||||||
}
|
tx any,
|
||||||
|
) ([]model.ExpensesListRetrieved, error)
|
||||||
type TransactionRepository interface {
|
UpdateExpenseByID(
|
||||||
Create()
|
ctx context.Context,
|
||||||
// Delete() might be handled in the Expense
|
expenseUpdate *model.ExpenseUpdateEntity,
|
||||||
// Transaction is a joined entity, we don't provide diret read operation
|
tx any,
|
||||||
|
) (*model.ExpenseEntity, error)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserRepository interface {
|
type UserRepository interface {
|
||||||
Create(ctx context.Context, transaction interface{}, u *model.UserEntity) (*model.UserEntity, error)
|
Create(
|
||||||
GetByEmail(ctx context.Context, email string) (*model.UserEntity, error)
|
ctx context.Context,
|
||||||
GetByID(ctx context.Context, id int) (*model.UserEntity, error)
|
u *model.UserEntity,
|
||||||
|
tx any,
|
||||||
|
) (*model.UserEntity, error)
|
||||||
|
GetByEmail(ctx context.Context, email string, tx any) (*model.UserEntity, error)
|
||||||
|
GetByID(ctx context.Context, id int, tx any) (*model.UserEntity, error)
|
||||||
}
|
}
|
||||||
|
@ -36,8 +36,6 @@ type eventUsecase struct {
|
|||||||
userUC User
|
userUC User
|
||||||
eventRepo repo.EventRepository
|
eventRepo repo.EventRepository
|
||||||
expenseRepo repo.ExpenseRepository
|
expenseRepo repo.ExpenseRepository
|
||||||
participationRepo repo.ParticipationRepository
|
|
||||||
transactionRepo repo.TransactionRepository
|
|
||||||
|
|
||||||
dbRepo repo.DBRepository
|
dbRepo repo.DBRepository
|
||||||
}
|
}
|
||||||
@ -55,18 +53,16 @@ func NewEventUsecase(
|
|||||||
uuc User,
|
uuc User,
|
||||||
ev repo.EventRepository,
|
ev repo.EventRepository,
|
||||||
ex repo.ExpenseRepository,
|
ex repo.ExpenseRepository,
|
||||||
pa repo.ParticipationRepository, // XXX: Might be handled in event
|
|
||||||
tr repo.TransactionRepository, // XXX: Might be handled in event
|
|
||||||
db repo.DBRepository,
|
db repo.DBRepository,
|
||||||
) Event {
|
) Event {
|
||||||
return &eventUsecase{uuc, ev, ex, pa, tr, db}
|
return &eventUsecase{uuc, ev, ex, db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (evuc *eventUsecase) CreateEvent(
|
func (evuc *eventUsecase) CreateEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
evRequest *model.EventCreateRequest,
|
evRequest *model.EventCreateRequest,
|
||||||
) (*model.EventInfoResponse, error) {
|
) (*model.EventInfoResponse, error) {
|
||||||
// transfer evRequest to PO
|
// transfer evRequest to evEntity
|
||||||
|
|
||||||
evEntity := &model.EventEntity{
|
evEntity := &model.EventEntity{
|
||||||
Name: evRequest.Name,
|
Name: evRequest.Name,
|
||||||
@ -78,12 +74,37 @@ func (evuc *eventUsecase) CreateEvent(
|
|||||||
|
|
||||||
data, err := evuc.dbRepo.Transaction(
|
data, err := evuc.dbRepo.Transaction(
|
||||||
ctx,
|
ctx,
|
||||||
func(txCtx context.Context, tx interface{}) (interface{}, error) {
|
func(txCtx context.Context, tx any) (any, error) {
|
||||||
created, err := evuc.eventRepo.Create(ctx, evEntity)
|
// Create the event
|
||||||
|
created, err := evuc.eventRepo.Create(ctx, evEntity, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// participate to the event
|
||||||
|
participation, err := evuc.eventRepo.InsertParticipation(
|
||||||
|
ctx,
|
||||||
|
created.OwnerID,
|
||||||
|
created.ID,
|
||||||
|
0,
|
||||||
|
tx,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if participation == nil {
|
||||||
|
// Unexpected error
|
||||||
|
log.ErrorLog(
|
||||||
|
"participation existed for event-user pair",
|
||||||
|
"userID",
|
||||||
|
created.OwnerID,
|
||||||
|
"eventID",
|
||||||
|
created.ID,
|
||||||
|
)
|
||||||
|
return nil, errno.InternalServerErr
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: App log, maybe can be sent to some third party service.
|
// TODO: App log, maybe can be sent to some third party service.
|
||||||
log.InfoLog(
|
log.InfoLog(
|
||||||
"created new event",
|
"created new event",
|
||||||
@ -93,6 +114,7 @@ func (evuc *eventUsecase) CreateEvent(
|
|||||||
created.OwnerID,
|
created.OwnerID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Construct the response
|
||||||
ownerResponse, err := evuc.userUC.GetUserBaseResponseByID(ctx, created.OwnerID)
|
ownerResponse, err := evuc.userUC.GetUserBaseResponseByID(ctx, created.OwnerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -108,9 +130,12 @@ func (evuc *eventUsecase) CreateEvent(
|
|||||||
),
|
),
|
||||||
Owner: ownerResponse,
|
Owner: ownerResponse,
|
||||||
CreatedAt: created.CreatedAt,
|
CreatedAt: created.CreatedAt,
|
||||||
|
UpdatedAt: created.UpdatedAt,
|
||||||
|
Users: []model.UserBaseResponse{*ownerResponse},
|
||||||
}
|
}
|
||||||
return evResponse, err
|
return evResponse, err
|
||||||
})
|
},
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -123,8 +148,28 @@ func (evuc *eventUsecase) CreateEvent(
|
|||||||
func (evuc *eventUsecase) ListEvents(
|
func (evuc *eventUsecase) ListEvents(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID int,
|
userID int,
|
||||||
) ([]model.EventBaseItemResponse, error) {
|
) ([]model.EventListResponse, error) {
|
||||||
return nil, nil
|
eventListRetrieved, err := evuc.eventRepo.ListEventsByUserID(ctx, userID, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user is a member of the event
|
||||||
|
|
||||||
|
responses := make([]model.EventListResponse, len(eventListRetrieved))
|
||||||
|
|
||||||
|
for i, retrieved := range eventListRetrieved {
|
||||||
|
ownner := model.UserBaseResponse(*retrieved.Owner)
|
||||||
|
res := model.EventListResponse{
|
||||||
|
ID: retrieved.ID,
|
||||||
|
Name: retrieved.Name,
|
||||||
|
Description: retrieved.Description,
|
||||||
|
Owner: &ownner,
|
||||||
|
CreatedAt: retrieved.CreatedAt,
|
||||||
|
}
|
||||||
|
responses[i] = res
|
||||||
|
}
|
||||||
|
return responses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEventDetail
|
// GetEventDetail
|
||||||
@ -133,10 +178,10 @@ func (evuc *eventUsecase) GetEventDetail(
|
|||||||
userID, eventID int,
|
userID, eventID int,
|
||||||
) (*model.EventInfoResponse, error) {
|
) (*model.EventInfoResponse, error) {
|
||||||
// Check if the user has the right to get this event
|
// Check if the user has the right to get this event
|
||||||
err := evuc.participationRepo.CheckParticipation(ctx, userID, eventID)
|
// err := evuc.participationRepo.CheckParticipation(ctx, userID, eventID)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return nil, ErrNoParticipation
|
// return nil, ErrNoParticipation
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Get the eventDetail
|
// Get the eventDetail
|
||||||
// TODO: This can also be put into the cache
|
// TODO: This can also be put into the cache
|
||||||
|
@ -22,7 +22,9 @@
|
|||||||
|
|
||||||
package repomock
|
package repomock
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
type TestDBRepository struct{}
|
type TestDBRepository struct{}
|
||||||
|
|
||||||
|
@ -36,8 +36,8 @@ type TestUserRepository struct{}
|
|||||||
|
|
||||||
func (tur *TestUserRepository) Create(
|
func (tur *TestUserRepository) Create(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
transaction interface{},
|
|
||||||
u *model.UserEntity,
|
u *model.UserEntity,
|
||||||
|
tx any,
|
||||||
) (*model.UserEntity, error) {
|
) (*model.UserEntity, error) {
|
||||||
user := *u
|
user := *u
|
||||||
|
|
||||||
@ -53,6 +53,7 @@ func (tur *TestUserRepository) Create(
|
|||||||
func (tur *TestUserRepository) GetByEmail(
|
func (tur *TestUserRepository) GetByEmail(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
email string,
|
email string,
|
||||||
|
tx any,
|
||||||
) (*model.UserEntity, error) {
|
) (*model.UserEntity, error) {
|
||||||
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
|
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
|
||||||
switch email {
|
switch email {
|
||||||
@ -71,7 +72,11 @@ func (tur *TestUserRepository) GetByEmail(
|
|||||||
return nil, UserTestDummyErr
|
return nil, UserTestDummyErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tur *TestUserRepository) GetByID(ctx context.Context, id int) (*model.UserEntity, error) {
|
func (tur *TestUserRepository) GetByID(
|
||||||
|
ctx context.Context,
|
||||||
|
id int,
|
||||||
|
tx any,
|
||||||
|
) (*model.UserEntity, error) {
|
||||||
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
|
hashedPwd, _ := bcrypt.GenerateFromPassword([]byte("strongHashed"), 12)
|
||||||
switch id {
|
switch id {
|
||||||
case 123:
|
case 123:
|
||||||
|
@ -86,12 +86,12 @@ func (uuc *userUsecase) Create(
|
|||||||
data, err := uuc.dbRepo.Transaction(
|
data, err := uuc.dbRepo.Transaction(
|
||||||
ctx,
|
ctx,
|
||||||
func(txCtx context.Context, tx interface{}) (interface{}, error) {
|
func(txCtx context.Context, tx interface{}) (interface{}, error) {
|
||||||
created, err := uuc.userRepo.Create(txCtx, tx, &model.UserEntity{
|
created, err := uuc.userRepo.Create(txCtx, &model.UserEntity{
|
||||||
Email: u.Email,
|
Email: u.Email,
|
||||||
Password: u.Password,
|
Password: u.Password,
|
||||||
FirstName: u.FirstName,
|
FirstName: u.FirstName,
|
||||||
LastName: u.LastName,
|
LastName: u.LastName,
|
||||||
})
|
}, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
match, _ := regexp.MatchString("SQLSTATE 23505", err.Error())
|
match, _ := regexp.MatchString("SQLSTATE 23505", err.Error())
|
||||||
if match {
|
if match {
|
||||||
@ -132,7 +132,7 @@ func (uuc *userUsecase) Create(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistRequest) error {
|
func (uuc *userUsecase) Exist(ctx context.Context, u *model.UserExistRequest) error {
|
||||||
got, err := uuc.userRepo.GetByEmail(ctx, u.Email)
|
got, err := uuc.userRepo.GetByEmail(ctx, u.Email, nil)
|
||||||
// Any query error?
|
// Any query error?
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -160,7 +160,7 @@ func (uuc *userUsecase) GetUserBaseResponseByID(
|
|||||||
// If not exists, get from the DB. And then put back
|
// If not exists, get from the DB. And then put back
|
||||||
// into the cache with a timeout.
|
// into the cache with a timeout.
|
||||||
// Refresh the cache when the user data is updated (for now it cannot be updated)
|
// Refresh the cache when the user data is updated (for now it cannot be updated)
|
||||||
got, err := uuc.userRepo.GetByID(ctx, userID)
|
got, err := uuc.userRepo.GetByID(ctx, userID, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
43
web/package-lock.json
generated
43
web/package-lock.json
generated
@ -8,6 +8,7 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.7.7",
|
||||||
"vue": "^3.4.29",
|
"vue": "^3.4.29",
|
||||||
"vue-router": "^4.3.3"
|
"vue-router": "^4.3.3"
|
||||||
},
|
},
|
||||||
@ -2299,8 +2300,17 @@
|
|||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
"dev": true
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.7.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||||
|
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -2497,7 +2507,6 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
},
|
},
|
||||||
@ -2710,7 +2719,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
@ -3256,6 +3264,25 @@
|
|||||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||||
@ -3276,7 +3303,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
@ -4041,7 +4067,6 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@ -4050,7 +4075,6 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
},
|
},
|
||||||
@ -4614,6 +4638,11 @@
|
|||||||
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
"integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/psl": {
|
"node_modules/psl": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.7.7",
|
||||||
"vue": "^3.4.29",
|
"vue": "^3.4.29",
|
||||||
"vue-router": "^4.3.3"
|
"vue-router": "^4.3.3"
|
||||||
},
|
},
|
||||||
|
@ -37,6 +37,8 @@ import HelloWorld from './components/HelloWorld.vue'
|
|||||||
<nav>
|
<nav>
|
||||||
<RouterLink to="/">Home</RouterLink>
|
<RouterLink to="/">Home</RouterLink>
|
||||||
<RouterLink to="/about">About</RouterLink>
|
<RouterLink to="/about">About</RouterLink>
|
||||||
|
<RouterLink to="/login">Login</RouterLink>
|
||||||
|
<RouterLink to="/Signup">Signup</RouterLink>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -40,6 +40,16 @@ const router = createRouter({
|
|||||||
// this generates a separate chunk (About.[hash].js) for this route
|
// this generates a separate chunk (About.[hash].js) for this route
|
||||||
// which is lazy-loaded when the route is visited.
|
// which is lazy-loaded when the route is visited.
|
||||||
component: () => import('../views/AboutView.vue')
|
component: () => import('../views/AboutView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'login',
|
||||||
|
component: () => import('../views/LoginView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/signup',
|
||||||
|
name: 'signup',
|
||||||
|
component: () => import('../views/SignupView.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
83
web/src/views/LoginView.vue
Normal file
83
web/src/views/LoginView.vue
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<!--
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||||
|
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<form @submit.prevent="handleLogin">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email:</label>
|
||||||
|
<input type="text" id="email" v-model="email" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" v-model="password" required />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://localhost:8000/v1/session/create', {
|
||||||
|
email: email.value,
|
||||||
|
password: password.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
// Clear error message
|
||||||
|
error.value = ''
|
||||||
|
// Redirect to dashboard or another route
|
||||||
|
router.push('/about')
|
||||||
|
} else {
|
||||||
|
error.value = 'Invalid email or password'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'An error occurred. Please try again.'
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.about {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
95
web/src/views/SignupView.vue
Normal file
95
web/src/views/SignupView.vue
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<!--
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
|
||||||
|
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="signup-container">
|
||||||
|
<h2>Signup</h2>
|
||||||
|
<form @submit.prevent="handleSignup">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email:</label>
|
||||||
|
<input type="text" id="email" v-model="email" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password:</label>
|
||||||
|
<input type="password" id="password" v-model="password" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="first_name">First Name:</label>
|
||||||
|
<input type="first_name" id="first_name" v-model="first_name" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="last_name">Last Name:</label>
|
||||||
|
<input type="last_name" id="last_name" v-model="last_name" required />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Signup</button>
|
||||||
|
</form>
|
||||||
|
<p v-if="error" class="error">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const first_name = ref('')
|
||||||
|
const last_name = ref('')
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleSignup = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('http://localhost:8000/v1/user/create', {
|
||||||
|
email: email.value,
|
||||||
|
password: password.value,
|
||||||
|
first_name: first_name.value,
|
||||||
|
last_name: last_name.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
// Clear error message
|
||||||
|
error.value = ''
|
||||||
|
// Redirect to dashboard or another route
|
||||||
|
router.push('/login')
|
||||||
|
} else {
|
||||||
|
error.value = 'Failed to signup'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'An error occurred. Please try again.'
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.about {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Reference in New Issue
Block a user