From 9ff792746330e09a53b94b475a985116e07dc7e6 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Mon, 26 Jan 2026 13:55:02 +0700 Subject: [PATCH] Add hubsList, suppliersList, productsList resolvers and update nearestOffers - Add hubsList resolver for paginated hub list - Add suppliersList resolver for paginated supplier list - Add productsList resolver for paginated product list - Update nearestOffers to support hubUuid parameter with route calculation - Add OfferWithRouteType for offers with embedded routes - Add supplier_name to OfferNodeType --- geo_app/schema.py | 190 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 183 insertions(+), 7 deletions(-) diff --git a/geo_app/schema.py b/geo_app/schema.py index 2e1bc6b..1487622 100644 --- a/geo_app/schema.py +++ b/geo_app/schema.py @@ -130,6 +130,7 @@ class OfferNodeType(graphene.ObjectType): product_uuid = graphene.String() product_name = graphene.String() supplier_uuid = graphene.String() + supplier_name = graphene.String() latitude = graphene.Float() longitude = graphene.Float() country = graphene.String() @@ -141,6 +142,25 @@ class OfferNodeType(graphene.ObjectType): distance_km = graphene.Float() +class OfferWithRouteType(graphene.ObjectType): + """Offer with route information to destination.""" + uuid = graphene.String() + product_uuid = graphene.String() + product_name = graphene.String() + supplier_uuid = graphene.String() + supplier_name = graphene.String() + latitude = graphene.Float() + longitude = graphene.Float() + country = graphene.String() + country_code = graphene.String() + price_per_unit = graphene.String() + currency = graphene.String() + quantity = graphene.String() + unit = graphene.String() + distance_km = graphene.Float() + routes = graphene.List(lambda: RoutePathType) + + class Query(graphene.ObjectType): """Root query.""" MAX_EXPANSIONS = 20000 @@ -310,13 +330,14 @@ class Query(graphene.ObjectType): ) nearest_offers = graphene.List( - OfferNodeType, + OfferWithRouteType, 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"), + hub_uuid=graphene.String(description="Hub UUID - if provided, calculates routes to this hub"), limit=graphene.Int(default_value=50, description="Max results"), - description="Find nearest offers to coordinates (optionally filtered by product)", + description="Find nearest offers to coordinates with optional routes to hub", ) nearest_suppliers = graphene.List( @@ -337,6 +358,31 @@ class Query(graphene.ObjectType): description="Get route from offer to target coordinates (finds nearest hub to coordinate)", ) + # New unified list endpoints + hubs_list = graphene.List( + NodeType, + limit=graphene.Int(default_value=50, description="Max results"), + offset=graphene.Int(default_value=0, description="Offset for pagination"), + country=graphene.String(description="Filter by country name"), + transport_type=graphene.String(description="Filter by transport type"), + description="Get paginated list of logistics hubs", + ) + + suppliers_list = graphene.List( + SupplierType, + limit=graphene.Int(default_value=50, description="Max results"), + offset=graphene.Int(default_value=0, description="Offset for pagination"), + country=graphene.String(description="Filter by country name"), + description="Get paginated list of suppliers from graph", + ) + + products_list = graphene.List( + ProductType, + limit=graphene.Int(default_value=50, description="Max results"), + offset=graphene.Int(default_value=0, description="Offset for pagination"), + description="Get paginated list of products from graph", + ) + @staticmethod def _build_routes(db, from_uuid, to_uuid, limit): """Shared helper to compute K shortest routes between two nodes.""" @@ -1469,9 +1515,10 @@ class Query(graphene.ObjectType): 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.""" + def resolve_nearest_offers(self, info, lat, lon, radius=500, product_uuid=None, hub_uuid=None, limit=50): + """Find nearest offers to coordinates, optionally filtered by product. If hub_uuid provided, calculates routes.""" db = get_db() + ensure_graph() aql = """ FOR offer IN nodes @@ -1497,13 +1544,33 @@ class Query(graphene.ObjectType): try: cursor = db.aql.execute(aql, bind_vars=bind_vars) + offer_nodes = list(cursor) + logger.info("Found %d offers near (%.3f, %.3f) within %d km", len(offer_nodes), lat, lon, radius) + + # If hub_uuid provided, calculate routes for each offer + if hub_uuid: + nodes_col = db.collection('nodes') + hub = nodes_col.get(hub_uuid) + if not hub: + logger.warning("Hub %s not found for route calculation", hub_uuid) + hub_uuid = None + offers = [] - for node in cursor: - offers.append(OfferNodeType( + for node in offer_nodes: + routes = [] + + # Calculate route to hub if hub_uuid provided + if hub_uuid: + route_result = self.resolve_offer_to_hub(info, node['_key'], hub_uuid) + if route_result and route_result.routes: + routes = route_result.routes + + offers.append(OfferWithRouteType( 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'), @@ -1513,8 +1580,9 @@ class Query(graphene.ObjectType): quantity=node.get('quantity'), unit=node.get('unit'), distance_km=node.get('distance_km'), + routes=routes, )) - 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) @@ -1614,6 +1682,114 @@ class Query(graphene.ObjectType): logger.error("Error finding route to coordinates: %s", e) return None + def resolve_hubs_list(self, info, limit=50, offset=0, country=None, transport_type=None): + """Get paginated list of logistics hubs.""" + db = get_db() + + aql = """ + FOR node IN nodes + FILTER node.node_type == 'logistics' OR node.node_type == null + FILTER node.product_uuid == null + LET types = node.transport_types != null ? node.transport_types : [] + FILTER @transport_type == null OR @transport_type IN types + FILTER @country == null OR node.country == @country + SORT node.name ASC + LIMIT @offset, @limit + RETURN node + """ + + try: + cursor = db.aql.execute(aql, bind_vars={ + 'transport_type': transport_type, + 'country': country, + 'offset': offset, + 'limit': limit, + }) + + 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("Returning %d hubs (offset=%d, limit=%d)", len(hubs), offset, limit) + return hubs + except Exception as e: + logger.error("Error getting hubs list: %s", e) + return [] + + def resolve_suppliers_list(self, info, limit=50, offset=0, country=None): + """Get paginated list of suppliers from graph.""" + db = get_db() + + aql = """ + FOR node IN nodes + FILTER node.node_type == 'supplier' + FILTER @country == null OR node.country == @country + SORT node.name ASC + LIMIT @offset, @limit + RETURN node + """ + + try: + cursor = db.aql.execute(aql, bind_vars={ + 'country': country, + 'offset': offset, + 'limit': limit, + }) + + suppliers = [] + for node in cursor: + suppliers.append(SupplierType( + uuid=node['_key'], + name=node.get('name'), + latitude=node.get('latitude'), + longitude=node.get('longitude'), + )) + logger.info("Returning %d suppliers (offset=%d, limit=%d)", len(suppliers), offset, limit) + return suppliers + except Exception as e: + logger.error("Error getting suppliers list: %s", e) + return [] + + def resolve_products_list(self, info, limit=50, offset=0): + """Get paginated list of products from graph.""" + db = get_db() + + aql = """ + FOR node IN nodes + FILTER node.node_type == 'offer' + FILTER node.product_uuid != null + COLLECT product_uuid = node.product_uuid INTO offers + LET first_offer = FIRST(offers).node + SORT first_offer.product_name ASC + LIMIT @offset, @limit + RETURN { + uuid: product_uuid, + name: first_offer.product_name + } + """ + + try: + cursor = db.aql.execute(aql, bind_vars={ + 'offset': offset, + 'limit': limit, + }) + + products = [ProductType(uuid=p['uuid'], name=p.get('name')) for p in cursor] + logger.info("Returning %d products (offset=%d, limit=%d)", len(products), offset, limit) + return products + except Exception as e: + logger.error("Error getting products list: %s", e) + return [] + schema = graphene.Schema(query=Query)