turash/bugulma/backend/internal/graph/query_templates.go
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
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)
2025-11-25 06:01:16 +01:00

261 lines
8.7 KiB
Go

package graph
import "fmt"
// QueryTemplates contains reusable Cypher query templates
type QueryTemplates struct{}
// NewQueryTemplates creates a new query templates instance
func NewQueryTemplates() *QueryTemplates {
return &QueryTemplates{}
}
// FindResourceChainsQuery returns the Cypher query for finding resource chains
func (qt *QueryTemplates) FindResourceChainsQuery(maxLength int) string {
return fmt.Sprintf(`
MATCH (start:ResourceFlow {type: $resource_type, direction: "output"})
MATCH path = (start)-[:MATCHES*1..%d]-(connected:ResourceFlow)
WHERE connected.direction = "input"
AND connected.type = $resource_type
AND start <> connected
WITH path, nodes(path) AS pathNodes, relationships(path) AS pathRels
WHERE size(pathNodes) >= 2 AND size(pathNodes) <= %d
// Get sites for distance calculation
MATCH (startSite:Site)-[:HOSTS]->(start)
MATCH (endSite:Site)-[:HOSTS]->(connected)
WHERE startSite.latitude IS NOT NULL
AND startSite.longitude IS NOT NULL
AND endSite.latitude IS NOT NULL
AND endSite.longitude IS NOT NULL
// Calculate total distance along the path
WITH path, pathNodes, pathRels, startSite, endSite,
point.distance(
point({latitude: startSite.latitude, longitude: startSite.longitude}),
point({latitude: endSite.latitude, longitude: endSite.longitude})
) / 1000.0 AS total_distance
WHERE total_distance <= $max_distance
// Get organization information
OPTIONAL MATCH (startOrg:Organization)-[:OPERATES_AT]->(startSite)
OPTIONAL MATCH (endOrg:Organization)-[:OPERATES_AT]->(endSite)
// Calculate total value from relationships
WITH path, pathNodes, pathRels, total_distance, startOrg, endOrg,
reduce(totalValue = 0.0, rel IN pathRels |
totalValue + COALESCE(rel.economic_value, 0.0)
) AS total_value
WHERE total_value >= $min_value
RETURN
pathNodes,
pathRels,
total_distance,
total_value,
startOrg.id AS start_org_id,
startOrg.name AS start_org_name,
endOrg.id AS end_org_id,
endOrg.name AS end_org_name
ORDER BY total_value DESC, total_distance ASC
LIMIT $limit
`, maxLength, maxLength)
}
// FindSymbiosisNetworksQuery returns the Cypher query for finding symbiosis networks
func (qt *QueryTemplates) FindSymbiosisNetworksQuery() string {
return `
MATCH (o1:Organization)-[:PRODUCES|CONSUMES]->(rf1:ResourceFlow)
MATCH (rf1)-[m:MATCHES]-(rf2:ResourceFlow)
MATCH (rf2)<-[:PRODUCES|CONSUMES]-(o2:Organization)
WHERE o1 <> o2
WITH o1, o2, m, rf1, rf2
// Get site information for distance calculation
MATCH (s1:Site)-[:HOSTS]->(rf1)
MATCH (s2:Site)-[:HOSTS]->(rf2)
WHERE s1.latitude IS NOT NULL AND s2.latitude IS NOT NULL
WITH o1, o2, m, rf1, rf2, s1, s2,
point.distance(
point({latitude: s1.latitude, longitude: s1.longitude}),
point({latitude: s2.latitude, longitude: s2.longitude})
) / 1000.0 AS distance_km
// Group by connected organizations
WITH o1.id AS org1_id, o1.name AS org1_name, o1.latitude AS org1_lat, o1.longitude AS org1_lng,
o2.id AS org2_id, o2.name AS org2_name, o2.latitude AS org2_lat, o2.longitude AS org2_lng,
collect(DISTINCT {
flow_id: rf1.id,
resource_type: rf1.type,
match_id: m.match_id,
economic_value: m.economic_value,
distance: distance_km
}) AS connections
WHERE size(connections) >= 2
// Aggregate network metrics
WITH collect(DISTINCT org1_id) + collect(DISTINCT org2_id) AS network_orgs,
connections,
reduce(totalValue = 0.0, conn IN connections |
totalValue + COALESCE(conn.economic_value, 0.0)
) AS total_value,
reduce(maxDist = 0.0, conn IN connections |
CASE WHEN conn.distance > maxDist THEN conn.distance ELSE maxDist END
) AS max_distance
WHERE size(network_orgs) >= $min_orgs AND size(network_orgs) <= $max_size
RETURN
network_orgs,
connections,
total_value,
max_distance
ORDER BY total_value DESC
LIMIT $limit
`
}
// FindOptimalPathQuery returns the Cypher query for finding optimal paths
func (qt *QueryTemplates) FindOptimalPathQuery(maxHops int) string {
return fmt.Sprintf(`
MATCH (source:Organization {id: $source_org})
MATCH (target:Organization {id: $target_org})
// Find paths through resource flows
MATCH path = shortestPath(
(source)-[:PRODUCES|CONSUMES|OPERATES_AT*1..%d]-(target)
)
WITH path, nodes(path) AS pathNodes, relationships(path) AS pathRels
WHERE size(pathNodes) >= 2
// Extract resource flows from path
WITH path, pathNodes, pathRels,
[node IN pathNodes WHERE node:ResourceFlow] AS flowNodes,
[rel IN pathRels WHERE type(rel) = 'MATCHES'] AS matchRels
WHERE size(flowNodes) >= 1 AND size(matchRels) >= 1
// Calculate path cost
WITH path, pathNodes, pathRels, flowNodes, matchRels,
reduce(totalCost = 0.0, rel IN matchRels |
totalCost + COALESCE(rel.transport_cost, 0.0) + COALESCE(rel.processing_cost, 0.0)
) AS total_cost,
reduce(totalDist = 0.0, rel IN matchRels |
totalDist + COALESCE(rel.distance_km, 0.0)
) AS total_distance
WHERE total_distance <= $max_distance
RETURN pathNodes, pathRels, total_cost, total_distance, length(path) AS path_length
ORDER BY total_cost ASC, total_distance ASC
LIMIT $limit
`, maxHops*2)
}
// CalculateCentralityQuery returns the Cypher query for calculating centrality
func (qt *QueryTemplates) CalculateCentralityQuery() string {
return `
MATCH (o:Organization)
OPTIONAL MATCH (o)-[:PRODUCES|CONSUMES]->(rf1:ResourceFlow)-[:MATCHES]-(rf2:ResourceFlow)<-[:PRODUCES|CONSUMES]-(other:Organization)
WHERE o <> other
WITH o.id AS org_id,
count(DISTINCT other) AS degree_centrality,
count(DISTINCT rf1) AS out_flows,
count(DISTINCT rf2) AS in_flows
WHERE degree_centrality > 0
RETURN org_id, degree_centrality, out_flows, in_flows
ORDER BY degree_centrality DESC
`
}
// GetNetworkStatisticsQuery returns the Cypher query for network statistics
func (qt *QueryTemplates) GetNetworkStatisticsQuery() string {
return `
MATCH (o:Organization)
OPTIONAL MATCH (o)-[:PRODUCES|CONSUMES]->(rf:ResourceFlow)
OPTIONAL MATCH (rf)-[m:MATCHES]-(rf2:ResourceFlow)
OPTIONAL MATCH (rf2)<-[:PRODUCES|CONSUMES]-(o2:Organization)
WHERE o <> o2
WITH
count(DISTINCT o) AS total_organizations,
count(DISTINCT rf) AS total_resource_flows,
count(DISTINCT m) AS total_matches,
sum(DISTINCT m.economic_value) AS total_economic_value,
avg(m.distance_km) AS avg_distance,
count(DISTINCT CASE WHEN m.economic_value > 50000 THEN m END) AS high_value_matches
MATCH (o:Organization)-[:OPERATES_AT]->(s:Site)
WITH total_organizations, total_resource_flows, total_matches, total_economic_value, avg_distance, high_value_matches,
collect(DISTINCT {lat: s.latitude, lng: s.longitude}) AS locations
// Calculate geographic span
WITH total_organizations, total_resource_flows, total_matches, total_economic_value, avg_distance, high_value_matches, locations,
reduce(maxLat = -90, loc IN locations | CASE WHEN loc.lat > maxLat THEN loc.lat ELSE maxLat END) AS max_lat,
reduce(minLat = 90, loc IN locations | CASE WHEN loc.lat < minLat THEN loc.lat ELSE minLat END) AS min_lat,
reduce(maxLng = -180, loc IN locations | CASE WHEN loc.lng > maxLng THEN loc.lng ELSE maxLng END) AS max_lng,
reduce(minLng = 180, loc IN locations | CASE WHEN loc.lng < minLng THEN loc.lng ELSE minLng END) AS min_lng
RETURN
total_organizations,
total_resource_flows,
total_matches,
total_economic_value,
avg_distance,
high_value_matches,
point.distance(
point({latitude: min_lat, longitude: min_lng}),
point({latitude: max_lat, longitude: max_lng})
) / 1000.0 AS geographic_span_km
`
}
// FindCircularEconomyCyclesQuery returns the Cypher query for finding circular economy cycles
func (qt *QueryTemplates) FindCircularEconomyCyclesQuery() string {
return `
// Find circular resource flows (waste -> recycling -> reuse -> waste)
MATCH cycle = (start:ResourceFlow)-[r:MATCHES*3..6]-(start)
WHERE start.direction = "output"
AND ALL(i IN range(0, length(r)-2) WHERE
nodes(cycle)[i].direction <> nodes(cycle)[i+1].direction)
WITH cycle,
length(r) AS cycle_length,
[node IN nodes(cycle) | node.type] AS resource_types,
[node IN nodes(cycle) | node.id] AS flow_ids,
reduce(totalValue = 0.0, rel IN r | totalValue + rel.economic_value) AS cycle_value
WHERE cycle_value > 0
// Get organization information for each cycle
MATCH (org:Organization)-[:PRODUCES|CONSUMES]->(flow:ResourceFlow)
WHERE flow IN nodes(cycle)
WITH cycle, cycle_length, resource_types, flow_ids, cycle_value,
collect(DISTINCT org.name) AS organizations,
count(DISTINCT org) AS org_count
WHERE org_count >= 3
RETURN
cycle,
cycle_length,
resource_types,
flow_ids,
cycle_value,
organizations,
org_count
ORDER BY cycle_value DESC
LIMIT $limit
`
}