Creating a paginated chronological query in Firestore - javascript

Hopefully a relatively simple question here with using Firestore querys
I am trying to create essentially a news feed, sorting the newest content from the oldest content. That part is pretty easy, I use:
var first = db.collection("feeds/0/active").orderBy("timestamp", "desc").limit(3);
Which retrieves the 3 newest entries in the news feed. My then idea is in the next query, to pull down the next 3 items in the feed. So if we are going by age, 4,5,6 in the collection in terms of how new the items are.
To do this I grab the last item in query one, and use that node's timestamp as my start at value in query 2:
var first = db.collection("feeds/0/active").orderBy("timestamp", "desc").limit(3);
first.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
var next = db.collection("feeds/0/active").orderBy("timestamp", "desc").startAt(lastVisible.data().timestamp).limit(3);
next.get().then(function(docSn){
console.log("SECOND QUERY!")
docSn.forEach(function(doc) {
console.log(doc.data().message)
})
})
The Result of this code returns the same as the first query returns, the nodes 1, 2, 3 despite trying to tell the second query to start at node3
I also tried passing in a javascript object instead:
var datevalue = Date.parse(lastVisible.data().timestamp)
var next = db.collection("feeds/0/active").orderBy("timestamp", "desc").startAt(datevalue).limit(3);
Which also unfortunately did not work.
I also tried passing in the entire snapshot item, and got the error
"Malforormed Calls from JS: field sizes are different.
[[9,38,38,38],[0,0,1,0],
etc.."
Not really clue where to start with this as I have read through the docs and any examples and I could find and can't seem to figure it out. The only other way I can think of implementing this is by using a Cloud Function to number each node upon creation.. but that feels hacky
Any help would be huge! Thanks

I have a collection called "books" with a field called "created" of type Timestamp. This is not a string. This is not a Date. Please see the Timestamp docs. You need to specify in your settings that you want to use the new Timestamp type instead of Date for your timestamp fields. (See docs for firestore settings)
Example of initiating your firestore and set the TimestampsInSnapshots setting:
var firebaseApp = firebase.initializeApp({
apiKey: [APIKEY],
authDomain: [FIREBASEAPPDOMAIN],
projectId: [PROJECTID]
});
var firestore = firebase.firestore();
var settings = { timestampsInSnapshots: true }; // force Timestamp object instead of Date
firestore.settings(settings);
When this is done, i've run the below code and I get the expected results. First query returns three results. Newest first, latest last.
The second query returns the next three items.
var first = db.collection('books').orderBy("created", "desc").limit(3);
console.log("FIRST QUERY!")
first.get().then(function(documentSnapshots) {
documentSnapshots.forEach(function (doc) {
console.log(doc.data().created.toMillis())
});
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length - 1];
var next = db.collection('books').orderBy("created", "desc")
.startAfter(lastVisible.data().created).limit(3);
next.get().then(function (docSn) {
console.log("SECOND QUERY!")
docSn.forEach(function (doc) {
console.log(doc.data().created.toMillis())
});
});
});
The console from the above code:
FIRST QUERY!
1540573716371
1540573676652
1540573668643
SECOND QUERY!
1540573658893
1540573536420
1540536647891

Are you checking the entire list returned? Or, just the first node?
If you use startAt, then the first node in the second list will be the last node in the first list (docs). I suggest using startAfter (docs).
Example:
var db = firebase.firestore()
var query = db.collection('feeds/0/active').orderBy('timestamp', 'desc').limit(3)
query.get().then((firstSnapshot) => {
var first3 = snapshot.docs
query.startAfter(first3[2]).get().then((nextSnapshot) => {
var next3 = snapshot.docs
})
})

Related

Firebase: how to delete range of items inside node based on timestamp keys?

