fix: pagination, adds total pages count and total rows count to results.

This commit is contained in:
Abhinav Raut
2024-11-03 01:35:16 +05:30
parent ed314eb1a5
commit 4f4d79409c
10 changed files with 119 additions and 84 deletions

View File

@@ -11,7 +11,7 @@ import (
"github.com/zerodha/fastglue"
)
// handleGetAllConversations retrieves all conversations with pagination, sorting, and filtering.
// handleGetAllConversations retrieves all conversations.
func handleGetAllConversations(r *fastglue.Request) error {
var (
app = r.Context.(*App)
@@ -20,12 +20,22 @@ func handleGetAllConversations(r *fastglue.Request) error {
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
filters = string(r.RequestCtx.QueryArgs().Peek("filters"))
total = 0
)
c, err := app.conversation.GetAllConversationsList(order, orderBy, filters, page, pageSize)
conversations, pageSize, err := app.conversation.GetAllConversationsList(order, orderBy, filters, page, pageSize)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(c)
if len(conversations) > 0 {
total = conversations[0].Total
}
return r.SendEnvelope(envelope.PageResults{
Results: conversations,
Total: total,
PerPage: pageSize,
TotalPages: total / pageSize,
Page: page,
})
}
// handleGetAssignedConversations retrieves conversations assigned to the current user.
@@ -38,12 +48,22 @@ func handleGetAssignedConversations(r *fastglue.Request) error {
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
filters = string(r.RequestCtx.QueryArgs().Peek("filters"))
total = 0
)
c, err := app.conversation.GetAssignedConversationsList(user.ID, order, orderBy, filters, page, pageSize)
conversations, pageSize, err := app.conversation.GetAssignedConversationsList(user.ID, order, orderBy, filters, page, pageSize)
if err != nil {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "")
}
return r.SendEnvelope(c)
if len(conversations) > 0 {
total = conversations[0].Total
}
return r.SendEnvelope(envelope.PageResults{
Results: conversations,
Total: total,
PerPage: pageSize,
TotalPages: total / pageSize,
Page: page,
})
}
// handleGetUnassignedConversations retrieves unassigned conversations.
@@ -56,12 +76,22 @@ func handleGetUnassignedConversations(r *fastglue.Request) error {
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
filters = string(r.RequestCtx.QueryArgs().Peek("filters"))
total = 0
)
c, err := app.conversation.GetUnassignedConversationsList(user.ID, order, orderBy, filters, page, pageSize)
conversations, pageSize, err := app.conversation.GetUnassignedConversationsList(user.ID, order, orderBy, filters, page, pageSize)
if err != nil {
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, err.Error(), nil, "")
}
return r.SendEnvelope(c)
if len(conversations) > 0 {
total = conversations[0].Total
}
return r.SendEnvelope(envelope.PageResults{
Results: conversations,
Total: total,
PerPage: pageSize,
TotalPages: total / pageSize,
Page: page,
})
}
// handleGetConversation retrieves a single conversation by UUID with permission checks.

View File

