feat: add catalog navigation queries
All checks were successful
Build Docker Image / build (push) Successful in 1m34s
All checks were successful
Build Docker Image / build (push) Successful in 1m34s
- findProductsForHub: find products deliverable to a hub - findHubsForProduct: find hubs where product can be delivered - findSupplierProductHubs: find hubs for supplier's product - findOffersForHubByProduct: find offers with routes (wrapper for findProductRoutes)
This commit is contained in:
@@ -183,6 +183,34 @@ class Query(graphene.ObjectType):
|
||||
description="Get clustered nodes for map display (server-side clustering)",
|
||||
)
|
||||
|
||||
# Business-oriented queries for catalog navigation
|
||||
find_products_for_hub = graphene.List(
|
||||
graphene.String,
|
||||
hub_uuid=graphene.String(required=True),
|
||||
description="Find unique product UUIDs that can be delivered to this hub",
|
||||
)
|
||||
|
||||
find_hubs_for_product = graphene.List(
|
||||
NodeType,
|
||||
product_uuid=graphene.String(required=True),
|
||||
description="Find logistics hubs where this product can be delivered",
|
||||
)
|
||||
|
||||
find_supplier_product_hubs = graphene.List(
|
||||
NodeType,
|
||||
supplier_uuid=graphene.String(required=True),
|
||||
product_uuid=graphene.String(required=True),
|
||||
description="Find hubs where this supplier can deliver this product",
|
||||
)
|
||||
|
||||
find_offers_for_hub_by_product = graphene.List(
|
||||
ProductRouteOptionType,
|
||||
hub_uuid=graphene.String(required=True),
|
||||
product_uuid=graphene.String(required=True),
|
||||
limit_sources=graphene.Int(default_value=10),
|
||||
description="Find product offers that can be delivered to hub (with routes)",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_routes(db, from_uuid, to_uuid, limit):
|
||||
"""Shared helper to compute K shortest routes between two nodes."""
|
||||
@@ -768,6 +796,180 @@ class Query(graphene.ObjectType):
|
||||
clusters = get_clustered_nodes(db, west, south, east, north, zoom, transport_type)
|
||||
return [ClusterPointType(**c) for c in clusters]
|
||||
|
||||
def resolve_find_products_for_hub(self, info, hub_uuid):
|
||||
"""
|
||||
Find unique product UUIDs that can be delivered to this hub.
|
||||
Uses reverse traversal from hub to find reachable offer nodes.
|
||||
"""
|
||||
db = get_db()
|
||||
ensure_graph()
|
||||
|
||||
# Check hub exists
|
||||
nodes_col = db.collection('nodes')
|
||||
hub = nodes_col.get(hub_uuid)
|
||||
if not hub:
|
||||
logger.info("Hub %s not found", hub_uuid)
|
||||
return []
|
||||
|
||||
# Find all offer nodes reachable from hub via graph traversal
|
||||
# Offer nodes have product_uuid field
|
||||
aql = """
|
||||
FOR v, e, p IN 1..10 ANY @hub_id GRAPH 'optovia_graph'
|
||||
FILTER v.product_uuid != null
|
||||
COLLECT product_uuid = v.product_uuid
|
||||
RETURN product_uuid
|
||||
"""
|
||||
try:
|
||||
cursor = db.aql.execute(
|
||||
aql,
|
||||
bind_vars={'hub_id': f'nodes/{hub_uuid}'},
|
||||
)
|
||||
product_uuids = list(cursor)
|
||||
logger.info("Found %d products for hub %s", len(product_uuids), hub_uuid)
|
||||
return product_uuids
|
||||
except Exception as e:
|
||||
logger.error("Error finding products for hub: %s", e)
|
||||
return []
|
||||
|
||||
def resolve_find_hubs_for_product(self, info, product_uuid):
|
||||
"""
|
||||
Find logistics hubs where this product can be delivered.
|
||||
Finds offer nodes with this product, then finds reachable hubs.
|
||||
"""
|
||||
db = get_db()
|
||||
ensure_graph()
|
||||
|
||||
# Find all offer nodes with this product_uuid
|
||||
aql_offers = """
|
||||
FOR node IN nodes
|
||||
FILTER node.product_uuid == @product_uuid
|
||||
RETURN node._key
|
||||
"""
|
||||
try:
|
||||
cursor = db.aql.execute(aql_offers, bind_vars={'product_uuid': product_uuid})
|
||||
offer_keys = list(cursor)
|
||||
except Exception as e:
|
||||
logger.error("Error finding offers for product: %s", e)
|
||||
return []
|
||||
|
||||
if not offer_keys:
|
||||
logger.info("No offers found for product %s", product_uuid)
|
||||
return []
|
||||
|
||||
# Find hubs reachable from these offer nodes
|
||||
aql_hubs = """
|
||||
LET offer_ids = @offer_ids
|
||||
FOR offer_id IN offer_ids
|
||||
FOR v, e, p IN 1..10 ANY CONCAT('nodes/', offer_id) GRAPH 'optovia_graph'
|
||||
FILTER (v.node_type == 'logistics' OR v.node_type == null)
|
||||
FILTER v.product_uuid == null
|
||||
COLLECT hub_key = v._key INTO hubs
|
||||
LET hub = FIRST(hubs).v
|
||||
RETURN hub
|
||||
"""
|
||||
try:
|
||||
cursor = db.aql.execute(aql_hubs, bind_vars={'offer_ids': offer_keys})
|
||||
hubs = list(cursor)
|
||||
|
||||
result = []
|
||||
for hub in hubs:
|
||||
if hub:
|
||||
result.append(NodeType(
|
||||
uuid=hub.get('_key'),
|
||||
name=hub.get('name'),
|
||||
latitude=hub.get('latitude'),
|
||||
longitude=hub.get('longitude'),
|
||||
country=hub.get('country'),
|
||||
country_code=hub.get('country_code'),
|
||||
synced_at=hub.get('synced_at'),
|
||||
transport_types=hub.get('transport_types') or [],
|
||||
edges=[],
|
||||
))
|
||||
|
||||
logger.info("Found %d hubs for product %s", len(result), product_uuid)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error finding hubs for product: %s", e)
|
||||
return []
|
||||
|
||||
def resolve_find_supplier_product_hubs(self, info, supplier_uuid, product_uuid):
|
||||
"""
|
||||
Find hubs where this supplier can deliver this product.
|
||||
Finds offer nodes matching both supplier and product, then finds reachable hubs.
|
||||
"""
|
||||
db = get_db()
|
||||
ensure_graph()
|
||||
|
||||
# Find offer nodes with this supplier_uuid AND product_uuid
|
||||
aql_offers = """
|
||||
FOR node IN nodes
|
||||
FILTER node.product_uuid == @product_uuid
|
||||
FILTER node.supplier_uuid == @supplier_uuid
|
||||
RETURN node._key
|
||||
"""
|
||||
try:
|
||||
cursor = db.aql.execute(
|
||||
aql_offers,
|
||||
bind_vars={'product_uuid': product_uuid, 'supplier_uuid': supplier_uuid}
|
||||
)
|
||||
offer_keys = list(cursor)
|
||||
except Exception as e:
|
||||
logger.error("Error finding supplier offers: %s", e)
|
||||
return []
|
||||
|
||||
if not offer_keys:
|
||||
logger.info("No offers found for supplier %s, product %s", supplier_uuid, product_uuid)
|
||||
return []
|
||||
|
||||
# Find hubs reachable from these offer nodes
|
||||
aql_hubs = """
|
||||
LET offer_ids = @offer_ids
|
||||
FOR offer_id IN offer_ids
|
||||
FOR v, e, p IN 1..10 ANY CONCAT('nodes/', offer_id) GRAPH 'optovia_graph'
|
||||
FILTER (v.node_type == 'logistics' OR v.node_type == null)
|
||||
FILTER v.product_uuid == null
|
||||
COLLECT hub_key = v._key INTO hubs
|
||||
LET hub = FIRST(hubs).v
|
||||
RETURN hub
|
||||
"""
|
||||
try:
|
||||
cursor = db.aql.execute(aql_hubs, bind_vars={'offer_ids': offer_keys})
|
||||
hubs = list(cursor)
|
||||
|
||||
result = []
|
||||
for hub in hubs:
|
||||
if hub:
|
||||
result.append(NodeType(
|
||||
uuid=hub.get('_key'),
|
||||
name=hub.get('name'),
|
||||
latitude=hub.get('latitude'),
|
||||
longitude=hub.get('longitude'),
|
||||
country=hub.get('country'),
|
||||
country_code=hub.get('country_code'),
|
||||
synced_at=hub.get('synced_at'),
|
||||
transport_types=hub.get('transport_types') or [],
|
||||
edges=[],
|
||||
))
|
||||
|
||||
logger.info("Found %d hubs for supplier %s product %s", len(result), supplier_uuid, product_uuid)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error("Error finding supplier product hubs: %s", e)
|
||||
return []
|
||||
|
||||
def resolve_find_offers_for_hub_by_product(self, info, hub_uuid, product_uuid, limit_sources=10):
|
||||
"""
|
||||
Find product offers that can be delivered to hub (with routes).
|
||||
Same as find_product_routes but with business-oriented naming.
|
||||
"""
|
||||
return self.resolve_find_product_routes(
|
||||
info,
|
||||
product_uuid=product_uuid,
|
||||
to_uuid=hub_uuid,
|
||||
limit_sources=limit_sources,
|
||||
limit_routes=1,
|
||||
)
|
||||
|
||||
|
||||
schema = graphene.Schema(query=Query)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user