Firebase Cloud Functions: Keeping things in sync - node.js

I have users and companies and want to store a company for each user and all of the users of each company in Firebase.
user={
"id":"tjkdEnc3skdm2Jjknd"
"name":"Adam",
"street":"Sideway 4",
"company":"dHend4sdkn25"
}
companies={
"id":"dHend4sdkn25",
"name":"Comp Ltd.",
"members":[
{
"id":"tjkdEnc3skdm2Jjknd"
"name":"Adam"
},{
"id":"dfjnUkJKB3sdn8n2kj"
"name":"Berta"
}
]
}
All explanations say that duplicate data is the best way to deal with and so I want to write some cloud functions to keep thigs in sync when editing on one of the sides.
Basically I started with
exports.userChangedCompany = functions.firestore
.document('users/{userId}')
.onUpdate((change, context) => {
const data = change.after.data();
const previousData = change.before.data();
if (data.company == previousData.company) {
return null;
}
else{
admin.firestore().doc('companies/'+data.company).set({ ... });
}
});
to update the companies when a user changed the company. Unfortunately I haven't found any hint how to set the new company-data properly.
Can someone please help me?

It sounds like you just need to remove user from members array of old company and add in that array of new company. You just need IDs of both companies.
async function updateCompanies(userId, username, oldCompanyId, newCompanyId) {
const companiesRef = await admin.firestore().collection("companies")
const userObj = {id: userId, name: username}
// Removing from old company and adding in new company
await Promise.all([
companiesRef.doc(oldCompanyId).update({members: admin.firestore.FieldValue.arrayRemove(userObj)}),
companiesRef.doc(newCompanyId).update({members: admin.firestore.FieldValue.arrayUnion(userObj)})
])
return true
}
You can just call this function in your cloud function. Just make sure you pass correct params. The reason why you need to pass the username as well is you have array of objects (members) and hence you need the complete object to add/remove using arrayUnion/arrayRemove.

Related

Firebase function - how to create multiple documents from different collections

I am fairly new to writing functions and cannot figure out how to solve my issue or even search for it properly.
I have three collections:
current_projects
vendors
vendor_projects
when a new project is created I want the function to take all documents in vendors, add certain fields from them to vendor_projects, and include the project_id field that is created in current_projects.
Do I need a for loop to accomplish this or is there other syntax that could be utilized?
My current function is below. This creates on document using the new project_id field but doesnt take any of the fields from vendors. Any input is greatly appreciated.
exports.createProjVen = functions.firestore.document("/Current_projects/{id}")
.onCreate((snap, context)=>{
console.log(snap.data());
const id = snap.data().id;
// const collection = context.params.Current_projects;
// const id = context.params.id;
const projectVendors = admin.firestore().collection("project_vendors");
// const vendors = admin.firestore().collection("vendors");
return projectVendors.doc(id).set({
actual: "",
budget: "",
id: "23121",
project_id: id,
toggle: "true",
type: "Fixtures",
vendor_company: "tes",
vendor_contact: "tes",
vendor_email: "jj#j.com",
vendor_id: "test",
vendor_phone_number: "test"});
});
Adding more details:
When a new project is added it creates a record in current_projects.
I want the function to be able to query all documents in the vendor collection when a new record is created in current_projects.
From there, I want to grab the highlighted fields from the vendor documents, add the id from that was created from current_projects (highlighted in the first screen shot), and create a new document in project_vendors (third screen shot below) for every document in the vendors table.
If you are trying to created a document in project_vendors collection for every vendor in vendors after a project is created then you can map an array of promises and then use Promise.all() as shown below:
exports.createProjVen = functions.firestore.document("/Current_projects/{id}")
.onCreate((snap, context) => {
const docSnap = snap.data();
const id = context.params.id;
const vendorsSnap = await admin.firestore().collection("vendors").get();
const vendorsData = vendorsSnap.docs.map((d) => ({ id: d.id, ...d.data() }))
const promises = [];
const vendorPrsCol = admin.firestore().collection("project_vendors");
vendorsData.forEach((vendor) => {
const data = {
projectId: id,
email: vendor.email,
// other vendor fields
}
promises.push(vendorPrsCol.add(data));
})
await Promise.all(promises);
console.log("Vendor Projects added");
return null;
});

Update mongodb document, if the document exists, else create

