diff --git a/graphql-contracts b/graphql-contracts index c559275..ca08538 160000 --- a/graphql-contracts +++ b/graphql-contracts @@ -1 +1 @@ -Subproject commit c5592754fcbf52abf2bd749112211252859d1caa +Subproject commit ca08538d386e8c667c8d06d7116683a2401ae73b diff --git a/src/schema.ts b/src/schema.ts index 08274f9..5cee2ed 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -112,6 +112,26 @@ export const typeDefs = `#graphql distanceKm: Float } + enum LandingNetworkKind { + HUB + SUPPLIER + } + + type LandingNetworkPoint { + uuid: String! + kind: LandingNetworkKind! + name: String! + country: String + latitude: Float! + longitude: Float! + distanceKm: Float + } + + type LandingNetworkStory { + focus: LandingNetworkPoint! + neighbors: [LandingNetworkPoint!]! + } + type OfferNode { uuid: String productUuid: String @@ -177,6 +197,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! 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!]! @@ -237,6 +258,93 @@ function mapOffer(doc: ArangoDoc, catalog = new Map( } } +type LandingNetworkKind = 'HUB' | 'SUPPLIER' + +function mapLandingNetworkPoint(doc: ArangoDoc, kind: LandingNetworkKind) { + return { + uuid: doc._key, + kind, + name: doc.name, + country: doc.country ?? null, + latitude: doc.latitude, + longitude: doc.longitude, + distanceKm: doc.distance_km ?? null, + } +} + +async function resolveLandingNetworkStory(focusKind: LandingNetworkKind, neighborLimit?: 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 + ` : ` + FOR supplier IN nodes + FILTER supplier.node_type == 'supplier' + FILTER supplier.latitude != null AND supplier.longitude != null + 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`) + + if (focusKind === 'SUPPLIER') { + const neighborsCursor = 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 @limit + RETURN MERGE(hub, {distance_km: dist}) + `, { lat: focus.latitude, lon: focus.longitude, limit }) + const neighbors = await neighborsCursor.all() + return { + focus: mapLandingNetworkPoint(focus, 'SUPPLIER'), + neighbors: neighbors.map((neighbor: ArangoDoc) => mapLandingNetworkPoint(neighbor, 'HUB')), + } + } + + const neighborsCursor = await db.query(` + FOR offer IN nodes + FILTER offer.node_type == 'offer' + FILTER offer.supplier_uuid != 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() + return { + focus: mapLandingNetworkPoint(focus, 'HUB'), + neighbors: neighbors.map((neighbor: ArangoDoc) => mapLandingNetworkPoint(neighbor, 'SUPPLIER')), + } +} + async function loadProductCatalog() { const res = await fetch(EXCHANGE_GRAPHQL_URL, { method: 'POST', @@ -460,6 +568,10 @@ export const resolvers = { }, Query: { + landingNetworkStory: async (_: unknown, args: { focusKind: LandingNetworkKind; neighborLimit?: number }) => { + return resolveLandingNetworkStory(args.focusKind, args.neighborLimit) + }, + node: async (_: unknown, args: { uuid: string }) => { const db = getDb() const nodesCol = db.collection('nodes')