feat(messages): add unified conversation models and migration SQL
Add 5 new SQLAlchemy models (Conversation, ConversationMember, ConvMessage, MessageReaction, MessagePin) and extend MessageAttachment with conv_message_id FK. Migration 091 creates all tables with indexes, FKs, and grants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
29bd3b2e39
commit
d4fd1f3b06
131
database.py
131
database.py
@ -2344,7 +2344,10 @@ class MessageAttachment(Base):
|
||||
mime_type = Column(String(100), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
|
||||
conv_message_id = Column(Integer, ForeignKey('conv_messages.id', ondelete='CASCADE'), nullable=True)
|
||||
|
||||
message = relationship('PrivateMessage', backref=backref('attachments', cascade='all, delete-orphan'))
|
||||
conv_message = relationship('ConvMessage', back_populates='attachments', foreign_keys=[conv_message_id])
|
||||
|
||||
|
||||
# ============================================================
|
||||
@ -5820,6 +5823,134 @@ class InternalHealthLog(Base):
|
||||
return f'<InternalHealthLog {self.checked_at} app={self.app_ok} db={self.db_ok}>'
|
||||
|
||||
|
||||
# ============================================================
|
||||
# UNIFIED CONVERSATIONS (messaging redesign)
|
||||
# ============================================================
|
||||
|
||||
class Conversation(Base):
|
||||
"""Zunifikowana konwersacja — 1:1 lub grupowa"""
|
||||
__tablename__ = 'conversations'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(255), nullable=True)
|
||||
is_group = Column(Boolean, nullable=False, default=False)
|
||||
owner_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
last_message_id = Column(Integer, ForeignKey('conv_messages.id', name='fk_conversations_last_message', use_alter=True, ondelete='SET NULL'), nullable=True)
|
||||
|
||||
owner = relationship('User', foreign_keys=[owner_id])
|
||||
members = relationship('ConversationMember', backref='conversation', cascade='all, delete-orphan')
|
||||
messages = relationship('ConvMessage', backref='conversation',
|
||||
foreign_keys='ConvMessage.conversation_id',
|
||||
cascade='all, delete-orphan',
|
||||
order_by='ConvMessage.created_at')
|
||||
last_message = relationship('ConvMessage', foreign_keys=[last_message_id],
|
||||
post_update=True)
|
||||
pins = relationship('MessagePin', backref='conversation', cascade='all, delete-orphan')
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Nazwa wyświetlana — nazwa grupy lub lista imion uczestników"""
|
||||
if self.name:
|
||||
return self.name
|
||||
names = [m.user.name or m.user.email.split('@')[0] for m in self.members if m.user]
|
||||
return ', '.join(sorted(names)[:4]) + (f' +{len(names)-4}' if len(names) > 4 else '')
|
||||
|
||||
@property
|
||||
def member_count(self):
|
||||
return len(self.members)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Conversation {self.id} group={self.is_group} members={self.member_count}>'
|
||||
|
||||
|
||||
class ConversationMember(Base):
|
||||
"""Członkostwo w konwersacji"""
|
||||
__tablename__ = 'conversation_members'
|
||||
|
||||
conversation_id = Column(Integer, ForeignKey('conversations.id', ondelete='CASCADE'), primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), primary_key=True)
|
||||
role = Column(String(20), nullable=False, default='member')
|
||||
last_read_at = Column(DateTime, nullable=True)
|
||||
is_muted = Column(Boolean, nullable=False, default=False)
|
||||
is_archived = Column(Boolean, nullable=False, default=False)
|
||||
joined_at = Column(DateTime, default=datetime.now)
|
||||
added_by_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||
|
||||
user = relationship('User', foreign_keys=[user_id])
|
||||
added_by = relationship('User', foreign_keys=[added_by_id])
|
||||
|
||||
@property
|
||||
def is_owner(self):
|
||||
return self.role == 'owner'
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ConversationMember conv={self.conversation_id} user={self.user_id} role={self.role}>'
|
||||
|
||||
|
||||
class ConvMessage(Base):
|
||||
"""Wiadomość w konwersacji"""
|
||||
__tablename__ = 'conv_messages'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
conversation_id = Column(Integer, ForeignKey('conversations.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||
sender_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||
content = Column(Text, nullable=False)
|
||||
reply_to_id = Column(Integer, ForeignKey('conv_messages.id', ondelete='SET NULL'), nullable=True)
|
||||
edited_at = Column(DateTime, nullable=True)
|
||||
is_deleted = Column(Boolean, nullable=False, default=False)
|
||||
link_preview = Column(PG_JSONB, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.now, index=True)
|
||||
|
||||
sender = relationship('User', foreign_keys=[sender_id])
|
||||
reply_to = relationship('ConvMessage', remote_side=[id], foreign_keys=[reply_to_id])
|
||||
reactions = relationship('MessageReaction', backref='message', cascade='all, delete-orphan')
|
||||
attachments = relationship('MessageAttachment',
|
||||
foreign_keys='MessageAttachment.conv_message_id',
|
||||
back_populates='conv_message',
|
||||
cascade='all, delete-orphan')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ConvMessage {self.id} conv={self.conversation_id} sender={self.sender_id}>'
|
||||
|
||||
|
||||
class MessageReaction(Base):
|
||||
"""Reakcja emoji na wiadomość"""
|
||||
__tablename__ = 'message_reactions'
|
||||
__table_args__ = (
|
||||
UniqueConstraint('message_id', 'user_id', 'emoji', name='uq_message_reaction'),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
message_id = Column(Integer, ForeignKey('conv_messages.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
|
||||
emoji = Column(String(10), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
|
||||
user = relationship('User', foreign_keys=[user_id])
|
||||
|
||||
def __repr__(self):
|
||||
return f'<MessageReaction {self.emoji} msg={self.message_id} user={self.user_id}>'
|
||||
|
||||
|
||||
class MessagePin(Base):
|
||||
"""Przypięta wiadomość w konwersacji"""
|
||||
__tablename__ = 'message_pins'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
conversation_id = Column(Integer, ForeignKey('conversations.id', ondelete='CASCADE'), nullable=False, index=True)
|
||||
message_id = Column(Integer, ForeignKey('conv_messages.id', ondelete='CASCADE'), nullable=False)
|
||||
pinned_by_id = Column(Integer, ForeignKey('users.id', ondelete='SET NULL'), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
|
||||
message = relationship('ConvMessage', foreign_keys=[message_id])
|
||||
pinned_by = relationship('User', foreign_keys=[pinned_by_id])
|
||||
|
||||
def __repr__(self):
|
||||
return f'<MessagePin msg={self.message_id} conv={self.conversation_id}>'
|
||||
|
||||
|
||||
# ============================================================
|
||||
# DATABASE INITIALIZATION
|
||||
# ============================================================
|
||||
|
||||
96
database/migrations/091_messaging_redesign.sql
Normal file
96
database/migrations/091_messaging_redesign.sql
Normal file
@ -0,0 +1,96 @@
|
||||
-- 091_messaging_redesign.sql
|
||||
-- Unified conversation model: replaces separate private_messages + message_group
|
||||
-- Conversations (1:1 and group), messages, reactions, pins
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Unified conversations (1:1 or group)
|
||||
CREATE TABLE conversations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
is_group BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
owner_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
last_message_id INTEGER -- FK added below after conv_messages exists
|
||||
);
|
||||
|
||||
CREATE INDEX idx_conversations_owner ON conversations(owner_id);
|
||||
CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC);
|
||||
|
||||
-- Conversation membership
|
||||
CREATE TABLE conversation_members (
|
||||
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'member',
|
||||
last_read_at TIMESTAMP,
|
||||
is_muted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_archived BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
joined_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
added_by_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
PRIMARY KEY (conversation_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_conversation_members_user ON conversation_members(user_id);
|
||||
|
||||
-- Messages within a conversation
|
||||
CREATE TABLE conv_messages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
sender_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
content TEXT NOT NULL,
|
||||
reply_to_id INTEGER REFERENCES conv_messages(id) ON DELETE SET NULL,
|
||||
edited_at TIMESTAMP,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
link_preview JSONB,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_conv_messages_conversation ON conv_messages(conversation_id);
|
||||
CREATE INDEX idx_conv_messages_created ON conv_messages(created_at);
|
||||
CREATE INDEX idx_conv_messages_conversation_created ON conv_messages(conversation_id, created_at);
|
||||
|
||||
-- Now add the deferred FK from conversations.last_message_id -> conv_messages.id
|
||||
ALTER TABLE conversations
|
||||
ADD CONSTRAINT fk_conversations_last_message
|
||||
FOREIGN KEY (last_message_id) REFERENCES conv_messages(id) ON DELETE SET NULL;
|
||||
|
||||
-- Emoji reactions on messages
|
||||
CREATE TABLE message_reactions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
message_id INTEGER NOT NULL REFERENCES conv_messages(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
emoji VARCHAR(10) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT uq_message_reaction UNIQUE (message_id, user_id, emoji)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_message_reactions_message ON message_reactions(message_id);
|
||||
|
||||
-- Pinned messages
|
||||
CREATE TABLE message_pins (
|
||||
id SERIAL PRIMARY KEY,
|
||||
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
||||
message_id INTEGER NOT NULL REFERENCES conv_messages(id) ON DELETE CASCADE,
|
||||
pinned_by_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_message_pins_conversation ON message_pins(conversation_id);
|
||||
|
||||
-- Extend message_attachments to support conv_messages
|
||||
ALTER TABLE message_attachments ADD COLUMN conv_message_id INTEGER REFERENCES conv_messages(id) ON DELETE CASCADE;
|
||||
CREATE INDEX idx_message_attachments_conv ON message_attachments(conv_message_id) WHERE conv_message_id IS NOT NULL;
|
||||
|
||||
-- Grants
|
||||
GRANT ALL ON TABLE conversations TO nordabiz_app;
|
||||
GRANT ALL ON TABLE conversation_members TO nordabiz_app;
|
||||
GRANT ALL ON TABLE conv_messages TO nordabiz_app;
|
||||
GRANT ALL ON TABLE message_reactions TO nordabiz_app;
|
||||
GRANT ALL ON TABLE message_pins TO nordabiz_app;
|
||||
GRANT USAGE, SELECT ON SEQUENCE conversations_id_seq TO nordabiz_app;
|
||||
GRANT USAGE, SELECT ON SEQUENCE conv_messages_id_seq TO nordabiz_app;
|
||||
GRANT USAGE, SELECT ON SEQUENCE message_reactions_id_seq TO nordabiz_app;
|
||||
GRANT USAGE, SELECT ON SEQUENCE message_pins_id_seq TO nordabiz_app;
|
||||
|
||||
COMMIT;
|
||||
Loading…
Reference in New Issue
Block a user