So I am trying to make a discord bot for me and my friends for tracking stats in CS GO 10 mans, and I am using cheerio for webscraping from the site that provides us the stats, and then pass them into mongodb. The scraping functionality works fine, but im trying to figure out how to avoid creating duplicate documents for each user. If I enter *userid 857575 it pulls the stats for that user, and puts in the DB, but if i call that multiple times, its making multiple documents in the DB. My question is, how would I get mongodb to update the document based on if the message author in discord matches the username in the db? So if username bob sends *userid3939 and bob already exists in the db, update the document. If bob doesnt exist, create document. code below, appreciate any tips.
module.exports.run = async (bot, message, args) => {
console.log(args);
var userUrl = 'https://popflash.site/user/' +args;
console.log(userUrl);
console.log(message.member.user.tag);
rp(userUrl)
.then(function (html) {
const arr = [];
var i = 0;
$('.stat-container', html).each(function (key, value) {
arr[i++] = $(this).find(".stat").text();
});
const stats = new Stats({
_id: mongoose.Types.ObjectId(),
userName: message.member.user.tag,
userId: args,
HLTV: arr[0],
ADR: arr[1],
HS: arr[2],
W: arr[3],
L: arr[4],
T: arr[5],
win_percent: arr[6]
});
stats.save()
.then(function (result) {
let botembed = new Discord.RichEmbed()
.setDescription(message.member.user + "'s 10 Man stats")
.setColor("#15f153")
.addField("stats", result)
return message.channel.send(botembed);
})
})
}
module.exports.help = {
name: "userid"
}
Through db.collection.update, you can specify the upsert: true option to get the behavior I think you're desiring. It will update an existing record if matched, otherwise it will create a new record.

How do I increment a value for an existing object in Firebase?

