1

I want to query firebase like so but it doesn't allow me. Is there a way to dynamically query firebase? I need to handle very specific cases - for example if the user selects "Doesn't Matter" then I need to remove that preference filter from the query, some of the fields are array datatypes and some are strings and some are numbers. I need to be able to handle all forms of datatypes and populate the where clause appropriately. Manually checking each field and calling out for each field will be get taxing on the system (there are over 20 fields to query on). Any advise would be great!

for(ddp in docDataPreferences){
  if(docDataPreferences[ddp] == "Doesn't Matter"){
    allDoesntMatterPreferences.push(ddp);
  } else if(docDataPreferences[ddp] != "Doesn't Matter" && docDataPreferences[ddp] != '' && docDataPreferences[ddp] != undefined) {
    keysWithValues.push(ddp);
    whereClause += ".where('preferences."+ ddp + "', 'array-contains-any', " + JSON.stringify(docDataPreferences[ddp]) +")"
  }
}

var usersMatchesCollection = config.db.collection("Users");
var query = usersMatchesCollection + whereClause;
await query.get().then( function (matchesQuerySnapshot) {
  matchesQuerySnapshot.forEach(function(doc) {
      //...do something
  })
})

TypeError: query.get is not a function. (In 'query.get()', 'query.get' is undefined)]

I can see that the where clause is printing out correctly but I assume you can't concat the object and the string. When I manually add the returned whereClause like so:

var query = usersMatchesCollection.where('preferences.datingPrefDistance', '==', 22).where('preferences.datingPrefDrinking', '==', "No").where('preferences.datingPrefEducation', '==', "Doctorate").where('preferences.datingPrefKids', '==', "No").where('preferences.datingPrefMarried', '==', "No").where('preferences.datingPrefSmokingCig', '==', "No").where('preferences.datingPrefSmokingPot', '==', "No").where('preferences.prefDrinking', '==', "No").where('preferences.prefEducation', '==', "Doctorate").where('preferences.prefIdentifyAs', '==', "Male").where('preferences.prefKids', '==', "No").where('preferences.prefMarried', '==', "No").where('preferences.prefSmokingPot', '==', "No")

it works. So it is the concatenation that is causing an issue.

Is there a work around?

Olivia
  • 1,843
  • 3
  • 27
  • 48
  • 1
    I'm not sure I understand what the problem is with the code you shared. Can you explain what doesn't work when you run this code? – Frank van Puffelen Feb 09 '20 at 15:42
  • @FrankvanPuffelen sorry I should have put the error I am getting in the post. I am getting back "TypeError: query.get is not a function. (In 'query.get()', 'query.get' is undefined)]" . I have updated the post as well. I assume the issue is that I can't concat the whereClase and the usersMatchesCollection. So my problem is - how am I able to do a dynamic Firebase query if I can't concatenate? – Olivia Feb 09 '20 at 16:28

2 Answers2

3

It seems that you're trying to construct a query by concatenating a CollectionReference and a string, which won't work. If you console.log(query), you'll see that it's a string, instead of a Query, and since there's no get() method on a string, that explains the error you get.

To build a query with dynamic condition, you should follow this pattern:

var usersMatchesCollection = config.db.collection("Users");
var query = usersMatchesCollection;

for(ddp in docDataPreferences){
  if(docDataPreferences[ddp] == "Doesn't Matter"){
    allDoesntMatterPreferences.push(ddp);
  } else if(docDataPreferences[ddp] != "Doesn't Matter" && docDataPreferences[ddp] != '' && docDataPreferences[ddp] != undefined) {
    keysWithValues.push(ddp);
    query = query.where('preferences.'+ ddp, 'array-contains-any', docDataPreferences[ddp]))
  }
}

await query.get().then( function (matchesQuerySnapshot) {
  matchesQuerySnapshot.forEach(function(doc) {
      //...do something
  })
})

The crux here is that query = query.where keeps changing replacing the query with another query for each condition, adding the new condition. Then, once all conditions are processed, you can execute the final query.

