From 9db56c5edca67c6ef6cb9b8fee8eeaeb8d53a3a7 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Mon, 26 Jan 2026 21:35:20 +0700 Subject: [PATCH] feat(schema): add bounds filtering to list endpoints Add west, south, east, north params to: - hubs_list - suppliers_list - products_list This enables filtering by map viewport bounds for the catalog. --- geo_app/schema.py | 124 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 101 insertions(+), 23 deletions(-) diff --git a/geo_app/schema.py b/geo_app/schema.py index b8d08e3..a401c3e 100644 --- a/geo_app/schema.py +++ b/geo_app/schema.py @@ -365,6 +365,10 @@ class Query(graphene.ObjectType): 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"), + west=graphene.Float(description="Bounding box west longitude"), + south=graphene.Float(description="Bounding box south latitude"), + east=graphene.Float(description="Bounding box east longitude"), + north=graphene.Float(description="Bounding box north latitude"), description="Get paginated list of logistics hubs", ) @@ -373,6 +377,10 @@ class Query(graphene.ObjectType): 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"), + west=graphene.Float(description="Bounding box west longitude"), + south=graphene.Float(description="Bounding box south latitude"), + east=graphene.Float(description="Bounding box east longitude"), + north=graphene.Float(description="Bounding box north latitude"), description="Get paginated list of suppliers from graph", ) @@ -380,6 +388,10 @@ class Query(graphene.ObjectType): ProductType, limit=graphene.Int(default_value=50, description="Max results"), offset=graphene.Int(default_value=0, description="Offset for pagination"), + west=graphene.Float(description="Bounding box west longitude"), + south=graphene.Float(description="Bounding box south latitude"), + east=graphene.Float(description="Bounding box east longitude"), + north=graphene.Float(description="Bounding box north latitude"), description="Get paginated list of products from graph", ) @@ -1683,30 +1695,52 @@ 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): + def resolve_hubs_list(self, info, limit=50, offset=0, country=None, transport_type=None, + west=None, south=None, east=None, north=None): """Get paginated list of logistics hubs.""" db = get_db() - aql = """ + # Build bounds filter if all bounds are provided + bounds_filter = "" + if west is not None and south is not None and east is not None and north is not None: + bounds_filter = """ + FILTER node.latitude != null AND node.longitude != null + FILTER node.latitude >= @south AND node.latitude <= @north + FILTER node.longitude >= @west AND node.longitude <= @east + """ + + aql = f""" 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 + {bounds_filter} 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, + bind_vars = { + 'transport_type': transport_type, + 'country': country, + 'offset': offset, + 'limit': limit, + } + + # Only add bounds to bind_vars if they are used + if west is not None and south is not None and east is not None and north is not None: + bind_vars.update({ + 'west': west, + 'south': south, + 'east': east, + 'north': north, }) + try: + cursor = db.aql.execute(aql, bind_vars=bind_vars) + hubs = [] for node in cursor: hubs.append(NodeType( @@ -1726,26 +1760,48 @@ class Query(graphene.ObjectType): logger.error("Error getting hubs list: %s", e) return [] - def resolve_suppliers_list(self, info, limit=50, offset=0, country=None): + def resolve_suppliers_list(self, info, limit=50, offset=0, country=None, + west=None, south=None, east=None, north=None): """Get paginated list of suppliers from graph.""" db = get_db() - aql = """ + # Build bounds filter if all bounds are provided + bounds_filter = "" + if west is not None and south is not None and east is not None and north is not None: + bounds_filter = """ + FILTER node.latitude != null AND node.longitude != null + FILTER node.latitude >= @south AND node.latitude <= @north + FILTER node.longitude >= @west AND node.longitude <= @east + """ + + aql = f""" FOR node IN nodes FILTER node.node_type == 'supplier' FILTER @country == null OR node.country == @country + {bounds_filter} SORT node.name ASC LIMIT @offset, @limit RETURN node """ - try: - cursor = db.aql.execute(aql, bind_vars={ - 'country': country, - 'offset': offset, - 'limit': limit, + bind_vars = { + 'country': country, + 'offset': offset, + 'limit': limit, + } + + # Only add bounds to bind_vars if they are used + if west is not None and south is not None and east is not None and north is not None: + bind_vars.update({ + 'west': west, + 'south': south, + 'east': east, + 'north': north, }) + try: + cursor = db.aql.execute(aql, bind_vars=bind_vars) + suppliers = [] for node in cursor: suppliers.append(SupplierType( @@ -1760,30 +1816,52 @@ class Query(graphene.ObjectType): logger.error("Error getting suppliers list: %s", e) return [] - def resolve_products_list(self, info, limit=50, offset=0): + def resolve_products_list(self, info, limit=50, offset=0, + west=None, south=None, east=None, north=None): """Get paginated list of products from graph.""" db = get_db() - aql = """ + # Build bounds filter if all bounds are provided + bounds_filter = "" + if west is not None and south is not None and east is not None and north is not None: + bounds_filter = """ + FILTER node.latitude != null AND node.longitude != null + FILTER node.latitude >= @south AND node.latitude <= @north + FILTER node.longitude >= @west AND node.longitude <= @east + """ + + aql = f""" FOR node IN nodes FILTER node.node_type == 'offer' FILTER node.product_uuid != null + {bounds_filter} COLLECT product_uuid = node.product_uuid INTO offers LET first_offer = FIRST(offers).node SORT first_offer.product_name ASC LIMIT @offset, @limit - RETURN { + RETURN {{ uuid: product_uuid, name: first_offer.product_name - } + }} """ - try: - cursor = db.aql.execute(aql, bind_vars={ - 'offset': offset, - 'limit': limit, + bind_vars = { + 'offset': offset, + 'limit': limit, + } + + # Only add bounds to bind_vars if they are used + if west is not None and south is not None and east is not None and north is not None: + bind_vars.update({ + 'west': west, + 'south': south, + 'east': east, + 'north': north, }) + try: + cursor = db.aql.execute(aql, bind_vars=bind_vars) + 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