I'm working on one angular project and storing few records in index db using idb npm package and updating records on edit and save the item in the index db. But this updated value is displaying in the UI only on page reload or after refreshing the index db manually. Attaching screenshot of index db after update.
Code for update index db item : a) get the item b) save the updated item
/* for getting the item that needs to update */
updateItem(target, value: any): Observable<any> {
return this.dbConnection$.pipe(
map(db => {
let updatedObj = {};
const tx = db.transaction(target, "readwrite");
this.getItem(target, value.id).subscribe((res: any) => {
updatedObj = { ...res, ...value };
this.saveItem(target, updatedObj).subscribe();
});
return updatedObj;
})
);}
/* Get the idb item */
getItem<T>(target, value: string | number): Observable<T> {
return this.dbConnection$.pipe(
switchMap(db => {
const tx = db.transaction(target, "readwrite");
const store = tx.objectStore(target);
return from(store.get(value));
})
);}
/* For saving updated item */
saveItem(target, value) {
return this.dbConnection$.pipe(
map(db => {
const tx = db.transaction(target, "readwrite");
const store = tx.objectStore(target);
store.put(value);
return value;
})
);}
How can I refresh the index db programmatically?
Related
I have a collection of products stored in Firestore, each of which has a subcollection of product prices (the documents of this collection are generated through the Stripe for Firebase plugin, as an aside).
I am attempting to retrieve this list of products through a Vuex action, like so:
export default createStore({
state: {
productList: [],
},
mutations: {
setProductList(state, value) {
state.productList = value;
},
},
actions: {
async retrieveProducts({ commit }) {
let productList = []; //init empty list
//query all active products
const q = query(collection(db, "products"), where("active", "==", true));
const querySnapshot = await getDocs(q); //get product documents
querySnapshot.forEach(async (doc) => {
// for each document, map data in a useful way to productList
let product = doc.data();
product.id = doc.id;
product.priceList = []; // init empty pricelist
const priceSnap = collection(db, "products/", doc.id, "/prices"); //prepare to get all documents in the price subcollection
let priceDocs = await getDocs(priceSnap); // retrieve the pricelist
priceDocs.forEach((doc) => {
// format each document's data
let price = doc.data();
price.id = doc.id;
// add price data to product PriceList
product.priceList.push(price);
});
productList.push(product);
// add finished product object to product List
});
commit("setProductList", productList); // send productList to requester
console.log("Product List: ", productList); //verify productList integrity
},
},
getters: {
getProductList(state) {
return state.productList;
},
},
});
When the action is run, the final console.log shows the correctly formatted productList variable. However, on the Vue side, where this request takes place, it returns with an empty Array.
The request looks like this:
async mounted() {
await this.$store.dispatch({
type: "retrieveProducts",
});
this.pList = this.$store.getters.getProductList;
console.log("pList length: ", this.pList.length)
console.log("Pricing.Vue: ",this.pList)
if (this.pList.length > 0) {
console.log("great success");
} else {
console.log("error loading product list.");
}
}
The especially confusing part is that if I omit the second getDocs and not query the sub-collection, this code functions correctly.
I'm at a total loss for what is not working correctly here.
I have a Cloud Firestore DB with the following structure:
users
[uid]
name: "Test User"
posts
[id]
content: "Just some test post."
timestamp: (Dec. 22, 2017)
uid: [uid]
There is more data present in the actual DB, the above just illustrates the collection/document/field structure.
I have a view in my web app where I'm displaying posts and would like to display the name of the user who posted. I'm using the below query to fetch the posts:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
const postDocs = docSnaps.docs;
for (let i in postDocs) {
loadedPosts[postDocs[i].id] = postDocs[i].data();
}
});
// Render loadedPosts later
What I want to do is query the user object by the uid stored in the post's uid field, and add the user's name field into the corresponding loadedPosts object. If I was only loading one post at a time this would be no problem, just wait for the query to come back with an object and in the .then() function make another query to the user document, and so on.
However because I'm getting multiple post documents at once, I'm having a hard time figuring out how to map the correct user to the correct post after calling .get() on each post's user/[uid] document due to the asynchronous way they return.
Can anyone think of an elegant solution to this issue?
It seems fairly simple to me:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
db.collection('users').child(doc.data().uid).get().then((userDoc) => {
loadedPosts[doc.id].userName = userDoc.data().name;
});
})
});
If you want to prevent loading a user multiple times, you can cache the user data client side. In that case I'd recommend factoring the user-loading code into a helper function. But it'll be a variation of the above.
I would do 1 user doc call and the needed posts call.
let users = {} ;
let loadedPosts = {};
db.collection('users').get().then((results) => {
results.forEach((doc) => {
users[doc.id] = doc.data();
});
posts = db.collection('posts').orderBy('timestamp', 'desc').limit(3);
posts.get().then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
loadedPosts[doc.id].userName = users[doc.data().uid].name;
});
});
After trying multiple solution I get it done with RXJS combineLatest, take operator. Using map function we can combine result.
Might not be an optimum solution but here its solve your problem.
combineLatest(
this.firestore.collection('Collection1').snapshotChanges(),
this.firestore.collection('Collection2').snapshotChanges(),
//In collection 2 we have document with reference id of collection 1
)
.pipe(
take(1),
).subscribe(
([dataFromCollection1, dataFromCollection2]) => {
this.dataofCollection1 = dataFromCollection1.map((data) => {
return {
id: data.payload.doc.id,
...data.payload.doc.data() as {},
}
as IdataFromCollection1;
});
this.dataofCollection2 = dataFromCollection2.map((data2) => {
return {
id: data2.payload.doc.id,
...data2.payload.doc.data() as {},
}
as IdataFromCollection2;
});
console.log(this.dataofCollection2, 'all feeess');
const mergeDataFromCollection =
this.dataofCollection1.map(itm => ({
payment: [this.dataofCollection2.find((item) => (item.RefId === itm.id))],
...itm
}))
console.log(mergeDataFromCollection, 'all data');
},
my solution as below.
Concept: You know user id you want to get information, in your posts list, you can request user document and save it as promise in your post item. after promise resolve then you get user information.
Note: i do not test below code, but it is simplify version of my code.
let posts: Observable<{}[]>; // you can display in HTML directly with | async tag
this.posts = this.listenPosts()
.map( posts => {
posts.forEach( post => {
post.promise = this.getUserDoc( post.uid )
.then( (doc: DocumentSnapshot) => {
post.userName = doc.data().name;
});
}); // end forEach
return posts;
});
// normally, i keep in provider
listenPosts(): Observable<any> {
let fsPath = 'posts';
return this.afDb.collection( fsPath ).valueChanges();
}
// to get the document according the user uid
getUserDoc( uid: string ): Promise<any> {
let fsPath = 'users/' + uid;
return this.afDb.doc( fsPath ).ref.get();
}
Note: afDb: AngularFirestore it is initialize in constructor (by angularFire lib)
If you want to join observables instead of promises, use combineLatest. Here is an example joining a user document to a post document:
getPosts(): Observable<Post[]> {
let data: any;
return this.afs.collection<Post>('posts').valueChanges().pipe(
switchMap((r: any[]) => {
data = r;
const docs = r.map(
(d: any) => this.afs.doc<any>(`users/${d.user}`).valueChanges()
);
return combineLatest(docs).pipe(
map((arr: any) => arr.reduce((acc: any, cur: any) => [acc].concat(cur)))
);
}),
map((d: any) => {
let i = 0;
return d.map(
(doc: any) => {
const t = { ...data[i], user: doc };
++i;
return t;
}
);
})
);
}
This example joins each document in a collection, but you could simplify this if you wanted to just join one single document to another.
This assumes your post document has a user variable with the userId of the document.
J
I'm using firebase in my react native project.I am saving items and then getting those items in my app. But now I want to edit and delete items based on the key generated by the firebase for each item you can see in the ss.
When I get the data from firebase I'm getting on the items but not the key for each item.So that's why I'm unable to edit or delete items
Kindly help me out on how to do this.
enter image description here
here is my code of add item in the firebase.
export const addMeasurement = params => {
return async dispatch => {
dispatch(measurementLoading());
try {
firebaseService
.database()
.ref('/Measurements')
.push(params)
.then(res => {
measurementAdded(res);
});
} catch (err) {
dispatch(measurementFailed(err));
}
};
};
Here is the code to get items from firebase which was stored early
export const getMeasurements = () => {
return async dispatch => {
dispatch(measurementLoading());
try {
const ref = firebaseService.database().ref('/Measurements');
ref.on('value', snapshot => {
const values = snapshot.val();
if (values !== null) {
const newFreshArr = Object.values(values);
dispatch(measurementSuccess(newFreshArr));
}
});
} catch (err) {
dispatch(measurementFailed(err));
}
};
};
You can get the path of a snapshot like so:
let path = snapshot.ref.path.toString();.
You can then make changes to your Firebase database using that path.
Here’s the documentation on that: https://firebase.google.com/docs/reference/js/firebase.database.DataSnapshot#ref
On page load I'm getting the data of 15 records, after scrolling down I can able to call the API but it's returning empty array.(I have total 40 records in my firestore db).
// Service.ts
getUsers(config: any) {
return this.firestore
.collection(
'users',
ref => ref
.where('country', '==', 'India')
.orderBy('lastlogin')
.startAfter(config.page * config.limit)
.limit(config.limit)
)
.snapshotChanges();
}
// Component
getUsers() {
this.loader = true;
this.userService.getUsers(this.config).subscribe((res: any) => {
this.loader = false;
const data = res.map((e: any) => ({
id: e.payload.doc.id,
...e.payload.doc.data()
}));
this.usersCollection = this.usersCollection.concat(data);
});
// Infinite Scroll
onScroll() {
this.config.page += 1;
this.getUsers();
}
My Solution
Only problem is first time lastVisible have no data, have to set to 0.
getUsers(config: any, lastVisible) {
const { doc } = lastVisible.payload ? lastVisible.payload : { doc: 0}
return this.firestore
.collection(
'users',
ref => ref
.where('country', '==', 'India')
.orderBy('lastlogin')
.startAfter(doc)
.limit(config.limit)
)
.snapshotChanges();
}
// Component
getUsers() {
this.loader = true;
this.userService.getUsers(this.config, this.lastVisible).subscribe((res: any) => {
this.loader = false;
this.lastVisible = res[res.length - 1];
const data = res.map((e: any) => ({
id: e.payload.doc.id,
...e.payload.doc.data()
}));
this.usersCollection = this.usersCollection.concat(data);
});
Firestore pagination does not work based on a numeric offset, but is based on so-called cursors/anchor document. From the documentation on startAfter:
Creates and returns a new Query that starts after the provided document (exclusive). The starting position is relative to the order of the query. The document must contain all of the fields provided in the orderBy of this query.
So your call:
.startAfter(config.page * config.limit)
Tries to start after the document instance config.page * config.limit. Since this is not a document snapshot/reference but a number, it doesn't know what document to start after, so it returns nothing.
Instead of passing a numeric offset, you'll need to remember the last document snapshot of the current results and pass that into startAfter.
I highly recommend reading the documentation on paginating data with query cursors.
Init code:
let dbPormise = null;
const OBJECT_STORE_NAME = 'pages';
const DB_NAME = 'tracking-log';
To initiate an ObjectStore:
dbPromise = idb.open(DB_NAME, 3, upgradeDB => {
upgradeDB.createObjectStore(OBJECT_STORE_NAME, {
autoIncrement: true,
keypath: 'id'
});
});
This is how I generate a blank record in the IndexedDB:
const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite');
tx.objectStore(OBJECT_STORE_NAME).put(
{ id: newBucketID, data: [] });
Now, at a later point, I have some elements that I want to append to the data array for a particular id.
This is how I tried doing it:
const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite');
tx.objectStore(OBJECT_STORE_NAME).put(
{ id: localStorage.getItem("currentBucket"), data: item }
);
Schema
{
data: Array
}
Every item has a unique key generated and provided by me.
However, this doesn't work and returns an error: "Key already exists in the object store."
So, how can I append a value to a field inside a IDB objectt?
Not sure about the error, but regardless of that, the basic way of adding an item would be something like this:
function addItem(db, bucketId, item) {
return new Promise(addItemExecutor.bind(null, db, bucketId, item));
}
function addItemExecutor(db, bucketId, item, resolve, reject) {
// Start a single writable transaction that we will use for two requests. One to
// find the corresponding bucket, and one to update it.
const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite');
// If all requests completed without error, we are done
tx.oncomplete = resolve;
// If any request fails, the operation fails
tx.onerror = event => reject(event.target.error);
const store = tx.objectStore(OBJECT_STORE_NAME);
// Go find the corresponding bucket object to update
const findRequest = store.get(bucketId);
findRequest.onsuccess = findRequestOnsuccess.bind(findRequest, bucketId, item, reject);
}
// React to the resolution of the get request
function findRequestOnsuccess(bucketId, item, reject, event) {
const bucket = event.target.result;
// If no bucket exists for that id then fail
if(!bucket) {
const error = new Error('No bucket found for id ' + bucketId);
reject(error);
return;
}
// Lazily init the data array property
if(!bucket.data) {
bucket.data = [];
}
// Add our item to the data array
bucket.data.push(item);
// Save the bucket object back into the bucket object store, completely replacing
// the bucket that was there before.
const bucketStore = event.target.source;
bucketStore.put(bucket);
}
async function someCallingCodeExampleAvoidingTopLevelAwait() {
const bucketId = localStorage.currentBucket;
const item = {foo:bar};
const db = evilUnreliableGlobalDbVariableFromSomewhereMagicalForeverOpenAssumeInitialized;
try {
await addItem(db, bucketId, item);
} catch(error) {
console.debug(error);
}
// Leave the database connection open for page lifetime
}
Without a reduced example it's difficult to figure out what's going on. The best way to get help is to create a reduced example of the problem, as in, the smallest amount of code needed to recreate the issue you're seeing, then put it on something like jsbin.com or glitch.com so folks only have to click a link to see the error you're seeing.
I wasn't able to recreate the error you're seeing. You have keypath when it should be keyPath, but I don't think that creates the error you're seeing.
Anyway, here's how to modify a record in IDB:
async function main() {
// Set up the database.
const OBJECT_STORE_NAME = 'pages';
const DB_NAME = 'tracking-log';
const db = await idb.open(DB_NAME, 1, upgradeDB => {
upgradeDB.createObjectStore(OBJECT_STORE_NAME, {
autoIncrement: true,
keyPath: 'id'
});
});
// The OP didn't make it clear what this value was, so I'll guess.
const newBucketID = 1;
{
// Create the record.
const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite');
tx.objectStore(OBJECT_STORE_NAME).put({ id: newBucketID, data: ['first value'] });
}
{
const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite');
// Get the record.
const record = await tx.objectStore(OBJECT_STORE_NAME).get(newBucketID);
// Modify it.
record.data.push('second value');
// Put the modified record back.
tx.objectStore(OBJECT_STORE_NAME).put(record);
}
{
// Read the value to confirm everything worked.
const tx = db.transaction(OBJECT_STORE_NAME);
const value = await tx.objectStore(OBJECT_STORE_NAME).get(newBucketID);
console.log(value);
}
}
main();
And here's that example running: https://jsbin.com/dineguq/edit?js,console