Search code examples
node.jsgoogle-cloud-firestoregoogle-cloud-functionsfirebase-admin

Firestore query after previous queries last document?


I first do a query and get the first 30 "Most Liked" posts and send that to the client, along with the last document of that query.

Next I (try to) query the next 30 documents when the client loads more. I send the last document back and try to query off that, but every attempt either fails or loads the same posts as before.

exports.loadTrendingPosts = functions.https.onCall(async(data, context) => {
   var startData = data.startAt; //either null(first load) or previous last document
   var currentTime = admin.firestore.Timestamp.now();
   var lastDocument;
   var monthYearCompare = '202001';

   if(startData !== null && startData !== undefined){

    //produces the error "Cannot read property 'firestore' of undefined"
    //startData = new admin.firestore.QueryDocumentSnapshot(data.startData);

    //produces the error "Cannot read property 'firestore' of undefined"
    //startData = new admin.firestore.DocumentSnapshot(data.startData);

    var postSnap = await db.collection('posts')
        .where('nextTwelveMonths', 'array-contains', monthYearCompare)
        .orderBy('likes', 'desc')
        .startAfter(startData)
        .limit(30)
        .get();
   }
   else{ //initial load
      postSnap = await db.collection('posts').where('nextTwelveMonths', 'array-contains', monthYearCompare).orderBy('likes', 'desc').limit(30).get();
      lastDocument = postSnap.docs[postSnap.docs.length - 1]

   }
   
  return [postsSnap, lastDocument];
});

startData comes back to the function as JSON/maps. Things I've tried:

-If I try to query starting after startData without any changes, I get no errors, but I get the same 30 posts as the initial query.

-If without changes I try to use startData.data().likes I get the error startData.data is not a function

-If I try to convert the JSON/maps to a DocumentSnapshot or QueryDocumentSnapshot to enable me to use .data() I get the error Cannot read property 'firestore' of undefined.

I'm not able to add a where('likes', '<', lastMinLikes) due to lots of the posts having the same amount of likes.

Any ideas on how I can load the next 30 documents in this situation?

Edit: here is what startData looks like when sending back the document from the client side (via Flutter): (When I startAt using this, I get the same initial 30 documents ~ it makes no difference).

Edit: came up with a solution, see answer below.

