Cloud functions cron job: How to manage the nested collection's data - javascript

I want to trigger the scheduled task to the nested collection data using cloud functions cron job.
What I want to achieve is, if the timestamp value in "limit" field is later than the current time, "status" field will be added with the value "expired" in the nested collection.
The parent collection name is "OrdersUser" and the child collection name is "Orders".
And I want to manage the documents in "Orders" collection.
The scheduled task runs every one minute.
After I deployed, I got an error in the firebase console.
id is not defined.
I thought I could use wild card in the cloud functions, so I used {id} to refer the nested documents. But I couldn't.
I want to query all the data in โ€Ordersโ€ collection.
How can I fix this issue?
const functions = require('firebase-functions')
const admin = require('firebase-admin')
admin.initializeApp()
const db = admin.firestore()
const ref = functions.firestore
exports.statusOrdersUser = functions.runWith( { memory: '2GB' }).pubsub.schedule('* * * * *').onRun((context) => {
// Consistent timestamp
const now = admin.firestore.Timestamp.now();
// Query all documents ready to perform
//id is not defined.
const queryOrdersUser = db.collection('OrdersUser').doc({id}).collection('Orders').where('limit', '<=', now)
return queryOrdersUser.get()
.then(querySnapshot => {
if (querySnapshot.empty) {
return null;
} else {
const promises = []
querySnapshot.forEach(doc => {
promises.push(doc.ref.update({ status: 'expired' }));
});
return Promise.all(promises);
}
});
})

Firestore does not support wildcard operations for document IDs. If you want to query a subcollection, you need to provide the specific ID of the document where it is nested. It's not clear how your function is supposed to know exactly which subcollection to query, since it doesn't receive any arguments or have any context.
If you want to query all of the subcollections named "Order", no matter where they are nested anywhere in your database, you can use a collection group query for that.

Related

How do you create a new Firestore document within a transaction