My Firebase database data looks like this:
The numbers are timestamps in seconds (so the data is basically a
time series).
I want to delete all the data OLDER then 5 seconds.
I am using the new Version 9 Modular JavaScript SDK's
So for example if we take that current time is 1637149389 I want to delete all the data according to the picture below:
I tried using this code but the result is that my whole path to the data is deleted (including data not older then 5 seconds):
var timestamp = Math.round(Date.now() / 1000) - 5; //get Timestamp in Mili, convert to Seconds, and then set it 5 seconds to the past
var UID = 'uid1';
const ref = ref(
database,
'device_realtime/' + UID
);
var refQuery = query(
ref,
...[fb.orderByKey(), fb.endAt(timestamp.toString())]
);
remove(refQuery);
I am little confused because when I call get(refQuery) on the same
query my results are correct.
I only see data older then 5 seconds!
Console:
How would I delete only certain range of items based on their timestamp?
To remove a node, you need to know the complete path to that node. Firebase doesn't support update/remove queries, where you pass the root reference and a condition to the database.
So you'll need to call remove() on each individual child node that you want to remove.
Something like:
get(refQuery, (results) => {
results.forEach((snapshot) => {
remove(snapshot.ref);
})
})

How to use Firebase Firestore .push() method in Javascript front end

So let me start by saying Google's Firebase documentation is horrible. I am trying to append child values to a list stored on a document in Firestore db when a user adds an item to their cart on an eCommerce site. Simplest thing ever (or should be).. All of the documentation says to use .push() and there is not one actionable example of how to implement it. This is not enough for me to understand how to use it:
// Create a new post reference with an auto-generated id
var newPostRef = postListRef.push();
newPostRef.set({
// ...
});
Where does postListRef come from and how do I instantiate a reference that works with .push()? I've been flat out guessing for the last 3 days off and on about how to use the function, from getting the database reference to adding a value. Below are 2 of the ways I have tried to use it. This is such a stupidly simple question and it drives me insane that after hours of googling there is absolutely not one complete example anywhere I could find. Please offer a complete explanation with sample code in what may seem to be an unnecessary level of detail to you on how to implement .push() with firestore in a vanilla javascript front end script! Bonus points if you can explain how to remove a single item from the list as well.
Documentation Links:
https://firebase.google.com/docs/database/web/lists-of-data,
https://firebase.google.com/docs/database/admin/save-data
const db = firebase.firestore();
//set with a function that checks to see if user is logged in
var userUid;
//set if the user is not signed in to persist the cart
var guestUid;
function addItemToCart() {
console.log("Add item to cart triggered..");
const productName = "Assorted Oranges";
const productId = "123";
if (userUid != null) {
var testRef = firebase.firestore().collection('Carts').doc(userUid).push();
testRef.set({
productName: productName,
productId: productId });
} else {
guestUid = getUUID();
var testRef = db.push({
productName: productName,
productId: productId
});
}
}
No matter how I try to use it I keep getting a variation of the error: .push() is not a function like:
product-single:126 Uncaught TypeError: firebase.firestore(...).collection(...).doc(...).push is not a function
at addItemToCart (product-single:126)
The push() method is part of the Firebase Realtime Database API, not of the Firestore API. While both databases are part of Firebase, they each have their own API and are not compatible.
To add a document to a Firestore collection, you'd do something like:
var testRef = firebase.firestore().collection('Carts').doc(userUid);
testRef.set({
productName: productName,
productId: productId
});

Firestore startAfter() returning the same data in infinite scrolling when ordered by descending timestamp

