Skip to content

Preserve alias casing in chat fetch query #1696

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

foqc
Copy link

@foqc foqc commented Jul 7, 2025

Bug Fix: Inconsistent Column Alias Casing in fetchChats

Bug description: #1691

Context

In PostgreSQL, unquoted column aliases are automatically lowercased. In our channel.service.ts → fetchChats query, there was a mix of quoted and unquoted aliases, causing PostgreSQL to return fields like updatedat and windowstart instead of the expected updatedAt and windowStart.
This inconsistency broke or skipped field mappings in the application layer.

Additionally, the pagination logic was applied twice due to redundant filtering, which led to incorrect result sets when paginating chat data.


Changes Made

  • Quoted all column aliases consistently in the SQL query to preserve camelCase casing.
  • Removed duplicate pagination filter, ensuring correct OFFSET and LIMIT behavior for chat pagination.

Affected Environments

  • Evolution API: v2.3.0 (main branch)
  • Node.js: v20
  • Database: PostgreSQL (Evolution schema)
  • Caching: Redis
  • Deployed via Docker

Outcome

  • Chat fetch mappings now work correctly, returning all fields in expected casing.
  • Pagination is accurate, with no overlapping or missing items across pages.

Summary by Sourcery

Fix inconsistent column alias casing and eliminate duplicate pagination logic in the chat fetch query to restore proper field mappings and accurate paging.

Bug Fixes:

  • Quote all column aliases in the fetchChats SQL query to preserve expected camelCase field names
  • Remove redundant pagination filtering to ensure correct OFFSET and LIMIT behavior for chat data

Copy link
Contributor

sourcery-ai bot commented Jul 7, 2025

Reviewer's Guide

This PR standardizes column alias casing in the fetchChats SQL query by quoting all aliases to preserve camelCase, updates the mapping layer to consume the newly cased fields, and removes a duplicate client-side pagination slice to rely solely on DB OFFSET/LIMIT.

Entity relationship diagram for quoted SQL aliases in chat fetch