{_createTime: {_nanoseconds: 412572000, _seconds: 1613004211}, _fieldsProto: {fileName: {stringValue: image_picker_CDEC8665-93ED-4453-A021-2E2F2B7C86F7-3920-000002602CF620C2.jpg, valueType: stringValue}, comments: {valueType: arrayValue, arrayValue: {values: []}}, nextTwentyFour: {valueType: arrayValue, arrayValue: {values: [{stringValue: 2021011100, valueType: stringValue}, {stringValue: 2021011101, valueType: stringValue}, {stringValue: 2021011102, valueType: stringValue}, {stringValue: 2021011103, valueType: stringValue}, {stringValue: 2021011104, valueType: stringValue}, {stringValue: 2021011105, valueType: stringValue}, {stringValue: 2021011106, valueType: stringValue}, {stringValue: 2021011107, valueType: stringValue}, {stringValue: 2021011108, valueType: stringValue}, {stringValue: 2021011109, valueType: stringValue}, {stringValue: 2021011110, valueType: stringValue}, {stringValue: 2021011111, valueType: stringValue}, {stringValue: 2021011112, valueType: stringValue}, {stringValue: 2021011113, valueType: stringValue}, {stringValue: 2021011114, valueType: stringValue}, {stringValue: 2021011115, valueType: stringValue}, {stringValue: 2021011116, valueType: stringValue}, {stringValue: 2021011117, valueType: stringValue}, {stringValue: 2021011118, valueType: stringValue}, {stringValue: 2021011119, valueType: stringValue}, {stringValue: 2021011120, valueType: stringValue}, {stringValue: 2021011121, valueType: stringValue}, {stringValue: 2021011122, valueType: stringValue}, {stringValue: 2021011123, valueType: stringValue}]}}, nextTwelveMonths: {valueType: arrayValue, arrayValue: {values: [{stringValue: 202102, valueType: stringValue}, {stringValue: 202103, valueType: stringValue}, {stringValue: 202104, valueType: stringValue}, {stringValue: 202105, valueType: stringValue}, {stringValue: 202106, valueType: stringValue}, {stringValue: 202107, valueType: stringValue}, {stringValue: 202108, valueType: stringValue}, {stringValue: 202109, valueType: stringValue}, {stringValue: 202110, valueType: stringValue}, {stringValue: 202111, valueType: stringValue}, {stringValue: 202200, valueType: stringValue}, {stringValue: 202201, valueType: stringValue}]}}, campaignID: {stringValue: kLgdsaYbuqdf1w5UXRp9, valueType: stringValue}, dealID: {stringValue: 0b8ImRRtnKcsGWrTJ5rI, valueType: stringValue}, nextThirtyOneDays: {valueType: arrayValue, arrayValue: {values: [{stringValue: 20210111, valueType: stringValue}, {stringValue: 20210112, valueType: stringValue}, {stringValue: 20210113, valueType: stringValue}, {stringValue: 20210114, valueType: stringValue}, {stringValue: 20210115, valueType: stringValue}, {stringValue: 20210116, valueType: stringValue}, {stringValue: 20210117, valueType: stringValue}, {stringValue: 20210118, valueType: stringValue}, {stringValue: 20210119, valueType: stringValue}, {stringValue: 20210120, valueType: stringValue}, {stringValue: 20210121, valueType: stringValue}, {stringValue: 20210122, valueType: stringValue}, {stringValue: 20210123, valueType: stringValue}, {stringValue: 20210124, valueType: stringValue}, {stringValue: 20210125, valueType: stringValue}, {stringValue: 20210126, valueType: stringValue}, {stringValue: 20210127, valueType: stringValue}, {stringValue: 20210128, valueType: stringValue}, {stringValue: 20210201, valueType: stringValue}, {stringValue: 20210202, valueType: stringValue}, {stringValue: 20210203, valueType: stringValue}, {stringValue: 20210204, valueType: stringValue}, {stringValue: 20210205, valueType: stringValue}, {stringValue: 20210206, valueType: stringValue}, {stringValue: 20210207, valueType: stringValue}, {stringValue: 20210208, valueType: stringValue}, {stringValue: 20210209, valueType: stringValue}, {stringValue: 20210210, valueType: stringValue}, {stringValue: 20210211, valueType: stringValue}, {stringValue: 20210212, valueType: stringValue}, {stringValue: 20210213, valueType: stringValue}]}}, caption: {stringValue: Nails all done , valueType: stringValue}, userCreatorsName: {stringValue: John Onewick, valueType: stringValue}, commentCount: {valueType: integerValue, integerValue: 0}, usersCreatorsID: {stringValue: xaLbvMoFlzQgU0w1UeZqvWiVHy92, valueType: stringValue}, size: {valueType: integerValue, integerValue: 1}, usersName: {stringValue: Four Loko, valueType: stringValue}, usersID: {stringValue: C1oiRCuj6OQOF0aXgaAlzSKOe2y2, valueType: stringValue}, campaignName: {stringValue: Epc Gamer Moments, valueType: stringValue}, nextSevenDays: {valueType: arrayValue, arrayValue: {values: [{stringValue: 20210111, valueType: stringValue}, {stringValue: 20210112, valueType: stringValue}, {stringValue: 20210113, valueType: stringValue}, {stringValue: 20210114, valueType: stringValue}, {stringValue: 20210115, valueType: stringValue}, {stringValue: 20210116, valueType: stringValue}, {stringValue: 20210117, valueType: stringValue}]}}, fileType: {valueType: integerValue, integerValue: 0}, timestamp: {timestampValue: {seconds: 1613004211, nanos: 278000000}, valueType: timestampValue}, likes: {valueType: integerValue, integerValue: 0}}, _ref: {_firestore: {_clientPool: {terminateDeferred: {resolve: {}, reject: {}, promise: {}}, clientFactory: {}, clientDestructor: {}, maxIdleClients: 1, concurrentOperationLimit: 100, activeClients: {}, failedClients: {}, terminated: false}, _settings: {firebaseVersion: 9.4.2, libName: gccl, projectId: removeForStackOF-XXX, libVersion: 4.8.1 fire/9.4.2}, _settingsFrozen: true, _serializer: {allowUndefined: false, createReference: {}, createInteger: {}}, bulkWritersCount: 0, registeredListenersCount: 0, _backoffSettings: {backoffFactor: 1.3, maxDelayMs: 60000, initialDelayMs: 100}, _projectId: removeForStackOF-XXX}, _converter: {toFirestore: {}, fromFirestore: {}}, _path: {databaseId: (default), projectId: removeForStackOF-XXX, segments: [posts, 0b8ImRRtnKcsGWrTJ5rI]}}, _serializer: {allowUndefined: false, createReference: {}, createInteger: {}}, _readTime: {_nanoseconds: 522978000, _seconds: 1613095062}, _updateTime: {_nanoseconds: 412572000, _seconds: 1613004211}}

Solution

  • I suspect that the data coming in from startData which is being used for startAfter is not well formed. I created a sample collection '0' With the following entities.

    ID    n
    --------
    a     3
    b     1
    b2    1
    c     2
    

    The below code demonstrates if start after 'b' is specified the result is as expected.

    const Firestore = require('@google-cloud/firestore');
    const db = new Firestore()
    
    let query = db.collection('0');
    
    db.collection('0').doc('b').get().then(b => {
    
        query.orderBy('n').startAfter(b).get().then(snapshot => {
            snapshot.forEach(document => {
                console.log(document.id + " " + document.get('n'));
            });
        });
    
    });
    

    This returns

    b2 1
    c 2
    a 3
    

    If you don't have access to the complete object, you can order by id in addition to likes in your case, and then start after the id and the likes.

    for example:

    const lastRef = db.collection('0').doc('b');
    const lastValue = "1"
    query.orderBy('n').orderBy(Firestore.FieldPath.documentId()).startAfter(lastValue, lastRef).get().then(snapshot => {
        snapshot.forEach(document => {
            console.log(document.id + " " + document.get('n'));
        });
    });