I'm trying to log changes to a collection of customer records. In order to keep things watertight,the log records should obviously be created within a Firestore transaction. I have no problems using transaction.set to apply the customer document changes, but every such change needs to be accompanied by the creation of a new document within my transactionLogs collection. Here, things are going badly wrong The log documents are identified by a timestamp field and when I run the following code: ...
import { initializeApp } from 'firebase/app';
import {
getFirestore, collection, query, getDoc,
getDocs, where, orderBy, addDoc, doc, updateDoc, deleteDoc, runTransaction
} from 'firebase/firestore';
var firebaseApp = initializeApp(firebaseConfig);
var db = getFirestore(firebaseApp);
// code to read and validate update to customer documents and to call the following asynch function
function performCustomerUpdate(parameters) {
await runTransaction(db, async (transaction) => {
// update Customer document and build a transactionLog object
const newLogRef = collection(db, 'transactionLogs', transactionLog.timeStamp);
await transaction.set(newLogRef, transactionLog);
});
}
... the transaction.set instruction fails saying something like "Invalid collection reference. Collection references must have an odd number of segments, but transactionsLogs/1645451217221 has 2." In this particular instance, 1645451217221 would have been the value of transactionLog.timesStamp.
Does anyone have advice on what is going on here and how to fix the error? My understanding is that transaction.set will create a new record if the supplied reference doesn't exist, so I ought to be on the right lines. But why does Firestore think that I want to create it in a collection called transactionsLogs/1645451217221? How do I get it the create a reference for a document identified by the string '1645451217221' in a collection called 'transactionsLogs'?
If you are specifying the document ID (and not using the auto-generated IDs) then you must use doc() instead of collection() to create a DocumentReference:
const newLogRef = doc(db, 'transactionLogs', transactionLog.timeStamp);
The collection() function is used create a CollectionReference.
Also checkout: Firestore: What's the pattern for adding new data in Web v9?
My understanding is that transaction.set will create a new record if the supplied reference doesn't exist
If the documents exists, the it'll overwrite the existing document.
With regard to the wider question of how you add records within a transaction, the answer is that you would use pretty much the same code as you'd use outside a transaction.
So whereas outside a transaction you would create a new document with a data item as its identifier with the following Web Version 9 code:
const myDocRef = doc(db, "myCollection", myDocId);
await setDoc(myDocRef, myDocData);
Inside a transaction, you use exactly the same pattern with the one exception that setDoc() is replaced by transaction.set() and the whole operation (obviously) is surrounded by a runTransaction(db, async (transaction) => { instruction:
await runTransaction(db, async (transaction) => {
const myDocRef = doc(db, "myCollection", myDocId);
await transaction.set(myDocRef, myDocData);
}).catch((error) => {
alert(`Oops - transaction failed - error is : ${error}`);
});
Similarly, the pattern used to create a document with an automatically-generated id:
const myCollectionRef = collection(db, "myCollection");
const myDocRef = doc(myCollectionRef)
await setDoc(myDocRef, myDocData);
is replaced within a transaction by
const myCollectionRef = collection(db, "myCollection");
const myDocRef = doc(myCollectionRef)
await transaction.set(myDocRef, myDocData);
Note that if you find you need to find the value that's be assigned to your automatically-generated id, this is available as myDocRef.id

Get array of objects from real time data snapshot - Cloud Firestore

I'm trying to fetch real time data from Cloud Firestore using the below code.
export const getRealTimeData = () =>
db
.collection('posts')
.onSnapshot(
(querySnapshot) => {
const posts: any = [];
querySnapshot.forEach((doc) =>
posts.push(Object.assign({
id: doc.id
}, doc.data()))
);
},
);
};
And, I want to use the resultant array to display the data on UI. When I'm doing this, the resultant array is a function but not the actual array of data.
const posts = getRealTimeData();
Here's what I get when I log posts
function () {
i.kT(), o.al(s);
}
Could anyone please point where I went wrong?
Realtime listeners added with onSnapshot() are not compatible with returning values from function calls. That's because they continue to generate new results over time, and would never really "return" anything once. You should abandon the idea of making a synhronous getter type function in this case - they just don't work for what you're trying to do.
Ideally, you would use an architecture like Redux to manage the updates as they become available. Your realtime listener would dispatch query updates to a store, and your component would subscribe to that store that to receive those updates.
If you don't want to use Redux (which is too bad - you really should for this sort of thing), then you should wrap your query inside a useEffect hook, then have your listener set a state hook variable so your component can receive the updates.

Firebase realtime database get parent node value .val()

I have the below code that looks for a specific update in my /users object tree.
This works fine, but seems a bit redundant, since I already have a reference to a child of the user node that I want.
exports.onUserKeyHelloCreate = functions
.region('europe-west1')
.database
.ref('users/{userId}/hello')
.onCreate(async (snapshot, context) => {
const userId = context.params.userId
let userData = null
await database.ref(`users/${userId}`).once('value', (userSnapshot)=>{
userData = userSnapshot.val()
})
console.log('userData', userData)
Is there a more elegant way to access the entire user node that does not require an extra roundtrip to .once('value')?
Kind regards /K
If you need the entire contents of your database at "users/${userId}", what you're doing now is really the only way to do it.
Though you shouldn't really combine use of the callback along with the promise returned by once(). It's sufficient to just use the promise.
const snapshot = await database.ref(`users/${userId}`).once('value');
const userData = snapshot.val();
console.log('userData', userData)

How to get the last document from a VueFire query

Getting frustrated to solve this since I am no JS expert. ๐Ÿ˜ข
I am using Firestore as a database and VuexFire to bind the data to VueX state, like so.
getLeads: firestoreAction(async ({
bindFirestoreRef
}) => {
// return the promise returned by `bindFirestoreRef`
return bindFirestoreRef('leads', db.collection('leads').orderBy('updated.date', 'desc').limit(30))
}),
It gets the first 30 results and then i want to implement an infinite scroll feature to run a function every time the scroll reaches the bottom and fetch more data and bind to the same state. In Firestore pagination require passing a query cursor of the last fetched document as a reference
Below from firebase document, with vanilla JS
var first = db.collection("cities")
.orderBy("population")
.limit(25);
return first.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
console.log("last", lastVisible);
// Construct a new query starting at this document,
// get the next 25 cities.
var next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible)
.limit(25);
});
since I use VuexFire to bind the data to state, I dont see an option to get the snapshot of the last document fetched by VuexFire (lastVisible from the above code), in order to pass it to the next query.
Any help will be highly appreciated. ๐Ÿ™๐Ÿฝ
Lets say I have a collection of Customer records and i am displaying the first 5 ordered by last updated.
The query is
getLeads: firestoreAction(({ commit, bindFirestoreRef
}) => {
bindFirestoreRef('Leads', db.collection('leads')
.orderBy('updated.date', 'desc').limit(5)).then(documents => {
commit('POPULATE_TESTLEADS', documents);
commit('LAST_DOC', documents[documents.length - 1]);
});
}),
I am saving both the results and the lastdoc in the state, looping and showing the names, like so:
Nakheel
Emaar Group
Yapi Kredi Inc
Cairo Amman Bank
Arab Jordan Investment Bank LLC
I then call again with the last doc as query cursor and expect the next 5 docs to return from firebase, like so
moreLeadLeads: firestoreAction(({ state, bindFirestoreRef
}) => {
bindFirestoreRef('testLeads', db.collection('leads')
.orderBy('updated.date', 'desc')
.startAfter(state.lastDoc).limit(5))
}),
But I get the same 5 results as above from firestore. What am I doing wrong? :(
Internally VueFire and VuexFire use a serializer function that maps each Document returned by RTDB or Firestore into the data objects that are bound to the final component or Vuex store state.
The default serializer is implemented by the function createSnapshot that is part of the vuefire-core library:
/**
* #param {firebase.firestore.DocumentSnapshot} doc
* #return {DocumentData}
*/
export function createSnapshot (doc) {
// defaults everything to false, so no need to set
return Object.defineProperty(doc.data(), 'id', {
value: doc.id
})
}
As you can see it returns only doc.data() (with id added) and discards the doc object. However when implementing Firestore pagination via query.startAfter(doc) we need the original doc object. The good news is that VueFire and VuexFire APIs allow us to replace the serializer with our own that can preserve the doc object like so:
const serialize = (doc: firestore.DocumentSnapshot) => {
const data = doc.data();
Object.defineProperty(data, 'id', { value: doc.id });
Object.defineProperty(data, '_doc', { value: doc });
return data;
}
We can configure our new VuexFire serializer either globally via plugin options or per binding via binding options.
// Globally defined
Vue.use(firestorePlugin, { serialize });
// OR per binding
bindFirebaseRef('todos', db.ref('todos'), { serialize } )
For VuexFire, we can now get access to the first document as state.todos[0]._doc or last document state.todos[state.todos.length-1]._doc and use them to implement pagination queries for collections or "get next" & "get previous" queries that bind single documents (essential when your base query has multi-field sorting).
NOTE: Because _doc and id are non-enumerable properties, they won't appear on component or store objects in Vue DevTools.
From the VueFire documentation on binding data and using it, the $bind method (which I assume your bindFirestoreRef wraps) returns a promise with the result (as well as binding it to this). From that documentation:
this.$bind('documents', documents.where('creator', '==', this.id)).then(documents => {
// documents will point to the same property declared in data:
// this.documents === documents
})
So you should be able to do the same, and then get the document from the array with something like:
bindFirestoreRef('leads', db.collection('leads').orderBy('updated.date', 'desc').limit(30)).then(documents => {
this.lastDoc = documents[documents.length - 1];
})

Firebase query: "where" clause is not working, keep returning all documents

Fetching member with id == "1112" and it is not working
var query = this.afs.collection('members', ref => ref.where('id', '==', "1112"))
query.ref.get().then(doc => {
doc.forEach(postDoc => {
console.log(postDoc.data());
});
})
normally you define a ref that points to either a document or a collection, and you may add a .where. You fire .get on the ref.
...Rewrite it to say
var ref = [whatever SDK/homepath].firestore.collection('members).where('id', '==', "1112");
then pass that ref to your .get. and NOTE you appear to be storing numbers as strings which can negatively impact / throw off queries.
You're taking the query and then ask for its ref, which returns the collection that you created the query on. This means that you're calling get on the entire collection, which returns alls documents.
To only get the documents matching the conditions in your query, call get straight on the query. So:
query.get().then(doc => {
...
Alternatively you can bypass AngularFire2, since it has no benefits in this scenario. That means you end up with:
ref.where('id', '==', "1112").get().then(doc => {
...

Categories

Resources