Skip to content

Commit eb0fbff

Browse files
committed
Add more API's
1 parent 37fb426 commit eb0fbff

22 files changed

+581
-10
lines changed

api/pkg/di/container.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ func (container *Container) DB() (db *gorm.DB) {
139139
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Heartbeat{})))
140140
}
141141

142+
if err = db.AutoMigrate(&entities.User{}); err != nil {
143+
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.User{})))
144+
}
145+
146+
if err = db.AutoMigrate(&entities.Phone{}); err != nil {
147+
container.logger.Fatal(stacktrace.Propagate(err, fmt.Sprintf("cannot migrate %T", &entities.Phone{})))
148+
}
149+
142150
return container.db
143151
}
144152

@@ -228,6 +236,15 @@ func (container *Container) MessageThreadHandlerValidator() (validator *validato
228236
)
229237
}
230238

239+
// PhoneHandlerValidator creates a new instance of validators.PhoneHandlerValidator
240+
func (container *Container) PhoneHandlerValidator() (validator *validators.PhoneHandlerValidator) {
241+
container.logger.Debug(fmt.Sprintf("creating %T", validator))
242+
return validators.NewPhoneHandlerValidator(
243+
container.Logger(),
244+
container.Tracer(),
245+
)
246+
}
247+
231248
// EventDispatcher creates a new instance of services.EventDispatcher
232249
func (container *Container) EventDispatcher() (dispatcher *services.EventDispatcher) {
233250
if container.eventDispatcher != nil {
@@ -255,6 +272,16 @@ func (container *Container) MessageRepository() (repository repositories.Message
255272
)
256273
}
257274

275+
// PhoneRepository creates a new instance of repositories.PhoneRepository
276+
func (container *Container) PhoneRepository() (repository repositories.PhoneRepository) {
277+
container.logger.Debug("creating GORM repositories.PhoneRepository")
278+
return repositories.NewGormPhoneRepository(
279+
container.Logger(),
280+
container.Tracer(),
281+
container.DB(),
282+
)
283+
}
284+
258285
// MessageThreadRepository creates a new instance of repositories.MessageThreadRepository
259286
func (container *Container) MessageThreadRepository() (repository repositories.MessageThreadRepository) {
260287
container.logger.Debug("creating GORM repositories.MessageThreadRepository")
@@ -295,6 +322,16 @@ func (container *Container) HeartbeatService() (service *services.HeartbeatServi
295322
)
296323
}
297324

325+
// PhoneService creates a new instance of services.PhoneService
326+
func (container *Container) PhoneService() (service *services.PhoneService) {
327+
container.logger.Debug(fmt.Sprintf("creating %T", service))
328+
return services.NewPhoneService(
329+
container.Logger(),
330+
container.Tracer(),
331+
container.PhoneRepository(),
332+
)
333+
}
334+
298335
// UserService creates a new instance of services.UserService
299336
func (container *Container) UserService() (service *services.UserService) {
300337
container.logger.Debug(fmt.Sprintf("creating %T", service))
@@ -336,6 +373,17 @@ func (container *Container) UserHandler() (handler *handlers.UserHandler) {
336373
)
337374
}
338375

376+
// PhoneHandler creates a new instance of handlers.PhoneHandler
377+
func (container *Container) PhoneHandler() (handler *handlers.PhoneHandler) {
378+
container.logger.Debug(fmt.Sprintf("creating %T", handler))
379+
return handlers.NewPhoneHandler(
380+
container.Logger(),
381+
container.Tracer(),
382+
container.PhoneService(),
383+
container.PhoneHandlerValidator(),
384+
)
385+
}
386+
339387
// RegisterMessageListeners registers event listeners for listeners.MessageListener
340388
func (container *Container) RegisterMessageListeners() {
341389
container.logger.Debug(fmt.Sprintf("registering listners for %T", listeners.MessageListener{}))
@@ -409,6 +457,12 @@ func (container *Container) RegisterHeartbeatRoutes() {
409457
container.HeartbeatHandler().RegisterRoutes(container.App().Group("v1"))
410458
}
411459

460+
// RegisterPhoneRoutes registers routes for the /phone prefix
461+
func (container *Container) RegisterPhoneRoutes() {
462+
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.PhoneHandler{}))
463+
container.PhoneHandler().RegisterRoutes(container.App().Group("v1"))
464+
}
465+
412466
// RegisterUserRoutes registers routes for the /users prefix
413467
func (container *Container) RegisterUserRoutes() {
414468
container.logger.Debug(fmt.Sprintf("registering %T routes", &handlers.UserHandler{}))

api/pkg/entities/Phone.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package entities
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
// Phone represents an android phone which has installed the http sms app
10+
type Phone struct {
11+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
12+
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
13+
FcmToken *string `json:"fcm_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzd....."`
14+
PhoneNumber string `json:"phone_number" example:"+18005550199"`
15+
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
16+
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
17+
}

api/pkg/entities/heartbeat.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
type Heartbeat struct {
1111
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
1212
Owner string `json:"owner" gorm:"index:idx_heartbeats_owner_timestamp" example:"+18005550199"`
13+
UserID UserID `json:"userID" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
1314
Timestamp time.Time `json:"timestamp" gorm:"index:idx_heartbeats_owner_timestamp" example:"2022-06-05T14:26:01.520828+03:00"`
1415
Quantity int `json:"quantity" example:"2"`
1516
}

api/pkg/entities/message.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ const (
5757
// Message represents a message sent between 2 phone numbers
5858
type Message struct {
5959
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
60-
Owner string `json:"owner" gorm:"index:idx_messages_owner__contact" example:"+18005550199"`
61-
Contact string `json:"contact" gorm:"index:idx_messages_owner__contact" example:"+18005550100"`
60+
Owner string `json:"owner" gorm:"index:idx_messages_user_id__owner__contact" example:"+18005550199"`
61+
UserID UserID `json:"user_id" gorm:"index:idx_messages_user_id__owner__contact" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
62+
Contact string `json:"contact" gorm:"index:idx_messages_user_id__owner__contact" example:"+18005550100"`
6263
Content string `json:"content" example:"This is a sample text message"`
6364
Type MessageType `json:"type" example:"mobile-terminated"`
6465
Status MessageStatus `json:"status" gorm:"index:idx_messages_status" example:"pending"`

api/pkg/entities/message_thread.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ type MessageThread struct {
1111
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703ca"`
1212
Owner string `json:"owner" example:"+18005550199"`
1313
Contact string `json:"contact" example:"+18005550100"`
14+
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
1415
Color string `json:"color" example:"indigo"`
1516
LastMessageContent string `json:"last_message_content" example:"This is a sample message content"`
1617
LastMessageID uuid.UUID `json:"last_message_id" example:"32343a19-da5e-4b1b-a767-3298a73703ca"`

api/pkg/handlers/message_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ func (h *MessageHandler) Index(c *fiber.Ctx) error {
184184
}
185185

186186
// PostEvent registers an event on a message
187-
// @Summary Store an event for a message on the mobile phone
187+
// @Summary Upsert an event for a message on the mobile phone
188188
// @Description Use this endpoint to send events for a message when it is failed, sent or delivered by the mobile phone.
189189
// @Security ApiKeyAuth
190190
// @Tags Messages

api/pkg/handlers/phone_handler.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package handlers
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/NdoleStudio/http-sms-manager/pkg/requests"
7+
"github.com/NdoleStudio/http-sms-manager/pkg/validators"
8+
"github.com/davecgh/go-spew/spew"
9+
10+
"github.com/NdoleStudio/http-sms-manager/pkg/services"
11+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
12+
"github.com/gofiber/fiber/v2"
13+
"github.com/palantir/stacktrace"
14+
)
15+
16+
// PhoneHandler handles phone http requests.
17+
type PhoneHandler struct {
18+
handler
19+
logger telemetry.Logger
20+
tracer telemetry.Tracer
21+
service *services.PhoneService
22+
validator *validators.PhoneHandlerValidator
23+
}
24+
25+
// NewPhoneHandler creates a new PhoneHandler
26+
func NewPhoneHandler(
27+
logger telemetry.Logger,
28+
tracer telemetry.Tracer,
29+
service *services.PhoneService,
30+
validator *validators.PhoneHandlerValidator,
31+
) (h *PhoneHandler) {
32+
return &PhoneHandler{
33+
logger: logger.WithService(fmt.Sprintf("%T", h)),
34+
tracer: tracer,
35+
validator: validator,
36+
service: service,
37+
}
38+
}
39+
40+
// RegisterRoutes registers the routes for the PhoneHandler
41+
func (h *PhoneHandler) RegisterRoutes(router fiber.Router) {
42+
router.Get("/phones", h.Index)
43+
router.Put("/phones/:phoneID", h.Upsert)
44+
}
45+
46+
// Index returns the phones of a user
47+
// @Summary Get phones of a user
48+
// @Description Get list of phones which a user has registered on the http sms application
49+
// @Security ApiKeyAuth
50+
// @Tags Phones
51+
// @Accept json
52+
// @Produce json
53+
// @Param skip query int false "number of heartbeats to skip" minimum(0)
54+
// @Param query query string false "filter phones containing query"
55+
// @Param limit query int false "number of phones to return" minimum(1) maximum(20)
56+
// @Success 200 {object} responses.PhonesResponse
57+
// @Failure 400 {object} responses.BadRequest
58+
// @Failure 403 {object} responses.Unauthorized
59+
// @Failure 422 {object} responses.UnprocessableEntity
60+
// @Failure 500 {object} responses.InternalServerError
61+
// @Router /phones [get]
62+
func (h *PhoneHandler) Index(c *fiber.Ctx) error {
63+
ctx, span := h.tracer.StartFromFiberCtx(c)
64+
defer span.End()
65+
66+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
67+
68+
var request requests.PhoneIndex
69+
if err := c.QueryParser(&request); err != nil {
70+
msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request)
71+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
72+
return h.responseBadRequest(c, err)
73+
}
74+
75+
if errors := h.validator.ValidateIndex(ctx, request.Sanitize()); len(errors) != 0 {
76+
msg := fmt.Sprintf("validation errors [%s], while fetching phones [%+#v]", spew.Sdump(errors), request)
77+
ctxLogger.Warn(stacktrace.NewError(msg))
78+
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching phones")
79+
}
80+
81+
phones, err := h.service.Index(ctx, h.userFromContext(c), request.ToIndexParams())
82+
if err != nil {
83+
msg := fmt.Sprintf("cannot index phones with params [%+#v]", request)
84+
ctxLogger.Error(stacktrace.Propagate(err, msg))
85+
return h.responseInternalServerError(c)
86+
}
87+
88+
return h.responseOK(c, fmt.Sprintf("fetched %d %s", len(*phones), h.pluralize("phone", len(*phones))), phones)
89+
}
90+
91+
// Upsert a phone
92+
// @Summary Upsert Phone
93+
// @Description Updates properties of a user's phone. If the phone with this number does not exist, a new one will be created. Think of this method like an 'upsert'
94+
// @Security ApiKeyAuth
95+
// @Tags Phones
96+
// @Accept json
97+
// @Produce json
98+
// @Param payload body requests.PhoneUpsert true "Payload of new phone number."
99+
// @Success 200 {object} responses.PhoneResponse
100+
// @Failure 400 {object} responses.BadRequest
101+
// @Failure 403 {object} responses.Unauthorized
102+
// @Failure 422 {object} responses.UnprocessableEntity
103+
// @Failure 500 {object} responses.InternalServerError
104+
// @Router /phones [put]
105+
func (h *PhoneHandler) Upsert(c *fiber.Ctx) error {
106+
ctx, span := h.tracer.StartFromFiberCtx(c)
107+
defer span.End()
108+
109+
ctxLogger := h.tracer.CtxLogger(h.logger, span)
110+
111+
var request requests.PhoneUpsert
112+
if err := c.QueryParser(&request); err != nil {
113+
msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request)
114+
ctxLogger.Warn(stacktrace.Propagate(err, msg))
115+
return h.responseBadRequest(c, err)
116+
}
117+
118+
if errors := h.validator.ValidateUpsert(ctx, request.Sanitize()); len(errors) != 0 {
119+
msg := fmt.Sprintf("validation errors [%s], while fetching phones [%+#v]", spew.Sdump(errors), request)
120+
ctxLogger.Warn(stacktrace.NewError(msg))
121+
return h.responseUnprocessableEntity(c, errors, "validation errors while fetching phones")
122+
}
123+
124+
phone, err := h.service.Upsert(ctx, request.ToUpsertParams(h.userFromContext(c)))
125+
if err != nil {
126+
msg := fmt.Sprintf("cannot update phones with params [%+#v]", request)
127+
ctxLogger.Error(stacktrace.Propagate(err, msg))
128+
return h.responseInternalServerError(c)
129+
}
130+
131+
return h.responseOK(c, "phone updated successfully", phone)
132+
}

