Refactoring FireBase query functions for different conditions

Hello Developers,

I’m currently developing an app that utilizes Firebase for database operations. In this application, I retrieve data using queries that are determined by various filter and ordering conditions set by the users. There are numerous conditions representing all possible search queries within the app.

To enhance the codebase, I’m seeking to create a function that dynamically checks these conditions and generates queries based on the user-defined criteria. Instead of having separate functions for each specific condition, I’m looking for a more efficient and DRY (Don’t Repeat Yourself) approach, where a single function can dynamically construct queries based on varying conditions.

I’ll provide snippets of my code where I’ve previously created dedicated functions for specific conditions. I’d appreciate your insights and suggestions on how to refactor this code, making it more modular and maintainable by developing a function that dynamically generates queries based on the given conditions.

Thank you for your assistance!

async function getPageinatedDocsInDb(offset, catId, priceMin, priceMax, orderFilter){
const catIdNum = catId?parseInt(catId):null
const priceMinNum = priceMin?parseInt(priceMin):0
const priceMaxNum = priceMax?parseInt(priceMax):2000
let tempArr =

  const productsRef = collection(db, "all-products")

  function allProductsQuery(x){
    // all products 
    return query(
      productsRef,
      limit(x)
      )
  }

  function allProductsQueryStartAt(x,y){
    // all products with start after property
    return query(
      productsRef,
      startAfter(y),
      limit(x)
      )
  }

  function allProductsSortPriceAscQuery(x){ 
    // all products price ascending
    return query(
      productsRef,
      orderBy("price"),
      limit(x)
      )
  }

  function allProductsSortPriceAscQueryStartAt(x,y){ 
    // all products price ascending with start after property
    return query(
      productsRef,
      orderBy("price"),
      startAfter(y),
      limit(x)
      )
  }

  function allProductsSortPriceDecQuery(x){
    // all products price decending
    return query(
      productsRef,
      orderBy("price", "desc"),
      limit(x)
      )
  }

  function allProductsSortPriceDecQueryStartAt(x,y){
    // all products price descending with start after property
    return query(
      productsRef,
      orderBy("price", "desc"),
      startAfter(y),
      limit(x)
      )
  }

  function priceQuery(x){ 
    // all products price filter
    return query(
      productsRef,
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      limit(x)
      )
  }

  function priceQueryStartAt(x,y){ 
    // all products price filter
    return query(
      productsRef,
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      StartAt(y),
      limit(x)
      )
  }

  function priceQuerySortPriceAsc(x){ 
    // all products price filter order by price ascending
    return query(
      productsRef,
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      orderBy("price"),
      limit(x)
      )
  }

  function priceQuerySortPriceAscStartAt(x,y){ 
    // all products price filter order by price ascending
    return query(
      productsRef,
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      orderBy("price"),
      startAfter(y),
      limit(x)
      )
  }

  function priceQuerySortPriceDec(x){ 
    // all products price filter order by price descending
    return query(
      productsRef,
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      orderBy("price", "desc"),
      limit(x)
      )
  }

  function priceQuerySortPriceDecStartAt(x,y){ 
    // all products price filter order by price descending
    return query(
      productsRef,
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      orderBy("price", "desc"),
      startAfter(y),
      limit(x)
      )
  }

  function categoryQuery(x){ 
    // all products category filter 
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      limit(x)
      )
  }

  function categoryQueryStartAt(x,y){ 
    // all products category filter with start after property
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      startAfter(y),
      limit(x)
      )
  }

  function categoryQuerySortPriceAsc(x){ 
    // all products category filter sorted by price ascending
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      orderBy("price"),
      limit(x)
      )
  }
  function categoryQuerySortPriceAscStartAt(x,y){ 
    // all products category filter sorted by price ascending with start after property
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      orderBy("price"),
      startAfter(y),
      limit(x)
      )
  }

  function categoryQuerySortPriceDec(x){ 
    // all products category filter sorted by price descending
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      orderBy("price", "desc"),
      limit(x)
      )
  }

  function categoryQuerySortPriceDecStartAt(x,y){ 
    // all products category filter sorted by price descending with start after property
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      orderBy("price", "desc"),
      startAfter(y),
      limit(x)
      )
  }

  function categoryAndPriceQuery(x){ 
    // all products category and price filter 
    console.log('products category and price filter')
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      limit(x)
      )
  }

  function categoryAndPriceQueryStartAt(x,y){ 
    // all products category and price filter with start after property
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      startAfter(y),
      limit(x)
      )
  }

  function categoryAndPriceQuerySortPriceAsc(x){ 
    // all products category and price filter sort by price ascending 
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      orderBy("price"),
      limit(x)
      )
  }
  function categoryAndPriceQuerySortPriceAscStartAt(x,y){ 
    // all products category and price filter sort by price ascending with start at property
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      orderBy("price"),
      startAfter(y),
      limit(x)
      )
  }

  function categoryAndPriceQuerySortPriceDec(x){ 
    // all products category and price filter sort by price decending 
    console.log('all products category and price filter sort by price decending')
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      orderBy("price", "desc"),
      limit(x)
      )
  }

  function categoryAndPriceQuerySortPriceDecStartAt(x,y){ 
    // all products category and price filter sort by price decending with start at property
    console.log('all products category and price filter sort by price decending with start at property')
    return query(
      productsRef,
      where("category.id", "==", catIdNum), 
      where("price", ">=", priceMinNum), 
      where("price", "<=", priceMaxNum),
      orderBy("price", "desc"),
      startAfter(y),
      limit(x)
      )
  }

  if (offset!==0){ 
    // if not first page
    console.log('not first page')

    if(catId&&priceMin&&priceMax&&orderFilter){ // if all filter and sort fields // condition 1
      console.log('all fields')
      if(orderFilter==="dec-price"){ // if sorting by price decending
        console.log('dec price')
        const first = categoryAndPriceQuerySortPriceDec(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = categoryAndPriceQuerySortPriceDecStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
      else if (orderFilter==="asc-price"){ 
        // else if sorting by price ascending
        const first = categoryAndPriceQuerySortPriceAsc(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = categoryAndPriceQuerySortPriceAscStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
    }

    else if(catId&&priceMin&&priceMax){ 
      // if category and price filters //condition 2
      const first = categoryAndPriceQuery(offset)
      const firstPageSnap = await getDocs(first)
      const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
      const next = categoryAndPriceQueryStartAt(9,lastIndexdoc)
      const querySnapshot = await getDocs(next)
      querySnapshot.forEach((doc) => {
        const data = doc.data()
        data.id = doc.id
        tempArr.push(data)
      })
    }

    else if(catId&&orderFilter){
      // if category filter and sorted by price // condition 3
      if(orderFilter==="asc-price"){
        const first = categoryQuerySortPriceAsc(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = categoryQuerySortPriceAscStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
      else if(orderFilter==="dec-price"){
        const first = categoryQuerySortPriceDec(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = categoryQuerySortPriceDecStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
    }

    else if(priceMin&&priceMax&&orderFilter){
      // if filtering by price and sorting by price // condition 4
      if(orderFilter==="asc-price"){
        // if sorting by ascending price
        const first = priceQuerySortPriceAsc(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = priceQuerySortPriceAscStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })            
      }
      else if(orderFilter==="dec-price"){
        //if sorting by decending price
        const first = priceQuerySortPriceDec(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = priceQuerySortPriceDecStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })   
      }
    }

    else if(priceMin&&priceMax){
      // if price filter // condition 5
      const first = priceQuery(offset)
      const firstPageSnap = await getDocs(first)
      const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
      const next = priceQueryStartAt(9,lastIndexdoc)
      const querySnapshot = await getDocs(next)
      querySnapshot.forEach((doc) => {
        const data = doc.data()
        data.id = doc.id
        tempArr.push(data)
      })
    }

    else if (catId){ 
      // if filter by category // condition 6
      const first = categoryQuery(offset)
      const firstPageSnap = await getDocs(first)
      const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
      const next = categoryQueryStartAt(9,lastIndexdoc)
      const querySnapshot = await getDocs(next)
      querySnapshot.forEach((doc) => {
        const data = doc.data()
        data.id = doc.id
        tempArr.push(data)
      })
    }

    else if(orderFilter){
      // if no filter sort by price // condition 7
      if(orderFilter==="asc-price"){
        // if sorting by ascending price
        const first = allProductsSortPriceAscQuery(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = allProductsSortPriceAscQueryStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
      else if(orderFilter==="dec-price"){
        // if sorting by descending price
        const first = allProductsSortPriceDecQuery(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = allProductsSortPriceDecQueryStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
    }

    else {
      // no filters and no sotring condition 8
      const first = allProductsQuery(offset)
      const firstPageSnap = await getDocs(first)
      const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
      const next = allProductsQueryStartAt(9,lastIndexdoc)
      const querySnapshot = await getDocs(next)
      querySnapshot.forEach((doc) => {
        const data = doc.data()
        data.id = doc.id
        tempArr.push(data)
      })
    }

  }

  else{
    console.log('first page')

    if(catId&&priceMin&&priceMax&&orderFilter){ // if all filter and sort fields
      if(orderFilter==="dec-price"){ // if sorting by price decending
        const next = categoryAndPriceQuerySortPriceDec(9)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
      else if (orderFilter==="asc-price"){ // if sorting by price ascending
        const next = categoryAndPriceQuerySortPriceAsc(9)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
    }

    else if(catId&&priceMin&&priceMax){ // if category and price filters
      console.log('category and price filters')
      const next = categoryAndPriceQuery(9)
      const querySnapshot = await getDocs(next)
      querySnapshot.forEach((doc) => {
        const data = doc.data()
        data.id = doc.id
        tempArr.push(data)
      })
    }

    else if(catId&&orderFilter){
      // if category and sorted by price
      if(orderFilter==="asc-price"){
        // if sorting ascending
        const next = categoryQuerySortPriceAsc(9)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
      else if(orderFilter==="dec-price"){
        // if sorting decending
        const next = categoryQuerySortPriceDec(9)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
    }

    else if(priceMin&&priceMax&&orderFilter){
      // if filtering by price and sorting by price
      if(orderFilter==="asc-price"){
        // if sorting by ascending price
        const next = priceQuerySortPriceAsc(9)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })            
      }
      else if(orderFilter==="dec-price"){
        //if sorting by decending price
        const next = priceQuerySortPriceDec(9)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })   
      }
    }

    else if(priceMin&&priceMax){
      // if price filter
      const next = priceQuery(9)
      const querySnapshot = await getDocs(next)
      querySnapshot.forEach((doc) => {
        const data = doc.data()
        data.id = doc.id
        tempArr.push(data)
      })
    }

    else if(catId){
      // if category
      const next = categoryQuery(9)
      const querySnapshot = await getDocs(next)
      querySnapshot.forEach((doc) => {
        const data = doc.data()
        data.id = doc.id
        tempArr.push(data)
      })
    } 

    else if(orderFilter){
      // if no filter sort by price
      if(orderFilter==="asc-price"){
        // if sorting by ascending price
        const next = allProductsSortPriceAscQuery(9)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
      else if(orderFilter==="dec-price"){
        // if sorting by descending price
        const next = allProductsSortPriceDecQuery(9)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
      }
    }

    else {
      // no filters and no sotring
      const next = allProductsQuery(9)
      const querySnapshot = await getDocs(next)
      querySnapshot.forEach((doc) => {
        const data = doc.data()
        data.id = doc.id
        tempArr.push(data)
      })
    }

  }

  return tempArr

}

Hey Ibrahim!

This method you have so far might not actually be too inefficient, though you could DRY up some parts of it but I tried some other ways and the logic still required some amount of code.

I did look into another version where I collected queries with an object approach. Not sure if you’ve used much objects yet but an example is shown below:

async function generateDynamicQuery(options) {
  // this is probably going to be one of the biggest parts of the code 
 // since it's going to store most if not all your inputs
  const { filters, orders, pagination } = options;

Where an input option for pagination would look like this:

const options = {
  pagination: {
    startAfter: y,
    limit: x,
  },
};

The logic (in a non function form) to for your allProductsQueryStartAt would look something like

if (pagination && pagination.startAfter) {
  query = query.startAfter(pagination.startAfter);
}

The approach I had here was to reduce having a lot of functions be in reliance for a parameter and just capture all the known queries/components in one object so then I have one source to refer to when reference these values. I wouldn’t say it’s super better but as your code base scales it would potentially have things be a bit clearer with your variables.

Though, if you find more clarity in terms of logic with your current approach you can place these functions in a seperate file and then import them into your main code base. Not specifically a DRY approach in terms of code but it can help you refactor your code since it’s not all in one file.

I used to do this to help reduce the noise when refactoring my code and many times had one file dedicated to a function which I then refactor later.

Not exactly a full code breakdown but hopefully this gives you some insight.

Also to extend from this, I recommend you utilize ChatGPT and things like https://codeium.com/ (if you’re using an external code editor) to help with refactoring. I recently did a refactor of a codebase and did what I said in my earlier post - clean up my function and code files and then refactored them with help of AI.

Since we’re in the age of AI I got to mention this as a helping hand for you!

Hey Roku,
First of all… Thanks for ur reply and effort in solving this coding problem.
I don’t think the way i wrote this funtion is dry since there’s a piece of code that is being constantly repeated within the function. which is this for example:

        const first = categoryAndPriceQuerySortPriceDec(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = categoryAndPriceQuerySortPriceDecStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })

with the a little adjustments to first and next constants where i feed it different functions based on the condition.

i tried to create a function that takes in other functions as args to feed the first and next constants however it didnt work since those function need to take in other args as well.
its kind of complicated to explain what is happening over their.

regardless, i will continue working on my project for now and maybe ill come back to this problem when im done. might as well rewatch the advanced javascript module by tom to remember working with objects (i remember the orc/witch game app where we worked with objects alot).

Thank you

No worries! I thought that would be the case after looking at the code more. But I think you will be able to refactor this without much trouble if you go over it again and access it. Might not be the best method but I usually just get the program/code to work as intended first and then refactor later in segments. Going over my functions and seeing if I can DRY it up.

Also, sometimes your code might actually get bigger when you refactor but the layout is more dynamic and can adapt to scale. Just another thing to take point since sometimes we get fixated on removing code to DRY things up but end up making it more complicated by abstraction. You would want to keep things as simple and sometimes simple is more code.

Regarding the example code you gave:

// original code
const first = categoryAndPriceQuerySortPriceDec(offset)
        const firstPageSnap = await getDocs(first)
        const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length-1]
        const next = categoryAndPriceQuerySortPriceDecStartAt(9,lastIndexdoc)
        const querySnapshot = await getDocs(next)
        querySnapshot.forEach((doc) => {
          const data = doc.data()
          data.id = doc.id
          tempArr.push(data)
        })
// refactored v1
const fetchPageData = async (offset, pageSize) => {
  const firstQuery = categoryAndPriceQuerySortPriceDec(offset);
  const firstPageSnap = await getDocs(firstQuery);
  
  const lastIndexdoc = firstPageSnap.docs[firstPageSnap.docs.length - 1];
  
  const nextQuery = categoryAndPriceQuerySortPriceDecStartAt(pageSize, lastIndexdoc);
  const querySnapshot = await getDocs(nextQuery);
  
  const tempArr = [];
  querySnapshot.forEach((doc) => {
    const data = doc.data();
    data.id = doc.id;
    tempArr.push(data);
  });

  return tempArr;
};

I created a function called fetchPageData that takes offset and pageSize as parameters. This function encapsulates the logic of fetching data for a page which makes the code more modular and possibly easier to understand. Though I think your original code was already a function and you just didn’t copy the first line of the code? Either way, just an example of how some structuring, spacing, and grouping can help with refactoring.

The refactored version is almost identical to the original but just some minor changes for clarity. Hope this helps!