Add coordinate-based nearest endpoints to geo API

- Add nearestHubs(lat, lon, radius, productUuid?) - hubs near coordinates
- Add nearestOffers(lat, lon, radius, productUuid?) - offers near coordinates
- Add nearestSuppliers(lat, lon, radius, productUuid?) - suppliers near coordinates
- Add routeToCoordinate(offerUuid, lat, lon) - route from offer to coordinates

These unified endpoints work with coordinates instead of UUIDs, simplifying
the frontend logic by removing the need for entity-specific queries like
GetProductsNearHub, GetHubsNearOffer, etc.
This commit is contained in:
Ruslan Bakiev
2026-01-25 17:10:32 +07:00
parent 27b05cf362
commit 46c87c7caa

View File

@@ -292,6 +292,45 @@ class Query(graphene.ObjectType):
description="Get route from a specific offer to hub",
)
# New unified endpoints (coordinate-based)
nearest_hubs = graphene.List(
NodeType,
lat=graphene.Float(required=True, description="Latitude"),
lon=graphene.Float(required=True, description="Longitude"),
radius=graphene.Float(default_value=1000, description="Search radius in km"),
product_uuid=graphene.String(description="Filter hubs by product availability"),
limit=graphene.Int(default_value=12, description="Max results"),
description="Find nearest hubs to coordinates (optionally filtered by product)",
)
nearest_offers = graphene.List(
OfferNodeType,
lat=graphene.Float(required=True, description="Latitude"),
lon=graphene.Float(required=True, description="Longitude"),
radius=graphene.Float(default_value=500, description="Search radius in km"),
product_uuid=graphene.String(description="Filter by product UUID"),
limit=graphene.Int(default_value=50, description="Max results"),
description="Find nearest offers to coordinates (optionally filtered by product)",
)
nearest_suppliers = graphene.List(
SupplierType,
lat=graphene.Float(required=True, description="Latitude"),
lon=graphene.Float(required=True, description="Longitude"),
radius=graphene.Float(default_value=1000, description="Search radius in km"),
product_uuid=graphene.String(description="Filter by product UUID"),
limit=graphene.Int(default_value=12, description="Max results"),
description="Find nearest suppliers to coordinates (optionally filtered by product)",
)
route_to_coordinate = graphene.Field(
ProductRouteOptionType,
offer_uuid=graphene.String(required=True, description="Starting offer UUID"),
lat=graphene.Float(required=True, description="Destination latitude"),
lon=graphene.Float(required=True, description="Destination longitude"),
description="Get route from offer to target coordinates (finds nearest hub to coordinate)",
)
@staticmethod
def _build_routes(db, from_uuid, to_uuid, limit):
"""Shared helper to compute K shortest routes between two nodes."""
@@ -1348,6 +1387,197 @@ class Query(graphene.ObjectType):
logger.info("No route found from offer %s to hub %s", offer_uuid, hub_uuid)
return None
def resolve_nearest_hubs(self, info, lat, lon, radius=1000, product_uuid=None, limit=12):
"""Find nearest hubs to coordinates, optionally filtered by product availability."""
db = get_db()
if product_uuid:
# Find hubs that have offers for this product within radius
aql = """
FOR offer IN nodes
FILTER offer.node_type == 'offer'
FILTER offer.product_uuid == @product_uuid
FILTER offer.latitude != null AND offer.longitude != null
LET dist_to_offer = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
FILTER dist_to_offer <= @radius
FOR hub IN nodes
FILTER hub.node_type == 'logistics' OR hub.node_type == null
FILTER hub.product_uuid == null
FILTER hub.latitude != null AND hub.longitude != null
LET dist_to_hub = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000
FILTER dist_to_hub <= @radius
COLLECT hub_uuid = hub._key INTO hub_group
LET first_hub = FIRST(hub_group)[0].hub
LET hub_dist = DISTANCE(first_hub.latitude, first_hub.longitude, @lat, @lon) / 1000
SORT hub_dist ASC
LIMIT @limit
RETURN MERGE(first_hub, {_key: hub_uuid, distance_km: hub_dist})
"""
bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'product_uuid': product_uuid, 'limit': limit}
else:
# Simple nearest hubs search
aql = """
FOR hub IN nodes
FILTER hub.node_type == 'logistics' OR hub.node_type == null
FILTER hub.product_uuid == null
FILTER hub.latitude != null AND hub.longitude != null
LET dist = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000
FILTER dist <= @radius
SORT dist ASC
LIMIT @limit
RETURN MERGE(hub, {distance_km: dist})
"""
bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'limit': limit}
try:
cursor = db.aql.execute(aql, bind_vars=bind_vars)
hubs = []
for node in cursor:
hubs.append(NodeType(
uuid=node['_key'],
name=node.get('name'),
latitude=node.get('latitude'),
longitude=node.get('longitude'),
country=node.get('country'),
country_code=node.get('country_code'),
synced_at=node.get('synced_at'),
transport_types=node.get('transport_types') or [],
edges=[],
))
logger.info("Found %d hubs near (%.3f, %.3f) within %d km", len(hubs), lat, lon, radius)
return hubs
except Exception as e:
logger.error("Error finding nearest hubs: %s", e)
return []
def resolve_nearest_offers(self, info, lat, lon, radius=500, product_uuid=None, limit=50):
"""Find nearest offers to coordinates, optionally filtered by product."""
db = get_db()
aql = """
FOR offer IN nodes
FILTER offer.node_type == 'offer'
FILTER offer.product_uuid != null
FILTER offer.latitude != null AND offer.longitude != null
"""
if product_uuid:
aql += " FILTER offer.product_uuid == @product_uuid\n"
aql += """
LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
FILTER dist <= @radius
SORT dist ASC
LIMIT @limit
RETURN MERGE(offer, {distance_km: dist})
"""
bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'limit': limit}
if product_uuid:
bind_vars['product_uuid'] = product_uuid
try:
cursor = db.aql.execute(aql, bind_vars=bind_vars)
offers = []
for node in cursor:
offers.append(OfferNodeType(
uuid=node['_key'],
product_uuid=node.get('product_uuid'),
product_name=node.get('product_name'),
supplier_uuid=node.get('supplier_uuid'),
supplier_name=node.get('supplier_name'),
latitude=node.get('latitude'),
longitude=node.get('longitude'),
country=node.get('country'),
country_code=node.get('country_code'),
price_per_unit=node.get('price_per_unit'),
currency=node.get('currency'),
quantity=node.get('quantity'),
unit=node.get('unit'),
))
logger.info("Found %d offers near (%.3f, %.3f) within %d km", len(offers), lat, lon, radius)
return offers
except Exception as e:
logger.error("Error finding nearest offers: %s", e)
return []
def resolve_nearest_suppliers(self, info, lat, lon, radius=1000, product_uuid=None, limit=12):
"""Find nearest suppliers to coordinates, optionally filtered by product."""
db = get_db()
aql = """
FOR offer IN nodes
FILTER offer.node_type == 'offer'
FILTER offer.supplier_uuid != null
FILTER offer.latitude != null AND offer.longitude != null
"""
if product_uuid:
aql += " FILTER offer.product_uuid == @product_uuid\n"
aql += """
LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
FILTER dist <= @radius
COLLECT supplier_uuid = offer.supplier_uuid INTO offers
LET first_offer = FIRST(offers).offer
LET supplier_dist = DISTANCE(first_offer.latitude, first_offer.longitude, @lat, @lon) / 1000
SORT supplier_dist ASC
LIMIT @limit
RETURN {
uuid: supplier_uuid,
distance_km: supplier_dist
}
"""
bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'limit': limit}
if product_uuid:
bind_vars['product_uuid'] = product_uuid
try:
cursor = db.aql.execute(aql, bind_vars=bind_vars)
suppliers = []
for s in cursor:
suppliers.append(SupplierType(uuid=s['uuid']))
logger.info("Found %d suppliers near (%.3f, %.3f) within %d km", len(suppliers), lat, lon, radius)
return suppliers
except Exception as e:
logger.error("Error finding nearest suppliers: %s", e)
return []
def resolve_route_to_coordinate(self, info, offer_uuid, lat, lon):
"""Get route from offer to target coordinates (finds nearest hub automatically)."""
db = get_db()
# Find nearest hub to target coordinates
aql_hub = """
FOR hub IN nodes
FILTER hub.node_type == 'logistics' OR hub.node_type == null
FILTER hub.product_uuid == null
FILTER hub.latitude != null AND hub.longitude != null
LET dist = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000
SORT dist ASC
LIMIT 1
RETURN hub
"""
try:
cursor = db.aql.execute(aql_hub, bind_vars={'lat': lat, 'lon': lon})
hubs = list(cursor)
if not hubs:
logger.info("No hub found near coordinates (%.3f, %.3f)", lat, lon)
return None
nearest_hub = hubs[0]
hub_uuid = nearest_hub['_key']
logger.info("Found nearest hub %s to coordinates (%.3f, %.3f)", hub_uuid, lat, lon)
# Use existing offer_to_hub logic
return self.resolve_offer_to_hub(info, offer_uuid, hub_uuid)
except Exception as e:
logger.error("Error finding route to coordinates: %s", e)
return None
schema = graphene.Schema(query=Query)