snap.data is not a function in onUpdate - javascript

I have two functions that trigger onCreate and onUpdate however, the {uid} in onUpdate is returning undefined, whilst onCreate returns the {uid}.
How can I get the {uid} to work for onUpdate?
onUpdate.f.js - {uid} is undefined
exports = module.exports = functions.firestore
.document('users/{uid}/alerts/{name}') //UID is the User ID value stored in alerts
.onUpdate(snap => {
const user = snap.data();
console.log(user);
const msg = {
to: user.email,
from: 'notifications#example.com',
templateId: user.template,
dynamic_template_data: {
firstName: user.firstName,
email: user.email,
id: user.uid
}
};
return sgMail.send(msg).catch(err => console.log(`${user.email} - ${err}`));
});
onCreate.f.js - {uid} is correct
exports = module.exports = functions.firestore
.document('users/{uid}/alerts/{name}')
.onCreate(snap => {
const user = snap.data();
console.log(user);
const msg = {
to: user.email,
from: 'notifications#example.com',
templateId: user.template,
dynamic_template_data: {
firstName: user.firstName,
email: user.email,
id: user.uid
}
};
return sgMail.send(msg).catch(err => console.log(`${user.email} - ${err}`));
});
Fields in doc Alerts from frontend
doCreateAlert = (id, email, firstName, lastName, alertType, transactionEmailId) => {
const db = this.firestore;
return db.doc(`users/${id}/alerts/${alertType}`).set({
uid: id,
name: alertType,
email: email,
firstName: firstName,
lastName: lastName,
template: transactionEmailId,
dateCreated: new Date(),
dateModified: new Date()
});
};
The onUpdate is triggered by updating the database with onClick={this.updateAlert} as
updateAlert = () => {
const { firebase, userID } = this.props;
const companyTypeSetup = db.doc(`users/${userID}/alerts/emailVerified`);
companyTypeSetup.update({
dateModified: new Date()
});
};
on the frontend I receive the error of
Uncaught (in promise) Error: No document to update: projects/app/databases/(default)/documents/users/undefined/alerts/emailVerified
and the function is never run. If I manually update the doc in Firestore, I get an error in the firebase functions log as
TypeError: snap.data is not a function
at module.exports.functions.firestore.document.onUpdate.snap (/user_code/lib/auth/onUpdate.f.js:17:23)
at cloudFunctionNewSignature (/user_code/node_modules/firebase-functions/lib/cloud-functions.js:105:23)
at cloudFunction (/user_code/node_modules/firebase-functions/lib/cloud-functions.js:135:20)
at /var/tmp/worker/worker.js:754:24
at process._tickDomainCallback (internal/process/next_tick.js:135:7)

As the guide shows, onUpdate has two parameters: change and context. You use change since you may want to access the value before the update or after the update. Assuming you want the value after the update, that would look like this:
exports = module.exports = functions.firestore
.document('users/{uid}/alerts/{name}') //UID is the User ID value stored in alerts
.onUpdate((change, context) => {
const user = change.after.data();
console.log(user);
const msg = {
to: user.email,
from: 'notifications#example.com',
templateId: user.template,
dynamic_template_data: {
firstName: user.firstName,
email: user.email,
id: user.uid
}
};
return sgMail.send(msg).catch(err => console.log(`${user.email} - ${err}`));
});