erDiagram
    CONTACT {
        STRING contactId
        STRING remoteJid
        STRING pushName
        STRING profilePicUrl
        DATE updatedAt
        DATE windowStart
        DATE windowExpires
        BOOLEAN windowActive
        NUMBER unreadMessages
    }
    MESSAGE {
        STRING id
        OBJECT key
        STRING pushName
        STRING participant
        STRING messageType
        STRING message
        OBJECT contextInfo
        STRING source
        DATE messageTimestamp
        STRING instanceId
        STRING sessionId
        STRING status
    }
    CONTACT ||--o{ MESSAGE : has_lastMessage
Loading

Class diagram for updated chat fetch result mapping

classDiagram
    class ContactResult {
        +String contactId
        +String remoteJid
        +String pushName
        +String profilePicUrl
        +Date updatedAt
        +Date windowStart
        +Date windowExpires
        +Boolean windowActive
        +Number unreadMessages
        +LastMessage lastMessage
        +Number unreadCount
        +Boolean isSaved
    }
    class LastMessage {
        +String id
        +Object key
        +String pushName
        +String participant
        +String messageType
        +String message
        +Object contextInfo
        +String source
        +Date messageTimestamp
        +String instanceId
        +String sessionId
        +String status
    }
    ContactResult --> LastMessage : lastMessage
Loading

File-Level Changes

Change Details Files
Consistently quote SQL column aliases to preserve camelCase
  • Wrapped all selected aliases in double quotes
  • Added alias for unreadMessages
  • Standardized quoting for all message-related aliases
src/api/services/channel.service.ts
Update result mapping to use camelCase properties
  • Replaced lowercase property accesses with camelCase (e.g., lastmessageid → lastMessageId)
  • Mapped unreadMessages to unreadCount
  • Preserved camelCase for all contact fields
src/api/services/channel.service.ts
Remove redundant manual pagination slicing
  • Deleted the client-side skip/take slice block
  • Relied solely on DB OFFSET and LIMIT for pagination
src/api/services/channel.service.ts

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @foqc - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 773 to 806
const mappedResults = results.map((contact) => {
const lastMessage = contact.lastmessageid
const lastMessage = contact.lastMessageId
? {
id: contact.lastmessageid,
key: contact.lastmessage_key,
pushName: contact.lastmessagepushname,
participant: contact.lastmessageparticipant,
messageType: contact.lastmessagemessagetype,
message: contact.lastmessagemessage,
contextInfo: contact.lastmessagecontextinfo,
source: contact.lastmessagesource,
messageTimestamp: contact.lastmessagemessagetimestamp,
instanceId: contact.lastmessageinstanceid,
sessionId: contact.lastmessagesessionid,
status: contact.lastmessagestatus,
id: contact.lastMessageId,
key: contact.lastMessage_key,
pushName: contact.lastMessagePushName,
participant: contact.lastMessageParticipant,
messageType: contact.lastMessageMessageType,
message: contact.lastMessageMessage,
contextInfo: contact.lastMessageContextInfo,
source: contact.lastMessageSource,
messageTimestamp: contact.lastMessageMessageTimestamp,
instanceId: contact.lastMessageInstanceId,
sessionId: contact.lastMessageSessionId,
status: contact.lastMessageStatus,
}
: undefined;

return {
id: contact.contactid || null,
remoteJid: contact.remotejid,
pushName: contact.pushname,
profilePicUrl: contact.profilepicurl,
updatedAt: contact.updatedat,
windowStart: contact.windowstart,
windowExpires: contact.windowexpires,
windowActive: contact.windowactive,
id: contact.contactId || null,
remoteJid: contact.remoteJid,
pushName: contact.pushName,
profilePicUrl: contact.profilePicUrl,
updatedAt: contact.updatedAt,
windowStart: contact.windowStart,
windowExpires: contact.windowExpires,
windowActive: contact.windowActive,
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
unreadCount: 0,
isSaved: !!contact.contactid,
unreadCount: contact.unreadMessages,
isSaved: !!contact.contactId,
};
});

if (query?.take && query?.skip) {
const skip = query.skip || 0;
const take = query.take || 20;
return mappedResults.slice(skip, skip + take);
}

return mappedResults;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Inline variable that is immediately returned (inline-immediately-returned-variable)

Suggested change
const mappedResults = results.map((contact) => {
const lastMessage = contact.lastmessageid
const lastMessage = contact.lastMessageId
? {
id: contact.lastmessageid,
key: contact.lastmessage_key,
pushName: contact.lastmessagepushname,
participant: contact.lastmessageparticipant,
messageType: contact.lastmessagemessagetype,
message: contact.lastmessagemessage,
contextInfo: contact.lastmessagecontextinfo,
source: contact.lastmessagesource,
messageTimestamp: contact.lastmessagemessagetimestamp,
instanceId: contact.lastmessageinstanceid,
sessionId: contact.lastmessagesessionid,
status: contact.lastmessagestatus,
id: contact.lastMessageId,
key: contact.lastMessage_key,
pushName: contact.lastMessagePushName,
participant: contact.lastMessageParticipant,
messageType: contact.lastMessageMessageType,
message: contact.lastMessageMessage,
contextInfo: contact.lastMessageContextInfo,
source: contact.lastMessageSource,
messageTimestamp: contact.lastMessageMessageTimestamp,
instanceId: contact.lastMessageInstanceId,
sessionId: contact.lastMessageSessionId,
status: contact.lastMessageStatus,
}
: undefined;
return {
id: contact.contactid || null,
remoteJid: contact.remotejid,
pushName: contact.pushname,
profilePicUrl: contact.profilepicurl,
updatedAt: contact.updatedat,
windowStart: contact.windowstart,
windowExpires: contact.windowexpires,
windowActive: contact.windowactive,
id: contact.contactId || null,
remoteJid: contact.remoteJid,
pushName: contact.pushName,
profilePicUrl: contact.profilePicUrl,
updatedAt: contact.updatedAt,
windowStart: contact.windowStart,
windowExpires: contact.windowExpires,
windowActive: contact.windowActive,
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
unreadCount: 0,
isSaved: !!contact.contactid,
unreadCount: contact.unreadMessages,
isSaved: !!contact.contactId,
};
});
if (query?.take && query?.skip) {
const skip = query.skip || 0;
const take = query.take || 20;
return mappedResults.slice(skip, skip + take);
}
return mappedResults;
return results.map((contact) => {
const lastMessage = contact.lastMessageId
? {
id: contact.lastMessageId,
key: contact.lastMessage_key,
pushName: contact.lastMessagePushName,
participant: contact.lastMessageParticipant,
messageType: contact.lastMessageMessageType,
message: contact.lastMessageMessage,
contextInfo: contact.lastMessageContextInfo,
source: contact.lastMessageSource,
messageTimestamp: contact.lastMessageMessageTimestamp,
instanceId: contact.lastMessageInstanceId,
sessionId: contact.lastMessageSessionId,
status: contact.lastMessageStatus,
}
: undefined;
return {
id: contact.contactId || null,
remoteJid: contact.remoteJid,
pushName: contact.pushName,
profilePicUrl: contact.profilePicUrl,
updatedAt: contact.updatedAt,
windowStart: contact.windowStart,
windowExpires: contact.windowExpires,
windowActive: contact.windowActive,
lastMessage: lastMessage ? this.cleanMessageData(lastMessage) : undefined,
unreadCount: contact.unreadMessages,
isSaved: !!contact.contactId,
};
});


ExplanationSomething that we often see in people's code is assigning to a result variable
and then immediately returning it.

Returning the result directly shortens the code and removes an unnecessary
variable, reducing the mental load of reading the function.

Where intermediate variables can be useful is if they then get used as a
parameter or a condition, and the name can act like a comment on what the
variable represents. In the case where you're returning it from a function, the
function name is there to tell you what the result is, so the variable name
is unnecessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant