I understand the issue but can't figure out the workaround. I am querying a specific document to extract an array of token strings. I need to append a new token to the end of this string and then update the current document with this new token array.
To do this, I have subscribed to a query and within, I update that document. But of course, when you update the same object, the subscription runs again thus creating an infinite loop. I tried incorporating a take(1) pipe rxjs operator but that did not change anything. Any suggestions?
Here's my code:
this.afs.collection('users').doc(user.userUID).valueChanges().pipe(take(1)).subscribe((user: userModel) => {
const currentTokens: string[] = user.notifTokens ? user.notifTokens : [];
//token variable is provided outside this query
currentTokens.push(token);
//this next lines causes the subscription to trigger again
userRef.doc(user.userUID).update({notifTokens: currentTokens})
})
I would recommend you avoid using a subscription in this situation, for exactly this reason. I realize the Angularfire2 docs don't list this method, but the base Firebase package includes a .get() method... and while the AF2 docs don't mention the .get() method... the source code shows that it is supported.
Try something like:
this.afs.collection('users').doc(user.userUID).get().then( (user: userModel) => {
if (user.exists) {
console.log("Document data:", user.data());
// Do stuff with the info you get back here
const currentTokens: string[] = user.data().notifTokens ? user.data().notifTokens : [];
currentTokens.push(token);
userRef.doc(user.data().userUID).update({notifTokens: currentTokens})
} else {
// user.data() will be undefined in this case
console.log("No such document!");
}
}).catch(function(error) {
console.log("Error getting document:", error);
});
Related
I am using Angular 8 and have a form where a user can choose what he wants to query the database for and then click either of two buttons - one to view data in realtime on the website, and the other to download the data.
I thought I could make use of one function to make a query and then call different functions depending on what button the user clicked, using get() for the download and valueChanges() for the realtime data view. But when I try this, I get the following errors in the browser console. (This is with query as type any - if I specify the type as AngularFirestoreCollection I get errors regarding my type for the get() part in VSCode)
ERROR Error: "Uncaught (in promise): TypeError: this.query.get is not
a function
I can add that I previously had two completely separate (working) functions for downloading and viewing in realtime. And for downloading I used the below query. I gather this is actually a Firestore Query, whereas the "query" I'm trying to use in my updated code is an AngularFirestoreCollection. But is there a way I can make some kind of Query/Collection that will work for both get() and valueChanges()?
Old (working) query:
var query = this.afs.collection(collection).ref.where('module', 'in', array_part);
Trying a common function makeQuery():
onSubmit(value, buttonType): void {
if (buttonType=='realtime') {
this.getRealTimeData(value);
}
if (buttonType=='download') {
this.downloadCsv(value);
}
}
async downloadCsv(value) {
this.query = this.makeQuery(value);
this.dataForDownload = await this.getDataForDownload();
this.dataForDownload = JSON.stringify(this.dataForDownload['data']);
console.log('Data: ', this.dataForDownload);
var date = new Date();
var date_str = this.datePipe.transform(date, 'yyyy-MM-ddTHH-mm');
this.makeFileService.downloadFile(this.dataForDownload, 'OPdata-' + date_str);
}
getDataForDownload() {
return this.query.get()
.then(function (querySnapshot) {
var jsonStr = '{"data":[]}';
var dataObj = JSON.parse(jsonStr); //making object we can push to
querySnapshot.forEach(function (doc) {
JSON.stringify(doc.data()), ', id: ', doc.id);
dataObj['data'].push(doc.data());
});
return dataObj;
})
.catch(function (error) {
console.log("Error getting documents: ", error);
});
}
async getRealTimeData(value) {
this.query = await this.makeQuery(value);
this.data = this.query.valueChanges();
}
async makeQuery(value) {
var collection: string;
return this.query = this.afs.collection<DataItem>('CollectionName', ref => ref.where('datetime', '>=', '2020-01-15T09:51:00.000Z').orderBy('datetime', 'desc').limit(100));
}
The valueChanges() is a method used in angularfire to retrieve data from firestore, while the get() method is used to retrieve from firestore but using the vanilla javascript.
Mixing both methods will return an error as you have seen in your code. Therefore, since angularfire was created above the javascript firebase code, then you should be able to use valueChanges() to view data in realtime on the website, and to download the data.
I've been trying to implement just a simple Firebase fetch since November. At this point, I wish I'd just created a new Rails api; it would have been faster.
But everyone insists Firebase is Oh So Simple.
In app.js,
import firebase from 'nativescript-plugin-firebase';
That part seems OK.
Instructions are all over the place after that.
The plugin's ReadMe suggests an initialization:
firebase.init({
// Optionally pass in properties for database, authentication and cloud messaging,
// see their respective docs.
}).then(
function () {
console.log("firebase.init done");
},
function (error) {
console.log("firebase.init error: " + error);
}
);
Several others have insisted that the init code is unnecessary. It does run without errors, but the code he gives after that produces nothing. Also,
const db = firebase.firestore;
const UserStatusCollection = db.collection("UserStatus");
UserStatusCollection.get();
produce an empty object {}.
Here's my Firebase collection:
If I wrap the firebase call in async/await (and no one is showing it as this complicated),
async function getFireStoreData() {
try {
let result = await this.UserStatusCollection.get();
console.log(result);
return result;
}
catch (error) {
console.error(
"UserStatusCollection.get()" + error
);
}
}
And call that
let temp2 = getFireStoreData();
console.log("temp2:" + temp2);
All I ever get is an object promise.
As I said, I wish I had just built up a new Rails API and had a far simpler life since November.
Your getFireStoreData method is asynchronous and you're not awaiting it. That is probably the reason why you're getting a promise back. Try to await getFireStoreData(). See if that works.
Since it's also a promise, you can try to use .then.
getFireStoreData().then(data => {
console.log(data);
})
So basically I have a collection a User and within each user there is a subcollection for the pending friend request that the user have, something like that:
/users/[auto-id]/friend_requests/[auto-id]/{user: ref to another user}
But one user can obviously have multiple requests at the same time and I have an hard time to get the data correctly.
What I'm correctly trying to do is to get a list of user that are in the subcollection "friend_requests":
_loadFriendRequests() async {
try {
this._users.addAll(await _dataService.fetchFriend());
} catch (e, stackTrace) {
printException(e, stackTrace, "Error loading friends");
}
}
And in dataService:
Future<List<User>> fetchFriend() async {
final querySnapshot =
await _currentUserDoc.reference.collection("friend_requests").getDocuments();
return await Future.wait(querySnapshot.documents.map((doc) async {
final user = await doc["user"].get();
User(
id: user["id"],
name: user["name"],
);
}).toList(growable: false));
}
This is just the last thing that I tried but I tried in so many ways, with Future.wait() too, with/without async/await, etc
I just can't seem to make it work...
You're missing a return statement after final user = await doc["user"].get();
This could become a lot easier if you use Streams or fancy rxdart
Suppose I have 2 collections "PlanSubscriptions" and "ClientActivations". I am serially doing a insert on both the collections.
Later one depends on previous one, if any of the transaction fails then the entire operation must rollback.
How can I achieve that in Meteor 1.4?
Since MongoDB doesn't support atomicity, you will have to manage it with Method Chaining.
You can write a method, say, transaction where you will call PlanSubscriptions.insert(data, callback). Then in the callback function you will call ClientActivations.insert(data, callback1) if the first insertion is success and in callback1 return truthy if second insertion is succes, otherwise falsy. If the first insertion returns error you don't need to do anything, but if the second insertion returns error then remove the id got from the insertion in first collection.
I can suggest following structure:
'transaction'(){
PlanSubscriptions.insert(data, (error, result)=>{
if(result){
// result contains the _id
let id_plan = result;
ClientActivations.insert(data, (error, result)=>{
if(result){
// result contains the _id
return true;
}
else if(error){
PlanSubscriptions.remove(id_plan);
return false;
}
})
}
else if(error){
return false;
}
})
}
There is no way to do that in Meteor, since mongodb is not an ACID-compliant database. It has a single-document update atomicity, but not a multiple-document one, which is your case with the two collections.
From the mongo documentation:
When a single write operation modifies multiple documents, the modification of each document is atomic, but the operation as a whole is not atomic and other operations may interleave.
A way to isolate the visibility of your multi-document updates is available, but it's probably not what you need.
Using the $isolated operator, a write operation that affects multiple documents can prevent other processes from interleaving once the write operation modifies the first document. This ensures that no client sees the changes until the write operation completes or errors out.
An isolated write operation does not provide “all-or-nothing” atomicity. That is, an error during the write operation does not roll back all its changes that preceded the error.
However, there are a couple of libraries which try to tackle the problem at the app-level. I recommend taking a look at fawn
In your case, where you have exactly two dependent collections, it's possible to take advantage of the two phase commits technique. Read more about it here: two-phase-commits
Well I figured it out myself.
I added a package babrahams:transactions
At server side Meteor Method call, I called tx Object that is globally exposed by the package. The overall Server Side Meteor.method({}) looks like below.
import { Meteor } from 'meteor/meteor';
import {PlanSubscriptions} from '/imports/api/plansubscriptions/plansubscriptions.js';
import {ClientActivations} from '/imports/api/clientactivation/clientactivations.js';
Meteor.methods({
'createClientSubscription' (subscriptionData, clientActivationData) {
var txid;
try {
txid = tx.start("Adding Subscription to our database");
PlanSubscriptions.insert(subscriptionData, {tx: true})
ClientActivations.insert(activation, {tx: true});
tx.commit();
return true;
} catch(e){
tx.undo(txid);
}
return false;
}
});
With every insert I had added {tx : true}, this concluded it to be a apart of transaction.
Server Console Output:
I20170523-18:43:23.544(5.5)? Started "Adding Subscription to our database" with
transaction_id: vdJQvFgtyZuWcinyF
I20170523-18:43:23.547(5.5)? Pushed insert command to stack: vdJQvFgtyZuWcinyF
I20170523-18:43:23.549(5.5)? Pushed insert command to stack: vdJQvFgtyZuWcinyF
I20170523-18:43:23.551(5.5)? Beginning commit with transaction_id: vdJQvFgtyZuWcinyF
I20170523-18:43:23.655(5.5)? Executed insert
I20170523-18:43:23.666(5.5)? Executed insert
I20170523-18:43:23.698(5.5)? Commit reset transaction manager to clean state
For more Information you can goto link : https://github.com/JackAdams/meteor-transactions
NOTE: I am using Meteor 1.4.4.2
Just sharing this link for future readers:
https://forums.meteor.com/t/solved-transactions-with-mongodb-meteor-methods/48677
import { MongoInternals } from 'meteor/mongo';
// utility async function to wrap async raw mongo operations with a transaction
const runTransactionAsync = async asyncRawMongoOperations => {
// setup a transaction
const { client } = MongoInternals.defaultRemoteCollectionDriver().mongo;
const session = await client.startSession();
await session.startTransaction();
try {
// running the async operations
let result = await asyncRawMongoOperations(session);
await session.commitTransaction();
// transaction committed - return value to the client
return result;
} catch (err) {
await session.abortTransaction();
console.error(err.message);
// transaction aborted - report error to the client
throw new Meteor.Error('Database Transaction Failed', err.message);
} finally {
session.endSession();
}
};
import { runTransactionAsync } from '/imports/utils'; // or where you defined it
Meteor.methods({
async doSomething(arg) {
// remember to check method input first
// define the operations we want to run in transaction
const asyncRawMongoOperations = async session => {
// it's critical to receive the session parameter here
// and pass it to every raw operation as shown below
const item = await collection1.rawCollection().findOne(arg, { session: session });
const response = await collection2.rawCollection().insertOne(item, { session: session });
// if Mongo or you throw an error here runTransactionAsync(..) will catch it
// and wrap it with a Meteor.Error(..) so it will arrive to the client safely
return 'whatever you want'; // will be the result in the client
};
let result = await runTransactionAsync(asyncRawMongoOperations);
return result;
}
});
I want to get a single value from Firebase Database within Firebase function. However, the Promise never returns and the chained method never executes. Here is the method that fetches a value from the database
function getFcmToken(username){
return admin.database().ref('tokens/{username}/fcmToken').once('value').then(snap => {
if(snap.exists()){
const token = Object.keys(snap.val());
console.log("FCM Token", token);
return token;
}
return [];
});
}
The above method was supposed to return a token, but I am not sure it is, so the method below does not get executed.
function sendNotification(keyword, username){
return getFcmToken(username).then(token => {
if(token.length > 0){
//Notification Detail
let payload = {
data:{
keyword: keyword
}
};
return admin.messaging().sendToDevice(token, payload);
}
});
}
In the console log, all I see is Promise pending.
How can I update the code above to return a single value, it appears it is returning an array?
Thanks
Your database ref path is wrong. You might wanted to replace username in path, but single quoted won't do that.
Firebase is listening on tokens/{username}/fcmToken, which doesn't exists. Hence on value event will not be triggered and so downline callback will not be executed.
You can use Template literals for building dynamic strings.
Try ref path as
`tokens/${username}/fcmToken`
Code:
function getFcmToken(username){
return admin.database().ref(`tokens/${username}/fcmToken`).once(...)
}