2
votes

Promesses JavaScript imbriquées - Récupération des données de Firestore

Je suis coincé avec cette erreur depuis 3 jours et j'ai littéralement tout essayé, essayé de structurer les promesses de 1000 façons mais rien ne semble fonctionner. Peut-être que je perds la «vue d'ensemble», alors j'espère que de nouveaux yeux aideront. Merci d'avoir lu:

J'ai une fonction planifiée en cours d'exécution dans Firebase Cloud Functions. Ce que le code essaie d'accomplir est

  1. vérifier si un document a expiré et le changer en "inactif" >> cette partie fonctionne
  2. si un document a été mis à inactif, je veux voir si j'ai d'autres documents dans la base de données Firestore du même «type». s'il n'y a pas d'autre document du même type, alors je veux supprimer ce type de mes «types» de document.

Dans ma dernière tentative (copiée ci-dessous), je vérifie s'il y a un document dans l'instantané (ce qui signifierait qu'il y a un autre document du même type, donc le document n'a pas à être supprimé). Ensuite, si res! == true, je supprimerais le document.

Le problème est que pour une raison quelconque, res n'est jamais vrai ... peut-être que la promesse "res" se résout avant la promesse "instantané"?

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.scheduledFunction = functions.pubsub
.schedule('0 23 * * *').timeZone('Europe/Madrid')
.onRun( async (context) => {

    const expiredDocs = await admin.firestore().collection('PROMOTIONS_INFO')
    .where('active','==',true)
    .where('expiration', '<=', new Date())
    .get()
    .then(async (snapshot) => {
        await Promise.all(snapshot.docs.map( async (doc) => {
            doc.ref.update({active: false})
            const type = doc.data().business.type
            const id = doc.data().id

            const exists = await admin.firestore().collection('PROMOTIONS_INFO')
                .where('active','==',true)
                .where('business.type','==', type)
                .where('id', '!=', id)
                .limit(1)
                .get()
                .then((snapshot) => {
                    snapshot.docs.map((doc)=>{return true})
                }).then(async (res) => {
                    res===true ? null : 
                    (await admin.firestore().collection('PROMOTIONS_INFO').doc('types')
                    .update('types', admin.firestore.FieldValue.arrayRemove(type)))
                })
            }))
        });
});


1 commentaires

Vous devez return éléments de toutes vos fonctions si vous comptez utiliser les fonctions fléchées de style () => { /* code */ } . Il y a aussi une "pyramide de malheur" qui se développe ici que les promesses et l'async / attendent étaient censés éliminer. expiredDocs si vous pouvez attendre la collection expiredDocs après la fin .get() . Supprimez le .then() . Alors attendez la collection exists . Ensuite, bouclez dessus et exécutez la mise à jour sur chaque valeur.


3 Réponses :


1
votes

Je suppose dans ce cas que res est indéfini et évalue comme faux.

Avant votre .then promise avec le paramètre res , vous avez une promesse .then précédente qui retourne void:

//...
.then((snapshot) => {
    return snapshot.docs.map((doc)=>{return true})
}).then(async (res) => {
    res===true ? null : 
    (await admin.firestore().collection('PROMOTIONS_INFO').doc('types')
    .update('types', admin.firestore.FieldValue.arrayRemove(type)))
})
//...

En fonction de votre intention, vous devrez renvoyer une valeur dans cette promesse précédente. Il semble que vous créez un tableau de valeurs booléennes correspondant à la longueur du nombre de snapshot.docs que vous avez, donc si vous mettez une instruction return simplement dans la clause .then précédente, res serait quelque chose comme, [true, true, true, true, /* ... */]

//...
.then((snapshot) => {
    snapshot.docs.map((doc)=>{return true}) // <--- This is not actually returning a resolved value
}).then(async (res) => {
    res===true ? null : 
    (await admin.firestore().collection('PROMOTIONS_INFO').doc('types')
    .update('types', admin.firestore.FieldValue.arrayRemove(type)))
})
//...


0 commentaires

0
votes