@@ -5,6 +5,7 @@ import (
"github.com/abhinavxd/artemis/internal/conversation"
cmodels "github.com/abhinavxd/artemis/internal/conversation/models"
"github.com/abhinavxd/artemis/internal/envelope"
medModels "github.com/abhinavxd/artemis/internal/media/models"
umodels "github.com/abhinavxd/artemis/internal/user/models"
"github.com/valyala/fasthttp"
@@ -24,6 +25,7 @@ func handleGetMessages(r *fastglue.Request) error {
user = r.RequestCtx.UserValue("user").(umodels.User)
page, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page")))
pageSize, _ = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("page_size")))
total = 0
)
// Check permission
@@ -32,17 +34,24 @@ func handleGetMessages(r *fastglue.Request) error {
return sendErrorEnvelope(r, err)
}
messages, err := app.conversation.GetConversationMessages(uuid, page, pageSize)
messages, pageSize, err := app.conversation.GetConversationMessages(uuid, page, pageSize)
if err != nil {
return sendErrorEnvelope(r, err)
}
for i := range messages {
total = messages[i].Total
for j := range messages[i].Attachments {
messages[i].Attachments[j].URL = app.media.GetURL(messages[i].Attachments[j].UUID)
}
}
return r.SendEnvelope(messages)
return r.SendEnvelope(envelope.PageResults{
Total: total,
Results: messages,
Page: page,
PerPage: pageSize,
TotalPages: total / pageSize,
})
}
func handleGetMessage(r *fastglue.Request) error {

View File

@@ -66,7 +66,7 @@ const nonInlineAttachments = computed(() =>
)
const getFullName = computed(() => {
return convStore.current.contact.first_name + ' ' + convStore.current.last_name
return convStore.current.contact.first_name + ' ' + convStore.current.contact.last_name
})
const avatarFallback = computed(() => {

View File

@@ -146,29 +146,19 @@ export const useConversationStore = defineStore('conversation', () => {
messages.loading = true
try {
const response = await api.getConversationMessages(uuid, messages.page)
const fetchedMessages = response.data?.data || []
const result = response.data?.data || {}
const results = result.results || []
// Filter out messages already seen.
const newMessages = fetchedMessages.filter((message) => {
const newMessages = results.filter((message) => {
if (!seenMessageUUIDs.has(message.uuid)) {
seenMessageUUIDs.add(message.uuid)
return true
}
return false
})
if (newMessages.length === 0 && messages.page === 1) {
messages.data = []
return
}
if (newMessages.length === 0 && messages.page > 1) {
messages.hasMore = false
}
// Add new messages to the messages state.
if (newMessages.length === 0 && messages.page === 1) messages.data = []
if (result.total_pages <= messages.page) messages.hasMore = false
messages.data.unshift(...newMessages)
} catch (error) {
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {
title: 'Something went wrong',
@@ -250,22 +240,17 @@ export const useConversationStore = defineStore('conversation', () => {
default:
return
}
if (response?.data?.data) {
const newConversations = response.data.data.filter((conversation) => {
if (!seenConversationUUIDs.has(conversation.uuid)) {
seenConversationUUIDs.set(conversation.uuid, true)
return true
}
return false
})
if (!conversations.data) conversations.data = []
if (newConversations.length === 0) conversations.hasMore = false
conversations.data.push(...newConversations)
} else {
conversations.hasMore = false
}
const apiResponse = response.data.data
const newConversations = apiResponse.results.filter((conversation) => {
if (!seenConversationUUIDs.has(conversation.uuid)) {
seenConversationUUIDs.set(conversation.uuid, true)
return true
}
return false
})
if (apiResponse.total_pages <= conversations.page) conversations.hasMore = false
if (!conversations.data) conversations.data = []
conversations.data.push(...newConversations)
} catch (error) {
conversations.errorMessage = handleHTTPError(error).message
emitter.emit(EMITTER_EVENTS.SHOW_TOAST, {

View File

@@ -289,59 +289,48 @@ func (c *Manager) GetConversationUUID(id int) (string, error) {
}
// GetAllConversationsList retrieves all conversations with optional filtering, ordering, and pagination.
func (c *Manager) GetAllConversationsList(order, orderBy, filters string, page, pageSize int) ([]models.Conversation, error) {
func (c *Manager) GetAllConversationsList(order, orderBy, filters string, page, pageSize int) ([]models.Conversation, int, error) {
return c.GetConversations(0, models.AllConversations, order, orderBy, filters, page, pageSize)
}
// GetAssignedConversationsList retrieves conversations assigned to a specific user with optional filtering, ordering, and pagination.
func (c *Manager) GetAssignedConversationsList(userID int, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, error) {
func (c *Manager) GetAssignedConversationsList(userID int, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, int, error) {
return c.GetConversations(userID, models.AssignedConversations, order, orderBy, filters, page, pageSize)
}
// GetUnassignedConversationsList retrieves conversations assigned to a team the user is part of with optional filtering, ordering, and pagination.
func (c *Manager) GetUnassignedConversationsList(userID int, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, error) {
func (c *Manager) GetUnassignedConversationsList(userID int, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, int, error) {
return c.GetConversations(userID, models.UnassignedConversations, order, orderBy, filters, page, pageSize)
}
// GetConversations retrieves conversations list based on user ID, type, and optional filtering, ordering, and pagination.
func (c *Manager) GetConversations(userID int, listType, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, error) {
func (c *Manager) GetConversations(userID int, listType, order, orderBy, filters string, page, pageSize int) ([]models.Conversation, int, error) {
var conversations = make([]models.Conversation, 0)
if orderBy == "" {
orderBy = "last_message_at"
}
if order == "" {
order = "DESC"
}
if filters == "" {
filters = "[]"
}
query, qArgs, err := c.makeConversationsListQuery(userID, c.q.GetConversations, listType, order, orderBy, page, pageSize, filters)
query, pageSize, qArgs, err := c.makeConversationsListQuery(userID, c.q.GetConversations, listType, order, orderBy, page, pageSize, filters)
if err != nil {
return conversations, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil)
return conversations, pageSize, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil)
}
tx, err := c.db.BeginTxx(context.Background(), nil)
defer tx.Rollback()
if err != nil {
c.lo.Error("error preparing get conversations query", "error", err)
return conversations, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil)
return conversations, pageSize, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil)
}
if err := tx.Select(&conversations, query, qArgs...); err != nil {
c.lo.Error("error fetching conversations", "error", err)
return conversations, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil)
return conversations, pageSize, envelope.NewError(envelope.GeneralError, c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.entities.conversations}"), nil)
}
return conversations, nil
return conversations, pageSize, nil
}
// GetConversationsListUUIDs retrieves the UUIDs of conversations list.
func (c *Manager) GetConversationsListUUIDs(userID, page, pageSize int, typ string) ([]string, error) {
var ids = make([]string, 0)
query, qArgs, err := c.makeConversationsListQuery(userID, c.q.GetConversationsListUUIDs, typ, "", "", page, pageSize, "")
query, _, qArgs, err := c.makeConversationsListQuery(userID, c.q.GetConversationsListUUIDs, typ, "", "", page, pageSize, "")
if err != nil {
c.lo.Error("error generating conversations query", "error", err)
return ids, err
@@ -560,12 +549,27 @@ func (t *Manager) UpsertConversationTags(uuid string, tagIDs []int) error {
}
// makeConversationsListQuery prepares a SQL query string for conversations list
func (c *Manager) makeConversationsListQuery(userID int, baseQuery, listType, order, orderBy string, page, pageSize int, filtersJSON string) (string, []interface{}, error) {
func (c *Manager) makeConversationsListQuery(userID int, baseQuery, listType, order, orderBy string, page, pageSize int, filtersJSON string) (string, int, []interface{}, error) {
var qArgs []interface{}
if orderBy == "" {
orderBy = "last_message_at"
}
if order == "" {
order = "DESC"
}
if filtersJSON == "" {
filtersJSON = "[]"
}
if pageSize > conversationsListMaxPageSize {
pageSize = conversationsListMaxPageSize
}
if pageSize < 1 {
pageSize = 10
}
if page < 1 {
page = 1
}
// Set condition based on the list type.
switch listType {
@@ -578,7 +582,7 @@ func (c *Manager) makeConversationsListQuery(userID int, baseQuery, listType, or
case models.AllConversations:
baseQuery = fmt.Sprintf(baseQuery, "")
default:
return "", nil, fmt.Errorf("invalid conversation type %s", listType)
return "", pageSize, nil, fmt.Errorf("invalid conversation type %s", listType)
}
query, qArgs, err := dbutil.PaginateAndFilterQuery(baseQuery, qArgs, dbutil.PaginationOptions{
@@ -591,10 +595,10 @@ func (c *Manager) makeConversationsListQuery(userID int, baseQuery, listType, or
})
if err != nil {
c.lo.Error("error preparing query", "error", err)
return "", nil, err
return "", pageSize, nil, err
}
return query, qArgs, err
return query, pageSize, qArgs, err
}
// GetToAddress retrieves the recipient addresses for a conversation.

View File

@@ -212,32 +212,32 @@ func (m *Manager) RenderContentInTemplate(channel string, message *models.Messag
}
// GetConversationMessages retrieves messages for a specific conversation.
func (m *Manager) GetConversationMessages(conversationUUID string, page, pageSize int) ([]models.Message, error) {
func (m *Manager) GetConversationMessages(conversationUUID string, page, pageSize int) ([]models.Message, int, error) {
var (
messages = make([]models.Message, 0)
qArgs []interface{}
)
qArgs = append(qArgs, conversationUUID)
query, qArgs, err := m.generateMessagesQuery(m.q.GetMessages, qArgs, page, pageSize)
query, pageSize, qArgs, err := m.generateMessagesQuery(m.q.GetMessages, qArgs, page, pageSize)
if err != nil {
m.lo.Error("error generating messages query", "error", err)
return messages, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil)
return messages, pageSize, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil)
}
tx, err := m.db.BeginTxx(context.Background(), nil)
defer tx.Rollback()
if err != nil {
m.lo.Error("error preparing get messages query", "error", err)
return messages, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil)
return messages, pageSize, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil)
}
if err := tx.Select(&messages, query, qArgs...); err != nil {
m.lo.Error("error fetching conversations", "error", err)
return messages, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil)
return messages, pageSize, envelope.NewError(envelope.GeneralError, "Error fetching messages", nil)
}
return messages, nil
return messages, pageSize, nil
}
// GetMessage retrieves a message by UUID.
@@ -489,14 +489,16 @@ func (m *Manager) GetConversationByMessageID(id int) (models.Conversation, error
}
// generateMessagesQuery generates the SQL query for fetching messages in a conversation.
func (c *Manager) generateMessagesQuery(baseQuery string, qArgs []interface{}, page, pageSize int) (string, []interface{}, error) {
// Set default values for page and page size if they are invalid
func (c *Manager) generateMessagesQuery(baseQuery string, qArgs []interface{}, page, pageSize int) (string, int, []interface{}, error) {
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > maxMessagesPerPage {
if pageSize > maxMessagesPerPage {
pageSize = maxMessagesPerPage
}
if pageSize <= 0 {
pageSize = 10
}
// Calculate the offset
offset := (page - 1) * pageSize
@@ -506,7 +508,7 @@ func (c *Manager) generateMessagesQuery(baseQuery string, qArgs []interface{}, p
// Include LIMIT and OFFSET in the SQL query
sqlQuery := fmt.Sprintf(baseQuery, fmt.Sprintf("LIMIT $%d OFFSET $%d", len(qArgs)-1, len(qArgs)))
return sqlQuery, qArgs, nil
return sqlQuery, pageSize, qArgs, nil
}
// uploadMessageAttachments uploads attachments for a message.

View File

@@ -44,6 +44,7 @@ type Conversation struct {
LastMessageAt null.Time `db:"last_message_at" json:"last_message_at"`
LastMessage string `db:"last_message" json:"last_message"`
Contact cmodels.Contact `db:"contact" json:"contact"`
Total int `db:"total" json:"-"`
}
type ConversationParticipant struct {
@@ -93,6 +94,7 @@ type Message struct {
InReplyTo string `json:"-"`
Headers textproto.MIMEHeader `json:"-"`
Media []mmodels.Media `db:"-" json:"-"`
Total int `db:"total" json:"-"`
}
// IncomingMessage links a message with the contact information and inbox id.

View File

@@ -9,6 +9,7 @@ RETURNING id, uuid;
-- name: get-conversations
SELECT
COUNT(*) OVER() AS total,
conversations.updated_at,
conversations.uuid,
conversations.assignee_last_seen_at,
@@ -368,6 +369,7 @@ attachments AS (
GROUP BY message_id
)
SELECT
COUNT(*) OVER() AS total,
m.created_at,
m.updated_at,
m.status,

View File

@@ -47,14 +47,6 @@ func PaginateAndFilterQuery(baseQuery string, existingArgs []interface{}, opts P
return "", nil, err
}
// Validate and set default values for pagination
if opts.Page <= 0 {
opts.Page = 1
}
if opts.PageSize <= 0 {
opts.PageSize = 10 // Default page size
}
// Calculate offset
offset := (opts.Page - 1) * opts.PageSize

View File

@@ -32,6 +32,15 @@ func (e Error) Error() string {
return e.Message
}
// PageResults is a generic struct for paginated results.
type PageResults struct {
Results interface{} `json:"results"`
Total int `json:"total"`
PerPage int `json:"per_page"`
TotalPages int `json:"total_pages"`
Page int `json:"page"`
}
// NewError creates and returns a new instance of Error with custom error metadata.
func NewError(etype string, message string, data interface{}) error {
err := Error{