diff --git a/flowsint-api/app/api/routes/chat.py b/flowsint-api/app/api/routes/chat.py index b7fd1cb..f13c1ac 100644 --- a/flowsint-api/app/api/routes/chat.py +++ b/flowsint-api/app/api/routes/chat.py @@ -83,6 +83,7 @@ def get_chats_by_investigation( .filter( Chat.investigation_id == investigation_id, Chat.owner_id == current_user.id ) + .order_by(Chat.created_at.asc()) .all() ) diff --git a/flowsint-api/app/api/routes/flows.py b/flowsint-api/app/api/routes/flows.py index 2c5bed9..7eadfd2 100644 --- a/flowsint-api/app/api/routes/flows.py +++ b/flowsint-api/app/api/routes/flows.py @@ -6,7 +6,7 @@ from datetime import datetime from flowsint_core.utils import extract_input_schema_flow from flowsint_core.core.registry import TransformRegistry from flowsint_core.core.celery import celery -from flowsint_types import Domain, Phrase, Ip, SocialProfile, Organization, Email +from flowsint_types import Domain, Phrase, Ip, SocialProfile, Organization, Email, Phone from flowsint_core.core.types import Node, Edge, FlowStep, FlowBranch from sqlalchemy.orm import Session from flowsint_core.core.postgre_db import get_db @@ -102,6 +102,7 @@ async def get_material_list(): extract_input_schema_flow(Domain), extract_input_schema_flow(Website), extract_input_schema_flow(Ip), + extract_input_schema_flow(Phone), extract_input_schema_flow(ASN), extract_input_schema_flow(CIDR), extract_input_schema_flow(SocialProfile), @@ -172,7 +173,7 @@ def update_flow( flow = db.query(Flow).filter(Flow.id == flow_id).first() if not flow: raise HTTPException(status_code=404, detail="flow not found") - update_data = payload.dict(exclude_unset=True) + update_data = payload.model_dump(exclude_unset=True) for key, value in update_data.items(): print(f"only update {key}") if key == "category": diff --git a/flowsint-api/app/api/routes/sketches.py b/flowsint-api/app/api/routes/sketches.py index e3712a4..79d5e7f 100644 --- a/flowsint-api/app/api/routes/sketches.py +++ b/flowsint-api/app/api/routes/sketches.py @@ -17,6 +17,45 @@ from app.api.deps import get_current_user router = APIRouter() +class NodeData(BaseModel): + label: str = Field(default="Node", description="Label/name of the node") + color: str = Field(default="Node", description="Color of the node") + type: str = Field(default="Node", description="Type of the node") + # Add any other specific data fields that might be common across nodes + + class Config: + extra = "allow" # Accept any additional fields + + +class NodeInput(BaseModel): + type: str = Field(..., description="Type of the node") + data: NodeData = Field( + default_factory=NodeData, description="Additional data for the node" + ) + + +def dict_to_cypher_props(props: dict, prefix: str = "") -> str: + return ", ".join(f"{key}: ${prefix}{key}" for key in props) + + +class NodeDeleteInput(BaseModel): + nodeIds: List[str] + + +class NodeEditInput(BaseModel): + nodeId: str + data: NodeData = Field( + default_factory=NodeData, description="Updated data for the node" + ) + + +class NodeMergeInput(BaseModel): + id: str + data: NodeData = Field( + default_factory=NodeData, description="Updated data for the node" + ) + + @router.post("/create", response_model=SketchRead, status_code=status.HTTP_201_CREATED) def create_sketch( data: SketchCreate, @@ -54,14 +93,19 @@ def get_sketch_by_id( @router.put("/{id}", response_model=SketchRead) def update_sketch( id: UUID, - data: SketchUpdate, + payload: SketchUpdate, db: Session = Depends(get_db), current_user: Profile = Depends(get_current_user), ): - sketch = db.query(Sketch).filter(Sketch.owner_id == current_user.id).get(id) + sketch = ( + db.query(Sketch) + .filter(Sketch.owner_id == current_user.id) + .filter(Sketch.id == id) + .first() + ) if not sketch: raise HTTPException(status_code=404, detail="Sketch not found") - for key, value in data.dict(exclude_unset=True).items(): + for key, value in payload.model_dump(exclude_unset=True).items(): setattr(sketch, key, value) db.commit() db.refresh(sketch) @@ -177,27 +221,6 @@ async def get_sketch_nodes( return {"nds": nodes, "rls": rels} -class NodeData(BaseModel): - label: str = Field(default="Node", description="Label/name of the node") - color: str = Field(default="Node", description="Color of the node") - type: str = Field(default="Node", description="Type of the node") - # Add any other specific data fields that might be common across nodes - - class Config: - extra = "allow" # Accept any additional fields - - -class NodeInput(BaseModel): - type: str = Field(..., description="Type of the node") - data: NodeData = Field( - default_factory=NodeData, description="Additional data for the node" - ) - - -def dict_to_cypher_props(props: dict, prefix: str = "") -> str: - return ", ".join(f"{key}: ${prefix}{key}" for key in props) - - @router.post("/{sketch_id}/nodes/add") def add_node( sketch_id: str, node: NodeInput, current_user: Profile = Depends(get_current_user) @@ -254,8 +277,8 @@ def add_node( class RelationInput(BaseModel): - source: Any - target: Any + source: str + target: str type: Literal["one-way", "two-way"] label: str = "RELATED_TO" # Optionnel : nom de la relation @@ -275,8 +298,8 @@ def add_edge( """ params = { - "from_id": relation.source["id"], - "to_id": relation.target["id"], + "from_id": relation.source, + "to_id": relation.target, "sketch_id": sketch_id, } @@ -295,17 +318,6 @@ def add_edge( } -class NodeDeleteInput(BaseModel): - nodeIds: List[str] - - -class NodeEditInput(BaseModel): - nodeId: str - data: NodeData = Field( - default_factory=NodeData, description="Updated data for the node" - ) - - @router.put("/{sketch_id}/nodes/edit") def edit_node( sketch_id: str, @@ -394,6 +406,112 @@ def delete_nodes( return {"status": "nodes deleted", "count": len(nodes.nodeIds)} +@router.post("/{sketch_id}/nodes/merge") +def merge_nodes( + sketch_id: str, + oldNodes: List[str], + newNode: NodeMergeInput, + db: Session = Depends(get_db), + current_user: Profile = Depends(get_current_user), +): + # 1. Vérifier le sketch + sketch = db.query(Sketch).filter(Sketch.id == sketch_id).first() + if not sketch: + raise HTTPException(status_code=404, detail="Sketch not found") + + oldNodeIds = [id for id in oldNodes] + + # 2. Préparer le node unique (utiliser nodeId) + node_id = getattr(newNode, "id", None) + if not node_id: + raise HTTPException(status_code=400, detail="newNode.id is required") + + properties = {} + if newNode.data: + flattened_data = flatten(newNode.data.dict()) + properties.update(flattened_data) + + cypher_props = dict_to_cypher_props(properties) + node_type = getattr(newNode, "type", "Node") + + # 3. Créer ou merger le nouveau node + create_query = f""" + MERGE (new:`{node_type}` {{nodeId: $nodeId}}) + SET new += $nodeData + RETURN elementId(new) as newElementId + """ + try: + result = neo4j_connection.query( + create_query, {"nodeId": node_id, "nodeData": cypher_props} + ) + new_node_element_id = result[0]["newElementId"] + except Exception as e: + print(f"Error creating/merging new node: {e}") + raise HTTPException(status_code=500, detail="Failed to create new node") + + # 4. Récupérer tous les types de relations des oldNodes + rel_types_query = """ + MATCH (old) + WHERE elementId(old) IN $oldNodeIds AND old.sketch_id = $sketch_id + MATCH (old)-[r]-() + RETURN DISTINCT type(r) AS relType + """ + try: + rel_types_result = neo4j_connection.query( + rel_types_query, {"oldNodeIds": oldNodeIds, "sketch_id": sketch_id} + ) + rel_types = [row["relType"] for row in rel_types_result] or [] + except Exception as e: + print(f"Error fetching relation types: {e}") + raise HTTPException(status_code=500, detail="Failed to fetch relation types") + + # 5. Construire la query pour copier les relations + blocks = [] + for rel_type in rel_types: + block = f""" + // Relations entrantes + MATCH (new) WHERE elementId(new) = $newElementId + MATCH (old) WHERE elementId(old) IN $oldNodeIds + OPTIONAL MATCH (src)-[r:`{rel_type}`]->(old) + WITH src, new, r WHERE src IS NOT NULL + MERGE (src)-[newRel:`{rel_type}`]->(new) + ON CREATE SET newRel = r + ON MATCH SET newRel += r + WITH DISTINCT new + + // Relations sortantes + MATCH (new) WHERE elementId(new) = $newElementId + MATCH (old) WHERE elementId(old) IN $oldNodeIds + OPTIONAL MATCH (old)-[r:`{rel_type}`]->(dst) + WITH dst, new, r WHERE dst IS NOT NULL + MERGE (new)-[newRel2:`{rel_type}`]->(dst) + ON CREATE SET newRel2 = r + ON MATCH SET newRel2 += r + WITH DISTINCT new + """ + blocks.append(block) + + # 6. Supprimer les anciens nodes + delete_query = """ + MATCH (old) + WHERE elementId(old) IN $oldNodeIds + DETACH DELETE old + """ + + full_query = "\n".join(blocks) + delete_query + + # 7. Exécuter la query + try: + neo4j_connection.query( + full_query, {"newElementId": new_node_element_id, "oldNodeIds": oldNodeIds} + ) + except Exception as e: + print(f"Node merging error: {e}") + raise HTTPException(status_code=500, detail="Failed to merge node relations") + + return {"status": "nodes merged", "count": len(oldNodeIds)} + + @router.get("/{sketch_id}/nodes/{node_id}") def get_related_nodes( sketch_id: str, diff --git a/flowsint-app/package.json b/flowsint-app/package.json index bcea71c..152007f 100644 --- a/flowsint-app/package.json +++ b/flowsint-app/package.json @@ -33,6 +33,7 @@ "@electron-toolkit/preload": "^3.0.1", "@electron-toolkit/utils": "^3.0.0", "@hookform/resolvers": "^5.0.1", + "@lukemorales/query-key-factory": "^1.3.4", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", "@radix-ui/react-aspect-ratio": "^1.1.7", diff --git a/flowsint-app/src/renderer/index.html b/flowsint-app/src/renderer/index.html index 153b424..c41e657 100644 --- a/flowsint-app/src/renderer/index.html +++ b/flowsint-app/src/renderer/index.html @@ -1,11 +1,11 @@
- + --> diff --git a/flowsint-app/src/renderer/src/api/README-query-keys.md b/flowsint-app/src/renderer/src/api/README-query-keys.md new file mode 100644 index 0000000..bd0b0b9 --- /dev/null +++ b/flowsint-app/src/renderer/src/api/README-query-keys.md @@ -0,0 +1,234 @@ +# Query Key Factory Implementation + +This project now uses the [@lukemorales/query-key-factory](https://tanstack.com/query/v4/docs/framework/react/community/lukemorales-query-key-factory) library to manage TanStack Query keys in a type-safe and organized way. + +## Overview + +Query Key Factory provides: +- **Type-safe query keys** with auto-completion +- **Centralized key management** - all keys in one place +- **Easy invalidation** - no more hardcoded key strings +- **Consistent patterns** across your application + +## Files structure + +``` +src/api/ +├── query-keys.ts # Main query key definitions +├── query-keys-examples.ts # Usage examples and custom hooks +└── README-query-keys.md # This documentation +``` + +## How to use + +### 1. Basic query usage + +```tsx +import { useQuery } from '@tanstack/react-query' +import { queryKeys } from '@/api/query-keys' +import { investigationService } from '@/api/investigation-service' + +// Before (hardcoded keys) +const { data } = useQuery({ + queryKey: ['investigations', 'list'], + queryFn: investigationService.get, +}) + +// After (using query key factory) +const { data } = useQuery({ + queryKey: queryKeys.investigations.list, + queryFn: investigationService.get, +}) +``` + +### 2. Dynamic Keys with Parameters + +```tsx +// Before +const { data } = useQuery({ + queryKey: ['investigations', investigationId], + queryFn: () => investigationService.getById(investigationId), +}) + +// After +const { data } = useQuery({ + queryKey: queryKeys.investigations.detail(investigationId), + queryFn: () => investigationService.getById(investigationId), +}) +``` + +### 3. Mutation with Invalidation + +```tsx +import { useMutation, useQueryClient } from '@tanstack/react-query' + +const createAnalysisMutation = useMutation({ + mutationFn: analysisService.create, + onSuccess: (data, variables) => { + const queryClient = useQueryClient() + + // Invalidate specific queries + queryClient.invalidateQueries({ + queryKey: queryKeys.investigations.analyses(variables.investigation_id) + }) + + // Invalidate general lists + queryClient.invalidateQueries({ + queryKey: queryKeys.analyses.list + }) + }, +}) +``` + +### 4. Cache Updates + +```tsx +const updateAnalysisMutation = useMutation({ + mutationFn: analysisService.update, + onSuccess: (data, variables) => { + const queryClient = useQueryClient() + + // Update cache directly for better UX + queryClient.setQueryData( + queryKeys.analyses.detail(variables.analysisId), + data + ) + + // Invalidate related queries + queryClient.invalidateQueries({ + queryKey: queryKeys.analyses.byInvestigation(data.investigation_id) + }) + }, +}) +``` + +## Available Query Keys + +### Auth +- `queryKeys.auth.session` +- `queryKeys.auth.currentUser` + +### Investigations +- `queryKeys.investigations.list` +- `queryKeys.investigations.detail(investigationId)` +- `queryKeys.investigations.sketches(investigationId)` +- `queryKeys.investigations.analyses(investigationId)` +- `queryKeys.investigations.flows(investigationId)` + +### Sketches/Graphs +- `queryKeys.sketches.list` +- `queryKeys.sketches.detail(sketchId)` +- `queryKeys.sketches.byInvestigation(investigationId)` +- `queryKeys.sketches.graph(investigationId, sketchId)` +- `queryKeys.sketches.types` + +### Analyses +- `queryKeys.analyses.list` +- `queryKeys.analyses.detail(analysisId)` +- `queryKeys.analyses.byInvestigation(investigationId)` + +### Flows +- `queryKeys.flows.list` +- `queryKeys.flows.detail(flowId)` +- `queryKeys.flows.byInvestigation(investigationId)` + +### Chats +- `queryKeys.chats.list` +- `queryKeys.chats.detail(chatId)` +- `queryKeys.chats.byInvestigation(investigationId)` +- `queryKeys.chats.messages(chatId)` + +### API Keys +- `queryKeys.keys.list` +- `queryKeys.keys.detail(keyId)` + +### Logs/Events +- `queryKeys.logs.bySketch(sketchId)` + +### Action Items +- `queryKeys.actionItems` + +### Scans +- `queryKeys.scans.list` +- `queryKeys.scans.detail(scanId)` + +### Transforms +- `queryKeys.transforms.list` +- `queryKeys.transforms.detail(transformId)` + +## Migration Guide + +### Step 1: Update Existing Hooks + +Find your existing `useQuery` calls and replace hardcoded keys: + +```tsx +// Before +queryKey: ['investigations', 'list'] + +// After +queryKey: queryKeys.investigations.list +``` + +### Step 2: Update Mutations + +Replace hardcoded invalidation keys: + +```tsx +// Before +queryClient.invalidateQueries({ queryKey: ['investigations'] }) + +// After +queryClient.invalidateQueries({ queryKey: queryKeys.investigations.list }) +``` + +### Step 3: Update Cache Operations + +Replace hardcoded keys in `setQueryData` and `removeQueries`: + +```tsx +// Before +queryClient.setQueryData(['analyses', analysisId], data) + +// After +queryClient.setQueryData(queryKeys.analyses.detail(analysisId), data) +``` + +## Benefits + +1. **Type Safety**: TypeScript will catch typos and provide autocomplete +2. **Refactoring**: Change a key structure in one place +3. **Consistency**: All keys follow the same pattern +4. **Maintainability**: Easy to see all available keys +5. **IDE Support**: Better autocomplete and IntelliSense + +## Best Practices + +1. **Always use the factory**: Don't hardcode query keys +2. **Group related keys**: Use the logical grouping provided +3. **Invalidate properly**: Use the exact key for invalidation +4. **Update cache**: Use `setQueryData` with the factory keys +5. **Consistent naming**: Follow the established patterns + +## Troubleshooting + +### Common Issues + +1. **Type errors**: Make sure you're calling the key function with parameters +2. **Invalidation not working**: Ensure you're using the exact same key structure +3. **Cache updates failing**: Verify the key matches the query key exactly + +### Debug Tips + +1. **Console log keys**: `console.log(queryKeys.investigations.list)` +2. **Check key structure**: Compare generated keys with existing ones +3. **Verify imports**: Ensure you're importing from the correct path + +## Examples + +See `query-keys-examples.ts` for comprehensive usage examples including: +- Basic queries +- Mutations with invalidation +- Cache updates +- Complex invalidation patterns +- Prefetching strategies diff --git a/flowsint-app/src/renderer/src/api/query-keys-examples.ts b/flowsint-app/src/renderer/src/api/query-keys-examples.ts new file mode 100644 index 0000000..4e8b732 --- /dev/null +++ b/flowsint-app/src/renderer/src/api/query-keys-examples.ts @@ -0,0 +1,216 @@ +// Examples of how to use the Query Key Factory with your existing hooks +// This file demonstrates the proper usage patterns + +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { queryKeys } from './query-keys' +import { analysisService } from './analysis-service' +import { investigationService } from './investigation-service' +import { sketchService } from './sketch-service' +import { chatCRUDService } from './chat-service' +import { KeyService } from './key-service' +import { logService } from './log-service' +import { scanService } from './scan-service' +import { transformService } from './transform-service' + +// Example 1: Using the query keys directly +export const useInvestigationsList = () => { + return useQuery({ + queryKey: queryKeys.investigations.list, + queryFn: investigationService.get, + }) +} + +export const useInvestigationDetail = (investigationId: string) => { + return useQuery({ + queryKey: queryKeys.investigations.detail(investigationId), + queryFn: () => investigationService.getById(investigationId), + enabled: !!investigationId, + }) +} + +export const useInvestigationAnalyses = (investigationId: string) => { + return useQuery({ + queryKey: queryKeys.investigations.analyses(investigationId), + queryFn: () => analysisService.getByInvestigationId(investigationId), + enabled: !!investigationId, + }) +} + +// Example 2: Using the individual key groups +export const useSketchesList = () => { + return useQuery({ + queryKey: queryKeys.sketches.list, + queryFn: sketchService.get, + }) +} + +export const useSketchDetail = (sketchId: string) => { + return useQuery({ + queryKey: queryKeys.sketches.detail(sketchId), + queryFn: () => sketchService.getById(sketchId), + enabled: !!sketchId, + }) +} + +export const useSketchGraph = (investigationId: string, sketchId: string) => { + return useQuery({ + queryKey: queryKeys.sketches.graph(investigationId, sketchId), + queryFn: () => sketchService.getById(sketchId), + enabled: !!investigationId && !!sketchId, + }) +} + +// Example 3: Using with mutations and invalidation +export const useCreateAnalysis = () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (data: { title: string; investigation_id: string; content: any }) => { + return analysisService.create(JSON.stringify(data)) + }, + onSuccess: (data, variables) => { + // Invalidate the specific investigation's analyses + queryClient.invalidateQueries({ + queryKey: queryKeys.investigations.analyses(variables.investigation_id) + }) + + // Also invalidate the general analyses list + queryClient.invalidateQueries({ + queryKey: queryKeys.analyses.list + }) + }, + }) +} + +export const useUpdateAnalysis = () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ analysisId, data }: { analysisId: string; data: any }) => { + return analysisService.update(analysisId, JSON.stringify(data)) + }, + onSuccess: (data, variables) => { + // Update the cache directly for better UX + queryClient.setQueryData( + queryKeys.analyses.detail(variables.analysisId), + data + ) + + // Invalidate related queries + queryClient.invalidateQueries({ + queryKey: queryKeys.analyses.byInvestigation(data.investigation_id) + }) + }, + }) +} + +// Example 4: Using with chat functionality +export const useChatDetail = (chatId: string) => { + return useQuery({ + queryKey: queryKeys.chats.detail(chatId), + queryFn: () => chatCRUDService.getById(chatId), + enabled: !!chatId, + }) +} + +export const useChatsByInvestigation = (investigationId: string) => { + return useQuery({ + queryKey: queryKeys.chats.byInvestigation(investigationId), + queryFn: () => chatCRUDService.getByInvestigationId(investigationId), + enabled: !!investigationId, + }) +} + +// Example 5: Using with logs and events +export const useLogsBySketch = (sketchId: string) => { + return useQuery({ + queryKey: queryKeys.logs.bySketch(sketchId), + queryFn: () => logService.get(sketchId), + enabled: !!sketchId, + staleTime: 30_000, // 30 seconds + }) +} + +// Example 6: Using with API keys +export const useKeysList = () => { + return useQuery({ + queryKey: queryKeys.keys.list, + queryFn: KeyService.get, + }) +} + +export const useKeyDetail = (keyId: string) => { + return useQuery({ + queryKey: queryKeys.keys.detail(keyId), + queryFn: () => KeyService.getById(keyId), + enabled: !!keyId, + }) +} + +// Example 7: Using with scans +export const useScanDetail = (scanId: string) => { + return useQuery({ + queryKey: queryKeys.scans.detail(scanId), + queryFn: () => scanService.get(scanId), + enabled: !!scanId, + }) +} + +// Example 8: Using with transforms +export const useTransformsList = () => { + return useQuery({ + queryKey: queryKeys.transforms.list, + queryFn: transformService.get, + }) +} + +// Example 9: Complex invalidation patterns +export const useDeleteInvestigation = () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: investigationService.delete, + onSuccess: (data, variables) => { + // Invalidate all investigation-related queries + queryClient.invalidateQueries({ + queryKey: queryKeys.investigations.list + }) + + // Invalidate all related data + queryClient.invalidateQueries({ + queryKey: queryKeys.investigations.detail(variables) + }) + + // Clear related caches + queryClient.removeQueries({ + queryKey: queryKeys.investigations.analyses(variables) + }) + + queryClient.removeQueries({ + queryKey: queryKeys.investigations.sketches(variables) + }) + + queryClient.removeQueries({ + queryKey: queryKeys.investigations.flows(variables) + }) + }, + }) +} + +// Example 10: Prefetching with query keys +export const usePrefetchInvestigation = () => { + const queryClient = useQueryClient() + + return (investigationId: string) => { + queryClient.prefetchQuery({ + queryKey: queryKeys.investigations.detail(investigationId), + queryFn: () => investigationService.getById(investigationId), + }) + + // Also prefetch related data + queryClient.prefetchQuery({ + queryKey: queryKeys.investigations.analyses(investigationId), + queryFn: () => analysisService.getByInvestigationId(investigationId), + }) + } +} diff --git a/flowsint-app/src/renderer/src/api/query-keys.ts b/flowsint-app/src/renderer/src/api/query-keys.ts new file mode 100644 index 0000000..5424f9a --- /dev/null +++ b/flowsint-app/src/renderer/src/api/query-keys.ts @@ -0,0 +1,91 @@ +// Simple query key factory that actually works +export const queryKeys = { + // Auth related queries + auth: { + session: ['auth', 'session'], + currentUser: ['auth', 'currentUser'], + }, + + // Investigations + investigations: { + list: ['investigations', 'list'], + detail: (investigationId: string) => ['investigations', investigationId], + sketches: (investigationId: string) => [investigationId, 'sketches'], + analyses: (investigationId: string) => [investigationId, 'analyses'], + flows: (investigationId: string) => [investigationId, 'flows'], + dashboard: ['investigations', 'dashboard'], + selector: (investigationId: string) => ['dashboard', 'selector', investigationId], + }, + + // Sketches/Graphs + sketches: { + list: ['sketches', 'list'], + detail: (sketchId: string) => ['sketches', sketchId], + byInvestigation: (investigationId: string) => [investigationId, 'sketches'], + graph: (investigationId: string, sketchId: string) => [investigationId, 'graph', sketchId], + types: ['sketches', 'types'], + dashboard: (investigationId: string) => ['dashboard', 'investigation', investigationId], + }, + + // Analyses + analyses: { + list: ['analyses', 'list'], + detail: (analysisId: string) => ['analyses', analysisId], + byInvestigation: (investigationId: string) => [investigationId, 'analyses'], + dashboard: (investigationId: string) => ['analyses', investigationId], + }, + + // Flows + flows: { + list: ['flows', 'list'], + detail: (flowId: string) => ['flows', flowId], + byInvestigation: (investigationId: string) => [investigationId, 'flows'], + }, + + // Chats + chats: { + list: ['chats', 'list'], + detail: (chatId: string) => ['chats', chatId], + byInvestigation: (investigationId: string) => [investigationId, 'chats'], + messages: (chatId: string) => [chatId, 'messages'], + }, + + // API Keys + keys: { + list: ['keys'], + detail: (keyId: string) => ['keys', keyId], + }, + + // Logs/Events + logs: { + bySketch: (sketchId: string) => [sketchId, 'logs'], + }, + + // Action Items + actionItems: ['actionItems'], + + // Scans + scans: { + list: ['scans', 'list'], + detail: (scanId: string) => ['scans', scanId], + }, + + // Transforms + transforms: { + list: ['transforms', 'list'], + detail: (transformId: string) => ['transforms', transformId], + }, +} + +// Export individual key groups for easier imports +export const authKeys = queryKeys.auth +export const investigationKeys = queryKeys.investigations +export const sketchKeys = queryKeys.sketches +export const analysisKeys = queryKeys.analyses +export const flowKeys = queryKeys.flows +export const chatKeys = queryKeys.chats +export const keyKeys = queryKeys.keys +export const logKeys = queryKeys.logs +export const actionItemKeys = queryKeys.actionItems +export const scanKeys = queryKeys.scans +export const transformKeys = queryKeys.transforms diff --git a/flowsint-app/src/renderer/src/api/sketch-service.ts b/flowsint-app/src/renderer/src/api/sketch-service.ts index fc9d8c4..9be7ebf 100644 --- a/flowsint-app/src/renderer/src/api/sketch-service.ts +++ b/flowsint-app/src/renderer/src/api/sketch-service.ts @@ -40,6 +40,12 @@ export const sketchService = { body: body }); }, + mergeNodes: async (sketchId: string, body: BodyInit): Promise