I'm building a step counter app.
I got an iOS app that pushes the sum of each day to /users/{mobile}/steps/{date}/
When a new steps child is updated or added, I want to sum the value of all the steps for that particular user and update his stepsTotal.
To achieve that I need to
Find the original user and sum all the steps.
Save the new value to stepsTotal.
I would be most grateful if someone could give some help here. :-)
database
{
"users": {
"92291000": {
"firstName": "Tore",
"stepsTotal": "1500",
"steps": {
"02-09-2017": "500",
"03-09-2017": "1000"
},
import.js
var db = admin.database();
var dbRoot = db.ref("/");
var usersRef = dbRoot.child("users");
// This works
function saveUser(attributes) {
let mobile = attributes.mobile;
delete attributes['mobile']
let user = usersRef.child(mobile);
user.update(attributes);
}
function increaseSteps( { mobile=null, steps=null } = {}) {
// Find the User
console.log("looking for mobile", mobile); // OK
let userRef = usersRef.child(mobile);
// Here I'm not able to read the old data from the user.
userRef.transaction(function(user) {
console.log("user: ", user); // null
// ^ User is null.
});
/*
If I mangage to find user above, I expect to do something like this.
Or it is possible to only update *stepsTotal*?
*/
let attributes = {
firstName: user.firstName,
lastName: user.lastName,
stepsTotal: user.stepsTotal + steps,
}
user.update( attributes );
}
If I understand correctly, you have a problem in this snippet of the code:
let userRef = usersRef.child(mobile);
// Here I'm not able to read the old data from the user.
userRef.transaction(function(user) {
console.log("user: ", user); // null
// ^ User is null.
});
In Firebase Database transactions the initial value is often null. From the Firebase documentation on transactions:
Transaction Function is Called Multiple Times
Your transaction handler is called multiple times and must be able to handle null data. Even if there is existing data in your database it may not be locally cached when the transaction function is run.
This is due to how Firebase transactions work behind the scenes. To learn more about that, see my answers here Transcation updateFunction parameter is null and Firebase runTransaction not working.
The solution is to handle both cases: if the user node doesn't exist yet count the initial number of steps, otherwise update the number of steps:
let userRef = usersRef.child(mobile);
userRef.transaction(function(user) {
return (user || 0) + new_steps_for_user;
});

Securing access to collection from the client's side

I have a meteor app prototype that works well, but is very insecure as of now: I needed to display a list of matching users to the currently logged-in user. For starters, I decided to publish all users, limiting the fields to what I would need to filter the user list on the client side.
Meteor.publish('users', function () {
return Meteor.users.find({}, {
fields: {
'profile.picture': 1,
'profile.likes': 1,
'profile.friends': 1,
'profile.type': 1
}
});
});
Then in my router, I would do a request to only show what I wanted on the client side:
Router.map(function() {
this.route('usersList', {
path: '/users',
waitOn: function () {
return Meteor.subscribe('users');
},
data: function () {
var user = Meteor.user();
return {
users: Meteor.users.find({ $and: [
{_id: {$ne : user._id}},
{'profile.type': user.profile.interest}
]})
};
}
});
});
In the code above, I query all users who are not the current user and whose type correspond the current user's interest. I also display a certain border on the photos of users who have my user in their "profile.friends" array, using this client helper:
Template.userItem.helpers({
getClass: function(userId) {
var user = Meteor.user();
var lookedup = Meteor.users.findOne({_id: userId});
if ($.inArray(user._id, lookedup.profile.friends) !== -1)
return "yes";
return "no";
}
});
Now this all worked great, but with this setup, every client can query every users and get their type, picture, list of friends and number of likes. If I was in an MVC, this info would only be accessible on server side. So I decided my next iteration to be a security one. I would move my query from the router file to the publications file. That's where trouble began...
Meteor.publish('users', function () {
var user = Meteor.users.findOne({_id: this.userId});
var interest = user.profile.interest;
// retrieve all users, with their friends for now
allUsers = Meteor.users.find({ $and: [
{'_id': {$ne: user._id}},
{'profile.type':interest}
]},
{ fields: {'profile.picture': 1, 'profile.friends': 1}}
);
return allUsers;
});
And in the router:
Router.map(function() {
this.route('usersList', {
path: '/users',
waitOn: function () {
return Meteor.subscribe('users');
},
data: function () {
var user = Meteor.user();
return {users: Meteor.users.find({_id: {$ne : user._id}})};
}
});
});
(note that I still need to exclude the current user from the router query since the current user is always fully published)
This works, but:
the user list does not get updated when I change the user interest and then do a Router.go('usersList'). Only when I refresh the browser, my list is updated according to the user's new interest. No idea why.
this solution still publishes the users' friends in order to display my matching borders. I wish to add a temporary field in my publish query, setting it to "yes" if the user is in the user's friends and "no" otherwise, but... no success so far. I read I could use aggregate to maybe achieve that but haven't managed to so far. Also, aggregate doesn't return a cursor which is what is expected from a publication.
This problem makes me doubt about the praises about meteor being suitable for secure apps... This would be so easy to achieve in Rails or others!
EDIT: As requested, here is the code I have so far for the transition of my "matching" check to the server:
Meteor.publish('users', function () {
var user = Meteor.users.findOne({_id: this.userId});
var interest = user.profile.interest;
// retrieve all users, with their friends for now
allUsers = Meteor.users.find({ $and: [
{'_id': {$ne: user._id}},
{'profile.type':interest}
]},
{ fields: {'profile.picture': 1, 'profile.friends': 1}}
);
// ------------- ADDED ---------------
allUsers.forEach(function (lookedup) {
if (_.contains(lookedup.profile.friends, user._id))
lookedup.profile.relation = "yes";
else
lookedup.profile.relation = "no";
lookedup.profile.friends = undefined;
return lookedup;
});
// ------------- END ---------------
return allUsers;
});
Obviously this code doesn't work at all, since I cannot modify cursor values in a foreach loop. But it gives an idea of what I want to achieve: give a way to the client to know if a friend is matched or not, without giving access to the friend lists of all users to the client. (and also, avoiding having to do one request per each user during display to ask the server if this specific user matches this specific one)
You can add a transform function and modify a cursor docs on the fly
meteor Collection.find

NodeJS Module Pattern

I'm about to begin writing a new module for a system I'm developing. We use a MySQL database (so I'm using node-mysql) which contains a customers table.
What I want to achieve is:
Outside of the module I'm looking to do var C = new Customer(1) where 1 is the customer ID.
Now when I want to get something from this customer, I can do C.email or C.first_name which will simply return a value.
I also need to be able to set values back on this customer, C.email = 'example#example.com' or perhaps:
C.set('email', 'example#example.com')
What would be the best pattern to create such a model?
I already have something like this... Not exactly what you demanded but very close to that
I have generalized the core part and here is the code..Hope this will help....
var mysql = require('mysql');
var con = mysql.createConnection({
host:"yourHostName",
user:"yourUserName",
password:"yourPassword"
});
con.query("use databaseName");
function getCustomerDetails(custId){
con.query("select * from customer where custId = "+custId,function(err,result,fields){
if(!err)
return result;
else
console.log(err);
});
}
function updateCustomerDetails(custId,fieldName,fieldValue){
con.query("update customer set "+fieldName+" = "+fieldValue+" where custId = "+custId,function(err,result,fields){
if(!err)
return true;
else
console.log(err);
return false;
});
}
exports.getCustomerDetails = getCustomerDetails;
exports.updateCustomerDetails = updateCustomerDetails;
And then suppose you saved the module as dbAccessModule.js Then you can use the functions like this
var C = require('./dbAccessModule');
result = C.getCustomerDetails(1);
console.log(result.fieldName);
var success = C.updateCustomerDetails(1,'name','sumit');
if(success)
console.log('Table Updated successfully....');
else
// take necessary action according to your application
One thing you need to take care of is that if you are updating any field with string value
then please don't forget to surround the value of fieldValue with single quotes.
If this is not what you asked for then please ignore it....
I recently created two database modules you might be interested in checking out to see if they fit your needs - an ORM: http://bookshelfjs.org and Query Builder: http://knexjs.org
The ORM is based off of the design patterns of Backbone.js
So, you'd be able to do something like this:
// Create the base customer object
var Customer = Bookshelf.Model.extend({
tableName: 'customers'
});
// Create a new customer instance with an id of 1, fetch it, and then
// act on the result model 'customer'.
new Customer({id: 1}).fetch().then(function(customer) {
console.log(customer.get('name'))
customer.set('email', 'email#example.com')
return customer.save();
});
You could also extend the base Customer class to enable a shortened syntax, similar to what you're looking for:
// Create the base customer object, with a static findOne method.
var Customer = Bookshelf.Model.extend({
tableName: 'customers'
}, {
find: function(id) {
return new this({id: id}).fetch();
}
});
Customer.find(1).then(function(C) {
console.log(C.get('name'))
C.set('email', 'email#example.com')
});

Resources