Reapply "Build post chat and attachment workflows"
Some checks failed
CI / frontend (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / backend (push) Has been cancelled

This reverts commit 5bc4a551af.
This commit is contained in:
Cauê Faleiros
2026-06-10 09:24:44 -03:00
parent 5bc4a551af
commit 3314c2e863
30 changed files with 3021 additions and 204 deletions

View File

@@ -0,0 +1,377 @@
package calendar
import (
"context"
"errors"
"time"
"github.com/jackc/pgx/v5"
)
var ErrChatMessageNotFound = errors.New("chat message not found")
const (
ChatChannelExternal = "external"
ChatChannelInternal = "internal"
)
type ChatMessage struct {
ID string `json:"id"`
CalendarItemID string `json:"calendar_item_id"`
Channel string `json:"channel"`
Body string `json:"body"`
CreatedBy *string `json:"created_by"`
AuthorName string `json:"author_name"`
AuthorRole string `json:"author_role"`
CreatedAt time.Time `json:"created_at"`
EditedAt *time.Time `json:"edited_at"`
DeletedAt *time.Time `json:"deleted_at"`
Attachments []ChatAttachment `json:"attachments"`
}
type ChatAttachment struct {
ID string `json:"id"`
ChatMessageID string `json:"chat_message_id"`
OriginalFilename string `json:"original_filename"`
StoredFilename string `json:"stored_filename"`
MimeType string `json:"mime_type"`
SizeBytes int64 `json:"size_bytes"`
StorageDriver string `json:"storage_driver"`
StoragePath string `json:"-"`
CreatedBy *string `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
}
type CreateChatMessageInput struct {
CalendarItemID string
Channel string
Body string
CreatedBy string
}
type CreateChatAttachmentInput struct {
ChatMessageID string
OriginalFilename string
StoredFilename string
MimeType string
SizeBytes int64
StorageDriver string
StoragePath string
CreatedBy string
}
func (s Store) ListChatMessages(ctx context.Context, calendarItemID string, channel string) ([]ChatMessage, error) {
rows, err := s.db.Query(ctx, `
SELECT
calendar_item_chat_messages.id::text,
calendar_item_chat_messages.calendar_item_id::text,
calendar_item_chat_messages.channel,
calendar_item_chat_messages.body,
calendar_item_chat_messages.created_by::text,
COALESCE(users.name, 'Usuario removido'),
COALESCE(users.role, ''),
calendar_item_chat_messages.created_at,
calendar_item_chat_messages.edited_at,
calendar_item_chat_messages.deleted_at
FROM calendar_item_chat_messages
LEFT JOIN users ON users.id = calendar_item_chat_messages.created_by
WHERE calendar_item_chat_messages.calendar_item_id = $1::uuid
AND calendar_item_chat_messages.channel = $2
ORDER BY calendar_item_chat_messages.created_at ASC
`, calendarItemID, channel)
if err != nil {
return nil, err
}
defer rows.Close()
messages := []ChatMessage{}
for rows.Next() {
message, err := scanChatMessage(rows)
if err != nil {
return nil, err
}
messages = append(messages, message)
}
if err := rows.Err(); err != nil {
return nil, err
}
if err := s.attachChatAttachments(ctx, messages); err != nil {
return nil, err
}
return messages, nil
}
func (s Store) CreateChatMessage(ctx context.Context, input CreateChatMessageInput) (ChatMessage, error) {
row := s.db.QueryRow(ctx, `
INSERT INTO calendar_item_chat_messages (
calendar_item_id,
channel,
body,
created_by
)
VALUES ($1::uuid, $2, $3, NULLIF($4, '')::uuid)
RETURNING
calendar_item_chat_messages.id::text,
calendar_item_chat_messages.calendar_item_id::text,
calendar_item_chat_messages.channel,
calendar_item_chat_messages.body,
calendar_item_chat_messages.created_by::text,
COALESCE((SELECT users.name FROM users WHERE users.id = calendar_item_chat_messages.created_by), 'Usuario removido'),
COALESCE((SELECT users.role FROM users WHERE users.id = calendar_item_chat_messages.created_by), ''),
calendar_item_chat_messages.created_at,
calendar_item_chat_messages.edited_at,
calendar_item_chat_messages.deleted_at
`, input.CalendarItemID, input.Channel, input.Body, input.CreatedBy)
return scanChatMessage(row)
}
func (s Store) FindChatMessage(ctx context.Context, id string) (ChatMessage, error) {
row := s.db.QueryRow(ctx, `
SELECT
calendar_item_chat_messages.id::text,
calendar_item_chat_messages.calendar_item_id::text,
calendar_item_chat_messages.channel,
calendar_item_chat_messages.body,
calendar_item_chat_messages.created_by::text,
COALESCE(users.name, 'Usuario removido'),
COALESCE(users.role, ''),
calendar_item_chat_messages.created_at,
calendar_item_chat_messages.edited_at,
calendar_item_chat_messages.deleted_at
FROM calendar_item_chat_messages
LEFT JOIN users ON users.id = calendar_item_chat_messages.created_by
WHERE calendar_item_chat_messages.id = $1::uuid
`, id)
message, err := scanChatMessage(row)
if err != nil {
return ChatMessage{}, err
}
messages := []ChatMessage{message}
if err := s.attachChatAttachments(ctx, messages); err != nil {
return ChatMessage{}, err
}
return messages[0], nil
}
func (s Store) UpdateChatMessage(ctx context.Context, id string, body string) (ChatMessage, error) {
row := s.db.QueryRow(ctx, `
UPDATE calendar_item_chat_messages
SET body = $2, edited_at = now()
WHERE id = $1::uuid
AND deleted_at IS NULL
RETURNING
calendar_item_chat_messages.id::text,
calendar_item_chat_messages.calendar_item_id::text,
calendar_item_chat_messages.channel,
calendar_item_chat_messages.body,
calendar_item_chat_messages.created_by::text,
COALESCE((SELECT users.name FROM users WHERE users.id = calendar_item_chat_messages.created_by), 'Usuario removido'),
COALESCE((SELECT users.role FROM users WHERE users.id = calendar_item_chat_messages.created_by), ''),
calendar_item_chat_messages.created_at,
calendar_item_chat_messages.edited_at,
calendar_item_chat_messages.deleted_at
`, id, body)
message, err := scanChatMessage(row)
if err != nil {
return ChatMessage{}, err
}
messages := []ChatMessage{message}
if err := s.attachChatAttachments(ctx, messages); err != nil {
return ChatMessage{}, err
}
return messages[0], nil
}
func (s Store) DeleteChatMessage(ctx context.Context, id string) (ChatMessage, error) {
row := s.db.QueryRow(ctx, `
UPDATE calendar_item_chat_messages
SET deleted_at = now(), body = ''
WHERE id = $1::uuid
AND deleted_at IS NULL
RETURNING
calendar_item_chat_messages.id::text,
calendar_item_chat_messages.calendar_item_id::text,
calendar_item_chat_messages.channel,
calendar_item_chat_messages.body,
calendar_item_chat_messages.created_by::text,
COALESCE((SELECT users.name FROM users WHERE users.id = calendar_item_chat_messages.created_by), 'Usuario removido'),
COALESCE((SELECT users.role FROM users WHERE users.id = calendar_item_chat_messages.created_by), ''),
calendar_item_chat_messages.created_at,
calendar_item_chat_messages.edited_at,
calendar_item_chat_messages.deleted_at
`, id)
return scanChatMessage(row)
}
func (s Store) CreateChatAttachment(ctx context.Context, input CreateChatAttachmentInput) (ChatAttachment, error) {
row := s.db.QueryRow(ctx, `
INSERT INTO calendar_item_chat_attachments (
chat_message_id,
original_filename,
stored_filename,
mime_type,
size_bytes,
storage_driver,
storage_path,
created_by
)
VALUES ($1::uuid, $2, $3, $4, $5, $6, $7, NULLIF($8, '')::uuid)
RETURNING
id::text,
chat_message_id::text,
original_filename,
stored_filename,
mime_type,
size_bytes,
storage_driver,
storage_path,
created_by::text,
created_at
`, input.ChatMessageID, input.OriginalFilename, input.StoredFilename, input.MimeType, input.SizeBytes, input.StorageDriver, input.StoragePath, input.CreatedBy)
return scanChatAttachment(row)
}
func (s Store) FindChatAttachment(ctx context.Context, id string, viewerUserID string) (ChatAttachment, error) {
row := s.db.QueryRow(ctx, `
SELECT
calendar_item_chat_attachments.id::text,
calendar_item_chat_attachments.chat_message_id::text,
calendar_item_chat_attachments.original_filename,
calendar_item_chat_attachments.stored_filename,
calendar_item_chat_attachments.mime_type,
calendar_item_chat_attachments.size_bytes,
calendar_item_chat_attachments.storage_driver,
calendar_item_chat_attachments.storage_path,
calendar_item_chat_attachments.created_by::text,
calendar_item_chat_attachments.created_at
FROM calendar_item_chat_attachments
INNER JOIN calendar_item_chat_messages ON calendar_item_chat_messages.id = calendar_item_chat_attachments.chat_message_id
INNER JOIN calendar_items ON calendar_items.id = calendar_item_chat_messages.calendar_item_id
WHERE calendar_item_chat_attachments.id = $1::uuid
AND calendar_item_chat_messages.deleted_at IS NULL
AND (
$2 = ''
OR (
calendar_item_chat_messages.channel = 'external'
AND EXISTS (
SELECT 1
FROM client_users
WHERE client_users.client_id = calendar_items.client_id
AND client_users.user_id = $2::uuid
)
)
)
`, id, viewerUserID)
return scanChatAttachment(row)
}
func (s Store) attachChatAttachments(ctx context.Context, messages []ChatMessage) error {
if len(messages) == 0 {
return nil
}
ids := make([]string, 0, len(messages))
indexByID := map[string]int{}
for index, message := range messages {
ids = append(ids, message.ID)
indexByID[message.ID] = index
}
rows, err := s.db.Query(ctx, `
SELECT
id::text,
chat_message_id::text,
original_filename,
stored_filename,
mime_type,
size_bytes,
storage_driver,
storage_path,
created_by::text,
created_at
FROM calendar_item_chat_attachments
WHERE chat_message_id::text = ANY($1)
ORDER BY created_at ASC
`, ids)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
attachment, err := scanChatAttachment(rows)
if err != nil {
return err
}
index, ok := indexByID[attachment.ChatMessageID]
if ok {
messages[index].Attachments = append(messages[index].Attachments, attachment)
}
}
return rows.Err()
}
type chatMessageScanner interface {
Scan(dest ...any) error
}
func scanChatMessage(row chatMessageScanner) (ChatMessage, error) {
var message ChatMessage
err := row.Scan(
&message.ID,
&message.CalendarItemID,
&message.Channel,
&message.Body,
&message.CreatedBy,
&message.AuthorName,
&message.AuthorRole,
&message.CreatedAt,
&message.EditedAt,
&message.DeletedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return ChatMessage{}, ErrChatMessageNotFound
}
if err != nil {
return ChatMessage{}, err
}
message.Attachments = []ChatAttachment{}
return message, nil
}
type chatAttachmentScanner interface {
Scan(dest ...any) error
}
func scanChatAttachment(row chatAttachmentScanner) (ChatAttachment, error) {
var attachment ChatAttachment
err := row.Scan(
&attachment.ID,
&attachment.ChatMessageID,
&attachment.OriginalFilename,
&attachment.StoredFilename,
&attachment.MimeType,
&attachment.SizeBytes,
&attachment.StorageDriver,
&attachment.StoragePath,
&attachment.CreatedBy,
&attachment.CreatedAt,
)
if errors.Is(err, pgx.ErrNoRows) {
return ChatAttachment{}, ErrAttachmentNotFound
}
if err != nil {
return ChatAttachment{}, err
}
return attachment, nil
}