I'm writing a profile page with chronological user posts (latest post on top) using Firestore on Ionic by setting orderBy() to "timestamp" descending. I'm using Ionic's infinite loading to load more posts when the user reaches the bottom, but the result is that Firestore loads the exact same posts over and over again. Please help!
Hi!
Sorry if this is a beginner question, but I've been wrapping my head around this for sever hours to no avail. The pagination works properly when in ascending order, but loads the same posts when in descending order. I've looked at trying the following alternatives, but they would not be cost-efficient:
Change the limit when user reaches the bottom: this would lead to reading all the posts again and again
Do it in ascending order but reverse the array: would defeat the purpose of pagination
use a query (where) to grab the documents before x timestamp: works in theory, but is a bit hacky and I would really want to know how startAfter() works since it's already there.
.
'''
READ_posts__profile(uid,limit, start) : Promise<[]>
{
return new Promise((resolve, reject) =>
{
if (start == null)
{
resolve();
}
else if(start == 'head')
{
start = new Date();
}
console.log(start);
this._DB.collection(this.ref.posts + uid + '/userposts' )
.orderBy("timestamp", 'desc').startAfter(start).limit(limit)
.get()
.then((querySnapshot) =>
{
let obj : any = [];
console.log(obj);
querySnapshot
.forEach((doc : any) =>
{
obj.push({
id : doc.id,
content : doc.data().content,
likes : doc.data().likes,
timestamp : doc.data().timestamp,
comments : doc.data().comments
});
});
resolve(obj);
})
.catch((error : any) =>
{
reject(error);
});
});
}
'''
Expected result: Infinite scrolling of posts from latest to oldest
Result: Infinite scrolling of first x posts again and again (limit is x)
Thank you for taking the time to read. Hope to hear from you guys soon!
UPDATE FOR FUTURE REFERENCE:
Rather than using the doc itself in .startAfter(), it worked with using doc.timestamp like so:
this._DB.collection(this.ref.posts + uid + '/userposts' )
.orderBy("timestamp", "desc").startAfter(start.timestamp).limit(this.limit)
.get()
.then(querySnapshot =>
{
In my case I added the query like this
query.startAfter(lastId)
where it should have been
query = query.startAfter(lastId)
The problem is that startAfter() is expecting a query cursor for the parameter, not the timestamp that you are passing.
A query cursor identifies the document:
Paginate data with query cursors | Firebase
What you need to do is save the doc to a variable at the class level and then pass that in with the next one:
// in the class variables at the top
latestEntry: any;
// then save a reference to it
this.latestEntry = data[data.length - 1].doc;
// Now you can use the latestEntry to query with startAfter
.startAfter(this.latestEntry)
This is the general theory. When I got this working myself in an app I used the code in this answer.
It's going to need quite a lot of rewriting of your code to do this, which is beyond the scope of the time I have right now, but hopefully this will help you resolve it yourself.
The problem is, that you have to match your last query order:
The order of the field values must ""match the order of the order by clauses of the query""
so if you first make a query like this
collectionReference
.whereEqualTo("id", someId)
.orderBy("timestamp", Query.Direction.DESCENDING)
.limit(5)
.get()
.addOnSuccessListener { documentSnapshots ->
//do some stuff
}
Then your next query should be like:
collectionReference
.whereEqualTo("id", someId)
.orderBy("timestamp", Query.Direction.DESCENDING) //FIELD
.limit(5)
.startAfter(lastVisible.get("timestamp"))
.get()
.addOnSuccessListener { documentSnapshots ->
//do some stuff
}
So you see that startAfter({somecriteria}) has to match the orderBy({somecriteria}) of your first query where you got your last doc visible from, otherwise it will return your first query again.
The query should work if you make sure that the data type are the same between timestamp in firestore and variable start
I understand the OP uses Javascript SDK, I think I find a workaround for this issue. unfortunately currently I am using Flutter, but I think you can tweak it easily.
so the idea is to convert the Date or DateTime object from the language (Dart, Javascript, Kotlin etc) to Timestamp object from the Firestore library. and then pass that Timestamp object to startAfter
in Flutter, you can do it like this
import 'package:cloud_firestore/cloud_firestore.dart'; // to import Timestamp class from Firestore
var lastCreatedAtDateTime = yourObject.createdAt; // this is Date Time Object from your language
var firestoreTimestamp = Timestamp.fromDate(lastCreatedAtDateTime); // convert Date to be Firestore's timestamp
query = query.startAfter([firestoreTimestamp)]);

Write pre-determined node id when pushing to Firebase array

This question has been asked before, but none of the solutions provided seem to help my problem:
I have an array of data on Firebase, that I loop through to generate content in my app, using a "for" loop and therefore an index value. This works fine with the predetermined sets of data, as the IDs are simple array numerics but when a user adds some new data, which is added to the array with push(), firebase creates a unique node key (such as -KyWRU7RRCE_V1w_OiZx) for the new set of data which for some reason prevents my loop from working (no errors shown in console).
If I go to firebase and manually ensure that the new set of data has a numeric value, loop starts working again. How do I make it so that when the user pushes new data to the array, the key being generated is numeric? I tried this:
// Push new routine to Firebase.
var firebaseRef = firebase.database().ref();
var createdExercise = JSON.parse(localStorage.newRoutine);
$("#save").click(function()
{
firebaseRef.child("workouts").key(some_indexed_value).push(createdExercise);
});
but the console returns the following error: Uncaught TypeError: firebaseRef.child(...).key is not a function.
In case it is useful, here is the loop I am using:
// Initialize Firebase.
firebase.initializeApp(config);
// Reference data.
var dbRef = firebase.database().ref().child("workouts");
// Sync with Firebase in real time.
dbRef.on("value", snap =>
{
var workouts = snap.val();
for (var i = 0; i < workouts.length; i++)
{
var routine = workouts[i].title;
var time = 3;
var exercises = workouts[i].exercises;
var icons;
$("#cardList").append(createCards(routine));
}
});
And a pic of my JSON data on Firebase:
Answer to your question is using .set() instead of .push() like this
firebaseRef.child("workouts").child(some_indexed_value).set(createdExercise);
this will create a node under workouts with your custom key.
but it is recommended to use push keys and use forEach() method to get the result than using for loop

How to delete mongodb collections based on their age or created date?

I have a bunch of mongodb collections as the below and they are created on the different date and use the date as their names:
2015-06-01
2015-06-02
2015-06-03
...
2015-06-30
total 30 collections
Let's say I ONLY want to keep the latest 10 collections today and today is 2015-06-30. Based on their names, you should know I only want to keep
2015-06-21
2015-06-22
...
2015-06-30
10 collections
Then tomorrow is 2015-07-01, I still want to keep the latest 10 collections and at the same time, 2015-07-01 collection is created.
So I have to delete 2015-06-21 to keep the total collection number equals to 10.
Then I will repeat this process every day to keep total 10 days collections in the mongodb.
How to achieve that using Mongoose API and Node.JS (JavaScript)?
setInterval and db.dropCollection will be used, I assume.
So presuming you have a list of collections in your database by your naming of the date like so:
[ '2015-01-01',
'2015-01-02',
'2015-01-03',
'2015-01-04',
'2015-01-05',
'2015-01-06',
'2015-01-07',
'2015-01-08',
'2015-01-09',
'2015-01-10',
'2015-01-11',
'2015-01-12' ]
Then if you want to remove all but the last 10 collections that are older than "today" then you would take this approach:
var async = require('async'),
crontab = require('node-crontab'),
mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/crontest');
var conn = mongoose.connection;
conn.once("open",function() {
var jobId = crontab.scheduleJob("0 0 * * *",function() {
var today = new Date().toISOString().substr(0,10);
var db = conn.db;
db.listCollections({ "name": { "$lt": today } })
.toArray(function(err,items) {
var names = items.map(function(item) {
return item.name;
})
async.eachLimit(
names.slice(0,names.length-10),10,function(name,callback) {
db.dropCollection(name,callback);
},function(err) {
if (err) throw err;
console.log('done');
}
);
});
});
});
I'm just putting the whole thing in node-crontab but shedule however you want.
The first thing you need to do is grab a handle to underlying native Db object from your mongoose connection:
var conn = mongoose.connection;
{ ... }
var db = conn.db;
And make sure that the connection is "actually open" when you run. It will be if other "mongoose methods" on your models have actually invoked, but really you should be sure, hence:
conn.once("open",function() {
// everything in here
});
Then it's simply just a matter of getting the ISOString for todays date in order to apply to the "filter" query called on .listCollections() which is going to return your collection names ( and other properties ) based on that filter. You always don't want to remove the current day.
Then it's just a matter of "slicing" that array in order to get only those collections that are not in the last 10 items. So slice with .length-10 to get the position to get from the starting index of the array.
Then just pass those into a loop, using async.eachLimit because the .dropCollection() call is async, also to "limit" how many tasks to run in parallel.
Once those are done then the job is complete and the collections are removed. Also there is no point in "creating" a collection ( add if you want ) as MongoDB will just do that as soon as you write something to your new name.

Categories