I tried to ensure the code matches yours, but there might be a syntax error or two in that line as I had to guess what docDataPreferences contains.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • I am not seeing how this will allow me to create the appropriate query string as (like you mentioned) it is going to overwrite the query before it. I want to query all of the fields once so I am not calling out 20 times. – Olivia Feb 09 '20 at 16:42
  • You're not building a query **string**, but a single query with multiple conditions. If you can't see how it works from reading the code, I recommend trying it. Step through the code in a debugger, and see what happens to `query` before, during, and after the loop. – Frank van Puffelen Feb 09 '20 at 18:09
  • you aren't adding on to "query" you are overwriting it. so the only query that will run is going to be the last item in docDataPreferences. Query needs to be compiled like so: `var query = usersMatchesCollection.where('preferences.datingPrefDistance', '==', 22).where('preferences.datingPrefDrinking', '==', "No").where('preferences.datingPrefEducation', '==', "Doctorate")...` unless I am totally loosing it. – Olivia Feb 10 '20 at 00:03
  • 1
    Queries in Firestore follow a builder pattern. Each time you call `where` it returns a new query with all the condition of the previous query and the new condition added to it. Please try it, as that is best way to see that this approach works. – Frank van Puffelen Feb 10 '20 at 00:22
  • thank you I was not able to find any information on the builder pattern, this worked! – Olivia Feb 16 '20 at 14:05
3

Olivia, I use a variation of Frank's pattern constantly - I pass in an array where each entry is an object {fieldref: string, opstr: string, value: value}. My code .reduce's through the array, passing the resulting extended query, like so:

/********************************************************
 * @function collectRecordsByFilter returns an array of documents from Firestore
 * 
 * @param table a properly formatted string representing the requested collection
 * - always an ODD number of elements
 * @param filterArray an array of argument object, each of the form:
 *        {
 *          fieldRef: string, // field name, or dotted field name
 *          opstr: string // operation string representation
 *          value: value // value for query operation
 *        }
 * @param ref (optional) allows "table" parameter to reference a sub-collection
 * of an existing document reference (I use a LOT of structered collections)
 * 
 * The array is assumed to be sorted in the correct order -
 * i.e. filterArray[0] is added first; filterArray[length-1] last
 * returns data as an array of objects (not dissimilar to Redux State objects)
 * with both the documentID and documentReference added as fields.
 */
export const collectRecordsByFilter = (table, filterArray, ref = null) => {
  const db = ref ? ref : firebase.firestore();

  //assumes filterArray is in processing order
  let query = db.collection(table);
  query = filterArray.reduce((accQuery, filter) => {
    return accQuery.where(filter.fieldRef, filter.opStr, filter.value);
  }, query);
  return query
    .get() //get the resulting filtered query results
    .then(querySnapshot => {
      return Promise.resolve(
        querySnapshot.docs.map(doc => {
          return {
            ...doc.data(),
            Id: doc.id,
            ref: doc.ref
          };
        })
      );
    })
    .catch(err => {
      return Promise.reject(err + ":collectRecordsByFilter");
    });
};

The key is that "query" is not a method, but a query object that has a method "where" which effectively APPENDS the new condition to the query object - it is not over-written, but extended.

in use:

export const fetchTourByDateAndArtist = (dateString, artist) => {
  //for each artist, only *one* tour bridges any particular data (except the default)
  const filterArray = [
    { fieldRef: "dateStart", opStr: "<=", value: dateString }
  ];
  return collectRecordsByFilter("Tours", filterArray, artist.ref).then(
    tours => {
      return Promise.resolve(
        tours.find(tour => {
          return moment(tour.dateEnd).isSameOrAfter(dateString);
        })
      );
    }
  );
};

This approach abstracts the Firestore for me, and allows a lot of re-use.

Tracy Hall (LeadDreamer)

LeadDreamer
  • 3,303
  • 2
  • 15
  • 18