Match landing stories to catalog navigation
Build and deploy Docker image / build (push) Successful in 1m10s

This commit is contained in:
Ruslan Bakiev
2026-08-22 19:14:52 +07:00
parent a09a67797d
commit b2a2c840e4
2 changed files with 145 additions and 47 deletions
+144 -46
View File
@@ -127,9 +127,31 @@ export const typeDefs = `#graphql
distanceKm: Float
}
type LandingNetworkOffer {
uuid: String!
productUuid: String!
productName: String!
supplierUuid: String!
supplierName: String!
country: String
latitude: Float!
longitude: Float!
distanceKm: Float
pricePerUnit: String
currency: String
}
type LandingNetworkProduct {
uuid: String!
name: String!
}
type LandingNetworkStory {
focus: LandingNetworkPoint!
neighbors: [LandingNetworkPoint!]!
offers: [LandingNetworkOffer!]!
hubs: [LandingNetworkPoint!]!
products: [LandingNetworkProduct!]!
suppliers: [LandingNetworkPoint!]!
}
type OfferNode {
@@ -197,7 +219,7 @@ export const typeDefs = `#graphql
nearestHubs(lat: Float!, lon: Float!, radius: Float, productUuid: String, limit: Int): [Node!]!
nearestOffers(lat: Float!, lon: Float!, radius: Float, productUuid: String, hubUuid: String, limit: Int): [OfferWithRoute!]!
nearestSuppliers(lat: Float!, lon: Float!, radius: Float, productUuid: String, limit: Int): [Supplier!]!
landingNetworkStory(focusKind: LandingNetworkKind!, neighborLimit: Int): LandingNetworkStory!
landingNetworkStory(focusKind: LandingNetworkKind!, itemLimit: Int): LandingNetworkStory!
routeToCoordinate(offerUuid: String!, lat: Float!, lon: Float!): ProductRouteOption
hubsList(limit: Int, offset: Int, country: String, transportType: String, west: Float, south: Float, east: Float, north: Float): [Node!]!
suppliersList(limit: Int, offset: Int, country: String, west: Float, south: Float, east: Float, north: Float): [Supplier!]!
@@ -272,30 +294,87 @@ function mapLandingNetworkPoint(doc: ArangoDoc, kind: LandingNetworkKind) {
}
}
async function resolveLandingNetworkStory(focusKind: LandingNetworkKind, neighborLimit?: number) {
function mapLandingNetworkOffer(doc: ArangoDoc) {
return {
uuid: doc._key,
productUuid: doc.product_uuid,
productName: doc.product_name,
supplierUuid: doc.supplier_uuid,
supplierName: doc.supplier_name,
country: doc.country ?? null,
latitude: doc.latitude,
longitude: doc.longitude,
distanceKm: doc.distance_km ?? null,
pricePerUnit: doc.price_per_unit ?? null,
currency: doc.currency ?? null,
}
}
function landingProductsFromOffers(offers: ArangoDoc[], limit: number) {
const products = new Map<string, { uuid: string; name: string }>()
for (const offer of offers) {
if (!products.has(offer.product_uuid)) {
products.set(offer.product_uuid, { uuid: offer.product_uuid, name: offer.product_name })
}
}
return [...products.values()].slice(0, limit)
}
function landingSuppliersFromOffers(offers: ArangoDoc[], limit: number) {
const suppliers = new Map<string, ReturnType<typeof mapLandingNetworkPoint>>()
for (const offer of offers) {
if (!suppliers.has(offer.supplier_uuid)) {
suppliers.set(offer.supplier_uuid, mapLandingNetworkPoint({
_key: offer.supplier_uuid,
name: offer.supplier_name,
country: offer.country,
latitude: offer.latitude,
longitude: offer.longitude,
distance_km: offer.distance_km,
}, 'SUPPLIER'))
}
}
return [...suppliers.values()].slice(0, limit)
}
async function resolveLandingNetworkStory(focusKind: LandingNetworkKind, itemLimit?: number) {
const db = getDb()
const limit = Math.max(1, Math.min(neighborLimit ?? 3, 6))
const focusCursor = await db.query(focusKind === '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
SORT RAND()
LIMIT 1
RETURN hub
` : `
const limit = Math.max(1, Math.min(itemLimit ?? 3, 6))
if (focusKind === 'SUPPLIER') {
const focusCursor = await db.query(`
FOR supplier IN nodes
FILTER supplier.node_type == 'supplier'
FILTER supplier.latitude != null AND supplier.longitude != null
FILTER LENGTH(
FOR offer IN nodes
FILTER offer.node_type == 'offer'
FILTER offer.supplier_uuid == supplier._key
FILTER offer.product_uuid != null AND offer.product_name != null
LIMIT 1
RETURN 1
) > 0
SORT RAND()
LIMIT 1
RETURN supplier
`)
const focus = await focusCursor.next() as ArangoDoc | undefined
if (!focus) throw new Error(`No ${focusKind.toLowerCase()} is available for the landing network story`)
`)
const focus = await focusCursor.next() as ArangoDoc | undefined
if (!focus) throw new Error('No supplier with offers is available for the landing network story')
if (focusKind === 'SUPPLIER') {
const neighborsCursor = await db.query(`
const offersCursor = await db.query(`
FOR offer IN nodes
FILTER offer.node_type == 'offer'
FILTER offer.supplier_uuid == @supplier_uuid
FILTER offer.product_uuid != null AND offer.product_name != null
FILTER offer.supplier_name != null
FILTER offer.latitude != null AND offer.longitude != null
SORT RAND()
LIMIT @limit
RETURN MERGE(offer, {distance_km: 0})
`, { supplier_uuid: focus._key, limit })
const offers = await offersCursor.all() as ArangoDoc[]
const hubsCursor = await db.query(`
FOR hub IN nodes
FILTER hub.node_type == 'logistics' OR hub.node_type == null
FILTER hub.product_uuid == null
@@ -305,43 +384,62 @@ async function resolveLandingNetworkStory(focusKind: LandingNetworkKind, neighbo
LIMIT @limit
RETURN MERGE(hub, {distance_km: dist})
`, { lat: focus.latitude, lon: focus.longitude, limit })
const neighbors = await neighborsCursor.all()
const hubs = await hubsCursor.all()
return {
focus: mapLandingNetworkPoint(focus, 'SUPPLIER'),
neighbors: neighbors.map((neighbor: ArangoDoc) => mapLandingNetworkPoint(neighbor, 'HUB')),
offers: offers.map(mapLandingNetworkOffer),
hubs: hubs.map((hub: ArangoDoc) => mapLandingNetworkPoint(hub, 'HUB')),
products: [],
suppliers: [],
}
}
const neighborsCursor = await db.query(`
const sourceOfferCursor = await db.query(`
FOR offer IN nodes
FILTER offer.node_type == 'offer'
FILTER offer.supplier_uuid != null
FILTER offer.product_uuid != null AND offer.product_name != null
FILTER offer.supplier_uuid != null AND offer.supplier_name != null
FILTER offer.latitude != null AND offer.longitude != null
SORT RAND()
LIMIT 1
RETURN offer
`)
const sourceOffer = await sourceOfferCursor.next() as ArangoDoc | undefined
if (!sourceOffer) throw new Error('No offer is available for the landing network story')
const focusCursor = await db.query(`
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
`, { lat: sourceOffer.latitude, lon: sourceOffer.longitude })
const focus = await focusCursor.next() as ArangoDoc | undefined
if (!focus) throw new Error('No hub is available for the landing network story')
const offersCursor = await db.query(`
FOR offer IN nodes
FILTER offer.node_type == 'offer'
FILTER offer.product_uuid != null AND offer.product_name != null
FILTER offer.supplier_uuid != null AND offer.supplier_name != null
FILTER offer.latitude != null AND offer.longitude != null
LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
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
LET supplier_node = FIRST(
FOR supplier IN nodes
FILTER supplier._key == supplier_uuid
FILTER supplier.node_type == 'supplier'
RETURN supplier
)
SORT supplier_dist ASC
LIMIT @limit
RETURN {
_key: supplier_uuid,
name: supplier_node != null ? supplier_node.name : first_offer.supplier_name,
country: supplier_node != null ? supplier_node.country : first_offer.country,
latitude: supplier_node != null ? supplier_node.latitude : first_offer.latitude,
longitude: supplier_node != null ? supplier_node.longitude : first_offer.longitude,
distance_km: supplier_dist
}
`, { lat: focus.latitude, lon: focus.longitude, limit })
const neighbors = await neighborsCursor.all()
FILTER dist <= 500
SORT dist ASC
LIMIT @offer_limit
RETURN MERGE(offer, {distance_km: dist})
`, { lat: focus.latitude, lon: focus.longitude, offer_limit: limit * 4 })
const offers = await offersCursor.all() as ArangoDoc[]
const visibleOffers = offers.slice(0, limit)
return {
focus: mapLandingNetworkPoint(focus, 'HUB'),
neighbors: neighbors.map((neighbor: ArangoDoc) => mapLandingNetworkPoint(neighbor, 'SUPPLIER')),
offers: visibleOffers.map(mapLandingNetworkOffer),
hubs: [],
products: landingProductsFromOffers(visibleOffers, limit),
suppliers: landingSuppliersFromOffers(visibleOffers, limit),
}
}
@@ -568,8 +666,8 @@ export const resolvers = {
},
Query: {
landingNetworkStory: async (_: unknown, args: { focusKind: LandingNetworkKind; neighborLimit?: number }) => {
return resolveLandingNetworkStory(args.focusKind, args.neighborLimit)
landingNetworkStory: async (_: unknown, args: { focusKind: LandingNetworkKind; itemLimit?: number }) => {
return resolveLandingNetworkStory(args.focusKind, args.itemLimit)
},
node: async (_: unknown, args: { uuid: string }) => {