Merge pull request #86 from KevinMidboe/feat/vinmonopolet-elastic-cache

Feat/vinmonopolet elastic cache
This commit is contained in:
2021-04-30 09:56:58 +02:00
committed by GitHub
3 changed files with 179 additions and 41 deletions

View File

@@ -11,7 +11,15 @@ function searchWines(req, res) {
page: page, page: page,
success: true 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) { function wineByEAN(req, res) {
@@ -28,12 +36,20 @@ function wineByEAN(req, res) {
function wineById(req, res) { function wineById(req, res) {
const { id } = req.params; const { id } = req.params;
return vinmonopoletRepository.wineById(id).then(wines => return vinmonopoletRepository.wineById(id).then(wine =>
res.json({ res.json({
wine: wines[0], wine: wine,
success: true 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) { function allStores(req, res) {

View File

@@ -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..`)
}
})
.then(resp => resp.json())
.catch(err => console.error(err));
if (vinmonopoletResponse.errors != null) { const url = `https://www.vinmonopolet.no/api/search?q=${name}:relevance:visibleInSearch:true&searchType=product&pageSize=${pageSize}&currentPage=${page-1}`
return vinmonopoletResponse.errors.map(error => { const options = {
if (error.type == "UnknownProductError") { headers: { "Content-Type": 'application/json' }
return res.status(404).json({ };
message: error.message
});
} else {
return next();
}
});
}
const winesConverted = vinmonopoletResponse.map(convertToOurWineObject).filter(Boolean);
return winesConverted; 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.`,
})
}
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)
const options = { .catch(_ => {
headers: { console.log(`Wine id: ${id} not found in elastic index, searching vinmonopolet..`)
"Ocp-Apim-Subscription-Key": config.vinmonopoletToken
}
};
return fetch(url, options) const url = `https://www.vinmonopolet.no/api/products/${id}?fields=FULL`
.then(resp => resp.json()) const options = {
.then(response => response.map(convertToOurWineObject)); headers: {
"Content-Type": 'application/json'
}
};
return fetch(url, options)
.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 = () => { const allStores = () => {

98
api/vinmonopoletCache.js Normal file
View 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
}