I am working on my first Firebase project using AngularFire2. Below is the overall design of my learning project.
Users uploads photos and it's stored in the Firebase storage as images.
The uploaded photos are listed in the homepage sorted based on timestamp.
Below is the structure that I have now when I started. But I feel difficulty when doing joins. I should be able to get user details for each uploads and able to sort uploads by timestamp.
User:
- Name
- Email
- Avatar
Uploads:
- ImageURL
- User ID
- Time
I read few blogs de-normalising the data structure. For my given scenario, how best can i re-model my database structure?
Any example for creating some sample data in the new proposed solution will be great for my understanding.
Once the image upload is done, I am calling the below code to create an entry in the database.
addUpload(image: any): firebase.Promise<any> {
return firebase.database().ref('/userUploads').push({
user: firebase.auth().currentUser.uid,
image: image,
time: new Date().getTime()
});
}
I am trying to join 2 entities as below. i am not sure how can I do it efficiently and correctly.
getUploads(): any {
const rootDef = this.db.database.ref();
const uploads = rootDef.child('userUploads').orderByChild('time');
uploads.on('child_added',snap => {
let userRef =rootDef.child('userProfile/' + snap.child('user').val());
userRef.once('value').then(userSnap => {
???? HOW TO HANDLE HERE
});
});
return ?????;
}
I would like to get a final list having all upload details and its corresponding user data for each upload.
This type of join will always be tricky if you write it from scratch. But I'll try to walk you through it. I'm using this JSON for my answer:
{
uploads: {
Upload1: {
uid: "uid1",
url: "https://firebase.com"
},
Upload2: {
uid: "uid2",
url: "https://google.com"
}
},
users: {
uid1: {
name: "Purus"
},
uid2: {
name: "Frank"
}
}
}
We're taking a three-stepped approach here:
Load the data from uploads
Load the users for that data from users
Join the user data to the upload data
1. Load the data uploads
Your code is trying to return a value. Since the data is loaded from Firebase asynchronously, it won't be available yet when your return statement executes. That gives you two options:
Pass in a callback to getUploads() that you then call when the data has loaded.
Return a promise from getUploads() that resolves when the data has loaded.
I'm going to use promises here, since the code is already difficult enough.
function getUploads() {
return ref.child("uploads").once("value").then((snap) => {
return snap.val();
});
}
This should be fairly readable: we load all uploads and, once they are loaded, we return the value.
getUploads().then((uploads) => console.log(uploads));
Will print:
{
Upload1 {
uid: "uid1",
url: "https://firebase.com"
},
Upload2 {
uid: "uid2",
url: "https://google.com"
}
}
2. Load the users for that data from users
Now in the next step, we're going to be loading the user for each upload. For this step we're not returning the uploads anymore, just the user node for each upload:
function getUploads() {
return ref.child("uploads").once("value").then((snap) => {
var promises = [];
snap.forEach((uploadSnap) => {
promises.push(
ref.child("users").child(uploadSnap.val().uid).once("value")
);
});
return Promise.all(promises).then((userSnaps) => {
return userSnaps.map((userSnap) => userSnap.val());
});
});
}
You can see that we loop over the uploads and create a promise for loading the user for that upload. Then we return Promise.all(), which ensures its then() only gets called once all users are loaded.
Now calling
getUploads().then((uploads) => console.log(uploads));
Prints:
[{
name: "Purus"
}, {
name: "Frank"
}]
So we get an array of users, one for each upload. Note that if the same user had posted multiple uploads, you'd get that user multiple times in this array. In a real production app you'd want to de-duplicate the users. But this answer is already getting long enough, so I'm leaving that as an exercise for the reader...
3. Join the user data to the upload data
The final step is to take the data from the two previous steps and joining it together.
function getUploads() {
return ref.child("uploads").once("value").then((snap) => {
var promises = [];
snap.forEach((uploadSnap) => {
promises.push(
ref.child("users").child(uploadSnap.val().uid).once("value")
);
});
return Promise.all(promises).then((userSnaps) => {
var uploads = [];
var i=0;
snap.forEach((uploadSnap) => {
var upload = uploadSnap.val();
upload.username = userSnaps[i++].val().name;
uploads.push(upload);
});
return uploads;
});
});
}
You'll see we added a then() to the Promise.all() call, which gets invoked after all users have loaded. At that point we have both the users and their uploads, so we can join them together. And since we loaded the users in the same order as the uploads, we can just join them by their index (i). Once you de-duplicate the users this will be a bit trickier.
Now if you call the code with:
getUploads().then((uploads) => console.log(uploads));
It prints:
[{
uid: "uid1",
url: "https://firebase.com",
username: "Purus"
}, {
uid: "uid2",
url: "https://google.com",
username: "Frank"
}]
The array of uploads with the name of the user who created that upload.
The working code for each step is in https://jsbin.com/noyemof/edit?js,console
I did the following based on Franks answer and it works. I am not sure if this is the best way for dealing with large number of data.
getUploads() {
return new Promise((resolve, reject) => {
const rootDef = this.db.database.ref();
const uploadsRef = rootDef.child('userUploads').orderByChild('time');
const userRef = rootDef.child("userProfile");
var uploads = [];
uploadsRef.once("value").then((uploadSnaps) => {
uploadSnaps.forEach((uploadSnap) => {
var upload = uploadSnap.val();
userRef.child(uploadSnap.val().user).once("value").then((userSnap) => {
upload.displayName = userSnap.val().displayName;
upload.avatar = userSnap.val().avatar;
uploads.push(upload);
});
});
});
resolve(uploads);
});
}
Related
I have an endpoint uploads images and updates a database table.
I send 3 requests to this endpoint at same time. Actually this problem happens when I send more than 2 API requests.
First request that comes to endpoint uploads images and updates database table successfully.
Second request that comes to endpoint uploads images sees database changes of first request, and updates database table successfully.
Third request that comes to endpoint uploads images, doesn't see database changes of second request, and updates database table successfully.
As a result; only database changes of first request and third request apply. Database changes of second request is not able to applied successfully or is overridden, somehow.
I use pg npm package.
Is problem in my code or in pg package.
How can this problem be solved?
Controller:
#UseStaffPermissionsGuards('upsert', 'VehicleCondition')
#ApiBody({ type: VehiclePhotoConditionInfoImageDTO })
#ApiResponse({ status: 201 })
#Post(':id/photos/:photoConditionId/image')
#ApiConsumes('multipart/form-data')
#UseInterceptors(FilesInterceptor('images'), FilesToBodyInterceptor)
async upsertImages(
#Param('id') vehicleId: string,
#Param('photoConditionId') photoConditionId: string,
#Body() vehiclePhotoConditionInfoImages: VehiclePhotoConditionInfoImageDTO,
): Promise<void> {
return this.vehiclePhotoConditionService.upsertImages(
vehicleId,
photoConditionId,
vehiclePhotoConditionInfoImages,
);
}
Service:
async upsertImages(
vehicleId: string,
vehiclePhotoConditionId: string,
vehiclePhotoConditionImage: VehiclePhotoConditionInfoImageDTO,
): Promise<void> {
await this.isVehicleExist(vehicleId);
const vehiclePhotoCondition = await this.getOne(vehicleId, vehiclePhotoConditionId);
if (!vehiclePhotoCondition) {
throw new BadRequestException(
`The vehicle photo condition ${vehiclePhotoConditionId} is not found`,
);
}
const imageKeys = await this.handleImages(vehiclePhotoConditionId, vehiclePhotoConditionImage);
const updatedVehiclePhotoConditions = vehiclePhotoCondition.info.map((data) => {
if (data.vehiclePart === vehiclePhotoConditionImage.vehiclePart) {
data.uploadedImagesKeys.push(...imageKeys);
}
return data;
});
const query = sql
.update('vehicle_photo_condition', {
info: JSON.stringify(updatedVehiclePhotoConditions),
updated_at: sql('now()'),
})
.where({ id: vehiclePhotoConditionId });
await this.db.query(query.toParams());
}
I solved the problem. I am posting correct code.
Here is the explanation:
In previous code, I was updating inner jsonb array of objects in code and because of the fact that the below code took some time and asynchronicity of the NodeJS, previous request can take more time than the other requests that will come later and this situation can cause data inconsistency.
Here is the previous code:
const updatedVehiclePhotoConditions = vehiclePhotoCondition.info.map((data) => {
if (data.vehiclePart === vehiclePhotoConditionImage.vehiclePart) {
data.uploadedImagesKeys.push(...imageKeys);
}
return data;
});
In current code, I am updating inner jsonb array of object in database and let database do this operation. So, no data consistency happened.
Here is current code:
const query = {
text: `
UPDATE vehicle_photo_condition
SET info = s.json_array
FROM (
SELECT
jsonb_agg(
CASE WHEN obj ->> 'vehiclePart' = $1 THEN
jsonb_set(obj, '{uploadedImagesKeys}', $2)
ELSE obj END
) as json_array
FROM vehicle_photo_condition, jsonb_array_elements(info) obj WHERE id = $3
) s WHERE id = $3`,
values: [
vehiclePhotoConditionImage.vehiclePart,
JSON.stringify(imageKeys),
vehiclePhotoConditionId,
],
};
async upsertImages(
vehicleId: string,
vehiclePhotoConditionId: string,
vehiclePhotoConditionImage: VehiclePhotoConditionInfoImageDTO,
): Promise<void> {
const vehiclePhotoCondition = await this.getOne(vehicleId, vehiclePhotoConditionId);
if (!vehiclePhotoCondition) {
throw new BadRequestException(
`The vehicle photo condition ${vehiclePhotoConditionId} is not found`,
);
}
const imageKeys = await this.handleImages(vehiclePhotoConditionId, vehiclePhotoConditionImage);
const query = {
text: `
UPDATE vehicle_photo_condition
SET info = s.json_array
FROM (
SELECT
jsonb_agg(
CASE WHEN obj ->> 'vehiclePart' = $1 THEN
jsonb_set(obj, '{uploadedImagesKeys}', $2)
ELSE obj END
) as json_array
FROM vehicle_photo_condition, jsonb_array_elements(info) obj WHERE id = $3
) s WHERE id = $3`,
values: [
vehiclePhotoConditionImage.vehiclePart,
JSON.stringify(imageKeys),
vehiclePhotoConditionId,
],
};
await this.db.query(query);
}
I am a react-native developer and new to firebase. I am performing firebase realtime database operation, have a look at code below;
firebase.database().ref('events/wedding/items').push(object).then((data) => {
//success callback
dispatch(addPendingInvoice({ ...invoice, id: data.key }))
Alert.alert('Successfully added to Invoices', 'Please go to invoice section to clear first and continue.', [{ text: 'Ok' }])
}).catch((error) => {
//error callback
Alert.alert("Can't book package.", 'Please check your internet connection!', [{ text: 'OK', style: 'destructive' }])
})
Now, I wish to push another object to another node events/wedding/packages right after this firebase database function above. I can use another function inside then callback in above firebase functions. This is not a professional way to do this.
Is there any way to do this?
You can use the update() method to "simultaneously write to specific children of a node without overwriting other child nodes". Note that "simultaneous updates made this way are atomic: either all updates succeed or all updates fails", see the doc.
So in your case you would do along the following lines:
var newNodeKey = firebase.database().ref().child('events/wedding/items').push().key;
var updates = {};
updates['events/wedding/items/' + newNodeKey] = { foo: "bar" };
updates['events/wedding/packages/' + newNodeKey] = { bar: "foo" };
firebase.database().ref().update(updates)
.then(() => {
// The two writes are completed, do whatever you need
// e.g. dispatch(...);
});
All Firebase operations return a promise so you can use Promise.all() to run them all simultaneously.
Promise.all([
firebase.database().ref(reference).set({}),
firebase.database().ref(reference2).set({})
]).then(() => {
console.log("Operations Successful")
}).catch((e) => console.log(e))
You can also push all your operations to an array and then pass that array in Promise.all()
In my site:
Users have many Activities
Each Activity has encoded_polyline data
I display these encoded_polylines on a map
I want to use IndexedDB (via Dexie) as an in-browser cache so that they don't need to re-download their full Activity set every time they view their map. I've never used IndexedDB before, so I don't know if I'm doing anything silly or overlooking any edge cases.
Here's a high-level description of what I think the overall process is:
Figure out what exists on the server
Remove anything that is present in IndexedDB but is not present on the server
Figure out what exists in IndexedDB
Request only the data missing in IndexedDB
Store the new data in IndexedDB
Query all of the data out of IndexedDB
Throughout all of this, we need to be focusing on this user. A person might view many people's pages, and therefore have a copy of many people's data in IndexedDB. So the queries to the server and IndexedDB need to be aware of which User ID is being referenced.
Here's the English Language version of what I decided to do:
Collect all of this User's Activty IDs from the server
Remove anything in IndexedDB that shouldn't be there (stuff deleted from the site that might still exist in IndexedDB)
Collect all of this User's Activty IDs from IndexedDB
Filter out anything that's present in IndexedDB and the server
If there are no new encoded_polylines to retrieve then putItemsFromIndexeddbOnMap (described below)
If there are new encoded_polylines to retrieve: retrieve those from the server, then store them in IndexedDB, then putItemsFromIndexeddbOnMap
For putItemsFromIndexeddbOnMap:
Get all of this user's encoded_polylines from IndexedDB
Push that data into an array
Display that array of data on the map
Here's the JavaScript code that does what I've explained above (with some ERB because this JavaScript is embedded in a Rails view):
var db = new Dexie("db_name");
db.version(1).stores({ activities: "id,user_id" });
db.open();
// get this user's activity IDs from the server
fetch('/users/' + <%= #user.id %> + '/activity_ids.json', { credentials: 'same-origin' }
).then(response => { return response.json() }
).then(activityIdsJson => {
// remove items from IndexedDB for this user that are not in activityIdsJson
// this keeps data that was deleted in the site from sticking around in IndexedDB
db.activities
.where('id')
.noneOf(activityIdsJson)
.and(function(item) { return item.user_id === <%= #user.id %> })
.keys()
.then(removeActivityIds => {
db.activities.bulkDelete(removeActivityIds);
});
// get this user's existing activity IDs out of IndexedDB
db.activities.where({user_id: <%= #user.id %>}).primaryKeys(function(primaryKeys) {
// filter out the activity IDs that are already in IndexedDB
var neededIds = activityIdsJson.filter((id) => !primaryKeys.includes(id));
if(Array.isArray(neededIds) && neededIds.length === 0) {
// we do not need to request any new data so query IndexedDB directly
putItemsFromIndexeddbOnMap();
} else if(Array.isArray(neededIds)) {
if(neededIds.equals(activityIdsJson)) {
// we need all data so do not pass the `only` param
neededIds = [];
}
// get new data (encoded_polylines for display on the map)
fetch('/users/' + <%= #user.id %> + '/encoded_polylines.json?only=' + neededIds, { credentials: 'same-origin' }
).then(response => { return response.json() }
).then(newEncodedPolylinesJson => {
// store the new encoded_polylines in IndexedDB
db.activities.bulkPut(newEncodedPolylinesJson).then(_unused => {
// pull all encoded_polylines out of IndexedDB
putItemsFromIndexeddbOnMap();
});
});
}
});
});
function putItemsFromIndexeddbOnMap() {
var featureCollection = [];
db.activities.where({user_id: <%= #user.id %>}).each(activity => {
featureCollection.push({
type: 'Feature',
geometry: polyline.toGeoJSON(activity['encoded_polyline'])
});
}).then(function() {
// if there are any polylines, add them to the map
if(featureCollection.length > 0) {
if(map.isStyleLoaded()) {
// map has fully loaded so add polylines to the map
addPolylineLayer(featureCollection);
} else {
// map is still loading, so wait for that to complete
map.on('style.load', addPolylineLayer(featureCollection));
}
}
}).catch(error => {
console.error(error.stack || error);
});
}
function addPolylineLayer(data) {
map.addSource('polylineCollection', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: data
}
});
map.addLayer({
id: 'polylineCollection',
type: 'line',
source: 'polylineCollection'
});
}
...Am I doing it right?
I'm just starting out with React and ES6 so apologies if this is a bit of a simple one.
I'm currently playing round with the FoursquareAPI. I'm using ES6 fetch to return a series of objects (they're actually different venues in different parts of the world) which are then mapped and returned and stored in the application's state. This works fine and returns what I want:
// method to call api
getVenues: (searchTerm) => {
const fetchVenuesURL = `${urlExplore}${searchTerm}&limit=10&client_id=${clientId}&client_secret=${clientSecret}&v=20180602`;
return fetch(fetchVenuesURL).then( response => {
return response.json();
}).then( jsonResponse => {
if (jsonResponse.response.groups[0].items) {
return jsonResponse.response.groups[0].items.map(item => (
// populate venues
{
id: item.venue.id,
name: item.venue.name,
address : item.venue.location.address,
city : item.venue.location.city,
country : item.venue.location.country,
icon : item.venue.categories[0].icon
}
));
} else {
return [];
}
});
// method in App.js to setState
search(term){
Foursquare.getVenues(term).then(foursquareResponse => {
this.setState({venues: foursquareResponse});
});
}
The problem arises when I need to fetch photographs associated with each of the 'venues' returned by the original fetch. These come from a different endpoint. I'm not sure what the best approach is.
One way would be to have two separate api calling methods and then somehow populate an empty photos field of the first with the photos from the second back in App.js but that seems clunky.
My instinct is to somehow nest the Api calls but I'm uncertain about how to go about this. I'm hoping to do something along the lines of somehow applying a method to each iteration of the first mapped object, something along the lines of but not sure how to link them together so that the second goes into the photo property of the first:
{
id: item.venue.id,
name: item.venue.name,
address : item.venue.location.address,
city : item.venue.location.city,
country : item.venue.location.country,
icon : item.venue.categories[0].icon
photos : []
}
const fetchPhotosURL = `${urlPhotos}${venueId}/photos?limit=10&client_id=${clientId}&client_secret=${clientSecret}&v=20180602`;
return fetch(fetchPhotosURL).then( response => {
return response.json();
}).then( jsonResponse => {
if (jsonResponse.response.photos.items) {
console.log(jsonResponse.response.photos.items[0].venue)
return jsonResponse.response.photos.items.map(item => (
{
id : item.id,
created: item.createdAt,
prefix: item.prefix,
suffix: item.suffix,
width: item.width,
height: item.height,
venue: item.venue
}
));
} else {
return [];
}
})
Can anyone point me in the right direction with this. I'm guessing that it's one of those things that isn't that hard once you've done it once but I'm finding it difficult.
Thanks in advance.
I need to call an API recursively using request promise after getting result from API need to write in an excel file , API sample response given below
{
"totalRecords": 9524,
"size": 20,
"currentPage": 1,
"totalPages": 477,
"result": [{
"name": "john doe",
"dob": "1999-11-11"
},
{
"name": "john1 doe1",
"dob": "1989-12-12"
}
]
}
Now I want to call this API n times, here n is equal to totalPages, after calling each API I want to write response result to the excel files.
First write page 1 response result to excel then append page 2 response result to excel file and so on..
I have written some sample code given below
function callAPI(pageNo) {
var options = {
url: "http://example.com/getData?pageNo="+pageNo,
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
json: true
}
return request(options)
}
callAPI(1).then(function (res) {
// Write res.result to excel file
}).catch(function (err) {
// Handle error here
})
But facing problem calling recursively API and maintaining sequentially like write page 1 result first to excel file then page 2 result append to excel and so on..
Any code sample how to achieve in nodejs
You want to do something like this:
function getAllPages() {
function getNextPage(pageNo) {
return callAPI(pageNo).then(response => {
let needNextPage = true;
if (pageNo === 1) {
// write to file
} else {
// append to file
}
if (needNextPage) {
return getNextPage(pageNo+1);
} else {
return undefined;
}
});
}
return getNextPage(1);
}
Obviously change that 'needNextPage' to false to stop the recursion when you're done
So you want to do 477 requests in sequence? How long do you wanna wait for this to finish? Even in paralell, this would be still too long for me.
Best: write an API that can return you a batch of pages at once. Reducing the number of requests to the backend. Maybe something like http://example.com/getData?pages=1-100 and let it return an Array; maybe like
[
{
"totalRecords": 9524,
"currentPage": 1,
"totalPages": 477,
"result": [...]
},
{
"totalRecords": 9524,
"currentPage": 2,
"totalPages": 477,
"result": [...]
},
...
]
or more compact
{
"totalRecords": 9524,
"totalPages": 477,
"pages": [
{
"currentPage": 1,
"result": [...]
},
{
"currentPage": 2,
"result": [...]
},
...
]
}
Sidenote: writing the size of the results array into the json is unnecessary. This value can easily be determined from data.result.length
But back to your question
Imo. all you want to run in sequence is adding the pages to the sheet. The requests can be done in paralell. That already saves you a lot of overall runtime for the whole task.
callApi(1).then(firstPage => {
let {currentPage, totalPages} = firstPage;
//`previous` ensures that the Promises resolve in sequence,
//even if some later request finish sooner that earlier ones.
let previous = Promise.resolve(firstPage).then(writePageToExcel);
while(++currentPage <= totalPages){
//make the next request in paralell
let p = callApi(currentPage);
//execute `writePageToExcel` in sequence
//as soon as all previous ones have finished
previous = previous.then(() => p.then(writePageToExcel));
}
return previous;
})
.then(() => console.log("work done"));
or you wait for all pages to be loaded, before you write them to excel
callApi(1).then(firstPage => {
let {currentPage, totalPages} = firstPage;
let promises = [firstPage];
while(++currentPage < totalPages)
promises.push(callApi(currentPage));
//wait for all requests to finish
return Promise.all(promises);
})
//write all pages to excel
.then(writePagesToExcel)
.then(() => console.log("work done"));
or you could batch the requests
callApi(1).then(firstPage => {
const batchSize = 16;
let {currentPage, totalPages} = firstPage;
return Promise.resolve([ firstPage ])
.then(writePagesToExcel)
.then(function nextBatch(){
if(currentPage > totalPages) return;
//load a batch of pages in paralell
let batch = [];
for(let i=0; i<batchSize && ++currentPage <= totalPages; ++i){
batch[i] = callApi(currentPage);
}
//when the batch is done ...
return Promise.all(batch)
//... write it to the excel sheet ...
.then(writePagesToExcel)
//... and process the next batch
.then(nextBatch);
});
})
.then(() => console.log("work done"));
But don't forget to add the error handling. Since I'm not sure how you'd want to handle errors with the approaches I've posted, I didn't include the error-handling here.
Edit:
can u pls modify batch requests, getting some error, where you are assigning toalPages it's not right why the totalPages should equal to firstPage
let {currentPage, totalPages} = firstPage;
//is just a shorthand for
let currentPage = firstPage.currentPage, totalPages = firstPage.totalPages;
//what JS version are you targeting?
This first request, callApi(1).then(firstPage => ...) is primarily to determine currentIndex and totalLength, as you provide these properties in the returned JSON. Now that I know these two, I can initiate as many requests in paralell, as I'd want to. And I don't have to wait for any one of them to finish to determine at what index I am, and wether there are more pages to load.
and why you are writing return Promise.resolve([ firstPage ])
To save me some trouble and checking, as I don't know anything about how you'd implement writePagesToExcel.
I return Promise.resolve(...) so I can do .then(writePagesToExcel). This solves me two problems:
I don't have to care wether writePagesToExcel returns sync or a promise and I can always follow up with another .then(...)
I don't need to care wether writePagesToExcel may throw. In case of any Error, it all ends up in the Promise chain, and can be taken care of there.
So ultimately I safe myself a few checks, by simply wrapping firstPage back up in a Promise and continue with .then(...). Considering the amounts of data you're processing here, imo. this ain't too much of an overhead to get rid of some potential pitfalls.
why you are passing array like in resolve
To stay consistent in each example. In this example, I named the function that processes the data writePagesToExcel (plural) wich should indicate that it deals with multiple pages (an array of them); I thought that this would be clear in that context.
Since I still need this seperate call at the beginning to get firstPage, and I didn't want to complicate the logic in nextBatch just to concat this first page with the first batch, I treat [firstPage] as a seperate "batch", write it to excel and continue with nextBatch
function callAPI(pageNo) {
var options = {
url: "http://example.com/getData?pageNo="+pageNo,
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
json: true
}
return request(options)
}
function writeToExcel(res){console.log(res)} //returns promise.
callAPI(1).then(function (res) {
if(res){
writeToExcel(res).then(() => {
var emptyPromise = new Promise(res => setTimeout(res, 0));
while(res && res.currentPage < res.totalPages){
emptyPromise = emptyPromise.then(() => {
return callAPI(res.currentPage).then(function (res){
if(res){
writeToExcel(res)
}
});
}
}
return emptyPromise;
});
}
}).catch(function (err) {
// Handle error here
})