reactjs - I min anpassade React-hook, är det möjligt att skriva en funktion som kan returnera en giltig Firebase-fråga dynamiskt?


För min React-applikation försöker jag skriva en egen hook som ska användas för att göra förfrågningar till en Firestore-databas i Firebase.

Hittills har jag skrivit en hook som använder en fördefinierad fråga som fungerar. Vad jag inte kan lista ut är hur man skriver en funktion som kapabla att dynamiskt generera den frågan.

Här är vad jag har hittills:

import { useState, useCallback } from 'react';
import { initializeApp } from "firebase/app";
import { getFirestore, getDocs, collection, query, where, orderBy, limit } from 'firebase/firestore';

import firebaseConfig from "./firebase.config";

const useQuery = () => {
  
  // NOTE for clarity i am showing initialization of firebase here
  // although this actually happens in another file and is imported.
  const firebaseApp = initializeApp(firebaseConfig);
  const firestore = getFirestore(firebaseApp);

  const firestoreRef = collection(firestore, 'myCollection');
  
  const [snapshot, setSnapshot] = useState([]);

  const getSnapshot = useCallback(async(queryConstraints:any) => {
  
  // instead of this pre-defined query ...
  const firestoreQuery = query(firestoreRef,
    where('someField', '>=', 'someValue'),
    where('anotherField', '==', 'anotherValue'),
    orderBy('timestamp', 'asc'),
    limit(10)
  );
    
  // ... Id like to be able to take the incoming argument
  // titled 'queryConstraints' of which its value
  // would be an object like this one:
      //  {[{
      //   where: 'someField',
      //   operator: '>=',
      //   value: 'someValue'
      //  },{
      //   where: 'anotherField',
      //   operator: '==',
      //   value: 'anotherValue'
      //  }], 10, 'timestamp', 'asc'}

  // ... and pass it to a function, lets call it 'configAValidQuery' which
  // would parse the object and return a valid query to be applied as follows:

  //  const myQuery = await configAValidQuery(queryConstraints);
  //  const firestoreQuery = query(firestoreRef, myQuery);

    try {
      const docs = await getDocs(firestoreQuery);
      if(docs.empty) {
        // TO DO: HANDLE EMPTY RESULT
      } else {
        setSnapshot(doc.data());
      }
    } catch(error:any) {
      // TO DO: HANDLE ERROR
    }

  }, [firestoreRef]);

  return {
    snapshot,
    getSnapshot
  }
}

export default useQuery;

Jag har problem med att lista ut hur man dynamiskt konstruerar det som i princip motsvarar en kommaseparerad lista av funktionsanrop, med tanke på att frågebegränsningarna består av de importerade Firebase-metoderna where, orderBy och limit.

Funktionen jag behöver skulle se ut ungefär som följande, även om detta uppenbarligen inte skulle fungera:

const configAValidQuery = useCallback((conditions:any, limit:number, orderBy:string, direction:string) => {
      let query:any;
      if(conditions.length > 0) {
        conditions.map((item:any) => {
          return(query = query.where(item.where, item.operator, item.value))
        });
        query = query.orderBy(orderBy, direction);
        if(limit > 0) {
          query = query.limit(limit);
        }
        return query;
      } else {
        query = query.orderBy(orderBy, direction);
        if(limit > 0) {
          query = query.limit(limit);
        }
        return query;
      }
  },[]);

Paketet firebase/firestore verkar inte innehålla ett fördefinierat gränssnitt eller en funktion för detta ändamål.

Några förslag?

Svar #1

kanske något liknande detta kan vara trevligt om du använder TypeScript

import {
  query,
  where,
  orderBy,
  limit
} from "firebase/firestore";



type Clause = "where" | "limit" | "orderBy";
type Operator = "==" | "!=" | "<" | ">" | "<=" | ">=";

interface WhereArgs {
  column: string;
  operator: Operator;
  value: string | number;
}

interface OrderByArgs {
  column: string;
  order: "asc" | "desc";
}

interface LimitArgs {
  amount: number;
}

type Command =
  | ({ clause: "where" } & WhereArgs)
  | ({ clause: "orderBy" } & OrderByArgs)
  | ({ clause: "limit" } & LimitArgs);

const options = {
  where: (args: WhereArgs) => {
    const { column, operator, value } = args;
    return where(column, operator, value);
  },
  limit: (args: LimitArgs) => {
    const { amount } = args;
    return limit(amount);
  },
  orderBy: (args: OrderByArgs) => {
    const { column, order } = args;
    return orderBy(column, order);
  }
};

const commands: Array<Extract<Command, { clause: Clause }>> = [
  {
    clause: "where",
    column: "anotherField",
    operator: ">=",
    value: "someValue"
  },
  {
    clause: "where",
    column: "anotherField",
    operator: "==",
    value: "anotherValue"
  },
  {
    clause: "limit",
    amount: 10
  },
  {
    clause: "orderBy",
    column: "timestamp",
    order: "asc"
  }
];

const constraints = [];

for (const command of commands) {
  const { clause, ...rest } = command;
  constraints.push(options[command.clause](rest as any));
}

query(null as any, ...constraints);

här är en länk till det

Svar #2

Firstival 'frågebegränsningar' i din fråga är inte ett objekt, jag vet inte vad det är.

Jag antar att detta objekt ska se ut så här:

const qConstrains = {
        where: [
            { val1: 'fieldName', operator: '>=', val2: 'valueToCompare'},
            { val1: 'anotherfieldName', operator: '>=', val2: 'valueToCompare'}
        ],
        orderBy: { timestamp: 'timestamp', direction: 'asc'},
        limit: 10
    }

Tja objektet ovan är dåligt eftersom frågefunktionen använder QueryConstraints[] och det är en array, inte ett objekt. Det är en array utan objekt men med funktioner, så allt du behöver göra är att skapa en array av objekt så att du kan spara det i databasen och när du använder det konverterar du det till en array av funktioner.

Här är ett exempel på en korrekt array:

const qConstrains = [
        { where: 'fieldName', operator: '>=', value: 'valueToCompare'},
        { where: 'anotherfieldName', operator: '>=', value: 'valueToCompare'},
        { orderBy: 'timestamp', direction: 'asc'},
        { limit: 10 }
    ]

Funktion som konverterar från en array av objekt till en array av funktioner:

function convertArrayObjectsToArrayFunctions(queryConstains: any[]) {
    let newQueryConstains: any[] = []
    for(let item of queryConstains) {
        if(item['where']) newQueryConstains.push(where(item.where, item.operator, item.value)) 
        if(item['orderBy']) newQueryConstains.push(orderBy(item.orderBy, item['direction'] ? item.direction : undefined))
        if(item['limit']) newQueryConstains.push(limit(item.limit))
    }
    return newQueryConstains
}

Exempel på användning:

// Do not forget to use ... operator when creating a query.
const q = query(collection(db,'posts'),
   ...convertArrayObjectsToArrayFunctions(qConstrains))

Hemmauppgift barn! XD funktionen ovan innehåller inte alla funktioner som kan förekomma. Ditt jobb är att lägga till extra if-satser för: 'limitToLast' | 'startAt' | 'startAfter' | 'endAt' | 'endBefore'

Viktigt: Om du får ett fel om index, ctrl+klicka på länken i felet så kommer det att ställa in index för dig i Firestore.