Feat/vinmonopolet elastic cache #86
| @@ -11,7 +11,15 @@ function searchWines(req, res) { | ||||
|       page: page, | ||||
|       success: true | ||||
|     }) | ||||
|   ); | ||||
|   ) | ||||
|   .catch(error => { | ||||
|     const { statusCode, message } = error; | ||||
|  | ||||
|     return res.status(statusCode || 500).send({ | ||||
|       message: message || `Unexpected error occured trying to search for wine: ${name} at page: ${page}`, | ||||
|       success: false | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| function wineByEAN(req, res) { | ||||
| @@ -28,12 +36,20 @@ function wineByEAN(req, res) { | ||||
| function wineById(req, res) { | ||||
|   const { id } = req.params; | ||||
|  | ||||
|   return vinmonopoletRepository.wineById(id).then(wines => | ||||
|   return vinmonopoletRepository.wineById(id).then(wine => | ||||
|     res.json({ | ||||
|       wine: wines[0], | ||||
|       wine: wine, | ||||
|       success: true | ||||
|     }) | ||||
|   ); | ||||
|   ) | ||||
|   .catch(error => { | ||||
|       const { statusCode, message } = error; | ||||
|  | ||||
|       return res.status(statusCode || 500).send({ | ||||
|         message: message || `Unexpected error occured trying to fetch wine with id: ${id}`, | ||||
|         success: false | ||||
|       }); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function allStores(req, res) { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| const fetch = require("node-fetch"); | ||||
| const path = require("path"); | ||||
| const config = require(path.join(__dirname + "/../config/env/lottery.config")); | ||||
| const vinmonopoletCache = require(path.join(__dirname, "vinmonopoletCache")); | ||||
|  | ||||
| const convertToOurWineObject = wine => { | ||||
|   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 => { | ||||
|   return { | ||||
|     id: store.storeId, | ||||
| @@ -26,37 +41,32 @@ const convertToOurStoreObject = store => { | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| const searchWinesByName = async (name, page = 1) => { | ||||
|   const pageSize = 15; | ||||
|   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 searchWinesByName = (name, page = 1) => { | ||||
|   const pageSize = 25; | ||||
|  | ||||
|   const vinmonopoletResponse = await fetch(url, { | ||||
|     headers: { | ||||
|       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken | ||||
|     } | ||||
|   return vinmonopoletCache.wineByQueryName(name, page, pageSize) | ||||
|     .catch(_ => { | ||||
|       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 => { | ||||
| @@ -67,16 +77,30 @@ const wineByEAN = ean => { | ||||
| }; | ||||
|  | ||||
| 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 = { | ||||
|         headers: { | ||||
|       "Ocp-Apim-Subscription-Key": config.vinmonopoletToken | ||||
|           "Content-Type": 'application/json' | ||||
|         } | ||||
|       }; | ||||
|  | ||||
|       return fetch(url, options) | ||||
|     .then(resp => resp.json()) | ||||
|     .then(response => response.map(convertToOurWineObject)); | ||||
|         .then(resp => { | ||||
|           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 = () => { | ||||
|   | ||||
							
								
								
									
										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