veza/fixtures/core/utils/data-relations.ts
2025-12-03 22:56:50 +01:00

442 lines
No EOL
14 KiB
TypeScript

import { UserGenerator } from '../generators/users'
import { AudioGenerator } from '../generators/audio'
import { ConversationGenerator } from '../generators/conversations'
import type { User, Audio, Playlist, Conversation, Message } from '../schemas/database'
/**
* Manages relationships and consistency between generated data
*/
export class DataRelationManager {
private static users: Map<string, User> = new Map()
private static tracks: Map<string, Audio> = new Map()
private static playlists: Map<string, Playlist> = new Map()
private static conversations: Map<string, Conversation> = new Map()
private static messages: Map<string, Message> = new Map()
// Relationship mappings
private static userTracks: Map<string, string[]> = new Map()
private static userPlaylists: Map<string, string[]> = new Map()
private static userConversations: Map<string, string[]> = new Map()
private static playlistTracks: Map<string, string[]> = new Map()
private static conversationParticipants: Map<string, string[]> = new Map()
private static conversationMessages: Map<string, string[]> = new Map()
/**
* Register entities and build relationships
*/
static registerUser(user: User): void {
this.users.set(user.id, user)
this.userTracks.set(user.id, [])
this.userPlaylists.set(user.id, [])
this.userConversations.set(user.id, [])
}
static registerTrack(track: Audio): void {
this.tracks.set(track.id, track)
// Link track to uploader
const userTracks = this.userTracks.get(track.uploadedById) || []
userTracks.push(track.id)
this.userTracks.set(track.uploadedById, userTracks)
}
static registerPlaylist(playlist: Playlist): void {
this.playlists.set(playlist.id, playlist)
this.playlistTracks.set(playlist.id, playlist.trackIds)
// Link playlist to creator
const userPlaylists = this.userPlaylists.get(playlist.createdById) || []
userPlaylists.push(playlist.id)
this.userPlaylists.set(playlist.createdById, userPlaylists)
}
static registerConversation(conversation: Conversation): void {
this.conversations.set(conversation.id, conversation)
this.conversationParticipants.set(conversation.id, conversation.participantIds)
this.conversationMessages.set(conversation.id, [])
// Link conversation to participants
conversation.participantIds.forEach(participantId => {
const userConversations = this.userConversations.get(participantId) || []
userConversations.push(conversation.id)
this.userConversations.set(participantId, userConversations)
})
}
static registerMessage(message: Message): void {
this.messages.set(message.id, message)
// Link message to conversation
const conversationMessages = this.conversationMessages.get(message.conversationId) || []
conversationMessages.push(message.id)
this.conversationMessages.set(message.conversationId, conversationMessages)
}
/**
* Generate complete dataset with consistent relationships
*/
static generateCompleteDataset(): {
users: User[]
tracks: Audio[]
playlists: Playlist[]
conversations: Conversation[]
messages: Message[]
} {
console.log('🎯 Generating complete dataset with consistent relationships...')
// Clear existing data
this.clearAll()
// Step 1: Generate users
console.log('👥 Generating users...')
const users = UserGenerator.generateWithDistribution()
users.forEach(user => this.registerUser(user))
console.log(`✅ Generated ${users.length} users`)
// Step 2: Generate audio content for artists and producers
console.log('🎵 Generating audio content...')
const tracks: Audio[] = []
const playlists: Playlist[] = []
users.forEach(user => {
if (user.role === 'artist' || user.role === 'producer') {
const content = AudioGenerator.generateUserContent(user.id, user.role)
// Register tracks
content.tracks.forEach(track => {
track.uploadedById = user.id // Ensure consistency
this.registerTrack(track)
tracks.push(track)
})
// Register playlists
content.playlists.forEach(playlist => {
playlist.createdById = user.id // Ensure consistency
// Use real track IDs for playlists
playlist.trackIds = this.getRandomTrackIds(playlist.trackIds.length)
this.registerPlaylist(playlist)
playlists.push(playlist)
})
} else if (user.role === 'user') {
// Regular users create playlists but don't upload tracks
const content = AudioGenerator.generateUserContent(user.id, user.role)
content.playlists.forEach(playlist => {
playlist.createdById = user.id
playlist.trackIds = this.getRandomTrackIds(playlist.trackIds.length)
this.registerPlaylist(playlist)
playlists.push(playlist)
})
}
})
console.log(`✅ Generated ${tracks.length} tracks and ${playlists.length} playlists`)
// Step 3: Generate conversations
console.log('💬 Generating conversations...')
const userIds = users.map(u => u.id)
const conversations = ConversationGenerator.generateWithDistribution(userIds)
conversations.forEach(conv => this.registerConversation(conv))
// Step 4: Generate messages for conversations
console.log('📝 Generating messages...')
const allMessages: Message[] = []
conversations.forEach(conversation => {
const messages = ConversationGenerator.generateMessages(
conversation.id,
conversation.participantIds,
this.getMessageCountForConversation(conversation.type)
)
messages.forEach(message => {
this.registerMessage(message)
allMessages.push(message)
})
// Update conversation with last message info
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1]
conversation.lastMessageId = lastMessage.id
conversation.lastActivityAt = lastMessage.createdAt
conversation.updatedAt = lastMessage.createdAt
}
})
console.log(`✅ Generated ${conversations.length} conversations with ${allMessages.length} messages`)
// Step 5: Update user statistics based on generated content
this.updateUserStatistics(users)
console.log('✅ Dataset generation complete!')
return {
users,
tracks,
playlists,
conversations,
messages: allMessages
}
}
/**
* Validation methods
*/
static validateRelations(): {
isValid: boolean
errors: string[]
warnings: string[]
} {
const errors: string[] = []
const warnings: string[] = []
// Validate user-track relationships
this.userTracks.forEach((trackIds, userId) => {
if (!this.users.has(userId)) {
errors.push(`User ${userId} not found but has tracks`)
}
trackIds.forEach(trackId => {
const track = this.tracks.get(trackId)
if (!track) {
errors.push(`Track ${trackId} not found`)
} else if (track.uploadedById !== userId) {
errors.push(`Track ${trackId} uploader mismatch`)
}
})
})
// Validate playlist-track relationships
this.playlistTracks.forEach((trackIds, playlistId) => {
const playlist = this.playlists.get(playlistId)
if (!playlist) {
errors.push(`Playlist ${playlistId} not found`)
return
}
trackIds.forEach(trackId => {
if (!this.tracks.has(trackId)) {
warnings.push(`Playlist ${playlistId} references non-existent track ${trackId}`)
}
})
})
// Validate conversation-participant relationships
this.conversationParticipants.forEach((participantIds, conversationId) => {
const conversation = this.conversations.get(conversationId)
if (!conversation) {
errors.push(`Conversation ${conversationId} not found`)
return
}
participantIds.forEach(participantId => {
if (!this.users.has(participantId)) {
errors.push(`Conversation ${conversationId} has non-existent participant ${participantId}`)
}
})
})
// Validate message-conversation relationships
this.conversationMessages.forEach((messageIds, conversationId) => {
if (!this.conversations.has(conversationId)) {
errors.push(`Messages reference non-existent conversation ${conversationId}`)
return
}
messageIds.forEach(messageId => {
const message = this.messages.get(messageId)
if (!message) {
errors.push(`Message ${messageId} not found`)
} else if (message.conversationId !== conversationId) {
errors.push(`Message ${messageId} conversation mismatch`)
}
})
})
return {
isValid: errors.length === 0,
errors,
warnings
}
}
/**
* Query methods
*/
static getUserTracks(userId: string): Audio[] {
const trackIds = this.userTracks.get(userId) || []
return trackIds.map(id => this.tracks.get(id)!).filter(Boolean)
}
static getUserPlaylists(userId: string): Playlist[] {
const playlistIds = this.userPlaylists.get(userId) || []
return playlistIds.map(id => this.playlists.get(id)!).filter(Boolean)
}
static getUserConversations(userId: string): Conversation[] {
const conversationIds = this.userConversations.get(userId) || []
return conversationIds.map(id => this.conversations.get(id)!).filter(Boolean)
}
static getPlaylistTracks(playlistId: string): Audio[] {
const trackIds = this.playlistTracks.get(playlistId) || []
return trackIds.map(id => this.tracks.get(id)!).filter(Boolean)
}
static getConversationMessages(conversationId: string): Message[] {
const messageIds = this.conversationMessages.get(conversationId) || []
return messageIds.map(id => this.messages.get(id)!).filter(Boolean)
}
/**
* Statistics and insights
*/
static getStatistics(): {
users: number
tracks: number
playlists: number
conversations: number
messages: number
relationships: {
avgTracksPerArtist: number
avgPlaylistsPerUser: number
avgMessagesPerConversation: number
avgParticipantsPerConversation: number
}
} {
const artistCount = Array.from(this.users.values())
.filter(u => u.role === 'artist' || u.role === 'producer').length
const totalTracks = Array.from(this.userTracks.values())
.reduce((sum, tracks) => sum + tracks.length, 0)
const totalPlaylists = Array.from(this.userPlaylists.values())
.reduce((sum, playlists) => sum + playlists.length, 0)
const totalMessages = Array.from(this.conversationMessages.values())
.reduce((sum, messages) => sum + messages.length, 0)
const totalParticipants = Array.from(this.conversationParticipants.values())
.reduce((sum, participants) => sum + participants.length, 0)
return {
users: this.users.size,
tracks: this.tracks.size,
playlists: this.playlists.size,
conversations: this.conversations.size,
messages: this.messages.size,
relationships: {
avgTracksPerArtist: artistCount > 0 ? Math.round(totalTracks / artistCount) : 0,
avgPlaylistsPerUser: this.users.size > 0 ? Math.round(totalPlaylists / this.users.size) : 0,
avgMessagesPerConversation: this.conversations.size > 0 ? Math.round(totalMessages / this.conversations.size) : 0,
avgParticipantsPerConversation: this.conversations.size > 0 ? Math.round(totalParticipants / this.conversations.size) : 0,
}
}
}
/**
* Export methods for different formats
*/
static exportForDatabase(): {
users: User[]
tracks: Audio[]
playlists: Playlist[]
conversations: Conversation[]
messages: Message[]
} {
return {
users: Array.from(this.users.values()),
tracks: Array.from(this.tracks.values()),
playlists: Array.from(this.playlists.values()),
conversations: Array.from(this.conversations.values()),
messages: Array.from(this.messages.values())
}
}
static exportForAPI(): any {
// Transform data for API responses
const users = Array.from(this.users.values()).map(user => ({
...user,
tracks: this.getUserTracks(user.id).length,
playlists: this.getUserPlaylists(user.id).length,
conversations: this.getUserConversations(user.id).length
}))
const tracks = Array.from(this.tracks.values()).map(track => ({
...track,
uploader: this.users.get(track.uploadedById)
}))
return { users, tracks }
}
/**
* Private helper methods
*/
private static getRandomTrackIds(count: number): string[] {
const allTrackIds = Array.from(this.tracks.keys())
if (allTrackIds.length === 0) return []
const selectedIds: string[] = []
for (let i = 0; i < Math.min(count, allTrackIds.length); i++) {
const randomId = allTrackIds[Math.floor(Math.random() * allTrackIds.length)]
if (!selectedIds.includes(randomId)) {
selectedIds.push(randomId)
}
}
return selectedIds
}
private static getMessageCountForConversation(type: string): number {
const counts = {
direct: Math.floor(Math.random() * 50) + 10,
group: Math.floor(Math.random() * 100) + 20,
channel: Math.floor(Math.random() * 200) + 50
}
return counts[type as keyof typeof counts] || 30
}
private static updateUserStatistics(users: User[]): void {
users.forEach(user => {
const tracks = this.getUserTracks(user.id)
const playlists = this.getUserPlaylists(user.id)
user.stats.tracksUploaded = tracks.length
user.stats.playlistsCreated = playlists.length
user.stats.totalPlays = tracks.reduce((sum, track) => sum + track.stats.plays, 0)
})
}
/**
* Cleanup methods
*/
static clearAll(): void {
this.users.clear()
this.tracks.clear()
this.playlists.clear()
this.conversations.clear()
this.messages.clear()
this.userTracks.clear()
this.userPlaylists.clear()
this.userConversations.clear()
this.playlistTracks.clear()
this.conversationParticipants.clear()
this.conversationMessages.clear()
}
static getAll(): {
users: Map<string, User>
tracks: Map<string, Audio>
playlists: Map<string, Playlist>
conversations: Map<string, Conversation>
messages: Map<string, Message>
} {
return {
users: this.users,
tracks: this.tracks,
playlists: this.playlists,
conversations: this.conversations,
messages: this.messages
}
}
}