mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools
Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
* GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
* GET /api/v1/users/me/organizations - User organizations
* POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue
API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules
Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
490 lines
12 KiB
Markdown
490 lines
12 KiB
Markdown
# Neo4j Go Driver Development Guide
|
|
|
|
**Library**: `github.com/neo4j/neo4j-go-driver/v5`
|
|
**Used In**: MVP - Graph database for resource matching
|
|
**Purpose**: Connect to Neo4j and execute Cypher queries
|
|
|
|
---
|
|
|
|
## Where It's Used
|
|
|
|
- Store graph structure (Business → Site → ResourceFlow)
|
|
- Query matches between ResourceFlows
|
|
- Create and manage relationships
|
|
- Graph traversal for matching algorithm
|
|
|
|
---
|
|
|
|
## Official Documentation
|
|
|
|
- **GitHub**: https://github.com/neo4j/neo4j-go-driver
|
|
- **Official Docs**: https://neo4j.com/docs/driver-manual/current/drivers/go/
|
|
- **GoDoc**: https://pkg.go.dev/github.com/neo4j/neo4j-go-driver/v5
|
|
- **Cypher Query Language**: https://neo4j.com/docs/cypher-manual/current/
|
|
|
|
---
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
go get github.com/neo4j/neo4j-go-driver/v5
|
|
```
|
|
|
|
---
|
|
|
|
## Key Concepts
|
|
|
|
### 1. Driver Initialization
|
|
|
|
```go
|
|
import (
|
|
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
|
|
"context"
|
|
)
|
|
|
|
func NewNeo4jDriver(uri, username, password string) (neo4j.DriverWithContext, error) {
|
|
driver, err := neo4j.NewDriverWithContext(
|
|
uri, // e.g., "neo4j://localhost:7687"
|
|
neo4j.BasicAuth(username, password, ""),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify connectivity
|
|
ctx := context.Background()
|
|
if err := driver.VerifyConnectivity(ctx); err != nil {
|
|
driver.Close(ctx)
|
|
return nil, err
|
|
}
|
|
|
|
return driver, nil
|
|
}
|
|
```
|
|
|
|
### 2. Session Management
|
|
|
|
```go
|
|
// Create session
|
|
session := driver.NewSession(ctx, neo4j.SessionConfig{
|
|
AccessMode: neo4j.AccessModeWrite, // or AccessModeRead
|
|
DatabaseName: "neo4j", // or your database name
|
|
})
|
|
defer session.Close(ctx)
|
|
|
|
// Execute query
|
|
result, err := session.Run(ctx, cypher, params)
|
|
```
|
|
|
|
### 3. Basic Cypher Queries
|
|
|
|
```go
|
|
// Create node
|
|
cypher := "CREATE (b:Business {id: $id, name: $name, email: $email}) RETURN b"
|
|
params := map[string]interface{}{
|
|
"id": uuid.New().String(),
|
|
"name": "Factory A",
|
|
"email": "contact@factorya.com",
|
|
}
|
|
|
|
result, err := session.Run(ctx, cypher, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Process single record
|
|
record, err := result.Single(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
node, _ := record.Get("b")
|
|
businessNode := node.(neo4j.Node)
|
|
```
|
|
|
|
### 4. Create Relationships
|
|
|
|
```go
|
|
// Match nodes and create relationship
|
|
cypher := `
|
|
MATCH (b:Business {id: $businessID})
|
|
MATCH (s:Site {id: $siteID})
|
|
CREATE (b)-[:OPERATES_AT]->(s)
|
|
RETURN b, s
|
|
`
|
|
params := map[string]interface{}{
|
|
"businessID": businessID,
|
|
"siteID": siteID,
|
|
}
|
|
|
|
_, err := session.Run(ctx, cypher, params)
|
|
```
|
|
|
|
### 5. Query with Results
|
|
|
|
```go
|
|
// Query multiple records
|
|
cypher := `
|
|
MATCH (b:Business)-[:OPERATES_AT]->(s:Site)
|
|
WHERE b.id = $businessID
|
|
RETURN b, s
|
|
`
|
|
params := map[string]interface{}{
|
|
"businessID": businessID,
|
|
}
|
|
|
|
result, err := session.Run(ctx, cypher, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var sites []Site
|
|
for result.Next(ctx) {
|
|
record := result.Record()
|
|
siteNode, _ := record.Get("s")
|
|
site := parseSiteNode(siteNode.(neo4j.Node))
|
|
sites = append(sites, site)
|
|
}
|
|
|
|
if err := result.Err(); err != nil {
|
|
return err
|
|
}
|
|
```
|
|
|
|
### 6. Transactions
|
|
|
|
```go
|
|
// Transaction with automatic commit/rollback
|
|
_, err = session.ExecuteWrite(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
|
|
// Create business
|
|
result, err := tx.Run(ctx,
|
|
"CREATE (b:Business {id: $id, name: $name}) RETURN b.id",
|
|
map[string]interface{}{"id": id, "name": name},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
record, err := result.Single(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
businessID, _ := record.Get("b.id")
|
|
|
|
// Create site in same transaction
|
|
_, err = tx.Run(ctx,
|
|
"MATCH (b:Business {id: $businessID}) CREATE (s:Site {id: $siteID}) CREATE (b)-[:OPERATES_AT]->(s)",
|
|
map[string]interface{}{"businessID": businessID, "siteID": siteID},
|
|
)
|
|
|
|
return businessID, err
|
|
})
|
|
```
|
|
|
|
### 7. Parameter Binding
|
|
|
|
```go
|
|
// Using struct for parameters
|
|
type BusinessParams struct {
|
|
ID string
|
|
Name string
|
|
Email string
|
|
}
|
|
|
|
params := BusinessParams{
|
|
ID: uuid.New().String(),
|
|
Name: "Factory A",
|
|
Email: "contact@factorya.com",
|
|
}
|
|
|
|
cypher := `
|
|
CREATE (b:Business {
|
|
id: $id,
|
|
name: $name,
|
|
email: $email
|
|
})
|
|
RETURN b
|
|
`
|
|
|
|
// Neo4j driver automatically converts struct to map
|
|
result, err := session.Run(ctx, cypher, params)
|
|
```
|
|
|
|
### 8. Extracting Values from Records
|
|
|
|
```go
|
|
record, _ := result.Single(ctx)
|
|
|
|
// Get by key
|
|
node, _ := record.Get("b")
|
|
relationship, _ := record.Get("r")
|
|
|
|
// Type assertions
|
|
businessNode := node.(neo4j.Node)
|
|
props := businessNode.Props
|
|
|
|
// Extract properties
|
|
id, _ := props["id"].(string)
|
|
name, _ := props["name"].(string)
|
|
|
|
// Or use helper function
|
|
func getString(record neo4j.Record, key string) string {
|
|
val, ok := record.Get(key)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
str, _ := val.(string)
|
|
return str
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## MVP-Specific Patterns
|
|
|
|
### Resource Flow Service
|
|
|
|
```go
|
|
type Neo4jService struct {
|
|
driver neo4j.DriverWithContext
|
|
}
|
|
|
|
func (s *Neo4jService) CreateResourceFlow(ctx context.Context, flow ResourceFlow) error {
|
|
session := s.driver.NewSession(ctx, neo4j.SessionConfig{
|
|
AccessMode: neo4j.AccessModeWrite,
|
|
})
|
|
defer session.Close(ctx)
|
|
|
|
cypher := `
|
|
MATCH (site:Site {id: $siteID})
|
|
CREATE (flow:ResourceFlow {
|
|
id: $id,
|
|
direction: $direction,
|
|
type: $type,
|
|
temperature_celsius: $temperature,
|
|
quantity_kwh_per_month: $quantity,
|
|
cost_per_kwh_euro: $cost
|
|
})
|
|
CREATE (site)-[:HOSTS]->(flow)
|
|
RETURN flow.id
|
|
`
|
|
|
|
params := map[string]interface{}{
|
|
"id": flow.ID,
|
|
"siteID": flow.SiteID,
|
|
"direction": flow.Direction,
|
|
"type": flow.Type,
|
|
"temperature": flow.TemperatureCelsius,
|
|
"quantity": flow.QuantityKwhPerMonth,
|
|
"cost": flow.CostPerKwhEuro,
|
|
}
|
|
|
|
_, err := session.Run(ctx, cypher, params)
|
|
return err
|
|
}
|
|
```
|
|
|
|
### Matching Query
|
|
|
|
```go
|
|
func (s *Neo4jService) FindMatches(ctx context.Context, flowID string, maxDistanceKm float64) ([]Match, error) {
|
|
session := s.driver.NewSession(ctx, neo4j.SessionConfig{
|
|
AccessMode: neo4j.AccessModeRead,
|
|
})
|
|
defer session.Close(ctx)
|
|
|
|
cypher := `
|
|
MATCH (sourceFlow:ResourceFlow {id: $flowID})-[:HOSTS]->(sourceSite:Site),
|
|
(targetFlow:ResourceFlow)-[:HOSTS]->(targetSite:Site)
|
|
WHERE sourceFlow.direction = 'output'
|
|
AND targetFlow.direction = 'input'
|
|
AND sourceFlow.type = 'heat'
|
|
AND targetFlow.type = 'heat'
|
|
AND ABS(sourceFlow.temperature_celsius - targetFlow.temperature_celsius) <= 10
|
|
WITH sourceFlow, targetFlow, sourceSite, targetSite,
|
|
point.distance(
|
|
point({longitude: sourceSite.longitude, latitude: sourceSite.latitude}),
|
|
point({longitude: targetSite.longitude, latitude: targetSite.latitude})
|
|
) / 1000 AS distance_km
|
|
WHERE distance_km <= $maxDistance
|
|
RETURN targetFlow.id AS target_flow_id,
|
|
targetFlow.temperature_celsius AS target_temp,
|
|
targetFlow.quantity_kwh_per_month AS target_quantity,
|
|
distance_km
|
|
ORDER BY distance_km ASC
|
|
LIMIT 20
|
|
`
|
|
|
|
params := map[string]interface{}{
|
|
"flowID": flowID,
|
|
"maxDistance": maxDistanceKm,
|
|
}
|
|
|
|
result, err := session.Run(ctx, cypher, params)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var matches []Match
|
|
for result.Next(ctx) {
|
|
record := result.Record()
|
|
match := Match{
|
|
TargetFlowID: getString(record, "target_flow_id"),
|
|
TargetTemp: getFloat(record, "target_temp"),
|
|
TargetQuantity: getFloat(record, "target_quantity"),
|
|
DistanceKm: getFloat(record, "distance_km"),
|
|
}
|
|
matches = append(matches, match)
|
|
}
|
|
|
|
return matches, result.Err()
|
|
}
|
|
```
|
|
|
|
### Connection Pooling
|
|
|
|
```go
|
|
// Driver automatically manages connection pool
|
|
// Configure during driver creation
|
|
driver, err := neo4j.NewDriverWithContext(
|
|
uri,
|
|
neo4j.BasicAuth(username, password, ""),
|
|
func(config *neo4j.Config) {
|
|
config.MaxConnectionPoolSize = 50
|
|
config.ConnectionAcquisitionTimeout = 30 * time.Second
|
|
config.MaxTransactionRetryTime = 30 * time.Second
|
|
},
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
```go
|
|
result, err := session.Run(ctx, cypher, params)
|
|
if err != nil {
|
|
// Check for specific Neo4j errors
|
|
if neo4jErr, ok := err.(*neo4j.Neo4jError); ok {
|
|
switch neo4jErr.Code {
|
|
case "Neo.ClientError.Statement.SyntaxError":
|
|
// Handle syntax error
|
|
case "Neo.ClientError.Security.Unauthorized":
|
|
// Handle auth error
|
|
default:
|
|
// Handle other errors
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Check result errors
|
|
if err := result.Err(); err != nil {
|
|
return err
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Tips
|
|
|
|
1. **Reuse sessions** - create session per request/operation, not per query
|
|
2. **Use transactions** - batch operations in single transaction
|
|
3. **Parameterize queries** - always use parameters, never string concatenation
|
|
4. **Create indexes** - for frequently queried properties
|
|
5. **Use LIMIT** - always limit query results
|
|
6. **Profile queries** - use `EXPLAIN` and `PROFILE` to optimize Cypher
|
|
|
|
---
|
|
|
|
## Indexes
|
|
|
|
```go
|
|
// Create indexes for better performance
|
|
indexes := []string{
|
|
"CREATE INDEX business_id IF NOT EXISTS FOR (b:Business) ON (b.id)",
|
|
"CREATE INDEX site_id IF NOT EXISTS FOR (s:Site) ON (s.id)",
|
|
"CREATE INDEX resource_flow_direction IF NOT EXISTS FOR (r:ResourceFlow) ON (r.direction)",
|
|
"CREATE INDEX resource_flow_type IF NOT EXISTS FOR (r:ResourceFlow) ON (r.type)",
|
|
}
|
|
|
|
for _, index := range indexes {
|
|
_, err := session.Run(ctx, index, nil)
|
|
if err != nil {
|
|
log.Printf("Failed to create index: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Tutorials & Resources
|
|
|
|
- **Neo4j Go Driver Examples**: https://github.com/neo4j/neo4j-go-driver/tree/5.0/examples
|
|
- **Getting Started Tutorial**: https://neo4j.com/developer/go/
|
|
- **Cypher Manual**: https://neo4j.com/docs/cypher-manual/current/
|
|
- **Best Practices**: https://neo4j.com/developer/go-driver/#_best_practices
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Repository Pattern
|
|
|
|
```go
|
|
type BusinessRepository struct {
|
|
driver neo4j.DriverWithContext
|
|
}
|
|
|
|
func (r *BusinessRepository) FindByID(ctx context.Context, id string) (*Business, error) {
|
|
session := r.driver.NewSession(ctx, neo4j.SessionConfig{
|
|
AccessMode: neo4j.AccessModeRead,
|
|
})
|
|
defer session.Close(ctx)
|
|
|
|
// ... query logic ...
|
|
}
|
|
|
|
func (r *BusinessRepository) Create(ctx context.Context, business *Business) error {
|
|
session := r.driver.NewSession(ctx, neo4j.SessionConfig{
|
|
AccessMode: neo4j.AccessModeWrite,
|
|
})
|
|
defer session.Close(ctx)
|
|
|
|
// ... create logic ...
|
|
}
|
|
```
|
|
|
|
### Helper Functions
|
|
|
|
```go
|
|
func parseBusinessNode(node neo4j.Node) *Business {
|
|
props := node.Props
|
|
return &Business{
|
|
ID: getString(props, "id"),
|
|
Name: getString(props, "name"),
|
|
Email: getString(props, "email"),
|
|
}
|
|
}
|
|
|
|
func getString(m map[string]interface{}, key string) string {
|
|
if val, ok := m[key]; ok {
|
|
if str, ok := val.(string); ok {
|
|
return str
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getFloat(m map[string]interface{}, key string) float64 {
|
|
if val, ok := m[key]; ok {
|
|
if f, ok := val.(float64); ok {
|
|
return f
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
```
|
|
|