Problem can easily be solved by reading the documents at Handle Event Data. However, if you are like me and skim the documents then the solution is
.onUpdate(change => {
const user = change.after.data();

Related

Firebase get UID from account create

On Creation of an account I need to make 2 collections 1 for Users 1 for Companies.
Within each one, I need to capture the UID. I just cant seem to grab it. I keep getting undefined when console.log it.
component
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
try {
await createUser(email, password).then(() => {
if (false) throw Error('Error')
//NEED Get UserID
addDoc(collection(db, 'Companies'), {
name: company,
//owner: userID,
users: [],
assets: []
}).then((docRef) => {
let companyID = docRef.id
addDoc(collection(db, 'Users'), {
name: name,
email: email,
company: [company],
companyID: [companyID]
//UserID: UserID
})
})
})
authContext
export const AuthContextProvider = ({ children }) => {
const [user, setUser] = useState({})
const createUser = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password)
}
I have tried everything in the firebase documentation but I believe since I am using context it may process data a bit differently.
The createUser() function is returning a UserCredential object. You can read the user's UID from it. Try refactoring the code as shown below:
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
const { user } = await createUser(email, password)
const docRef = await addDoc(collection(db, 'Companies'), {
name: company,
owner: user.uid,
users: [],
assets: []
})
await addDoc(collection(db, 'Users'), {
name: name,
email: email,
company: [company],
companyID: [docRef.id]
UserID: user.uid
})
}

Testing Firebase Cloud Functions give error

I am currently using Firebase Cloud Functions and MongoDB to create my app and here are the files that I have.
module.exports.updateUser = functions.https.onCall(async (data, context) => {
try {
// Retrieve the relevant data from the arguments
const { username, name, phone, postal, address, gender, dob } = data;
const userID = contect.auth.uid;
// Setting up the database
await connect(process.env.DB_URL);
// Create a schema for the database
const userSchema = new Schema({
name: String,
username: String,
phone: String,
postal: String,
address: String,
gender: String,
dob: String,
firebaseUID: String,
});
// Create a new database model
const User = model("users", userSchema);
const user = new User({
name: name,
username: username,
phone: phone,
postal: postal,
address: address,
gender: gender,
dob: dob,
firebaseUID: userID,
});
// Saving the user information
await user.save();
return { success: true, response: user };
} catch (e) {
return { success: false, error: e };
}
});
and I would like to test this file. Currently, my test code is as follows:
// Require and initialize firebase-functions-test. Since we are not passing in any parameters, it will
// be initialized in an "offline mode", which means we have to stub out all the methods that interact
// with Firebase services.
const test = require("firebase-functions-test")();
// Chai is a commonly used library for creating unit test suites. It is easily extended with plugins.
const assert = require("chai").assert;
// Sinon is a library used for mocking or verifying function calls in JavaScript.
const sinon = require("sinon");
// Require mongoose so that we can stub out some of its function
const mongoose = require("mongoose");
const { expect } = require("chai");
describe("Test addUser", () => {
let myFunction, mongooseSaveStub, mongooseConnectStub;
before(() => {
mongooseSaveStub = sinon.stub(mongoose, "save");
mongooseConnectStub = sinon.stub(mongoose, "connect");
myFunction = require("../index").updateUser;
});
after(() => {
mongooseInitStub.restore();
test.cleanup();
});
it("should pass", async (done) => {
const updateUser = test.wrap(myFunction);
const data = { name: "John Higgens" };
const context = { auth: { uid: "mock" } };
await updateUser(data, context).then((result) => {
expect(result.success).toBe(true);
done();
});
});
});
Can I ask 1) how I can test my cloud function, 2) how do I mock or stub the database and 3) where did I do wrongly? The current error that I have is:
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './lib/encoder' is not defined by "exports"

How to decipher between different Firebase errors in single Cloud Function catch block?

I have a Firebase Cloud Function that performs two async tasks (create user by Authentication and batch write by Firestore) with a single catch block for all errors. My question is how can I decipher if the error is thrown from Authentication or from Firestore in the catch block before I throw the HTTPS error to the client?
exports.createUser = functions.https.onCall((data, _context) => {
const email = data.email;
const password = data.password;
const birthday = data.birthday;
const name = data.name;
return admin.auth().createUser({
email: email,
password: password,
})
.then((userRecord) => {
const userId = userRecord.uid;
const db = admin.firestore();
const batch = db.batch();
const testDocRef1 = db.collection("test1").doc(userId);
const testDocRef2 = db.collection("test2").doc(userId);
batch.create(testDocRef1, {name: name, email: email});
batch.create(testDocRef2, {name: name, birthday: birthday});
return batch.commit().then(() => {
return Promise.resolve({"userId": userId});
});
})
.catch((error) => {
// how can I decipher which async task this error came from?
throw new functions.https.HttpsError("unknown", "Refer to details for error specifics.", error);
});
});

Firebase function: cannot read property 'userId' of undefined

