How do I query Firebase using Typescript? - javascript

I have push notifications set up for my app using Firebase Cloud Functions. It works well. Now I want to update the app's badge count as part of the push notification. I've read that the only way to do that is via server-side code; I can't do it locally.
So I'm trying to get the number of new users from the server and then use that number as the badge count when I send the push notification, but I can't figure out how to go about it. I've spent three days on this and now I'm hoping someone can point me in the right direction.
I'm using Firebase functions and Typescript (with VSCode). My course of action is to:
get list of userIDs from 'admin' node
iterate over those userIDs on 'user' node to query if user's 'newUser' parameter is true
append those results to an array
count the array and then send that to the badge on push notification
My 'users' database structure is like so:
"users": {
"2NBvNgdNRVe3nccTEDts2Xseboma": {
"email": "someone#someone.com"
"newUser": "true",
"referral": "none",
...
},
"hjC6os6wzIV1FyULmGxalU3fM7ef": {
"email": "someoneElse#someone.com"
"newUser": "false",
"referral": "Bennett",
...
}
And my 'admin' database is structured like so:
"admin": {
"2NBvNgdNRVe3nccTEDts2Xseboma": {
"email": "someone#someone.com"
"familyName": "Someone",
"memberSince": "1529119893",
},
"hjC6os6wzIV1FyULmGxalU3fM7ef": {
"email": "someoneElse#someone.com"
"familyName": "Someone Else",
"memberSince": "1529125722",
...
}
Here is my feeble attempt to code this:
exports.getNewUserCount =
functions.database.ref('/users/{userID}/newUser')
.onUpdate((snapshot, _context) => {
console.log('test 2')
// Get a database reference
const db = admin.database();
const ref = db.ref('admin');
return ref.once('value', function(adminSnap) {
const userData = adminSnap.val()
console.log('admin key:', adminSnap.key)
console.log('user data:', userData)
})
});
Right now I'm stuck on retrieving the list of users from the admin node (my step #1 above).
UPDATE
I finally got a list of the users as a snapshot, but I can't figure out how to iterate over them. How do I turn the snapshot into an array of the user keys?
And then once I get the list of user keys, then how do I use that to iterate over the 'users' node to get the list of new users (my step #2 above)?
And then how to put those new users into an array (my step #3 above), and then get the number of new users for the 'badge' parameter when I send my push notification (my step #4 above)?
The problem is that this seems really inefficient. There has to be a better way to simply get a list of new users. There has to be some sort of query I can perform that will go over my 'users' node, see which ones have 'true' for their 'newUser' node, and get a count of those--instead of my roundabout way of getting a list of user from 'admin' node, then using that list to get a list of 'new users' from the 'users' node, then creating an array and then counting that array, then using that number to send to the 'badge' parameter on my push notification.
Any thoughts? I've been at this for days.
If it helps, I know Swift and the app is iOS. Thanks!!
UPDATE #2
So I opted to try and just get a snapshot of all users and bypass the 'admin' node altogether. Here is the code:
const db = admin.database();
const ref = db.ref('users');
return ref.once('value').then((adminSnap) => {
console.log('admin key:', adminSnap.key)
// create blank array to store
let newUserCount = 0;
// iterate over adminSnap to get each individual snap
adminSnap.forEach(function (userSnap) {
const userData = userSnap.val();
const userKey = userSnap.key
// console.log('email?', userData.email, 'user key:', userKey, 'new user?', userData.newUser)
if (userData.newUser === true) {
newUserCount++
console.log('new user:', userKey, userData.newUser, userData.email)
}
});
console.log(newUserCount)
})
This new code works and gives me the number for my badge parameter for when I perform my push notification, but I'm wondering if it's the most efficient way to do things. Plus, as my database grows in size, won't it tax the server / slow way down? And won't it cost me a lot of bandwidth for my Firebase account?
I thought this would be a simple thing to do, but it's turning into a bit of a hassle. I'm open to a different way to complete this. Thanks!

After even more research, I ended up abandoning my original approach. I decided to just create a new node on my Firebase database with the new user count and then update it via code from elsewhere. It's the simplest approach and will use the least amount of bandwidth.
Here is my final code:
function sendAlertToiPhone() {
console.log('test E')
// Get a database reference
const db = admin.database();
const ref = db.ref('stats');
ref.child('newUserCount').once('value').then((snapshot) => {
const newUserCount = snapshot.val()
console.log('new user count:', newUserCount)
// send to Phontaine's iPhone 6
const FCMToken = "blahbehtyblahblah"
const payload = {
notification: {
title: 'New User',
body: 'Moneypants has a new download.',
sound: 'default',
badge: String(newUserCount)
}
};
return admin.messaging().sendToDevice(FCMToken, payload)
.then(function (response) {
console.log("Successfully sent message:", response);
})
.catch(function (error) {
console.log("Error sending message:", error);
});
}).catch(function (err) {
console.log('new user count error:', err);
})
}

Related

How to create a nested collection when creating a user in Firebase / Firestore where users can save bookmarked items

I want to be able to have a nested collection in firebase/firestore where I can save an authenticated users favorites. I was trying to create the collection when the user is created so I can just read/write to it later but I can't figure out how to create the collection. I have something like this:
//This function creates a new user. If the user already exists, no new document will be created
export const createUserDocumentFromAuth = async (
userAuth,
additionalInfo = {}
) => {
if (!userAuth) return;
const userDocRef = doc(db, 'users', userAuth.uid); //database instance, collection, identifier
const bookmarkRef = doc(db, 'users', userAuth.id, 'bookmarks'); //This triggers error
const userSnapshot = await getDoc(userDocRef);
if (!userSnapshot.exists()) {
//If user snapshot doesn't exist - create userDocRef
const { displayName, email } = userAuth;
const createdAt = new Date();
try {
await setDoc(userDocRef, {
displayName,
email,
createdAt,
...additionalInfo,
});
setDoc(bookmarkRef, { //Try to create a bookmarks collection here
favorites: []
})
} catch (error) {
console.log('Error creating user', error.message);
}
}
//if user data exists
return userDocRef;
};
I can create the user just fine but not another collection at the same time. I also tried just creating the collection when a signed-in user clicks on the bookmark button like this but I get a type error in both cases Uncaught (in promise) TypeError: n is undefined every time.
export const addBookmarkForUser = async (userAuth, showId) => {
const bookmarkRef = doc(db, 'users', userAuth.id, 'bookmarks');
try {
await setDoc(bookmarkRef, {
favorites: showId
});
}catch(error){
console.log('error creating bookmark', error.message)
}
};
I'm pretty new to Firebase / Firestore and all I want is to be able to save an item id in an array for an individual user when they click a button. If saving in an array is not ideal or there is any better way to do this, I am open to any suggestions at this point.
I was trying to create the collection when the user is created so I
can just read/write to it later but I can't figure out how to create
the collection.
A (sub)collection is only created when you create the first document in it. There is no way to materialize an empty collection without a document.
And it is normal that you get an error when using the doc() method as follows
const bookmarkRef = doc(db, 'users', userAuth.id, 'bookmarks');
because this method is used to create a DocumentReference and therefore you need to pass a path with an even number of path segments. In you case you pass 3 segments.
You could very well define the CollectionReference for the bookmarks subcollection as follows, using the collection() method and passing the 3 segments
const bookmarkRef = collection(db, 'users', userAuth.id, 'bookmarks');
but, until you add a document in it, it will not exist in the database.
Conclusion: You will automatically create the user's bookmarks subcollection the first time you create a bookmark for the user.
For example:
const bookmarksCollectionRef = collection(db, 'users', userAuth.id, 'bookmarks');
await bookmarksCollectionRef.add({ ... })

Sequelize delete query runs, but does not resolve Promise

I have a node js server that is creating and deleting from database using Sequelize. When i create new user in "Users" table, query normally runs and server returns response. But when i try to delete user from "Users" table, query runs but promise isn't resolved, therefore i get no response from server. Here is
my code:
const { User } = require("./models")
const user = {id: "...."} //Parsed delete request from client, id is not undefined
User.destroy({
where: {
id: user.id,
},
})
.then(res.status(200).clearCookie("refresh-token"));
.catch(res.status(400));
What i see in console:
Executing (default): DELETE FROM "Users" WHERE "id" = '6d3edbab-03b8-429b-b249-a9d3ba6bce7a'
And after a while:
DELETE /api/user/delete - - - - ms [2021-3-14 14:17:11]
I delete stuff from other tables too and they work, so it seems that Users table is somewhat special. Whats wierd is that when i look in database i see that record was deleted. I have no idea what is happening.
Thanks for help!
I solved my issue by creating a new function that opens a new Sequelize connection and
uses that to delete records in db. Here it is:
function deleteUsr(id, res) {
const { Sequelize } = require("sequelize");
if (!/^([0-9a-z]){8}-([0-9a-z]){4}-([0-9a-z]){4}-([0-9a-z]){4}-([0-9a-z]){12}$/.test(id)) {
res.status(400).send("Provide valid UUID")
}
const seq = new Sequelize(
"connection string"
);
seq
.authenticate()
.then(console.log("yipeee"))
.catch(err => console.error(err));
seq
.query(`delete from "Users" where id='${id}'`)
.then(x => {
res.status(200).clearCookie("refresh-token").send(x);
seq.close();
})
.catch(err => {
res.status(400).send(err);
seq.close();
});
}
Avoid using this function if your input isn't sanitized properly, because anyone who is signed could delete any user if using this. I am taking uuid from verified jwt access token and comparing it to encrypted refresh token, so that user cannot even input anything into the function.
Hope it helped!

How to send notification to multiple device token using firebase cloud functions

I am new in using Firebase Cloud Functions and JavaScript and I was able to send notification to a single device using Firebase Cloud Functions(JavaScript). Now I am trying to send push notification to multiple device token and I think I have a problem on it.
I want to send the notification to these device tokens in my firebase database:
/receivers
/event1
/uid1: device_token1
/uid2: device_token2
/uid3: device_token3
/uid4: device_token4
This is my code so far but it doesn't work..
exports.sendSOSNotif = functions.database.ref('/SOSNotifs/{sosId}').onWrite((data, context) => {
const eventId=data.after.child("eventId").val();
const uid=data.after.child("uid").val();
const displayName=data.after.child("displayName").val();
const photoUrl=data.after.child("photoUrl").val();
const status=data.after.child("status").val();
console.log("eventId:", eventId);
console.log("displayName:", displayName);
console.log("uid", uid);
const payload = {
notification: {
title: "SOS Alert",
body: displayName + " sent an alert!",
sound: "default"
},
data: {
eventId: eventId,
displayName: displayName
}
};
return Promise.all([admin.database().ref("/receivers/event1").once('value')]).then(results => {
const tokens = results[0];
if (!tokens.hasChildren()) return null;
const tokensList = Object.keys(tokens.val());
return admin.messaging().sendToDevice(tokensList, payload);
});
});
First of all, you shouldn't be adding tokens like below, if that's how you've organised your DB. There might be multiple token for a single uid
/receivers
/event1
/uid1: device_token1
/uid2: device_token2
/uid3: device_token3
/uid4: device_token4
And for sending notifications to multiple UIDs, I've written a script here
Also, update your question about what exactly the problem you are facing.

How to scale push notifications in Firebase Cloud functions for this use case?

In my app, I am sending push notifications to the followers of a user when that user creates a new post. As you can see in the code below, I have some additional settings that I need to query from each follower's profile to get their push token and check for some additional notification settings. I am afraid that this query of each user's profile might become a bottleneck if a user has a large number of followers i.e. 1000.
What is the best way to approach this?
// The cloud function to trigger when a post is created
exports.newPost = functions.database.ref('/posts/{postId}').onCreate(event => {
const postId = event.params.postId;
const post = event.data.val();
const userId = post.author;
let tokens = [];
let promises = [];
return admin.database().ref(`/followers/${userId}`).once('value', (followers) => {
followers.forEach((f) => {
let follower = f.key;
promises.push(
admin.database().ref(`users/${follower}`).once('value')
);
});
})
.then(() => {
return Promise.all(promises).then((users) => {
users.forEach((user) => {
const userDetails = user.val();
if (userDetails.post_notifications) {
if(userDetails.push_id != null) {
tokens.push(userDetails.push_id);
}
}
})
})
})
.then(() => {
if (tokens.length > 0) {
const payload = {
notification: {
title: 'New Post!',
body: 'A new post has been created'
}
};
// Send notifications to all tokens.
return admin.messaging().sendToDevice(tokens, payload);
}
});
})
EDIT:
We have thought about using topics. But we are not sure how we can still have our customized notifications settings working with topics. Here's our dilemma.
We have multiple actions that can create notifications and we provide individual switches for each type of notification in the app so a user can select which type of notifications they want to switch off.
Let's say when User A follows User B. We can subscribe User A to "User B's topic" so whenever User B performs an action which sends out notifications to his/her followers, I can send send out notifications to users subscribed to "User B topic".
Because we have multiple notification switches in the app and when user A changes his/her settings that they do not want notifications for new posts but still want other types of notifications from the users he/she follows, we have not been able to figure out how we can use topics in this case.
Instead of using tokens, you can use topics for this. So lets say a user started following someone, then he will register to that topic.
Lets say he followed someone called "Peter", then you can execute this:
FirebaseMessaging.getInstance().subscribeToTopic("Peter");
Now if you have this database:
posts
postid
postdetails: detailshere
author: Peter
then using onCreate():
exports.newPost = functions.database.ref('/posts/{postId}').onCreate(event => {
const postId = event.params.postId;
const post = event.data.val();
const authorname = post.author;
const details=post.postdetails;
const payload = {
data: {
title:userId,
body: details,
sound: "default"
},
};
const options = {
priority: "high",
timeToLive: 60 * 60 * 24
};
return admin.messaging().sendToTopic(authorname, payload, options);
});
You can use this, everytime the author creates a new post, onCreate() is triggered then you can add the details of the post and the author name in the notification (if you want) and sendToTopic() will send it to all users subscribed to the topic that is the authorname (ex: Peter)
After your edit, I think you want the user to be unsubscribed from a topic, but stay subscribed to other topics, then you have to use the admin sdk for this:
https://firebase.google.com/docs/cloud-messaging/admin/manage-topic-subscriptions
Using the admin sdk you can unsubscribe the user also from a topic, a simple example:
// These registration tokens come from the client FCM SDKs.
var registrationTokens = [
'YOUR_REGISTRATION_TOKEN_1',
// ...
'YOUR_REGISTRATION_TOKEN_n'
];
// Unsubscribe the devices corresponding to the registration tokens from
// the topic.
admin.messaging().unsubscribeFromTopic(registrationTokens, topic)
.then(function(response) {
// See the MessagingTopicManagementResponse reference documentation
// for the contents of response.
console.log('Successfully unsubscribed from topic:', response);
})
.catch(function(error) {
console.log('Error unsubscribing from topic:', error);
});

How to look for a specific value in a DataSnapshot with Firebase Cloud Functions

I'm trying to make a cloud function that will trigger on HTTP request (which is sent on a timer), that will remove all childs with a specific value.
The database node looks like this:
activities
4GI1QXUJG0MeQ8Bq19WOdCQFo9r1 //uid
activity: "hammer"
id: some ID
note: "some note"
timestamp: some timeintervalsince1970
7IDUiufhiws8939hdfIUHiuhwdi5
etc....
I want to look through all the activities, and if the activity value is "hammer", I want to remove the child.
This is what I have so far
exports.deleteHammerNotifications = functions.https.onRequest((req, res) => {
admin.database().ref('activities').once('value', (snapshot) => {
console.log(snapshot.val())
});
});
which prints:
{
'4GI1QXUJG0MeQ8Bq19WOdCQFo9r1':
{ activity: 'nn',
id: '4GI1QXUJG0MeQ8Bq19WOdCQFo9r1',
note: 'Blank note...',
timestamp: 1498032472 },
M6xQU5XWTEVbSqBnR3HBAEhA9hI3:
{ activity: 'hammer',
id: 'M6xQU5XWTEVbSqBnR3HBAEhA9hI3',
note: 'some note here...',
timestamp: 1497973839 },
}
My problem is I don't know how to cycle through the DataSnapshot and look for all the childs that has the activity: "hammer" value.
I have done similar function in my xcode project with arrays, but I don't know how to do it with JavaScript.
Any help is appreciated!
To cycle through the matching child nodes, use snapshot.forEach():
exports.deleteHammerNotifications = functions.https.onRequest((req, res) => {
admin.database().ref('activities').once('value', (snapshot) => {
snapshot.forEach((childSnapshot) => {
console.log(childSnapshot.val())
});
});
});
But you're still missing a query here to select the correct nodes. Without such a query you might as well call admin.database().ref('activities').remove().
To most efficiently delete a number of items from the database and write a single response back to the user, use this function (which I modified from something I needed recently):
exports.cleanup = functions.https.onRequest((req, res) => {
var query = admin.database().ref("activities").orderByChild("activity").equalTo("hammer");
query.once("value").then((snapshot) => {
console.log("cleanup: "+snapshot.numChildren()+" activities");
var updates = {};
snapshot.forEach((child) => {
updates["activities/"+child.key] = null;
});
admin.database().ref().update(updates).then(() => {
res.status(200).send(snapshot.numChildren()+" activities deleted");
}).catch((error) => {
res.status(500).send(error);
})
});
});
Learn more:
Firebase documentation on querying
Firebase blog post on multi-location updates
I am not sure if its possible, but IF you can start a "child_added" listener, once the HTTPS trigger has run, you can do like this.
ref.on('child_added', function(snapshot) {
if (snapshot.child("activity").val() === 'hammer') {
var value = snapshot.child("activity").val();
})
I am doing the exact same thing to see if people are still subscribed to my mailing list or not, and IF they are, they will receive a mail.
Hope that helps :-)

Categories

Resources