api/pkg/handlers/user_handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ func (h *UserHandler) RegisterRoutes(router fiber.Router) {
3939
// @Summary Get current user
4040
// @Description Get details of the currently authenticated user
4141
// @Security ApiKeyAuth
42-
// @Tags Messages
42+
// @Tags Users
4343
// @Accept json
4444
// @Produce json
45-
// @Success 200 {object} responses.MessagesResponse
45+
// @Success 200 {object} responses.UserResponse
4646
// @Failure 400 {object} responses.BadRequest
4747
// @Failure 403 {object} responses.Unauthorized
4848
// @Failure 422 {object} responses.UnprocessableEntity
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package repositories
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/NdoleStudio/http-sms-manager/pkg/entities"
9+
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
10+
"github.com/cockroachdb/cockroach-go/v2/crdb/crdbgorm"
11+
"github.com/palantir/stacktrace"
12+
"gorm.io/gorm"
13+
)
14+
15+
// gormPhoneRepository is responsible for persisting entities.Phone
16+
type gormPhoneRepository struct {
17+
logger telemetry.Logger
18+
tracer telemetry.Tracer
19+
db *gorm.DB
20+
}
21+
22+
func (repository *gormPhoneRepository) Upsert(ctx context.Context, phone *entities.Phone) error {
23+
ctx, span := repository.tracer.Start(ctx)
24+
defer span.End()
25+
26+
err := crdbgorm.ExecuteTx(ctx, repository.db, nil, func(tx *gorm.DB) error {
27+
existingPhone := new(entities.Phone)
28+
29+
err := tx.Model(&phone).
30+
Where("user_id = ?", phone.UserID).
31+
Where("phone_number = ?", phone.PhoneNumber).
32+
First(existingPhone).
33+
Error
34+
35+
if err == nil {
36+
existingPhone.FcmToken = phone.FcmToken
37+
if err = tx.Save(existingPhone).Error; err != nil {
38+
return stacktrace.Propagate(err, fmt.Sprintf("cannot update exiting phone [%s]", existingPhone.ID))
39+
}
40+
*phone = *existingPhone
41+
return nil
42+
}
43+
44+
if !errors.Is(err, gorm.ErrRecordNotFound) {
45+
msg := fmt.Sprintf("cannot find phone with user_Id [%s] and phone_number [%s]", phone.UserID, phone.PhoneNumber)
46+
return stacktrace.Propagate(err, msg)
47+
}
48+
49+
return tx.Create(phone).Error
50+
})
51+
if err != nil {
52+
msg := fmt.Sprintf("cannot upsert phone with params [%+#v]", err)
53+
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
54+
}
55+
56+
return nil
57+
}
58+
59+
func (repository *gormPhoneRepository) Index(ctx context.Context, userID entities.UserID, params IndexParams) (*[]entities.Phone, error) {
60+
ctx, span := repository.tracer.Start(ctx)
61+
defer span.End()
62+
63+
query := repository.db.Where("user_id = ?", userID)
64+
if len(params.Query) > 0 {
65+
queryPattern := "%" + params.Query + "%"
66+
query.Where("phone_number ILIKE ?", queryPattern)
67+
}
68+
69+
phones := new([]entities.Phone)
70+
if err := query.Order("created_at DESC").Limit(params.Limit).Offset(params.Skip).Find(&phones).Error; err != nil {
71+
msg := fmt.Sprintf("cannot fetch phones with userID [%s] and params [%+#v]", userID, params)
72+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
73+
}
74+
75+
return phones, nil
76+
}
77+
78+
// NewGormPhoneRepository creates the GORM version of the PhoneRepository
79+
func NewGormPhoneRepository(
80+
logger telemetry.Logger,
81+
tracer telemetry.Tracer,
82+
db *gorm.DB,
83+
) PhoneRepository {
84+
return &gormPhoneRepository{
85+
logger: logger.WithService(fmt.Sprintf("%T", &gormPhoneRepository{})),
86+
tracer: tracer,
87+
db: db,
88+
}
89+
}

api/pkg/repositories/gorm_user_repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func NewGormUserRepository(
3030
db *gorm.DB,
3131
) UserRepository {
3232
return &gormUserRepository{
33-
logger: logger.WithService(fmt.Sprintf("%T", &gormHeartbeatRepository{})),
33+
logger: logger.WithService(fmt.Sprintf("%T", &gormUserRepository{})),
3434
tracer: tracer,
3535
db: db,
3636
}

0 commit comments

Comments
 (0)