Enrich geo products from exchange catalog
Build and deploy Docker image / build (push) Successful in 1m43s
Build and deploy Docker image / build (push) Successful in 1m43s
This commit is contained in:
+86
-14
@@ -4,8 +4,22 @@ import { distanceKm, buildRouteFromEdges, type ArangoDoc, type RoutePath } from
|
|||||||
|
|
||||||
const GRAPHHOPPER_URL = process.env.GRAPHHOPPER_EXTERNAL_URL || 'https://graphhopper.optovia.ru'
|
const GRAPHHOPPER_URL = process.env.GRAPHHOPPER_EXTERNAL_URL || 'https://graphhopper.optovia.ru'
|
||||||
const OPENRAILROUTING_URL = process.env.OPENRAILROUTING_EXTERNAL_URL || 'https://openrailrouting.optovia.ru'
|
const OPENRAILROUTING_URL = process.env.OPENRAILROUTING_EXTERNAL_URL || 'https://openrailrouting.optovia.ru'
|
||||||
|
const EXCHANGE_GRAPHQL_URL = process.env.EXCHANGE_GRAPHQL_URL || 'https://exchange.optovia.ru/graphql'
|
||||||
const MAX_EXPANSIONS = 20000
|
const MAX_EXPANSIONS = 20000
|
||||||
|
|
||||||
|
type ProductCatalogItem = {
|
||||||
|
uuid: string
|
||||||
|
name: string | null
|
||||||
|
imageUrl: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProductCatalogPayload = {
|
||||||
|
data?: {
|
||||||
|
products?: ProductCatalogItem[]
|
||||||
|
}
|
||||||
|
errors?: { message?: string }[]
|
||||||
|
}
|
||||||
|
|
||||||
export const typeDefs = `#graphql
|
export const typeDefs = `#graphql
|
||||||
type Edge {
|
type Edge {
|
||||||
toUuid: String
|
toUuid: String
|
||||||
@@ -185,12 +199,29 @@ function mapNode(doc: ArangoDoc, includeEdges = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapOffer(doc: ArangoDoc) {
|
function catalogProduct(productUuid: unknown, catalog: Map<string, ProductCatalogItem>) {
|
||||||
|
const uuid = typeof productUuid === 'string' ? productUuid : ''
|
||||||
|
return uuid ? catalog.get(uuid) : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function productView(doc: ArangoDoc, catalog: Map<string, ProductCatalogItem>) {
|
||||||
|
const productUuid = doc.product_uuid ?? doc.uuid ?? null
|
||||||
|
const product = catalogProduct(productUuid, catalog)
|
||||||
|
return {
|
||||||
|
uuid: productUuid,
|
||||||
|
name: product?.name ?? doc.product_name ?? doc.name ?? null,
|
||||||
|
imageUrl: product?.imageUrl ?? null,
|
||||||
|
offersCount: doc.offers_count ?? doc.offersCount ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapOffer(doc: ArangoDoc, catalog = new Map<string, ProductCatalogItem>()) {
|
||||||
|
const product = catalogProduct(doc.product_uuid, catalog)
|
||||||
return {
|
return {
|
||||||
uuid: doc._key,
|
uuid: doc._key,
|
||||||
productUuid: doc.product_uuid ?? null,
|
productUuid: doc.product_uuid ?? null,
|
||||||
productName: doc.product_name ?? null,
|
productName: product?.name ?? doc.product_name ?? null,
|
||||||
productImageUrl: doc.product_image_url ?? null,
|
productImageUrl: product?.imageUrl ?? null,
|
||||||
supplierUuid: doc.supplier_uuid ?? null,
|
supplierUuid: doc.supplier_uuid ?? null,
|
||||||
supplierName: doc.supplier_name ?? null,
|
supplierName: doc.supplier_name ?? null,
|
||||||
supplierLogoUrl: doc.supplier_logo_url ?? null,
|
supplierLogoUrl: doc.supplier_logo_url ?? null,
|
||||||
@@ -206,6 +237,36 @@ function mapOffer(doc: ArangoDoc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadProductCatalog() {
|
||||||
|
const res = await fetch(EXCHANGE_GRAPHQL_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: `
|
||||||
|
query GeoProductCatalog {
|
||||||
|
products(limit: 1000) {
|
||||||
|
uuid
|
||||||
|
name
|
||||||
|
imageUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error(`Exchange product catalog request failed: ${res.status}`)
|
||||||
|
|
||||||
|
const payload = await res.json() as ProductCatalogPayload
|
||||||
|
if (payload.errors?.length) {
|
||||||
|
throw new Error(payload.errors.map((error) => error.message ?? 'Exchange GraphQL error').join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Map(
|
||||||
|
(payload.data?.products ?? [])
|
||||||
|
.filter((product) => product.uuid)
|
||||||
|
.map((product) => [product.uuid, product]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function mapEdge(doc: ArangoDoc) {
|
function mapEdge(doc: ArangoDoc) {
|
||||||
return {
|
return {
|
||||||
toUuid: doc.to_uuid ?? doc._key ?? null,
|
toUuid: doc.to_uuid ?? doc._key ?? null,
|
||||||
@@ -614,19 +675,22 @@ export const resolvers = {
|
|||||||
|
|
||||||
products: async () => {
|
products: async () => {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
|
const productCatalog = await loadProductCatalog()
|
||||||
const cursor = await db.query(`
|
const cursor = await db.query(`
|
||||||
FOR node IN nodes
|
FOR node IN nodes
|
||||||
FILTER node.node_type == 'offer'
|
FILTER node.node_type == 'offer'
|
||||||
FILTER node.product_uuid != null
|
FILTER node.product_uuid != null
|
||||||
COLLECT product_uuid = node.product_uuid INTO offers
|
COLLECT product_uuid = node.product_uuid INTO offers
|
||||||
LET first_offer = FIRST(offers).node
|
LET first_offer = FIRST(offers).node
|
||||||
RETURN { uuid: product_uuid, name: first_offer.product_name, imageUrl: first_offer.product_image_url }
|
RETURN { uuid: product_uuid, product_uuid, product_name: first_offer.product_name, offers_count: LENGTH(offers) }
|
||||||
`)
|
`)
|
||||||
return cursor.all()
|
const products = await cursor.all()
|
||||||
|
return products.map((product: ArangoDoc) => productView(product, productCatalog))
|
||||||
},
|
},
|
||||||
|
|
||||||
offersByProduct: async (_: unknown, args: { productUuid: string }) => {
|
offersByProduct: async (_: unknown, args: { productUuid: string }) => {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
|
const productCatalog = await loadProductCatalog()
|
||||||
const cursor = await db.query(`
|
const cursor = await db.query(`
|
||||||
FOR node IN nodes
|
FOR node IN nodes
|
||||||
FILTER node.node_type == 'offer'
|
FILTER node.node_type == 'offer'
|
||||||
@@ -634,7 +698,7 @@ export const resolvers = {
|
|||||||
RETURN node
|
RETURN node
|
||||||
`, { product_uuid: args.productUuid })
|
`, { product_uuid: args.productUuid })
|
||||||
const nodes = await cursor.all()
|
const nodes = await cursor.all()
|
||||||
return nodes.map(mapOffer)
|
return nodes.map((node: ArangoDoc) => mapOffer(node, productCatalog))
|
||||||
},
|
},
|
||||||
|
|
||||||
hubsNearOffer: async (_: unknown, args: { offerUuid: string; limit?: number }) => {
|
hubsNearOffer: async (_: unknown, args: { offerUuid: string; limit?: number }) => {
|
||||||
@@ -680,6 +744,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
productsBySupplier: async (_: unknown, args: { supplierUuid: string }) => {
|
productsBySupplier: async (_: unknown, args: { supplierUuid: string }) => {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
|
const productCatalog = await loadProductCatalog()
|
||||||
const cursor = await db.query(`
|
const cursor = await db.query(`
|
||||||
FOR node IN nodes
|
FOR node IN nodes
|
||||||
FILTER node.node_type == 'offer'
|
FILTER node.node_type == 'offer'
|
||||||
@@ -687,13 +752,15 @@ export const resolvers = {
|
|||||||
FILTER node.product_uuid != null
|
FILTER node.product_uuid != null
|
||||||
COLLECT product_uuid = node.product_uuid INTO offers
|
COLLECT product_uuid = node.product_uuid INTO offers
|
||||||
LET first_offer = FIRST(offers).node
|
LET first_offer = FIRST(offers).node
|
||||||
RETURN { uuid: product_uuid, name: first_offer.product_name, imageUrl: first_offer.product_image_url }
|
RETURN { uuid: product_uuid, product_uuid, product_name: first_offer.product_name, offers_count: LENGTH(offers) }
|
||||||
`, { supplier_uuid: args.supplierUuid })
|
`, { supplier_uuid: args.supplierUuid })
|
||||||
return cursor.all()
|
const products = await cursor.all()
|
||||||
|
return products.map((product: ArangoDoc) => productView(product, productCatalog))
|
||||||
},
|
},
|
||||||
|
|
||||||
offersBySupplierProduct: async (_: unknown, args: { supplierUuid: string; productUuid: string }) => {
|
offersBySupplierProduct: async (_: unknown, args: { supplierUuid: string; productUuid: string }) => {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
|
const productCatalog = await loadProductCatalog()
|
||||||
const cursor = await db.query(`
|
const cursor = await db.query(`
|
||||||
FOR node IN nodes
|
FOR node IN nodes
|
||||||
FILTER node.node_type == 'offer'
|
FILTER node.node_type == 'offer'
|
||||||
@@ -702,11 +769,12 @@ export const resolvers = {
|
|||||||
RETURN node
|
RETURN node
|
||||||
`, { supplier_uuid: args.supplierUuid, product_uuid: args.productUuid })
|
`, { supplier_uuid: args.supplierUuid, product_uuid: args.productUuid })
|
||||||
const nodes = await cursor.all()
|
const nodes = await cursor.all()
|
||||||
return nodes.map(mapOffer)
|
return nodes.map((node: ArangoDoc) => mapOffer(node, productCatalog))
|
||||||
},
|
},
|
||||||
|
|
||||||
productsNearHub: async (_: unknown, args: { hubUuid: string; radiusKm?: number }) => {
|
productsNearHub: async (_: unknown, args: { hubUuid: string; radiusKm?: number }) => {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
|
const productCatalog = await loadProductCatalog()
|
||||||
const nodesCol = db.collection('nodes')
|
const nodesCol = db.collection('nodes')
|
||||||
const hub = await nodesCol.document(args.hubUuid).catch(() => null)
|
const hub = await nodesCol.document(args.hubUuid).catch(() => null)
|
||||||
if (!hub || hub.latitude == null || hub.longitude == null) return []
|
if (!hub || hub.latitude == null || hub.longitude == null) return []
|
||||||
@@ -720,9 +788,10 @@ export const resolvers = {
|
|||||||
FILTER dist <= @radius_km
|
FILTER dist <= @radius_km
|
||||||
COLLECT product_uuid = node.product_uuid INTO offers
|
COLLECT product_uuid = node.product_uuid INTO offers
|
||||||
LET first_offer = FIRST(offers).node
|
LET first_offer = FIRST(offers).node
|
||||||
RETURN { uuid: product_uuid, name: first_offer.product_name, imageUrl: first_offer.product_image_url }
|
RETURN { uuid: product_uuid, product_uuid, product_name: first_offer.product_name, offers_count: LENGTH(offers) }
|
||||||
`, { lat: hub.latitude, lon: hub.longitude, radius_km: args.radiusKm ?? 500 })
|
`, { lat: hub.latitude, lon: hub.longitude, radius_km: args.radiusKm ?? 500 })
|
||||||
return cursor.all()
|
const products = await cursor.all()
|
||||||
|
return products.map((product: ArangoDoc) => productView(product, productCatalog))
|
||||||
},
|
},
|
||||||
|
|
||||||
suppliersForProduct: async (_: unknown, args: { productUuid: string }) => {
|
suppliersForProduct: async (_: unknown, args: { productUuid: string }) => {
|
||||||
@@ -914,6 +983,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
nearestOffers: async (_: unknown, args: { lat: number; lon: number; radius?: number; productUuid?: string; hubUuid?: string; limit?: number }) => {
|
nearestOffers: async (_: unknown, args: { lat: number; lon: number; radius?: number; productUuid?: string; hubUuid?: string; limit?: number }) => {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
|
const productCatalog = await loadProductCatalog()
|
||||||
await ensureGraph()
|
await ensureGraph()
|
||||||
const radius = args.radius ?? 500
|
const radius = args.radius ?? 500
|
||||||
const limit = args.limit ?? 50
|
const limit = args.limit ?? 50
|
||||||
@@ -946,7 +1016,7 @@ export const resolvers = {
|
|||||||
const routeResult = await resolveOfferToHubInternal(node._key, args.hubUuid)
|
const routeResult = await resolveOfferToHubInternal(node._key, args.hubUuid)
|
||||||
if (routeResult?.routes) routes = routeResult.routes as RoutePath[]
|
if (routeResult?.routes) routes = routeResult.routes as RoutePath[]
|
||||||
}
|
}
|
||||||
offers.push({ ...mapOffer(node), routes })
|
offers.push({ ...mapOffer(node, productCatalog), routes })
|
||||||
}
|
}
|
||||||
return offers
|
return offers
|
||||||
},
|
},
|
||||||
@@ -1062,6 +1132,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
productsList: async (_: unknown, args: { limit?: number; offset?: number; west?: number; south?: number; east?: number; north?: number }) => {
|
productsList: async (_: unknown, args: { limit?: number; offset?: number; west?: number; south?: number; east?: number; north?: number }) => {
|
||||||
const db = getDb()
|
const db = getDb()
|
||||||
|
const productCatalog = await loadProductCatalog()
|
||||||
const bounds = boundsFilter(args)
|
const bounds = boundsFilter(args)
|
||||||
|
|
||||||
const cursor = await db.query(`
|
const cursor = await db.query(`
|
||||||
@@ -1073,9 +1144,10 @@ export const resolvers = {
|
|||||||
LET first_offer = FIRST(offers).node
|
LET first_offer = FIRST(offers).node
|
||||||
SORT first_offer.product_name ASC
|
SORT first_offer.product_name ASC
|
||||||
LIMIT @offset, @limit
|
LIMIT @offset, @limit
|
||||||
RETURN { uuid: product_uuid, name: first_offer.product_name, imageUrl: first_offer.product_image_url }
|
RETURN { uuid: product_uuid, product_uuid, product_name: first_offer.product_name, offers_count: LENGTH(offers) }
|
||||||
`, { offset: args.offset ?? 0, limit: args.limit ?? 50, ...bounds.vars })
|
`, { offset: args.offset ?? 0, limit: args.limit ?? 50, ...bounds.vars })
|
||||||
return cursor.all()
|
const products = await cursor.all()
|
||||||
|
return products.map((product: ArangoDoc) => productView(product, productCatalog))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user