Added local elastic vinmonopolet cache for query get by id.
First tries to search local elastic instance, if no results are found a reject is thrown and we search vinmonopolet api manually.
This commit is contained in:
		| @@ -1,6 +1,7 @@ | |||||||
| const fetch = require("node-fetch"); | const fetch = require("node-fetch"); | ||||||
| const path = require("path"); | const path = require("path"); | ||||||
| const config = require(path.join(__dirname + "/../config/env/lottery.config")); | const config = require(path.join(__dirname + "/../config/env/lottery.config")); | ||||||
|  | const vinmonopoletCache = require(path.join(__dirname, "vinmonopoletCache")); | ||||||
|  |  | ||||||
| const convertToOurWineObject = wine => { | const convertToOurWineObject = wine => { | ||||||
|   if (wine.basic.ageLimit === "18") { |   if (wine.basic.ageLimit === "18") { | ||||||
| @@ -18,6 +19,20 @@ const convertToOurWineObject = wine => { | |||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const convertVinmonopoletProductResponseToWineObject = wine => { | ||||||
|  |   return { | ||||||
|  |     name: wine.name, | ||||||
|  |     vivinoLink: "https://www.vinmonopolet.no" + wine.url, | ||||||
|  |     rating: null, | ||||||
|  |     occurences: 0, | ||||||
|  |     id: wine.code, | ||||||
|  |     year: wine.year, | ||||||
|  |     image: wine.images[1].url, | ||||||
|  |     price: wine.price.value, | ||||||
|  |     country: wine.main_country.name | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
| const convertToOurStoreObject = store => { | const convertToOurStoreObject = store => { | ||||||
|   return { |   return { | ||||||
|     id: store.storeId, |     id: store.storeId, | ||||||
| @@ -26,37 +41,32 @@ const convertToOurStoreObject = store => { | |||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const searchWinesByName = async (name, page = 1) => { | const searchWinesByName = (name, page = 1) => { | ||||||
|   const pageSize = 15; |   const pageSize = 25; | ||||||
|   let url = new URL( |  | ||||||
|     `https://apis.vinmonopolet.no/products/v0/details-normal?productShortNameContains=gato&maxResults=15` |  | ||||||
|   ); |  | ||||||
|   url.searchParams.set("maxResults", pageSize); |  | ||||||
|   url.searchParams.set("start", pageSize * (page - 1)); |  | ||||||
|   url.searchParams.set("productShortNameContains", name); |  | ||||||
|  |  | ||||||
|   const vinmonopoletResponse = await fetch(url, { |   return vinmonopoletCache.wineByQueryName(name, page, pageSize) | ||||||
|     headers: { |     .catch(_ => { | ||||||
|       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken |       console.log(`No wines matching query: ${name} at page ${page} found in elastic index, searching vinmonopolet..`) | ||||||
|     } |  | ||||||
|  |       const url = `https://www.vinmonopolet.no/api/search?q=${name}:relevance:visibleInSearch:true&searchType=product&pageSize=${pageSize}¤tPage=${page-1}` | ||||||
|  |       const options = { | ||||||
|  |         headers: { "Content-Type": 'application/json' } | ||||||
|  |       }; | ||||||
|  |  | ||||||
|  |       return fetch(url, options) | ||||||
|  |         .then(resp => { | ||||||
|  |           if (resp.ok == false) { | ||||||
|  |             return Promise.reject({ | ||||||
|  |               statusCode: 404, | ||||||
|  |               message: `No wines matching query ${name} at page ${page} found in local cache or at vinmonopolet.`, | ||||||
|             }) |             }) | ||||||
|     .then(resp => resp.json()) |  | ||||||
|     .catch(err => console.error(err)); |  | ||||||
|  |  | ||||||
|   if (vinmonopoletResponse.errors != null) { |  | ||||||
|     return vinmonopoletResponse.errors.map(error => { |  | ||||||
|       if (error.type == "UnknownProductError") { |  | ||||||
|         return res.status(404).json({ |  | ||||||
|           message: error.message |  | ||||||
|         }); |  | ||||||
|       } else { |  | ||||||
|         return next(); |  | ||||||
|           } |           } | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|   const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean); |  | ||||||
|  |  | ||||||
|   return winesConverted; |           return resp.json() | ||||||
|  |             .then(response => response?.productSearchResult?.products) | ||||||
|  |         }) | ||||||
|  |     }) | ||||||
|  |     .then(wines => wines.map(convertVinmonopoletProductResponseToWineObject)) | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const wineByEAN = ean => { | const wineByEAN = ean => { | ||||||
| @@ -67,16 +77,30 @@ const wineByEAN = ean => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const wineById = id => { | const wineById = id => { | ||||||
|   const url = `https://apis.vinmonopolet.no/products/v0/details-normal?productId=${id}`; |   return vinmonopoletCache.wineById(id) | ||||||
|  |     .catch(_ => { | ||||||
|  |       console.log(`Wine id: ${id} not found in elastic index, searching vinmonopolet..`) | ||||||
|  |  | ||||||
|  |       const url = `https://www.vinmonopolet.no/api/products/${id}?fields=FULL` | ||||||
|       const options = { |       const options = { | ||||||
|         headers: { |         headers: { | ||||||
|       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken |           "Content-Type": 'application/json' | ||||||
|         } |         } | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       return fetch(url, options) |       return fetch(url, options) | ||||||
|     .then(resp => resp.json()) |         .then(resp => { | ||||||
|     .then(response => response.map(convertToOurWineObject)); |           if (resp.ok == false) { | ||||||
|  |             return Promise.reject({ | ||||||
|  |               statusCode: 404, | ||||||
|  |               message: `Wine with id ${id} not found in local cache or at vinmonopolet.`, | ||||||
|  |             }) | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           return resp.json() | ||||||
|  |         }) | ||||||
|  |     }) | ||||||
|  |     .then(wine => convertVinmonopoletProductResponseToWineObject(wine)) | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const allStores = () => { | const allStores = () => { | ||||||
|   | |||||||
							
								
								
									
										98
									
								
								api/vinmonopoletCache.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								api/vinmonopoletCache.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | const fetch = require("node-fetch"); | ||||||
|  |  | ||||||
|  | const ELASTIC_URL = 'http://localhost:9200'; | ||||||
|  | const INDEX_URL = `${ELASTIC_URL}/wines*`; | ||||||
|  |  | ||||||
|  | const verifyAndUnpackElasticSearchResult = response => { | ||||||
|  |   const searchHits = response?.hits?.hits; | ||||||
|  |  | ||||||
|  |   if (searchHits == null || searchHits.length == 0) { | ||||||
|  |     return Promise.reject({ | ||||||
|  |       statusCode: 404, | ||||||
|  |       message: `Nothing found in vinmonopolet cache matching this.`, | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return searchHits; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const getWineObjectFromSearchHit = hit => { | ||||||
|  |   const { wine } = hit?._source; | ||||||
|  |  | ||||||
|  |   if (wine == null) { | ||||||
|  |     return Promise.reject({ | ||||||
|  |       statusCode: 500, | ||||||
|  |       message: `Found response, but it's missing a wine object. Unable to convert!`, | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return wine; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const wineById = id => { | ||||||
|  | 	const url = `${INDEX_URL}/_search` | ||||||
|  |   const options = { | ||||||
|  |     method: 'POST', | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       "size": 1, | ||||||
|  |       "query": { | ||||||
|  |         "match": { | ||||||
|  |           "wine.code": id | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "_source": { | ||||||
|  |         "includes": "wine" | ||||||
|  |       }, | ||||||
|  |       "sort": [ | ||||||
|  |         { | ||||||
|  |           "@timestamp": "desc" | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return fetch(url, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .then(verifyAndUnpackElasticSearchResult) | ||||||
|  |     .then(searchHits => getWineObjectFromSearchHit(searchHits[0])) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const wineByQueryName = (name, page=1, size=25) => { | ||||||
|  |   const url = `${INDEX_URL}/_search` | ||||||
|  |   const options = { | ||||||
|  |     method: 'POST', | ||||||
|  |     headers: { 'Content-Type': 'application/json', }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       "from": page - 1, | ||||||
|  |       "size": size, | ||||||
|  |       "query": { | ||||||
|  |         "multi_match" : { | ||||||
|  |             "query" : name, | ||||||
|  |             "fields": ["wine.name"], | ||||||
|  |             "fuzziness": 2 | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "sort": [ | ||||||
|  |         { | ||||||
|  |           "_score": { | ||||||
|  |             "order": "desc" | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ],  | ||||||
|  |       "_source": { | ||||||
|  |         "includes": "wine" | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return fetch(url, options) | ||||||
|  |     .then(resp => resp.json()) | ||||||
|  |     .then(verifyAndUnpackElasticSearchResult) | ||||||
|  |     .then(searchHits => Promise.all(searchHits.map(getWineObjectFromSearchHit))) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   wineById, | ||||||
|  |   wineByQueryName | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user