Sangeetha-Grantha

Metadata Value
Status Archived
Version 0.1.0
Last Updated 2026-09-10
Author System
Document Type Archive


title: Graph Explorer Implementation Plan - PostgreSQL + Cytoscape.js status: Draft version: 1.0 last_updated: 2025-01-27 owners:

Graph Explorer Implementation Plan


[!NOTE] Historical evidence: results, counts, commands, and observations below belong to the original work described here. The editorial update date is not a new test or corpus verification. For present behavior, use current feature map.

PostgreSQL + Cytoscape.js Approach

Executive Summary

This document provides a comprehensive implementation plan for building a Music-Aware Graph Explorer using PostgreSQL (as the single source of truth) and Cytoscape.js (for client-side visualization). This approach avoids the operational complexity of Neo4j while delivering the required graph visualization functionality.

Note: This is a detailed implementation plan. For the complete feature requirements and architecture decision, see- Graph Explorer Feature Requirements .

Key Design Decisions:

Estimated Timeline: 11-15 days


Table of Contents

  1. Architecture Overview
  2. Backend Implementation
  3. Frontend Implementation
  4. Database Query Strategy
  5. Step-by-Step Implementation Guide
  6. Testing Strategy
  7. Documentation Requirements
  8. Acceptance Criteria

1. Architecture Overview

1.1 System Architecture

┌─────────────────┐
│  Admin Web UI   │
│  (React + TS)   │
│                 │
│  GraphExplorer  │
│  + Cytoscape.js │
└────────┬────────┘
         │ HTTP/REST
         │
┌────────▼────────┐
│  Ktor Backend   │
│                 │
│  GraphService   │
│  + Routes       │
└────────┬────────┘
         │
┌────────▼────────┐
│   PostgreSQL    │
│  (Single Source)│
│                 │
│  Recursive CTEs │
│  + Joins        │
└─────────────────┘

1.2 Data Flow

  1. User Interaction: User selects mode, searches, sets depth
  2. Frontend: Calls Graph API endpoints
  3. Backend: Executes PostgreSQL queries (recursive CTEs or multi-query)
  4. Response: Returns nodes + edges as JSON
  5. Frontend: Constructs graph, renders with Cytoscape.js
  6. User Interaction: Clicks nodes → fetches neighborhood → updates graph

1.3 Graph Model

Node Types:

Relationships:

Node Properties:


2. Backend Implementation

2.1 Shared Domain DTOs

File: modules/shared/domain/src/commonMain/kotlin/com/sangita/grantha/shared/domain/model/GraphDtos.kt

package com.sangita.grantha.shared.domain.model

import kotlinx.serialization.Serializable

@Serializable data class GraphNodeDto( val id: String, val label: String, val type: String, val properties: Map<String, String> = emptyMap() )

@Serializable data class GraphEdgeDto( val id: String, val source: String, val target: String, val type: String, val properties: Map<String, String> = emptyMap() )

@Serializable data class GraphResponseDto( val nodes: List, val edges: List )

@Serializable data class GraphSearchResultDto( val id: String, val label: String, val type: String )

@Serializable
enum class GraphEntityMode {
    KRITHI,
    COMPOSER,
    RAGA,
    TALA,
    DEITY,
    KSHETRAM,
    TAG
}

2.2 Graph Service

File: modules/backend/api/src/main/kotlin/com/sangita/grantha/backend/api/services/GraphService.kt

Responsibilities:

Key Methods: class GraphService(private val dal: SangitaDal) { suspend fun getNeighborhood( mode: GraphEntityMode, id: Uuid, depth: Int ): GraphResponseDto

suspend fun search(
    mode: GraphEntityMode,
    query: String
): List<GraphSearchResultDto>

```kotlin
suspend fun getPresetGraph(
    mode: GraphEntityMode,
    query: String?,
    depth: Int
): GraphResponseDto } ```

2.3 Graph Repository

File: modules/backend/dal/src/main/kotlin/com/sangita/grantha/backend/dal/repositories/GraphRepository.kt

Responsibilities:

Query Strategy:

2.4 API Routes

File: modules/backend/api/src/main/kotlin/com/sangita/grantha/backend/api/routes/graphRoutes.kt

Endpoints: fun Route.graphRoutes(graphService: GraphService) { route(“/v1/admin/graph”) { // GET /v1/admin/graph/neighborhood?mode=Krithi&id={uuid}&depth=2 get(“/neighborhood”) { … }

    // GET /v1/admin/graph/search?mode=Raga&q=shankarabharanam
    get("/search") { ... }
    
    ```text
    // GET /v1/admin/graph/preset?mode=Raga&q=shankarabharanam&depth=2
    get("/preset") { ... }
} } ```

Authentication: Uses existing authenticate("admin-auth") middleware

2.5 Route Registration

Update: modules/backend/api/src/main/kotlin/com/sangita/grantha/backend/api/plugins/Routing.kt

fun Application.configureRouting(
    // ... existing services
    graphService: GraphService,
) {
    // ... existing routes
    authenticate("admin-auth") {
        // ... existing routes
        graphRoutes(graphService)
    }
}

3. Frontend Implementation

3.1 API Client

File: modules/frontend/sangita-admin-web/src/api/graphApi.ts

import { request } from ‘./client’;

export interface GraphNode { id: string; label: string; type: string; properties: Record<string, string>; }

export interface GraphEdge { id: string; source: string; target: string; type: string; properties: Record<string, string>; }

export interface GraphResponse { nodes: GraphNode[]; edges: GraphEdge[]; }

export interface GraphSearchResult { id: string; label: string; type: string; }

export type GraphEntityMode = | ‘KRITHI’ | ‘COMPOSER’ | ‘RAGA’ | ‘TALA’ | ‘DEITY’ | ‘KSHETRAM’ | ‘TAG’;

export const graphApi = { search: (mode: GraphEntityMode, q: string): Promise<GraphSearchResult[]> => { const params = new URLSearchParams({ mode, q }); return request<GraphSearchResult[]>(/admin/graph/search?${params}); },

neighborhood: (
    mode: GraphEntityMode, 
    id: string, 
    depth: number
): Promise<GraphResponse> => {
    const params = new URLSearchParams({ 
        mode, 
        id, 
        depth: depth.toString() 
    });
    return request<GraphResponse>(`/admin/graph/neighborhood?${params}`);
},

```text
preset: (
    mode: GraphEntityMode, 
    q: string | null, 
    depth: number
): Promise<GraphResponse> => {
    const params = new URLSearchParams({ 
        mode, 
        depth: depth.toString() 
    });
    if (q) params.append('q', q);
    return request<GraphResponse>(`/admin/graph/preset?${params}`);
}, }; ```

3.2 Graph Explorer Page

File: modules/frontend/sangita-admin-web/src/pages/GraphExplorer.tsx

Layout:

┌─────────────────────────────────────────────────┐
│  [Mode ▼] [Search...] [Depth: 1─●─3] [Load]    │
├──────────────────────────┬──────────────────────┤
│                          │                      │
│   Cytoscape Canvas       │   Details Panel      │
│   (80% width)            │   (20% width)        │
│                          │                      │
│   [Graph Visualization] │   [Node Properties]  │
│                          │   [Quick Actions]    │
│                          │                      │
└──────────────────────────┴──────────────────────┘

Key Features:

3.3 Cytoscape.js Integration

**Dependencies:**
{
  "cytoscape": "^3.27.0",
  "cytoscape-fcose": "^2.2.0"
}

Component: modules/frontend/sangita-admin-web/src/components/graph/CytoscapeGraph.tsx

Node Shapes:

Layout: Use fcose layout algorithm with reasonable defaults

Styling: Minimal, neutral colors using Tailwind CSS

3.4 Graph State Management

File: modules/frontend/sangita-admin-web/src/pages/GraphExplorer.tsx

State:

Interactions:

3.5 Route Registration

Update: modules/frontend/sangita-admin-web/src/App.tsx

import GraphExplorer from ‘./pages/GraphExplorer’;

// In Routes:
<Route path="/graph-explorer" element={<GraphExplorer />} />

Update Sidebar: Add navigation link to Graph Explorer


4. Database Query Strategy

4.1 Neighborhood Query (Depth 1)

Simple Case: Direct relationships from a single entity

**Example: Krithi  Related Entities**
-- Get Krithi and direct relationships
WITH krithi_node AS (
    SELECT id, title as label, 'KRITHI' as type
    FROM krithis
    WHERE id = $id
),
composer_edge AS (
    SELECT 
        k.id as source,
        c.id as target,
        'COMPOSED_BY' as rel_type
    FROM krithis k
    JOIN composers c ON k.composer_id = c.id
    WHERE k.id = $id
),
raga_edges AS (
    SELECT 
        kr.krithi_id as source,
        r.id as target,
        'IN_RAGA' as rel_type
    FROM krithi_ragas kr
    JOIN ragas r ON kr.raga_id = r.id
    WHERE kr.krithi_id = $id
),
-- ... other relationships
all_nodes AS (
    SELECT id, label, type FROM krithi_node
    UNION
    SELECT id, name as label, 'COMPOSER' FROM composers WHERE id IN (SELECT target FROM composer_edge)
    UNION
    SELECT id, name as label, 'RAGA' FROM ragas WHERE id IN (SELECT target FROM raga_edges)
    -- ... other node types
),
all_edges AS (
    SELECT source, target, rel_type FROM composer_edge
    UNION
    SELECT source, target, rel_type FROM raga_edges
    -- ... other edges
)
SELECT * FROM all_nodes, all_edges;

4.2 Neighborhood Query (Depth 2-3)

Complex Case: Multi-hop traversal using recursive CTEs

Example: Raga → Krithis → Composers (Depth 2) WITH RECURSIVE graph_path AS ( – Base: Start node SELECT r.id, r.name as label, ‘RAGA’ as type, 0 as depth, r.id::text as path FROM ragas r WHERE r.id = $id

UNION ALL

-- Depth 1: Raga → Krithis
SELECT 
    k.id,
    k.title as label,
    'KRITHI' as type,
    1 as depth,
    gp.path || '->' || k.id::text
FROM graph_path gp
JOIN krithi_ragas kr ON kr.raga_id = gp.id
JOIN krithis k ON k.id = kr.krithi_id
WHERE gp.depth = 0
  AND gp.type = 'RAGA'
  AND k.workflow_state = 'published'  -- Filter published only if needed

UNION ALL

```sql
-- Depth 2: Krithis → Composers
SELECT 
    c.id,
    c.name as label,
    'COMPOSER' as type,
    2 as depth,
    gp.path || '->' || c.id::text
FROM graph_path gp
JOIN krithis k ON k.id = gp.id
JOIN composers c ON c.id = k.composer_id
WHERE gp.depth = 1
  AND gp.type = 'KRITHI' ) SELECT DISTINCT id, label, type, depth FROM graph_path WHERE depth <= $max_depth; ```

Note: This approach works but may be complex. Alternative: Use application-level expansion (fetch depth 1, then fetch neighbors of those nodes).

4.3 Search Query

Strategy: Use existing normalized indexes

**Example: Raga Search**
SELECT id, name as label, 'RAGA' as type
FROM ragas
WHERE name_normalized LIKE '%' || lower($query) || '%'
ORDER BY name
LIMIT 20;
**Example: Krithi Search**
SELECT id, title as label, 'KRITHI' as type
FROM krithis
WHERE title_normalized LIKE '%' || lower($query) || '%'
  AND workflow_state = 'published'  -- Or allow all for admin
ORDER BY title
LIMIT 20;

4.4 Preset Query

Strategy: Mode-specific curated queries

Example: Raga Preset (Janya chain + Krithis) – Get raga and its janya hierarchy WITH RECURSIVE raga_hierarchy AS ( SELECT id, name, parent_raga_id, 0 as level FROM ragas WHERE id = $id OR name_normalized LIKE ‘%’ || lower($query) || ‘%’

UNION ALL

```sql
SELECT r.id, r.name, r.parent_raga_id, rh.level + 1
FROM ragas r
JOIN raga_hierarchy rh ON r.parent_raga_id = rh.id
WHERE rh.level < 3 ), -- Get krithis in these ragas krithi_connections AS (
SELECT DISTINCT kr.krithi_id, kr.raga_id
FROM krithi_ragas kr
JOIN raga_hierarchy rh ON kr.raga_id = rh.id ) -- Combine nodes and edges SELECT ...; ```

4.5 Query Optimization

Indexes (should already exist):

Performance Targets:


5. Step-by-Step Implementation Guide

Phase 1: Backend Foundation (Days 1-3)

Day 1: DTOs and Service Structure

  1. ✅ Create GraphDtos.kt in shared domain
  2. ✅ Add GraphEntityMode enum
  3. ✅ Create GraphService.kt skeleton
  4. ✅ Create GraphRepository.kt skeleton
  5. ✅ Add basic error handling

Day 2: Database Queries

  1. ✅ Implement depth 1 neighborhood query
  2. ✅ Test with sample data
  3. ✅ Implement search query
  4. ✅ Add query result transformation to DTOs

Day 3: API Routes

  1. ✅ Create graphRoutes.kt
  2. ✅ Implement /neighborhood endpoint
  3. ✅ Implement /search endpoint
  4. ✅ Add route registration
  5. ✅ Test endpoints with curl/Postman

Phase 2: Backend Advanced (Days 4-5)

Day 4: Multi-Depth Queries

  1. ✅ Implement depth 2-3 queries (recursive CTE or app-level)
  2. ✅ Add relationship whitelist per mode
  3. ✅ Implement preset queries
  4. ✅ Add query result deduplication

Day 5: Polish and Testing

  1. ✅ Add input validation
  2. ✅ Add error handling and logging
  3. ✅ Write unit tests for service
  4. ✅ Write integration tests for routes
  5. ✅ Performance testing

Phase 3: Frontend Foundation (Days 6-8)

Day 6: API Client and Types

  1. ✅ Install Cytoscape.js and fcose
  2. ✅ Create graphApi.ts
  3. ✅ Add TypeScript types
  4. ✅ Test API client with mock data

Day 7: Graph Component

  1. ✅ Create CytoscapeGraph.tsx component
  2. ✅ Set up Cytoscape.js instance
  3. ✅ Configure node shapes and styles
  4. ✅ Implement fcose layout
  5. ✅ Add basic interaction (click, hover)

Day 8: Graph Explorer Page

  1. ✅ Create GraphExplorer.tsx page
  2. ✅ Add mode dropdown
  3. ✅ Add search input
  4. ✅ Add depth slider
  5. ✅ Wire up API calls
  6. ✅ Add route registration

Phase 4: Frontend Advanced (Days 9-11)

Day 9: Interactions and Details Panel

  1. ✅ Implement node click → fetch neighborhood
  2. ✅ Implement search → select → load
  3. ✅ Create details panel component
  4. ✅ Add node property display
  5. ✅ Add quick actions (link to entity editor)

Day 10: UI Polish

  1. ✅ Add loading states
  2. ✅ Add error handling and toasts
  3. ✅ Add reset view button
  4. ✅ Improve styling with Tailwind
  5. ✅ Add filters panel (optional)

Day 11: Testing and Documentation

  1. ✅ Test all user flows
  2. ✅ Fix bugs
  3. ✅ Add JSDoc comments
  4. ✅ Update documentation
  5. ✅ Code review

Phase 5: Documentation (Day 12)

Day 12: Documentation

  1. ✅ Create docs/architecture/graph-explorer.md
  2. ✅ Add API documentation
  3. ✅ Add user guide
  4. ✅ Add developer notes
  5. ✅ Update README if needed

6. Testing Strategy

6.1 Backend Tests

Unit Tests:

Integration Tests:

Test Data:

6.2 Frontend Tests

Component Tests:

E2E Tests (Optional):

6.3 Manual Testing Checklist


7. Documentation Requirements

7.1 Architecture Documentation

File: application_documentation/02-architecture/graph-explorer.md

Contents:

7.2 API Documentation

Update: application_documentation/03-api/api-contract.md

Add:

7.3 User Guide

File: application_documentation/05-frontend/admin-web/graph-explorer-user-guide.md

Contents:

7.4 Developer Notes

File: application_documentation/08-operations/runbooks/graph-explorer-dev.md

Contents:


8. Acceptance Criteria

8.1 Functional Requirements

8.2 Technical Requirements

8.3 Documentation Requirements

8.4 Quality Requirements


9. Implementation Details

9.1 Backend File Structure

modules/backend/
├── api/
│   ├── routes/
│   │   └── graphRoutes.kt          # NEW
│   ├── services/
│   │   └── GraphService.kt         # NEW
│   └── models/
│       └── GraphModels.kt          # NEW (if needed)
├── dal/
│   └── repositories/
│       └── GraphRepository.kt      # NEW
└── shared/
    └── domain/
        └── model/
            └── GraphDtos.kt         # NEW

9.2 Frontend File Structure

modules/frontend/sangita-admin-web/src/
├── api/
│   └── graphApi.ts                 # NEW
├── pages/
│   └── GraphExplorer.tsx          # NEW
├── components/
│   └── graph/
│       ├── CytoscapeGraph.tsx      # NEW
│       └── GraphDetailsPanel.tsx   # NEW
└── types.ts                        # UPDATE (add graph types)

9.3 Dependencies

Backend:

**Frontend:**
{
  "dependencies": {
    "cytoscape": "^3.27.0",
    "cytoscape-fcose": "^2.2.0"
  }
}

9.4 Configuration

No new configuration needed (uses existing database connection)


10. Risk Mitigation

10.1 Performance Risks

Risk: Recursive CTEs may be slow for depth 3 Mitigation:

10.2 Complexity Risks

Risk: Complex queries may be hard to maintain Mitigation:

10.3 Frontend Risks

Risk: Large graphs may cause performance issues Mitigation:


11. Future Enhancements

11.1 Phase 2 Features

11.2 Performance Optimizations

11.3 Advanced Features


12. Success Metrics

12.1 Performance Metrics

12.2 Usage Metrics

12.3 Quality Metrics


Appendix A: Example Queries

A.1 Krithi Neighborhood (Depth 1)

– Get Krithi with all direct relationships SELECT ‘node’ as element_type, k.id::text as id, k.title as label, ‘KRITHI’ as type, jsonb_build_object( ‘workflow_state’, k.workflow_state, ‘musical_form’, k.musical_form ) as properties FROM krithis k WHERE k.id = $id

UNION ALL

SELECT ‘node’ as element_type, c.id::text, c.name, ‘COMPOSER’, jsonb_build_object(‘birth_year’, c.birth_year) FROM krithis k JOIN composers c ON k.composer_id = c.id WHERE k.id = $id

UNION ALL

SELECT 
    'edge' as element_type,
    k.id::text || '-COMPOSED_BY->' || c.id::text,
    k.id::text,
    c.id::text,
    'COMPOSED_BY',
    '{}'::jsonb
FROM krithis k
JOIN composers c ON k.composer_id = c.id
WHERE k.id = $id;

A.2 Raga Janya Chain (Preset)

WITH RECURSIVE raga_tree AS ( SELECT id, name, parent_raga_id, 0 as depth FROM ragas WHERE id = $id

UNION ALL

```sql
SELECT r.id, r.name, r.parent_raga_id, rt.depth + 1
FROM ragas r
JOIN raga_tree rt ON r.parent_raga_id = rt.id
WHERE rt.depth < 5 ) SELECT * FROM raga_tree; ```

Appendix B: Cytoscape.js Configuration

B.1 Basic Setup

import cytoscape from ‘cytoscape’; import fcose from ‘cytoscape-fcose’;

cytoscape.use(fcose);

const cy = cytoscape({
    container: document.getElementById('cy'),
    elements: {
        nodes: nodes,
        edges: edges
    },
    style: [
        {
            selector: 'node',
            style: {
                'label': 'data(label)',
                'width': 60,
                'height': 60,
                'text-valign': 'center',
                'text-halign': 'center',
                'font-size': '12px',
                'background-color': '#e8e8e8',
                'border-width': 2,
                'border-color': '#888'
            }
        },
        {
            selector: 'node[type="KRITHI"]',
            style: {
                'shape': 'round-rectangle',
                'background-color': '#4a90e2'
            }
        },
        {
            selector: 'node[type="RAGA"]',
            style: {
                'shape': 'hexagon',
                'background-color': '#7b68ee'
            }
        },
        // ... other node types
        {
            selector: 'edge',
            style: {
                'width': 2,
                'line-color': '#999',
                'target-arrow-color': '#999',
                'target-arrow-shape': 'triangle',
                'curve-style': 'bezier',
                'label': 'data(type)',
                'font-size': '10px'
            }
        }
    ],
    layout: {
        name: 'fcose',
        quality: 'default',
        randomize: false,
        animate: true,
        animationDuration: 1000,
        fit: true,
        padding: 30
    }
});

Document Status: Ready for implementation

Next Steps:

  1. Review and approve this plan
  2. Create implementation tickets
  3. Begin Phase 1 implementation
  4. Regular progress reviews

Section index · Documentation home · Feature status