Skip to content

Commit

Permalink
feat(desktop): sync topic tree from connection
Browse files Browse the repository at this point in the history
  • Loading branch information
ysfscream committed Nov 12, 2024
1 parent a5274f2 commit 14c71b6
Show file tree
Hide file tree
Showing 5 changed files with 345 additions and 276 deletions.
269 changes: 124 additions & 145 deletions src/database/services/TopicNodeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ConnectionEntity from '../models/ConnectionEntity'
import MessageService from './MessageService'
import { getMessageId } from '@/utils/idGenerator'
import ConnectionService from './ConnectionService'
import { flattenTopicTree } from '@/utils/topicTree'

@Service()
export default class TopicNodeService {
Expand All @@ -20,72 +21,75 @@ export default class TopicNodeService {
) {}

/**
* Converts a TopicTreeNode model to a TopicNodeEntity
*
* @param model - The TopicTreeNode model to convert
* @param parent - Optional parent TopicNodeEntity
* @returns The converted TopicNodeEntity
* Retrieves the topic tree structure
* @returns {Promise<TopicTreeNode[]>} A promise that resolves to an array of TopicTreeNode objects
*/
public static modelToEntity(model: TopicTreeNode, parent?: TopicNodeEntity): TopicNodeEntity {
const entity = new TopicNodeEntity()
entity.id = model.id
entity.label = model.label
entity.messageCount = model.messageCount
entity.subTopicCount = model.subTopicCount
entity.message = model.message
entity.parent = parent
entity.connection = model.connectionInfo
return entity
}
async getTree(): Promise<TopicTreeNode[]> {
// Fetch all nodes with their associated connections and messages
const nodes = await this.topicNodeRepository
.createQueryBuilder('node')
.leftJoinAndSelect('node.connection', 'connection')
.leftJoinAndSelect('node.message', 'message')
.getMany()

public async mapTopicNodesToEntities(
topicNodes: TopicTreeNode[],
callback: (entity: TopicNodeEntity) => void,
): Promise<void> {
for (const node of topicNodes) {
let isRoot = node.connectionInfo !== undefined
let entity: TopicNodeEntity | null = null
if (isRoot) {
const connectionId = node.connectionInfo?.id
const connection = await this.connectionRepository.findOne({ id: connectionId })
if (!connection) {
console.warn(`Connection with id ${connectionId} not found`)
continue
}
node.connectionInfo = connection
}
entity = TopicNodeService.modelToEntity(node)
callback(entity)
// Retrieve the tree structure
const tree = await this.topicNodeRepository.findTrees()

// Create a node map for quick lookup and tree construction
const nodeMap = new Map<string, TopicTreeNode>()
nodes.forEach((node) => {
nodeMap.set(node.id, {
id: node.id,
label: node.label,
messageCount: node.messageCount,
subTopicCount: node.subTopicCount,
connectionInfo: node.connection ? ConnectionService.entityToModel(node.connection) : undefined,
message: node.message ? MessageService.entityToModel(node.message) : undefined,
children: [],
})
})

/**
* Recursively builds the final tree structure
* @param {TopicNodeEntity[]} treeNodes - An array of TopicNodeEntity objects
* @returns {TopicTreeNode[]} An array of constructed TopicTreeNode objects
*/
const buildFinalTree = (treeNodes: TopicNodeEntity[]): TopicTreeNode[] => {
return treeNodes
.map((treeNode) => {
const node = nodeMap.get(treeNode.id)
if (!node) return null
if (treeNode.children) {
node.children = buildFinalTree(treeNode.children)
}
return node
})
.filter(Boolean) as TopicTreeNode[]
}

const finalTree = buildFinalTree(tree)

return finalTree
}

/**
* Converts an array of TopicTreeNode models to TopicNodeEntity objects and groups them by connection ID.
* Also groups associated messages by connection ID.
* Creates a TopicNodeEntity from a TopicTreeNode model.
*
* @param nodes - An array of TopicTreeNode objects to be converted
* @returns A promise that resolves to an object containing:
* - groupedEntities: A Map of connection IDs to arrays of TopicNodeEntity objects
* - groupedMessages: A Map of connection IDs to arrays of MessageModel objects
* @param node - The TopicTreeNode model to convert
* @param connection - Optional ConnectionEntity to associate with the node
* @returns A new TopicNodeEntity
*/
async topicNodeModelToEntityWithMsgs(nodes: TopicTreeNode[]): Promise<{
groupedEntities: Map<string, TopicNodeEntity[]>
groupedMessages: Map<string, MessageModel[]>
}> {
const groupedEntities = new Map<string, TopicNodeEntity[]>()
const groupedMessages = this.groupedMessagesByConnectionID(nodes)

this.mapTopicNodesToEntities(nodes, (entity) => {
const connectionId = entity.id.split('_')[0]
if (!groupedEntities.has(connectionId)) {
groupedEntities.set(connectionId, [])
}
groupedEntities.get(connectionId)!.push(entity)
})

this.establishParentChildRelationships(nodes, Array.from(groupedEntities.values()).flat())

return { groupedEntities, groupedMessages }
private createTopicNodeEntity(node: TopicTreeNode): TopicNodeEntity {
const entity = new TopicNodeEntity()
entity.id = node.id
entity.label = node.label
entity.messageCount = node.messageCount
entity.subTopicCount = node.subTopicCount
entity.message = node.message
entity.connection = node.connectionInfo
? (ConnectionService.modelToEntity(node.connectionInfo) as ConnectionEntity)
: undefined
return entity
}

/**
Expand All @@ -94,9 +98,8 @@ export default class TopicNodeService {
* @param nodes - An array of TopicTreeNode models
* @param entities - An array of TopicNodeEntity objects
*/
private establishParentChildRelationships(nodes: TopicTreeNode[], entities: TopicNodeEntity[]) {
private establishNodeEntityRelationships(nodes: TopicTreeNode[], entities: TopicNodeEntity[]) {
const entityMap = new Map(entities.map((e) => [e.id, e]))

for (const node of nodes) {
const entity = entityMap.get(node.id)
if (entity && node.parentId) {
Expand Down Expand Up @@ -131,19 +134,41 @@ export default class TopicNodeService {
return messagesByConnection
}

/**
* Groups TopicNodeEntity objects by connection ID.
*
* @param updatedNodes - An array of TopicTreeNode models to be grouped
* @returns A Map where keys are connection IDs and values are arrays of TopicNodeEntity objects
*/
private groupedEntitiesByConnectionID(updatedNodes: TopicTreeNode[]): Map<string, TopicNodeEntity[]> {
const entitiesByConnection = new Map<string, TopicNodeEntity[]>()
updatedNodes.forEach((node) => {
const entity = this.createTopicNodeEntity(node)
const connectionId = node.id.split('_')[0]
if (!entitiesByConnection.has(connectionId)) {
entitiesByConnection.set(connectionId, [])
}
entitiesByConnection.get(connectionId)!.push(entity)
})
return entitiesByConnection
}

/**
* Saves TopicTreeNode models and messages to the database.
*
* @param nodes - An array of TopicTreeNode models to be saved
* @param topicNodes - An array of TopicTreeNode models to be saved
* @returns A promise that resolves to an object containing:
* - savedEntitiesCount: The number of TopicNodeEntity objects saved
* - savedMessagesCount: The number of MessageModel objects saved
*/
async saveTopicNodesWithMessages(nodes: TopicTreeNode[]): Promise<{
async saveTopicNodesWithMessages(topicNodes: TopicTreeNode[]): Promise<{
savedEntitiesCount: number
savedMessagesCount: number
}> {
const { groupedEntities, groupedMessages } = await this.topicNodeModelToEntityWithMsgs(nodes)
const groupedMessages = this.groupedMessagesByConnectionID(topicNodes)
const groupedEntities = this.groupedEntitiesByConnectionID(topicNodes)
this.establishNodeEntityRelationships(topicNodes, Array.from(groupedEntities.values()).flat())

const savedEntities: TopicNodeEntity[] = []
const savedMessages: MessageModel[] = []

Expand All @@ -154,24 +179,12 @@ export default class TopicNodeService {
await this.messageService.pushMsgsToConnection(messages, connectionId)
savedMessages.push(...messages)
}

// First, save entities without parent nodes
const rootEntities = entities.filter((e) => !e.parent)
const savedRootEntities = await this.topicNodeRepository.save(rootEntities)
savedEntities.push(...savedRootEntities)

// Then save entities with parent nodes
const childEntities = entities.filter((e) => e.parent)
for (const entity of childEntities) {
const parent = savedEntities.find((e) => e.id === entity.parent?.id)
if (parent) {
entity.parent = parent
const savedEntity = await this.topicNodeRepository.save(entity)
savedEntities.push(savedEntity)
} else {
console.warn(`Parent entity with id ${entity.parent?.id} not found for entity ${entity.id}`)
}
}
const rootEntities = entities.filter((entity) => !entity.parent)
await this.topicNodeRepository.save(rootEntities)
savedEntities.push(...rootEntities)
const otherEntities = entities.filter((entity) => entity.parent)
await this.topicNodeRepository.save(otherEntities)
savedEntities.push(...otherEntities)
}

return {
Expand All @@ -181,64 +194,30 @@ export default class TopicNodeService {
}

/**
* Retrieves the topic tree structure
* @returns {Promise<TopicTreeNode[]>} A promise that resolves to an array of TopicTreeNode objects
*/
async getTree(): Promise<TopicTreeNode[]> {
// Fetch all nodes with their associated connections and messages
const nodes = await this.topicNodeRepository
.createQueryBuilder('node')
.leftJoinAndSelect('node.connection', 'connection')
.leftJoinAndSelect('node.message', 'message')
.getMany()

// Retrieve the tree structure
const tree = await this.topicNodeRepository.findTrees()

// Create a node map for quick lookup and tree construction
const nodeMap = new Map<string, TopicTreeNode>()
nodes.forEach((node) => {
nodeMap.set(node.id, {
id: node.id,
label: node.label,
messageCount: node.messageCount,
subTopicCount: node.subTopicCount,
connectionInfo: node.connection ? ConnectionService.entityToModel(node.connection) : undefined,
message: node.message ? MessageService.entityToModel(node.message) : undefined,
children: [],
})
})

/**
* Recursively builds the final tree structure
* @param {TopicNodeEntity[]} treeNodes - An array of TopicNodeEntity objects
* @returns {TopicTreeNode[]} An array of constructed TopicTreeNode objects
*/
const buildFinalTree = (treeNodes: TopicNodeEntity[]): TopicTreeNode[] => {
return treeNodes
.map((treeNode) => {
const node = nodeMap.get(treeNode.id)
if (!node) return null
if (treeNode.children) {
node.children = buildFinalTree(treeNode.children)
}
return node
})
.filter(Boolean) as TopicTreeNode[]
}

const finalTree = buildFinalTree(tree)

return finalTree
}

/**
* Clears all topic nodes from the repository.
* Updates topic nodes based on a topic tree structure.
*
* @returns A promise that resolves when all nodes have been cleared
* This method performs the following steps:
* 1. Deletes existing topic nodes for the given connection
* 2. Flattens the topic tree into a linear array
* 3. Maps the flattened nodes to TopicNodeEntity objects
* 4. Establishes parent-child relationships between nodes
* 5. Saves all topic node entities to the database
*
* @param topicTree - The topic tree structure containing nodes to update
* @returns A Promise that resolves when the update operation is complete
*/
async clearTree() {
return await this.topicNodeRepository.clear()
public async updateTopicNodes(topicTree: TopicTreeNode): Promise<void> {
const connectionId = topicTree.connectionInfo?.id
if (!connectionId) return
await this.deleteTopicNodesForConnection(connectionId)
const flattenedNodes = flattenTopicTree(topicTree)
const topicNodesEntities: TopicNodeEntity[] = []
flattenedNodes.forEach((node) => {
const entity = this.createTopicNodeEntity(node)
topicNodesEntities.push(entity)
})
this.establishNodeEntityRelationships(flattenedNodes, topicNodesEntities)
await this.topicNodeRepository.save(topicNodesEntities)
}

/**
Expand Down Expand Up @@ -296,12 +275,12 @@ export default class TopicNodeService {
.execute()
}

// public async syncTopicNodesFromMessages(topicNodes: TopicTreeNode[]): Promise<void> {
// const topicNodesEntities: TopicNodeEntity[] = []
// await this.mapTopicNodesToEntities(topicNodes, (entity) => {
// topicNodesEntities.push(entity)
// })
// this.establishParentChildRelationships(topicNodes, topicNodesEntities)
// await this.topicNodeRepository.save(topicNodesEntities)
// }
/**
* Clears all topic nodes from the repository.
*
* @returns A promise that resolves when all nodes have been cleared
*/
async clearTree() {
return await this.topicNodeRepository.clear()
}
}
Loading

0 comments on commit 14c71b6

Please sign in to comment.