I have an API call in api.js:
export const getGraphData = (domain, userId, testId) => {
return axios({
url: `${domain}/api/${c.embedConfig.apiVersion}/member/${userId}/utests/${testId}`,
method: 'get',
});
};
I have a React helper that takes that data and transforms it.
import { getGraphData } from './api';
const dataObj = (domain, userId, testId) => {
const steps = getGraphData(domain, userId, testId)
.then((result) => {
return result.attributes;
});
console.log(steps);
// const steps = test.get('steps');
const expr = /select/;
// build array of steps that we have results in
const resultsSteps = [];
steps.forEach((step) => {
// check for types that contain 'select', and add them to array
if (expr.test(step.get('type'))) {
resultsSteps.push(step);
}
});
const newResultsSteps = [];
resultsSteps.forEach((item, i) => {
const newMapStep = new Map();
const itemDescription = item.get('description');
const itemId = item.get('id');
const itemOptions = item.get('options');
const itemAnswers = item.get('userAnswers');
const newOptArray = [];
itemOptions.forEach((element) => {
const optionsMap = new Map();
let elemName = element.get('value');
if (!element.get('value')) { elemName = element.get('caption'); }
const elemPosition = element.get('position');
const elemCount = element.get('count');
optionsMap.name = elemName;
optionsMap.position = elemPosition;
optionsMap.value = elemCount;
newOptArray.push(optionsMap);
});
newMapStep.chartType = 'horizontalBar';
newMapStep.description = itemDescription;
newMapStep.featured = 'false';
newMapStep.detailUrl = '';
newMapStep.featuredStepIndex = i + 1;
newMapStep.id = itemId;
newMapStep.isValid = 'false';
newMapStep.type = 'results';
const listForNewOptArray = List(newOptArray);
newMapStep.data = listForNewOptArray;
newMapStep.userAnswers = itemAnswers;
newResultsSteps.push(newMapStep);
});
return newResultsSteps;
};
export default dataObj;
The issue is steps, when logged outside the .then() returns a Promise {<pending>}. If I log results.attributes inside the .then(), I see the data fully returned.
You need to wait until your async call is resolved. You can do this by chaining on another then:
getGraphData(domain, userId, testId)
.then((result) => {
return result.attributes;
})
.then(steps => {
// put the rest of your method here
});
You can also look at async/await if your platform supports it which would allow code closer to your original
const steps = await getGraphData(domain, userId, testId)
.then((result) => {
return result.attributes;
});
// can use steps here
You have 2 options to transform your fetched data :
1st option : create a async function that returns a promise with the modified data :
const dataObj = (domain, userId, testId) => {
return getGraphData(domain, userId, testId).then((result) => {
const steps = result.attributes;
const expr = /select/;
// build array of steps that we have results in
const resultsSteps = [];
steps.forEach((step) => {
// check for types that contain 'select', and add them to array
if (expr.test(step.get('type'))) {
resultsSteps.push(step);
}
});
const newResultsSteps = [];
resultsSteps.forEach((item, i) => {
const newMapStep = new Map();
const itemDescription = item.get('description');
const itemId = item.get('id');
const itemOptions = item.get('options');
const itemAnswers = item.get('userAnswers');
const newOptArray = [];
itemOptions.forEach((element) => {
const optionsMap = new Map();
let elemName = element.get('value');
if (!element.get('value')) {
elemName = element.get('caption');
}
const elemPosition = element.get('position');
const elemCount = element.get('count');
optionsMap.name = elemName;
optionsMap.position = elemPosition;
optionsMap.value = elemCount;
newOptArray.push(optionsMap);
});
newMapStep.chartType = 'horizontalBar';
newMapStep.description = itemDescription;
newMapStep.featured = 'false';
newMapStep.detailUrl = '';
newMapStep.featuredStepIndex = i + 1;
newMapStep.id = itemId;
newMapStep.isValid = 'false';
newMapStep.type = 'results';
const listForNewOptArray = List(newOptArray);
newMapStep.data = listForNewOptArray;
newMapStep.userAnswers = itemAnswers;
newResultsSteps.push(newMapStep);
});
return newResultsSteps;
});
};
With es7 async/await syntax it should be :
const dataObj = async (domain, userId, testId) => {
const result = await getGraphData(domain, userId, testId);
const steps = result.attributes;
... modify the data
}
Then keep in mind that this function returns a promise, you'll need to wait for it to get the result, example in a react component :
componentDidMount(){
dataObj('mydomain', 'myuserId', 'mytestId').then((res) => {
this.setState({ data: res });
}
}
The component will update when the promise is resolve, you can then use the data (you'll need to handle the undefined data state in render method)
2nd option : Create a sync function to modify the data :
const dataObj = (steps) => {
const expr = /select/;
const resultsSteps = [];
steps.forEach((step) => {
...
}
return newResultsSteps;
};
To have the same result as option 1 in our component we'll use it like this :
componentDidMount(){
getGraphData('mydomain', 'myuserId', 'mytestId').then((res) => {
const modifiedData = dataObj(res);
this.setState({ data: modifiedData });
}
}
That's how promises work. The data is not ready when you are trying to use it so you should move all your processing into the .then. The reason your variable is a Promise {<pending>} is because you can chain other things onto it.
Something like:
steps.then((steps) => {
...
});
Related
I want to use async/await inside this promise.allSettled to convert currency by fetching some api or databased stored currency rates.
like i want to use await here like this, but not sure where to put async.
arrcars["price_client"] = await forexService.convertCurrency(item.regular_price, "EUR", userCurrency);
I have posted my entire code. need some guidance here.
This is controller
const httpStatus = require('http-status');
const pick = require('../utils/pick');
const catchAsync = require('../utils/catchAsync');
const async = require('async');
const axios = require('axios');
const { talixoService } = require('../services');
const { iwayService } = require('../services');
const { forexService } = require('../services');
const searchCars = function (req, res) {
const talixoData = talixoService.findTalixoCars(req.body) //call talixo api in services
const iwayData = iwayService.findiWayCars(req.body) //call iwaytransfers api in services
if (req.query.usercurrency) {
var userCurrency = req.query.usercurrency;
} else {
var userCurrency = "INR";
}
return Promise.allSettled([talixoData, iwayData]).then(([restalixo, resiway]) => {
const taxiresults = [];
if (restalixo.status === "fulfilled") {
//console.log(restalixo.value.data);
if (restalixo.value.data.taxis.length) {
restalixo.value.data.taxis.forEach(function (item) {
arrcars = {};
arrcars["carname"] = item.car_model;
arrcars["originprice"] = item.regular_price;
arrcars["price_client"] = forexService.convertCurrency(item.regular_price, "EUR", userCurrency);
arrcars["discountprice_client"] = forexService.convertCurrency(item.discount_price, "EUR", userCurrency);
arrcars["packageId"] = item.id;
arrcars["image"] = item.image_url;
arrcars["freewaittime"] = item.included_waiting_time;
arrcars["maxluggage"] = item.luggage;
arrcars["maxpassengers"] = item.seats;
arrcars["discountprice"] = item.discount_price;
arrcars["vehicletype"] = item.vehicle_type;
arrcars["vendor"] = "Talixo";
arrcars["vehicleremarks"] = "Or Similar";
taxiresults.push(arrcars);
});
}
if (restalixo.value.data.limousines.length) {
restalixo.value.data.limousines.forEach(function (item) {
arrcars = {};
arrcars["carname"] = item.car_model;
arrcars["originprice"] = item.regular_price;
arrcars["price_client"] = forexService.convertCurrency(item.regular_price, "EUR", userCurrency);
arrcars["discountprice_client"] = forexService.convertCurrency(item.discount_price, "EUR", userCurrency);
arrcars["packageId"] = item.id;
arrcars["image"] = item.image_url;
arrcars["freewaittime"] = item.included_waiting_time;
arrcars["maxluggage"] = item.luggage;
arrcars["maxpassengers"] = item.seats;
arrcars["discountprice"] = item.discount_price;
arrcars["vehicletype"] = item.vehicle_type;
arrcars["vendor"] = "Talixo";
arrcars["vehicleremarks"] = "Or Similar";
taxiresults.push(arrcars);
});
}
}
//iwaytransfers supplier data
if (resiway.status === "fulfilled") {
//console.log(resiway.value.data);
if (resiway.value.data.result.length) {
resiway.value.data.result.forEach(function (item) {
var imgsrc = "https://iwayex.com/images/cars/";
arrcars = {};
arrcars["carname"] = item.car_class.models[0];
arrcars["originprice"] = item.price;
arrcars["price_client"] = forexService.convertCurrency(item.price, "EUR", userCurrency);
arrcars["discountprice_client"] = forexService.convertCurrency(item.price, "EUR", userCurrency);
arrcars["packageId"] = item.price_uid;
arrcars["image"] = imgsrc + item.car_class.photo;
arrcars["freewaittime"] = item.allowable_time;
arrcars["maxluggage"] = "";
arrcars["maxpassengers"] = item.car_class.capacity;
arrcars["discountprice"] = item.price;
arrcars["vehicletype"] = item.vehicle_type;
arrcars["vendor"] = "iway Transfers";
arrcars["vehicleremarks"] = "Or Similar";
taxiresults.push(arrcars);
});
}
}
if (taxiresults.length) {
sortedresult = taxiresults.sort(function (a, b) {
return a.discountprice - b.discountprice;
});
res.status(200).send(sortedresult)
}else{
res.status(200).send({})
}
});
}
module.exports = {
searchCars
}
this is the result sample i am getting.
{
carname: "Toyota Prius"
discountprice: 27.5
discountprice_client: {}
freewaittime: 45
image: "https://static.talixo.de/images/vehicles/economy.png"
maxluggage: 3
maxpassengers: 3
originprice: 27.5
packageId: "16021"
price_client: {}
vehicleremarks: "Or Similar"
vehicletype: "limo"
vendor: "Talixo"
}
here the problem is,
i expect
discountprice_client: Converted Number,
but i am getting blank object there as can be seen in above result sample.
i have been using this in another function with async await, then it works fine. but in this case, i am not sure how to put async/await.
this is inside
forex.service.js
const convertCurrency = async (amount, basecurrency, reqCurrency) => {
if (basecurrency === reqCurrency) {
let result = Math.round(amount) + ' ' + reqCurrency;
return result;
} else {
const reqForexData = await getForexbyISO(reqCurrency);
const baseForexData = await getForexbyISO(basecurrency);
const amountInUSD = amount / baseForexData.forexRate;
const amountInreqC = amountInUSD * reqForexData.forexRate;
let result = Math.round(amountInreqC) + ' ' + reqCurrency;
return result;
}
}
module.exports = {
convertCurrency
}
You could do:
// notice the `async` before `function`
resiway.value.data.result.forEach(async function (item) {
// ...
arrcars = {};
arrcars["price_client"] = await forexService.convertCurrency(item.price, "EUR", userCurrency);
arrcars["discountprice_client"] = await forexService.convertCurrency(item.price, "EUR", userCurrency);
// ...
});
But it can lead to unexpected behaviours
I would do this
// add missing `async`
return Promise.allSettled([talixoData, iwayData]).then(async ([restalixo, resiway]) => {
// ...
// await for all loop at once to avoid odd side effects
await Promise.all(resiway.value.data.result.map(async function (item) {
// ...
arrcars = {};
arrcars["price_client"] = await
forexService.convertCurrency(item.price, "EUR", userCurrency);
arrcars["discountprice_client"] = await
forexService.convertCurrency(item.price, "EUR", userCurrency);
// ...
}));
// ...
});
simplify your services
You are writing the Talixo and Iway services but using the result is difficult. Instead of returning the entire response, return the fields that matter to your consumer -
// services/talixo_service.js
async function findTalixoCars(query) {
const result = await // ... perform request
return result.value.data ?? [] // <- return relevant data
}
Notice we use return _____ ?? [] to ensure that an array is always returned -
// services/iway_service.js
async function findiWayCars(query) {
const result = await // ... perform request
return result.value.data.result ?? [] // <- return relevant data
}
searchCars
With simplified services, we write a high-level implementation of searchCars. Instead of using Promise.allSettled we can use .catch(_ => []) on potential failures to guarantee we have an array result. This reduces the need for logical checks and code branches to make the code more declarative and less prone to errors -
async function searchCars(req, res) {
const [talixo, iway] = await Promise.all([
talixoService.findTalixoCars(req.body).catch(_ => []),
iwayService.findiWayCars(req.body).catch(_ => [])
])
const userCurrency = req.query.usercurrency ?? "INR"
const taxis = [
...await Promise.all(talixo.taxis.map(item => fromTalixo(item, userCurrency))),
...await Promise.all(talixo.limousines.map(item => fromTalixo(item, userCurrency))),
...await Promise.all(iway.map(item => fromIway(item, userCurrency)))
]
res.status(200).send(
taxis.sort((a,b) => a.discountprice - b.discountprice)
)
}
data mapping
The search function above depends on fromTalixo and fromIway. These mapping functions reduce code complexity and repitition and gives us a good conceptual model for how data gets mapped from the remote services to our local data -
async function fromTalixo(data, userCurrency) {
return {
carname: data.car_model,
originprice: data.regular_price,
price_client: await forexService.convertCurrency(data.regular_price, "EUR", userCurrency),
discountprice_client: await forexService.convertCurrency(data.discount_price, "EUR", userCurrency),
packageId: data.id,
image: data.image_url,
freewaittime: data.included_waiting_time,
maxluggage: data.luggage,
maxpassengers: data.seats,
discountprice: data.discount_price,
vehicletype: data.vehicle_type,
vendor: "Talixo",
vehicleremarks: "Or Similar",
}
}
async function fromIway(data, userCurrency) {
return {
carname: data.car_class.models[0],
originprice: data.price,
price_client: await forexService.convertCurrency(data.price, "EUR", userCurrency),
discountprice_client: await forexService.convertCurrency(data.price, "EUR", userCurrency),
packageId: data.price_uid,
image: `https://iwayex.com/images/cars/${data.car_class.photo}`,
freewaittime: data.allowable_time,
maxluggage: "",
maxpassengers: data.car_class.capacity,
discountprice: data.price,
vehicletype: data.vehicle_type,
vendor: "iway Transfers",
vehicleremarks: "Or Similar",
}
}
I have a firebase callable function like
exports.getUserInfo = functions.https.onCall(async (data, context) => {
const userIdRef = store.collection('UserID').doc('Document');
let res = null;
try {
res = await store.runTransaction(async t => {
const doc = await t.get(userIdRef);
currentUserId = doc.data().ID;
const newUserIdNum = currentUserId + 1;
await t.update(userIdRef, {ID: newUserIdNum});
let initUserData = {
'userID': newUserId,
}
return initUserData ;
});
console.log('Transaction success');
} catch (e) {
console.error('Transaction failure:', e);
}
return res;
})
but I am not sure how to create unit test about this. I want to mock UserID/Documment before I call
test.wrap(getUserInfo), like
const myFunctions = require('../index.js')
const getUserInfoWrapped = test.wrap(myFunctions.getUserInfo);
var assert = require('assert');
describe('User', function() {
describe('getUserInfo()', function() {
it('should create user', async function() {
// sample code, this won't work
const snapshot = test.firestore.makeDocumentSnapshot({ID: 1001}, 'UserID/Document');
const data = {}
const result = await getUserInfoWrapped (data, {});
assert.equal(result.userID, "1002");
});
});
});
seems this case not covered by firebase document
Reference:
https://firebase.google.com/docs/functions/unit-testing
https://github.com/firebase/firebase-functions-test/blob/c77aa92d345b8e4fb5ad98534989eb8dcf7d9bc4/spec/providers/https.spec.ts
I'm using React Native and Firebase V9. I am trying to download URLs from storage, but I'm not able to both download all the URLs in the correct order and have them load the first time I visit the screen. I know that getDownLoadURL() runs asynchronously, which is why the images get downloaded in random order each time. There is something I'm not understanding correctly.
Here are two main ways I've tried:
const [memNameLogs, setMemNameLogs] = useState([]);
const [memIDLogs, setMemIDLogs] = useState([]);
const [memImagesLogs, setMemImagesLogs] = useState([]);
const [memberCount, setMemberCount] = useState(0);
const getGroupInfo = async () => {
let memberIDs = [];
let memberNames = [];
let memberImages = [];
let userGroupsRef = collection(db, "groups", groupID, "members");
onSnapshot(userGroupsRef, (querySnapshot) => {
querySnapshot.forEach((document) => {
memberIDs.push(document.id);
onSnapshot(doc(db, "users", document.id), (snapshot) => {
const one = snapshot.data();
const two = one.firstName;
const three = one.lastName;
const four = two + ' ' + three;
memberNames.push(four);
console.log(memberNames);
});
const pathReference = ref(storage, 'userProfileImage/' + document.id);
// Get the download URL
getDownloadURL(pathReference)
.then((url) => {
memberImages.push(url);
})
});
setMemberCount(memberIDs.length);
setMemIDLogs(memberIDs);
setMemNameLogs(memberNames);
setMemImagesLogs(memberImages);
})
and
const getGroupMembers = async () => {
let memberIDs = [];
let memberNames = [];
let paths = [];
let userGroupsRef = collection(db, "groups", groupID, "members");
onSnapshot(userGroupsRef, (querySnapshot) => {
querySnapshot.forEach((document) => {
memberIDs.push(document.id)
onSnapshot(doc(db, "users", document.id), (snapshot) => {
const one = snapshot.data();
const two = one.firstName;
const three = one.lastName;
const four = two + ' ' + three;
memberNames.push(four);
});
const pathReference = ref(storage, 'userProfileImage/' + document.id);
paths.push(pathReference);
})
setMemberCount(memberIDs.length);
setMemIDLogs(memberIDs);
setMemNameLogs(memberNames);
setImagePathReferences(paths);
})
return Promise.all(imagePathReference)
}
const getGroupPictures = async (file) => {
let downloadURL;
await getDownloadURL(file)
.then((url) => {
downloadURL = (url);
})
.catch((error) => {
// Handle any errors
if (error.code === "storage/object-not-found") {
downloadURL = ('');
}
});
return (downloadURL)
}
const getGroupInfo = async () => {
const references = await getGroupMembers();
console.log(references);
for (let i = 0; i < memberCount; i++) {
let file = references[i];
const references2 = await getGroupPictures(file);
memImagesLogs.push(references2);
}
};
A simple fix is to store the download URLs in an associative array, with the file as the key.
Something like this:
const downloadURLs = {}; // 👈
const getGroupInfo = async () => {
const references = await getGroupMembers();
console.log(references);
for (let i = 0; i < memberCount; i++) {
const file = references[i];
const downloadURL = await getGroupPictures(file);
downloadURLs[file] = downloadURL; // 👈
}
};
With that, the references array determines the order, while the downloadURLs object allows you to look up the download URL for each file.
In the line setVotedPosts([...previousVotedPosts, postId]);
I'm trying to get the previous value of votedPosts, but I'm getting back the newest value.
full code : https://github.com/silvertechguy/reddit-clone/blob/main/src/components/vote-buttons.js
App live : https://reddit-clone-official.vercel.app/
const VoteButtons = ({ post }) => {
const [isVoting, setVoting] = useState(false);
const [votedPosts, setVotedPosts] = useState([]);
useEffect(() => {
const votesFromLocalStorage =
JSON.parse(localStorage.getItem("votes")) || [];
setVotedPosts(votesFromLocalStorage);
}, []);
const handleDisablingOfVoting = (postId) => {
const previousVotedPosts = votedPosts;
setVotedPosts([...previousVotedPosts, postId]);
localStorage.setItem(
"votes",
JSON.stringify([...previousVotedPosts, postId])
);
};
const handleClick = async (type) => {
setVoting(true);
// Do calculation to save the vote.
let upVotesCount = post.upVotesCount;
let downVotesCount = post.downVotesCount;
const date = new Date();
if (type === "upvote") {
upVotesCount = upVotesCount + 1;
} else {
downVotesCount = downVotesCount + 1;
}
await db.collection("posts").doc(post.id).set({
title: post.title,
upVotesCount,
downVotesCount,
createdAt: post.createdAt,
updatedAt: date.toUTCString(),
});
// Disable the voting button once the voting is successful.
handleDisablingOfVoting(post.id);
setVoting(false);
};
const checkIfPostIsAlreadyVoted = () => votedPosts.includes(post.id);
Problem
const previousVotedPosts = votedPosts;
In JavaScript, arrays are reference types, so you can't just create a new copy of an array using =.
Try this solution
Clone array using spread syntax(...).
const handleDisablingOfVoting = (postId) => {
const previousVotedPosts = [...votedPosts];
setVotedPosts([...previousVotedPosts, postId]);
localStorage.setItem(
"votes",
JSON.stringify([...previousVotedPosts, postId])
);
};
This is the module that collections and exports async data: scraper.js
const express = require('express')
const cheerio = require('cheerio')
const request = require("tinyreq")
const fs = require('fs')
const _ = require('lodash')
const uuid = require('uuid/v4')
const async = require('async')
const mental_models = {
url: 'https://www.farnamstreetblog.com/mental-models/',
data: {}
}
const decision_making = {
url: 'https://www.farnamstreetblog.com/smart-decisions/',
data: {}
}
const cognitive_bias = {
url: 'https://betterhumans.coach.me/cognitive-bias-cheat-sheet-55a472476b18',
data: {}
}
const DATA_URLS = [mental_models, decision_making, cognitive_bias]
const filterScrape = async (source, params) => {
let filtered_data = {
topics: [],
content: [],
additional_content: []
}
let response = await scrape(source)
try {
let $ = cheerio.load(response)
params.forEach((elem) => {
let headers = ['h1', 'h2', 'h3']
if ($(elem) && headers.includes(elem)) {
let topic = {}
let content = {}
let id = uuid()
topic.id = id
topic.text = $(elem).text()
if ($(elem).closest('p')) {
content.text = $(elem).closest('p').text()
content.id = id
}
filtered_data.topics.push(topic)
filtered_data.content.push(content)
} else if ($(elem) && !headers.includes(elem)) {
let content = {}
let id = uuid()
content.text = $(elem).text()
content.id = id
filtered_data.additional_content.push(content)
} else {
}
})
}
catch (err) {
console.log(err)
}
return filtered_data
}
const scrape = (source) => {
return new Promise((resolve, reject) => {
request(source.url, function (err, body) {
if (err) {
reject(err)
return
}
resolve(body)
})
})
}
const DATA = _.map(DATA_URLS, async (source) => {
let params = ['h1', 'h2', 'h3', 'p']
let new_data = await filterScrape(source, params)
try {
source.data = new_data
}
catch (err) {
console.log(err)
}
})
module.exports = DATA
This is the module that imports the data: neural.js
const brain = require('brain')
const neural_net = new brain.NeuralNetwork()
const DATA = require('./scraper')
console.log(DATA)
Obviously not much going on, I've removed the code since the variable doesn't resolve. When logged it logs a promise but the promise does not resolve. However in the imported module, the promise is logged and then resolves. What gives? Should I import a function that resolves the data?
Of course it would be best to import that function, however it won't change the issue in your code which is here:
const DATA = _.map(DATA_URLS, async (source) => {
Lodash doesn't support async iteration - so you need to have some other method, one would be to use the newest nodejs version (10.x) and make use of async iteration - but that won't use the full power of asynchronous code.
You can also use scramjet - a framework my company is supporting. The code above would take the following form:
const {DataStream} = require("scramjet");
const DATA_URLS = [mental_models, decision_making, cognitive_bias];
module.exports = async () => DataStream.fromArray(DATA_URLS)
.setOptions({maxParallel: 2}) // if you need to limit that at all.
.map(async ({url}) => {
let params = ['h1', 'h2', 'h3', 'p']
let data = await filterScrape(source, params);
return { url, data };
})
.toArray();
The other file would take the following form:
const brain = require('brain')
const neural_net = new brain.NeuralNetwork()
const scraper = require('./scraper')
(async (){
const DATA = await scraper();
console.log(DATA); // or do whatever else you were expecting...
})();