diff --git a/graphql-contracts b/graphql-contracts index ca08538..e96e5af 160000 --- a/graphql-contracts +++ b/graphql-contracts @@ -1 +1 @@ -Subproject commit ca08538d386e8c667c8d06d7116683a2401ae73b +Subproject commit e96e5afd012a9bcc3d8e9738ef795c6da1893f76 diff --git a/src/schema.ts b/src/schema.ts index 5cee2ed..e96cad3 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -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() + 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>() + 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 }) => {