492 lines
No EOL
14 KiB
TypeScript
492 lines
No EOL
14 KiB
TypeScript
import { Client } from 'pg'
|
|
import { createClient } from 'redis'
|
|
import { DataRelationManager } from '../../core/utils/data-relations'
|
|
import { globalConfig } from '../../core/config'
|
|
import type { User, Conversation, Message } from '../../core/schemas/database'
|
|
|
|
/**
|
|
* Chat server fixtures - Database seeding and Redis cache
|
|
*/
|
|
export class ChatServerFixtures {
|
|
private static dbClient: Client | null = null
|
|
private static redisClient: any = null
|
|
|
|
/**
|
|
* Initialize database connections
|
|
*/
|
|
static async initialize(): Promise<void> {
|
|
console.log('💬 Initializing chat server fixtures...')
|
|
|
|
// Initialize PostgreSQL connection
|
|
this.dbClient = new Client({
|
|
host: globalConfig.database.host,
|
|
port: globalConfig.database.port,
|
|
user: globalConfig.database.username,
|
|
password: globalConfig.database.password,
|
|
database: globalConfig.database.databases.chat,
|
|
})
|
|
|
|
try {
|
|
await this.dbClient.connect()
|
|
console.log('✅ Connected to PostgreSQL (chat)')
|
|
} catch (error) {
|
|
console.warn('⚠️ Could not connect to PostgreSQL (chat):', error)
|
|
this.dbClient = null
|
|
}
|
|
|
|
// Initialize Redis connection
|
|
this.redisClient = createClient({
|
|
socket: {
|
|
host: globalConfig.redis.host,
|
|
port: globalConfig.redis.port,
|
|
},
|
|
password: globalConfig.redis.password || undefined,
|
|
})
|
|
|
|
try {
|
|
await this.redisClient.connect()
|
|
console.log('✅ Connected to Redis')
|
|
} catch (error) {
|
|
console.warn('⚠️ Could not connect to Redis:', error)
|
|
this.redisClient = null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Seed PostgreSQL database with chat data
|
|
*/
|
|
static async seedDatabase(): Promise<void> {
|
|
if (!this.dbClient) {
|
|
console.warn('⚠️ No database connection available')
|
|
return
|
|
}
|
|
|
|
console.log('🗄️ Seeding chat database...')
|
|
|
|
try {
|
|
// Begin transaction
|
|
await this.dbClient.query('BEGIN')
|
|
|
|
// Clear existing data
|
|
await this.clearDatabase()
|
|
|
|
// Get data from relation manager
|
|
const allData = DataRelationManager.getAll()
|
|
const users = Array.from(allData.users.values())
|
|
const conversations = Array.from(allData.conversations.values())
|
|
const messages = Array.from(allData.messages.values())
|
|
|
|
// Seed users
|
|
console.log(`📝 Seeding ${users.length} users...`)
|
|
await this.seedUsers(users)
|
|
|
|
// Seed conversations
|
|
console.log(`📝 Seeding ${conversations.length} conversations...`)
|
|
await this.seedConversations(conversations)
|
|
|
|
// Seed messages
|
|
console.log(`📝 Seeding ${messages.length} messages...`)
|
|
await this.seedMessages(messages)
|
|
|
|
// Commit transaction
|
|
await this.dbClient.query('COMMIT')
|
|
|
|
console.log('✅ Chat database seeded successfully')
|
|
|
|
} catch (error) {
|
|
await this.dbClient.query('ROLLBACK')
|
|
console.error('❌ Error seeding chat database:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Seed Redis cache with session data
|
|
*/
|
|
static async seedRedis(): Promise<void> {
|
|
if (!this.redisClient) {
|
|
console.warn('⚠️ No Redis connection available')
|
|
return
|
|
}
|
|
|
|
console.log('🔄 Seeding Redis cache...')
|
|
|
|
try {
|
|
// Clear existing cache
|
|
await this.redisClient.flushDb()
|
|
|
|
const allData = DataRelationManager.getAll()
|
|
const users = Array.from(allData.users.values())
|
|
const conversations = Array.from(allData.conversations.values())
|
|
|
|
// Cache active users
|
|
const activeUsers = users.filter(user => user.status === 'active')
|
|
for (const user of activeUsers) {
|
|
const sessionData = {
|
|
userId: user.id,
|
|
username: user.username,
|
|
status: 'online',
|
|
lastSeen: new Date().toISOString(),
|
|
socketId: `socket_${user.id}_${Date.now()}`,
|
|
}
|
|
|
|
await this.redisClient.setEx(
|
|
`session:${user.id}`,
|
|
3600, // 1 hour TTL
|
|
JSON.stringify(sessionData)
|
|
)
|
|
|
|
// Add to online users set
|
|
await this.redisClient.sAdd('users:online', user.id)
|
|
}
|
|
|
|
// Cache conversation metadata
|
|
for (const conversation of conversations) {
|
|
const metadata = {
|
|
id: conversation.id,
|
|
name: conversation.name,
|
|
type: conversation.type,
|
|
participantCount: conversation.participantIds.length,
|
|
lastActivity: conversation.lastActivityAt.toISOString(),
|
|
}
|
|
|
|
await this.redisClient.setEx(
|
|
`conversation:${conversation.id}`,
|
|
1800, // 30 minutes TTL
|
|
JSON.stringify(metadata)
|
|
)
|
|
|
|
// Cache participant lists
|
|
await this.redisClient.sAdd(
|
|
`conversation:${conversation.id}:participants`,
|
|
...conversation.participantIds
|
|
)
|
|
|
|
// Set expiry for participant sets
|
|
await this.redisClient.expire(
|
|
`conversation:${conversation.id}:participants`,
|
|
1800
|
|
)
|
|
}
|
|
|
|
// Cache typing indicators (empty initially)
|
|
await this.redisClient.set('typing:indicators', '{}')
|
|
|
|
// Cache message delivery status
|
|
const recentMessages = Array.from(allData.messages.values())
|
|
.filter(msg => {
|
|
const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
|
return msg.createdAt > dayAgo
|
|
})
|
|
|
|
for (const message of recentMessages) {
|
|
await this.redisClient.setEx(
|
|
`message:${message.id}:status`,
|
|
86400, // 24 hours TTL
|
|
message.status
|
|
)
|
|
}
|
|
|
|
console.log(`✅ Redis cache seeded with ${activeUsers.length} sessions and ${conversations.length} conversations`)
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error seeding Redis cache:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate WebSocket event data
|
|
*/
|
|
static generateWebSocketEvents(): any[] {
|
|
const allData = DataRelationManager.getAll()
|
|
const users = Array.from(allData.users.values())
|
|
const conversations = Array.from(allData.conversations.values())
|
|
const messages = Array.from(allData.messages.values())
|
|
|
|
const events: any[] = []
|
|
|
|
// User connection events
|
|
users.filter(u => u.status === 'active').forEach(user => {
|
|
events.push({
|
|
type: 'user_connected',
|
|
userId: user.id,
|
|
data: {
|
|
username: user.username,
|
|
status: 'online',
|
|
lastSeen: new Date().toISOString()
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
})
|
|
})
|
|
|
|
// Recent message events
|
|
const recentMessages = messages
|
|
.filter(msg => {
|
|
const hourAgo = new Date(Date.now() - 60 * 60 * 1000)
|
|
return msg.createdAt > hourAgo
|
|
})
|
|
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
|
|
|
recentMessages.forEach(message => {
|
|
events.push({
|
|
type: 'message_sent',
|
|
conversationId: message.conversationId,
|
|
data: {
|
|
id: message.id,
|
|
senderId: message.senderId,
|
|
content: message.content,
|
|
type: message.type,
|
|
createdAt: message.createdAt.toISOString()
|
|
},
|
|
timestamp: message.createdAt.toISOString()
|
|
})
|
|
})
|
|
|
|
// Typing indicator events
|
|
conversations.forEach(conversation => {
|
|
const typingUser = conversation.participantIds[0]
|
|
events.push({
|
|
type: 'typing_start',
|
|
conversationId: conversation.id,
|
|
data: {
|
|
userId: typingUser,
|
|
isTyping: true
|
|
},
|
|
timestamp: new Date(Date.now() - Math.random() * 30000).toISOString()
|
|
})
|
|
})
|
|
|
|
return events.sort((a, b) =>
|
|
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Get conversation statistics
|
|
*/
|
|
static async getConversationStats(): Promise<any> {
|
|
if (!this.dbClient) {
|
|
return this.getMockConversationStats()
|
|
}
|
|
|
|
try {
|
|
const result = await this.dbClient.query(`
|
|
SELECT
|
|
c.type,
|
|
COUNT(*) as count,
|
|
AVG(array_length(c.participant_ids, 1)) as avg_participants,
|
|
COUNT(m.id) as total_messages
|
|
FROM conversations c
|
|
LEFT JOIN messages m ON c.id = m.conversation_id
|
|
GROUP BY c.type
|
|
`)
|
|
|
|
return result.rows.reduce((stats, row) => {
|
|
stats[row.type] = {
|
|
count: parseInt(row.count),
|
|
avgParticipants: Math.round(parseFloat(row.avg_participants)),
|
|
totalMessages: parseInt(row.total_messages)
|
|
}
|
|
return stats
|
|
}, {})
|
|
|
|
} catch (error) {
|
|
console.error('Error getting conversation stats:', error)
|
|
return this.getMockConversationStats()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Private helper methods
|
|
*/
|
|
private static async clearDatabase(): Promise<void> {
|
|
if (!this.dbClient) return
|
|
|
|
const tables = ['messages', 'conversation_members', 'conversations', 'users']
|
|
|
|
for (const table of tables) {
|
|
await this.dbClient.query(`DELETE FROM ${table}`)
|
|
}
|
|
|
|
console.log('🗑️ Cleared existing chat database data')
|
|
}
|
|
|
|
private static async seedUsers(users: User[]): Promise<void> {
|
|
if (!this.dbClient) return
|
|
|
|
const query = `
|
|
INSERT INTO users (
|
|
id, username, email, display_name, avatar_url,
|
|
is_active, last_seen, created_at, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
`
|
|
|
|
for (const user of users) {
|
|
await this.dbClient.query(query, [
|
|
user.id,
|
|
user.username,
|
|
user.email,
|
|
user.displayName || `${user.firstName} ${user.lastName}`,
|
|
user.avatar,
|
|
user.status === 'active',
|
|
user.lastSeenAt,
|
|
user.createdAt,
|
|
user.updatedAt
|
|
])
|
|
}
|
|
}
|
|
|
|
private static async seedConversations(conversations: Conversation[]): Promise<void> {
|
|
if (!this.dbClient) return
|
|
|
|
const conversationQuery = `
|
|
INSERT INTO conversations (
|
|
id, name, description, conversation_type, is_private,
|
|
created_by, created_at, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
`
|
|
|
|
const memberQuery = `
|
|
INSERT INTO conversation_members (conversation_id, user_id, role, joined_at)
|
|
VALUES ($1, $2, $3, $4)
|
|
`
|
|
|
|
for (const conversation of conversations) {
|
|
// Insert conversation
|
|
await this.dbClient.query(conversationQuery, [
|
|
conversation.id,
|
|
conversation.name,
|
|
conversation.description,
|
|
conversation.type,
|
|
conversation.isPrivate,
|
|
conversation.createdById,
|
|
conversation.createdAt,
|
|
conversation.updatedAt
|
|
])
|
|
|
|
// Insert participants
|
|
for (const participantId of conversation.participantIds) {
|
|
const role = participantId === conversation.createdById ? 'admin' : 'member'
|
|
await this.dbClient.query(memberQuery, [
|
|
conversation.id,
|
|
participantId,
|
|
role,
|
|
conversation.createdAt
|
|
])
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async seedMessages(messages: Message[]): Promise<void> {
|
|
if (!this.dbClient) return
|
|
|
|
const query = `
|
|
INSERT INTO messages (
|
|
id, conversation_id, sender_id, content, message_type,
|
|
parent_message_id, is_pinned, is_deleted, created_at, updated_at, status
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
`
|
|
|
|
for (const message of messages) {
|
|
await this.dbClient.query(query, [
|
|
message.id,
|
|
message.conversationId,
|
|
message.senderId,
|
|
message.content,
|
|
message.type,
|
|
message.parentMessageId,
|
|
false, // is_pinned
|
|
message.isDeleted,
|
|
message.createdAt,
|
|
message.updatedAt,
|
|
message.status
|
|
])
|
|
}
|
|
}
|
|
|
|
private static getMockConversationStats(): any {
|
|
const stats = DataRelationManager.getStatistics()
|
|
return {
|
|
direct: {
|
|
count: Math.floor(stats.conversations * 0.6),
|
|
avgParticipants: 2,
|
|
totalMessages: Math.floor(stats.messages * 0.4)
|
|
},
|
|
group: {
|
|
count: Math.floor(stats.conversations * 0.3),
|
|
avgParticipants: 6,
|
|
totalMessages: Math.floor(stats.messages * 0.4)
|
|
},
|
|
channel: {
|
|
count: Math.floor(stats.conversations * 0.1),
|
|
avgParticipants: 25,
|
|
totalMessages: Math.floor(stats.messages * 0.2)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup methods
|
|
*/
|
|
static async cleanup(): Promise<void> {
|
|
if (this.dbClient) {
|
|
await this.dbClient.end()
|
|
this.dbClient = null
|
|
}
|
|
|
|
if (this.redisClient) {
|
|
await this.redisClient.quit()
|
|
this.redisClient = null
|
|
}
|
|
|
|
console.log('🧹 Chat server fixtures cleanup completed')
|
|
}
|
|
|
|
/**
|
|
* Health check
|
|
*/
|
|
static async healthCheck(): Promise<{
|
|
database: boolean
|
|
redis: boolean
|
|
dataConsistency: boolean
|
|
}> {
|
|
let database = false
|
|
let redis = false
|
|
let dataConsistency = false
|
|
|
|
// Check database connection
|
|
if (this.dbClient) {
|
|
try {
|
|
await this.dbClient.query('SELECT 1')
|
|
database = true
|
|
} catch (error) {
|
|
console.error('Database health check failed:', error)
|
|
}
|
|
}
|
|
|
|
// Check Redis connection
|
|
if (this.redisClient) {
|
|
try {
|
|
await this.redisClient.ping()
|
|
redis = true
|
|
} catch (error) {
|
|
console.error('Redis health check failed:', error)
|
|
}
|
|
}
|
|
|
|
// Check data consistency
|
|
try {
|
|
const validation = DataRelationManager.validateRelations()
|
|
dataConsistency = validation.isValid
|
|
|
|
if (!validation.isValid) {
|
|
console.warn('Data consistency issues:', validation.errors)
|
|
}
|
|
} catch (error) {
|
|
console.error('Data consistency check failed:', error)
|
|
}
|
|
|
|
return { database, redis, dataConsistency }
|
|
}
|
|
} |