im trying to send an email through sendgrid via. a firestore trigger. I just cant seem to get the userId out from my context. Any suggestions?
Image link to error message
exports.firestoreEmail = functions.firestore
.document('users/{userId}')
.onCreate((context) => {
const userId = context.params.userId;
const db = admin.firestore();
return db
.collection("users")
.doc(userId)
.get()
.then((doc) => {
const user = doc.data();
const msg = {
to: user.email,
from: "<myEmail>",
subject: "New Follower",
// custom templates
templateId: "d-1584af76f10d475d8cc99d28e5501cf9",
substitutionWrappers: ["{{", "}}"],
substitutions :{
name: user.displayName
}
};
return sgMail.send(msg);
})
.then(() => console.log("email sent!"))
.catch((err) => console.log(err));
});
context should be the second parameter to your function. It doesn't matter that you named it "context" - the position matters entirely. The first argument is a DocumentSnapshot of the new document, so you'll have to give it name as the first parameter, even if you don't use it:
exports.firestoreEmail = functions.firestore
.document('users/{userId}')
.onCreate((snapshot, context) => {
const userId = context.params.userId;

Combining Firebase Firestore query results in Firestore Functions before sending email

I'm having several issues wrapping my head around promises. This time I'm trying to combine 4 Firestore queries into one and validating if any of those queries returns a result, if it does, I want to throw an error to the user, if not I want to proceed into sending the email and storing it's data.
How do I wait/combine the queries and validate the results?
Here is what I have done so far:
export const sendMail = functions.https.onRequest((req: functions.Request, res: functions.Response) => {
const { name, email, doc, state, city, phone, msg, } = req.body
var dbPromises = [];
const ip1 = req.headers["fastly-client-ip"]
dbPromises.push(firestore.collection('messages').where('ip1', '==', ip1).get())
const ip2 = req.headers["x-forwarded-for"]
dbPromises.push(firestore.collection('messages').where('ip2', '==', ip2).get())
dbPromises.push(firestore.collection('messages').where('email', '==', email).get())
dbPromises.push(firestore.collection('blocked-emails').where('email', '==', email).get())
Promise.all(dbPromises)
.then(() => {
// TODO validate if there is any result > 0, if any, throw error to the user, else continue into sending the email
});
const mailOptions = {
from: `"${name}" <${email}>`,
to: 'a_email#gmail.com',
replyTo: `"${name}" <${email}>`,
subject: `Contact - ${name}`,
html: `<div>${name}</div>
<div>${email}</div>
<div>${doc}</div>
<div>${state}</div>
<div>${city}</div>
<div>${phone}</div>
<div>${msg}</div>`,
}
cors()(req, res, () => {
transport
.sendMail(mailOptions)
.then(() => {
firestore
.collection('messages')
.add({
name: name,
email: email,
doc: doc,
state: state,
city: city,
phone: phone,
msg: msg,
ip1: ip1,
ip2: ip2,
})
.then(() => {
return res.status(201).send()
})
})
.catch(error => {
console.log('Internal error.', error)
return res.status(500).send()
})
})
})
How to really combine and check if any result is > 0, returning an error to the user?
Ok, so here's what I was able to come up with. I fired this up in a debugger to step through and make sure everything works.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const cors = require('cors');
// Needed this to connect to Firestore, my code not yours
admin.initializeApp();
admin.firestore().settings({ timestampsInSnapshots: true });
// Emulate the transport.sendMail() for debug purposes
let transport = {
sendMail: (options) => {
return new Promise((resolve, reject) => {
console.log(`Sending Mail: ${options}`);
resolve(options);
});
}
}
module.exports.sendMail = functions.https.onRequest((req, res) => {
if (req.method !== 'POST') // won't have a body
return res.status(400).send(`Error: ${req.method} is not Accepted.`);
// extract params from body
const { name, email, doc, state, city, phone, msg, } = req.body
let dbPromises = [];
let firestore = admin.firestore(); // alias to lineup with OP's code
// extract headers
const ip1 = req.headers["fastly-client-ip"];
const ip2 = req.headers["x-forwarded-for"];
// validate input, if bad: emit Client error
if (!ip1 || !ip2 || !email)
return res.status(400).send("Error: Invalid Request.");
// query previous message existence
dbPromises.push(firestore.collection('messages').where('ip1', '==', ip1).get());
dbPromises.push(firestore.collection('messages').where('ip2', '==', ip2).get())
dbPromises.push(firestore.collection('messages').where('email', '==', email).get())
dbPromises.push(firestore.collection('blocked-emails').where('email', '==', email).get())
// Need to return a promise so your function doesn't timeout
return Promise.all(dbPromises)
.then(resultDocs => {
if (resultDocs.length !== 4)
throw new Error("Programmer Error");
// validate if there is any result > 0, if any, throw error to the user
if (resultDocs[0] !== null && resultDocs[0].docs.length !== 0)
throw new Error(`${ip1} already exists`);
if (resultDocs[1] !== null && resultDocs[1].docs.length !== 0)
throw new Error(`${ip2} already exists`);
if (resultDocs[2] !== null && resultDocs[2].docs.length !== 0)
throw new Error(`${email} already exists`);
if (resultDocs[3] !== null && resultDocs[3].docs.length !== 0)
throw new Error(`${email} is blocked`);
return null;
})
.then(() => {
// Build message for mailer
const mailOptions = {
from: `"${name}" <${email}>`,
to: 'a_email#gmail.com',
replyTo: `"${name}" <${email}>`,
subject: `Contact - ${name}`,
html: `<div>${name}</div>
<div>${email}</div>
<div>${doc}</div>
<div>${state}</div>
<div>${city}</div>
<div>${phone}</div>
<div>${msg}</div>`,
}
let innerPromise = null;
// Fix headers for cross-origin
cors()(req, res, () => {
// send mail returns a promise
innerPromise = transport.sendMail(mailOptions);
});
return innerPromise; // return promise or null
})
.then(sendMailResult => {
if (!sendMailResult) {
// not sure if transport.sendMail even returns result
// do validation here if yes
}
// write message to store
return firestore
.collection('messages')
.add({
name: name,
email: email,
doc: doc,
state: state,
city: city,
phone: phone,
msg: msg,
ip1: ip1,
ip2: ip2,
});
})
.then(() => {
return res.status(201).send("Success")
})
.catch(err => {
console.log(err);
res.status(500).send(String(err));
})
})
The main take-away is how the promises are structured: always return finished data or another promise from inside, and chain them together using .then. The main function should also return a promise, so that the cloud-function doesn't time-out before everything completes.

Categories