snapshot.docs.map((doc)=>{return true}) renvoie Array comme [true, false] pas booléen comme true .
Donc .then( async (res) => { res===true ? null : await admin.firestore(... ne peut pas fonctionner. Et

Vous devriez peut-être modifier comme suit.

.then((snapshot) => 
    snapshot.docs.length > 0 ? null : 
        await admin.firestore(...


0 commentaires

1
votes

Pour obtenir le résultat souhaité, vous pouvez envisager d'utiliser les écritures par lots et de diviser votre code en étapes distinctes.

Un ensemble d'étapes possibles est:

  1. Obtenez tous les documents expirés qui sont toujours actifs
  2. Aucun document expiré? Résultat du journal et fonction de fin.
  3. Pour chaque document expiré:
    • Mettez-le à jour en inactif
    • Stocker son type pour vérifier plus tard
  4. Pour chaque type à vérifier, vérifiez si un document actif avec ce type existe et si ce n'est pas le cas, stockez ce type pour le supprimer ultérieurement.
  5. Aucun type à supprimer? Résultat du journal et fonction de fin.
  6. Supprimez tous les types qui doivent être supprimés.
  7. Résultat du journal et fonction de fin.

Dans les étapes ci-dessus, l'étape 3 peut utiliser les écritures par lots et l'étape 6 peut utiliser la transformation de champ arrayRemove() qui peut supprimer plusieurs éléments à la fois pour alléger le fardeau de votre base de données.

class MultiBatch {
    constructor(dbRef) {
        this.dbRef = dbRef;
        this.batchOperations = [];
        this.batches = [this.dbRef.batch()];
        this.currentBatch = this.batches[0];
        this.currentBatchOpCount = 0;
        this.committed = false;
    }
    
    /** Used when for basic update operations */
    update(ref, changesObj) {
        if (this.committed) throw new Error('MultiBatch already committed.');
        if (this.currentBatchOpCount + 1 > 500) {
            // operation limit exceeded, start a new batch
            this.currentBatch = this.dbRef.batch();
            this.currentBatchOpCount = 0;
            this.batches.push(this.currentBatch);
        }
        this.currentBatch.update(ref, changesObj);
        this.currentBatchOpCount++;
    }
    
    /** Used when an update contains serverTimestamp, arrayUnion, arrayRemove, increment or decrement (which all need to be counted as 2 operations) */
    transformUpdate(ref, changesObj) {
        if (this.committed) throw new Error('MultiBatch already committed.');
        if (this.currentBatchOpCount + 2 > 500) {
            // operation limit exceeded, start a new batch
            this.currentBatch = this.dbRef.batch();
            this.currentBatchOpCount = 0;
            this.batches.push(this.currentBatch);
        }
        this.currentBatch.update(ref, changesObj);
        this.currentBatchOpCount += 2;
    }
    
    commit() {
        this.committed = true;
        return Promise.all(this.batches.map(batch => batch.commit()));
    }
}

Remarque: la vérification des erreurs est omise et doit être mise en œuvre.

Limites de lot

Si vous prévoyez d'atteindre la limite de 500 opérations par lot, vous pouvez ajouter un wrapper autour des lots afin qu'ils soient automatiquement divisés selon les besoins. Un emballage possible est inclus ici:

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.scheduledFunction = functions.pubsub
.schedule('0 23 * * *').timeZone('Europe/Madrid')
.onRun( async (context) => {
    // get instance of Firestore to use below
    const db = admin.firestore();
    
    // this is reused often, so initialize it once.
    const promotionsInfoColRef = db.collection('PROMOTIONS_INFO');

    // find all documents that are active and have expired.
    const expiredDocsQuerySnapshot = await promotionsInfoColRef
        .where('active','==',true)
        .where('expiration', '<=', new Date())
        .get();

    if (expiredDocsQuerySnapshot.empty) {
        // no expired documents, log the result
        console.log(`No documents have expired recently.`);
        return; // done
    } 
    
    // initialize an object to store all the types to be checked
    // this helps ensure each type is checked only once
    const typesToCheckObj = {};
    
    // initialize a batched write to make changes all at once, rather than call out to Firestore multiple times
    // note: batches are limited to 500 read/write operations in a single batch
    const makeDocsInactiveBatch = db.batch();
    
    // for each snapshot, add their type to typesToCheckObj and update them to inactive
    expiredDocsQuerySnapshot.forEach(doc => {
        const type = doc.get("business.type"); // rather than use data(), parse only the property you need.
        typesToCheckObj[type] = true; // add this type to the ones to check
        makeDocsInactiveBatch.update(doc.ref, { active: false }); // add the "update to inactive" operation to the batch
    });
    
    // update database for all the now inactive documents all at once.
    // we update these documents first, so that the type check are done against actual "active" documents.
    await makeDocsInactiveBatch.commit();
    
    // this is a unique array of the types encountered above
    // this can now be used to check each type ONCE, instead of multiple times
    const typesToCheckArray = Object.keys(typesToCheckObj);
    
    // check each type and return types that have no active promotions
    const typesToRemoveArray = (await Promise.all(
        typesToCheckArray.map((type) => {
            return promotionsInfoColRef
                .where('active','==',true)
                .where('business.type','==', type)
                .limit(1)
                .get()
                .then((querySnapshot) => querySnapshot.empty ? type : null) // if empty, include the type for removal
        })
    ))
    .filter((type) => type !== null); // filter out the null values that represent types that don't need removal
    
    // typesToRemoveArray is now a unique list of strings, containing each type that needs to be removed
    
    if (typesToRemoveArray.length == 0) {
        // no types need removing, log the result
        console.log(`Updated ${expiredDocsQuerySnapshot.size} expired documents to "inactive" and none of the ${typesToCheckArray.length} unique types encountered needed to be removed.`);
        return; // done
    }
    
    // get the types document reference
    const typesDocRef = promotionsInfoColRef.doc('types');

    // use the arrayRemove field transform to remove all the given types at once
    await typesDocRef.update({types: admin.firestore.FieldValue.arrayRemove(...typesToRemoveArray) });

    // log the result
    console.log(`Updated ${expiredDocsQuerySnapshot.size} expired documents to "inactive" and ${typesToRemoveArray.length}/${typesToCheckArray.length} unique types encountered needed to be removed.\n\nThe types removed: ${typesToRemoveArray.sort().join(", ")}`);

Pour l'utiliser, remplacez db.batch() dans le code d'origine par le new MultiBatch(db) . Si une mise à jour dans le lot (comme someBatch.update(ref, { ... }) ) contient une transformation de champ (telle que FieldValue.arrayRemove() ), assurez-vous d'utiliser someMultiBatch.transformUpdate(ref, { ... }) place pour qu'une seule mise à jour soit correctement comptée comme 2 opérations (une lecture et une écriture).


4 commentaires

Ça marche! Je ne saurais trop vous remercier pour votre temps et votre réponse complète. Je pense que, étant donné que nous avons les typesToRemoveArray, peut-être un moyen d'éviter autant de lectures / écritures sur la base de données serait de lire le document Types, de fusionner les deux tableaux dans le code, puis de l'écrire dans la base de données. Cela aurait-il des conséquences sur l'approche multibatch? De plus, avez-vous des considérations concernant la durée de fonctionnement?


@Mireia Vous avez tout à fait raison. En revisitant le code avec un œil neuf aujourd'hui, je me suis souvenu que arrayRemove prend en charge plusieurs éléments à la fois - lorsqu'ils sont passés en tant qu'arguments séparés - ce qui peut être réalisé à l'aide de l'opérateur spread ( ... ). Cela élimine complètement la deuxième opération par lots. En termes de temps d'exécution, le code tel qu'il est ci-dessus est susceptible d'être le plus léger que vous puissiez atteindre. La partie la plus lente consistera à vérifier si les types existent toujours lorsque vous en avez un grand nombre à vérifier - mais vérifier chaque type une fois permet de maintenir ces performances à un niveau bas.


Si vous commencez à avoir des délais d'attente parce que les mises à jour prennent plus de 60 secondes, vous pouvez soit augmenter le délai d' expiration de la fonction jusqu'à 9 minutes, soit l'exécuter plusieurs fois par jour (ce dernier est le meilleur choix). Changez simplement 0 23 * * * en 0 5,11,17,23 * * * pour l'exécuter toutes les 6 heures.


C'est génial! Merci. Par curiosité, savez-vous si l'utilisation de arrayRemove avec plusieurs éléments compte pour 1 lecture et écriture ou 1x élément?