I'm trying to use await inside a function I think it's a passthrough function.
I'm creating a user with 'createUserWithEmailAndPassword' then passing the 'cred' object into what to do if the user is created successfully. If the user is created succesfully the code then writes the user info and sets place holders for further signup info in the database ready for the user to comeplete sign up.
The problem is, the page is refreshing to the next phase of sign up (a different page) before the database writes are being completed.
I have no idea how to get await to work inside the '.then()' section and if the database writes arent in that part where the 'cred' object is being fed the 'cred.user.uid' won't work which is integral
Here is my code:
await createUserWithEmailAndPassword(auth, email, password)
.then((cred) => {
console.log('User Created:', cred.user);
//Create Stand-In User Doc
setDoc(doc(db, 'users', cred.user.uid, 'userInfo', 'signupInfo'), {
dob: dob,
signupAge: age,
tosAccepted: true,
});
setDoc(doc(db, 'users', cred.user.uid, 'technical', 'signup'), {
passedGo: false,
userDetailsComplete: false,
phoneNoComplete: false,
initialized: false
});
signupForm.reset();
document.querySelector('#accCreated').removeAttribute('style', 'display: none;');
})
I have tried putting the database updates outside of the .then() section but I need the 'cred' data for it to work.
I have also tried creating a async function iniside the .then() section but that also cuts the contents of the function off from the 'cred' object
I have also tried putting the database updates inside the 'onAuthStateChanged' function hoping that would provide the 'cred' info / uid etc. But that didnt work. I don't think the user is logged in at sign up
Combining await with then is not the way to go. await lets you specifically skip the then part of the asynchronous function and store the return of said function into a variable.
const cred = await createUserWithEmailAndPassword(auth, email, password);
console.log('User Created:', cred.user);
Now, assuming that setDoc is also async, you'd need to await those, too, so the reset doesn't get triggerd before it's done.
await setDoc(...);
await setDoc(...);
signupForm.reset();
But to answer your other question: You can make a then function async like this:
.then(async (cred) => { ... })
Related
I'm trying to make a transaction with mongoose.
When I'm running the code, it looks like mongoose document remembers the session even after calling await session.endSession(). And later, when I call .save() on that document, I get error "Use of expired sessions is not permitted".
This is the demo (just a simplified example):
const MessageSchema = new mongoose.Schema({ text: String }, {strict: 'throw'});
const Message = mongoose.model('Message', MessageSchema);
const session = await mongoose.startSession();
let message;
await session.withTransaction(async () => {
message = (await Message.create([{text: 'My message'}], {session}))[0];
});
await session.endSession();
message.text = 'My message 2';
await message.save();
That final .save() is throwing that error. If you would log message.$session().hasEnded you would get true. My expectation was that, if session has ended, .save() would be smart not to use it.
What I want to achieve:
Create some documents with transaction, commit them, so they are in the database.
Later, use that same document, that is already in the database, to make some changes to it.
What am I doing wrong here? How can I prevent .save() from throwing an error and trying to use expired session?
There are no sections on endSession anywhere in the mongoose docs, but as you very well discovered it yourself, when this function is called, it only sets a flag to your session object rather than destroying it. If it was destroyed, you would have another error.
My expectation was that, if session has ended, .save() would be smart not to use it.
It is, in fact, smart enough not to use it, the framework only informs you that you are attempting an illegal instruction.
What I want to achieve: Create some documents with transaction, commit them, so they are in the database.
That's exactly what you do with your withTransaction call. This wrapper helps you create, commit and abort/retry in case something bad happened, so after that call and if all ended well you indeed successfully created a document in the database.
Later, use that same document, that is already in the database, to make some changes to it.
Depends on what you mean by "later". If it's part of the same endpoint (and I don't know why you would immediately modify that document rather than committing it rightfully in the first place), then as I said in the comments, moving the endSession call would likely fix the issue:
const MessageSchema = new mongoose.Schema({ text: String }, {strict: 'throw'});
const Message = mongoose.model('Message', MessageSchema);
const session = await mongoose.startSession();
let message;
await session.withTransaction(async () => {
message = (await Message.create([{text: 'My message'}], {session}))[0];
});
message.text = 'My message 2';
await message.save();
await session.endSession();
If it's part of another endpoint, then just make another transaction like you just did, except you modify the document instead of creating it. Or if you don't need transaction at all, use a method like findOneAndUpdate or findByIdAndUpdate. I reckon you seem to be familiar enough with JS and Mongoose to do that on your own.
What am I doing wrong here?
Basically, not much. You already understood that you can't call .save() after ending a session.
How can I prevent .save() from throwing an error and trying to use expired session?
You can't. It is merely a reminder that you are attempting an illegal operation. You can however try/catch the error and decide to do nothing about it in your catch clause, OR write a if statement checking for message.$session().hasEnded.
Well I made this Library app, where an user can login and add books. So, when a user login the app fetch data from a firestore collection, cool. The problem exists when the user login once, logout and then login again without refreshing the app. If the user do this twice, the fetch twice, if thrice, the fetch thrice. The code that executes multiple times its the fetchBooks(), the signInWithGoogle() only executes once. Here's the code involved:
function signInWithGoogle(){
const provider = new firebase.auth.GoogleAuthProvider()
auth.signInWithPopup(provider)
.then(result => {
// Create the new user document in firestore
createNewUserDocument(result.user)
// fetch feed data
auth.onAuthStateChanged(user =>{
user? fetchBooks() : null
})
}).catch(err => {
console.log(err)
})
signUpForm.reset()
signUpModal.hide()
signInForm.reset()
signInModal.hide()
}
function fetchBooks() {
const docRef = db.collection('users').doc(auth.currentUser.uid).collection('books')
docRef.get().then(querySnapshot =>{
console.log(querySnapshot)
querySnapshot.forEach(doc => {
const data = doc.data()
console.log(doc.data());
addCardToHTML(data.title, data.author, data.pages, data.description, data.read)
})
})
}
onAuthStateChanged is a subscription that triggers itself when there's a change in the user's authentication state.
So it will trigger when you log in, when you log out, etc.
So ideally you'd want to wait until the user logs in, and then call the fetchBooks() function, but if you keep doing it inside of the subscriber the function will trigger any time the subscriber emits a new value.
I would recommend starting with a restructure of your code to have functions that do individual things. Right now, you have a function signInWithGoogle. That function should only sign the user in with Google and return a promise with the result of that sign in. Instead, you have it signing in the user, fetching books (which itself is also fetching books AND modifying the DOM), and calling methods on your signUp elements.
Restructuring this to have some other top-level function would likely help you handle your problem easier. Specifically, try something like this:
function handleSignIn() {
signInWithGoogle()
.then(fetchBooks)
.then(books => {
books.forEach(book => addCardToHTML(...))
})
}
This is a good start because now it's clear what each individual function is doing. So now to handle your specific issue, I'll assume that the problem you're facing is that you're seeing the books be added multiple times. In that case, I would think what you'd want to happen is that:
When a user is signed in, you want to load their books and display them on the page.
When they log out, you want the books to be unloaded from the screen
When they log back in, the books are re-loaded and displayed.
If all of those assumptions are correct, then your problem wouldn't be with the code you have, but rather the signout functionality. When the user signs out, you need to add a function that will remove the books from the HTML. That way, when they sign back in after signing out, the handleSignIn function will kick off again and the addCardToHTML function will be running on a blank HTML page rather than a page that already has the cards.
Example:
function handleSignOut() {
signOut()
.then(clearBookCards)
}
function clearBookCards() {
// Manipulate DOM to remove all of the card HTML nodes
}
The main goal of my system is to update the name of the user who posted on my forum if the authenticated user change or rename his or her account name.
The whole process is error-free but unfortunately, the other user who posted in the forum also updated their name.
So this is the output:
I try the following:
I use the WHERE function in Firebase to filter the post made by the user (log in user itself). I dont know why the whole process is failed.
This is the snippet code.
async updateAll(username) {
const batch = this.afs.firestore.batch();
// cUser is the USER ID
const userRef = this.afs
.collection('post', (ref) => ref.where('postedById', '==', this.cUser))
.ref.get();
(await userRef).forEach((element) => {
batch.update(element.ref, {
postedBy: username,
});
});
return batch.commit();
}
You end your query with .ref.get(). The .ref in there, actually returns the collection on which you run the query, so you end up loading the entire post collection.
You'll want to subscribe to snapshotChanges instead, or just use the regular JavaScript SDK to accomplish this (as you're not accessing the UI directly, I typically find that easier):
const userRef = firebase.firestore()
.collection('post').where('postedById', '==', this.cUser).get();
(await userRef).forEach((element) => {
batch.update(element.ref, {
postedBy: username,
});
});
I'm tyring to create a new user with some additional data (eg: subscribeToEmail field etc.). From what I have read online, the way to go about doing this is to authenticate a user (eg: by using createUserWithEmailAndPassword), and then using the uid that I obtain from that to create a new document in a users collection. That is what I'm trying to do below:
const handleSignup = async (formData) => {
const {email, password, ...otherUserData} = formData;
const {user} = await auth.createUserWithEmailAndPassword(email, password);
console.log("generating user doc at signup...");
await generateUserDocument(user, otherUserData); // creates user doc (called 2nd)
}
The generateUserDocument will create a user document (if it doesn't already exist) in the users collection using the uid of the user obtained from the createUserWithEmailAndPassword function call. I also have also set up an auth state change event handler:
auth.onAuthStateChanged(async (userAuth) => { // get the current user
if (userAuth) {
const user = await generateUserDocument(userAuth); // user (should) already exists, so generateUserDocument will fetch the user rather than creating a new user doc, but this doesn't happen, as it is called 1st and not 2nd
dispatch(login(user)); // login logic
} else {
dispatch(logout());
}
});
The issue here is, when I call createUserWithEmailAndPassword the onAuthStateChanged callback triggers, which then calls generateUserDocument before I have actually created a user document with generateUserDocument(user, otherUserData); inside of the handleSignup method. In other words, the fetch user method: generateUserDocument inside of .onAuthStateChange() is being invoked before the user is actually created, which is done by the generateUserDocument inside of the handleSignup method. As a result, the user data I'm fetching inside of authStateChange doesn't include the details I'm after.
Is there a way to fix this so that my function call after the auth.createuserWithEmailAndPassword() is called before the onAuthStateChange event handler is executed (rather than after)? I have thaught about using something like .onSnapshot() perhaps, but I'm thinking that this might be a little overkill as the user data table shouldn't really need to be continously be listened too, as it will rarely changed. Preferably there is a lifecycle method that gets invoked before onAuthStateChanged that I could use to populate my users collection, but I haven't been able to find much on that.
For reference, I have been following this article regarding associating additional user data with a auth-user record.
Is there a way to fix this so that my function call after the
auth.createuserWithEmailAndPassword() is called before the
onAuthStateChange event handler is executed (rather than after)?
No, there is no out-of-the-box way, because
On successful creation of the user account with createuserWithEmailAndPassword() the user will also be signed in to your application (see the doc), and
The onAuthStateChange() observer is triggered on sign-in or sign-out.
So you indeed need to wait the user Firestore doc is created before proceeding. In my opinion, the best approach is the one you have mentioned, i.e. setting a listener to the user document.
You can do that in such a way you cancel the listener right after the first time you get the data from the user doc, as shown below. This way the user doc is not continuously being listened to.
auth.onAuthStateChanged((userAuth) => { // No more need to be async
// get the current user
if (userAuth) {
const userDocRef = firestore.collection('users').doc(userAuth.uid);
const listener = userDocRef.onSnapshot((doc) => {
if (doc.exists) {
console.log(doc.data());
// Do any other action you need with the user's doc data
listener(); // Calling the unsubscribe function that cancels the listener
dispatch(login(user));
}
});
} else {
dispatch(logout());
}
});
Another approach could be to use a Callable Cloud Function which, in the backend, creates the user in the Auth service AND the document in Firestore. Your handleSignup function would be as follows:
const handleSignup = async (formData) => {
const createUser = firebase.functions().httpsCallable('createUser');
await createUser({formData});
// The user is created in the Auth service
// and the user doc is created in Firestore
// We then need to signin the user, since the call to the Cloud Function did not do it!
const {email, password, ...otherUserData} = formData;
await auth.signInWithEmailAndPassword(email, password);
// The onAuthStateChanged listener is triggered and the Firestore doc does exist
}
I am building a Slack app using the JavaScript Bolt framework. The concept of the app is just listening to specific message keywords in channels and then forwarding those messages to the users of the app.
What I am trying to achieve is including a permalink in the forwarded message. I am trying to use the chat.getPermalink method to get the url and then include that in my chat.postMessage method. I am trying to leverage Bolt's 'Context' in order to pass the property in chat.getPermalink to chat.postMessage. I am asking for help here because I cannot get the Context to work..
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET
});
let token = process.env.SLACK_BOT_TOKEN,
web = new WebClient(token);
let jira_text = "jira";
let rdu_qa = '#rdu_qa';
//Get permalink
async function PermaLinks({payload, context, next}) {
let perm = app.client.chat.getPermalink({
token: context.botToken,
channel: "C0109KMQCFQ",
message_ts: payload.ts
});
context.permalink = perm.permalink;
await next();
}
app.event('message', PermaLinks, async ({ payload, message, context}) => {
let userzArray = ["D010Q34TQL9", "UVBBD8989"];
//if channel is general and incldues the text 'Jira' or 'rdu_qa'
if (payload.channel === "C0109KMQCFQ") {
if (payload.text.includes(jira_text) || payload.text.includes(rdu_qa)) {
try {
// Call the chat.postMessage to each of the users
let oneUser = await userzArray.forEach(userId => { app.client.chat.postMessage({
token: context.botToken,
bot_id: "USLACKBOT",
channel: userId,
blocks: [
{
type: "section",
text: {
text: payload.text,
type: "mrkdwn"
},
fields: [
{
type: "mrkdwn",
text: `posted by <#${message.user}>`
},
{
type:"mrkdwn",
text: "in General channel" //channel.name//getChannelNameGeneral
},
{
type:"mrkdwn",
text: context.permalink // Permalink should be right here
}
]
},
{
"type": "divider"
},
] // End of block of Jira notification stuff
});
});
// console.log(result);
} catch (error) {
console.error(error);
}
} // If text sent to General channel includes keyword 'Jira' or 'rdu_qa'
} //end of if message was posted in General channel
There are a couple problems I can see in the example code, but I think the main issue regarding the context is that you're storing a Promise as context.permalink, not the actual result of the method call. In order to store the result, you should use the await keyword before calling the method (app.client.chat.getPermalink(...)).
I've revised the code you shared here, and I'll explain the modifications below.
const { App } = require('#slack/bolt');
const token = process.env.SLACK_BOT_TOKEN
const app = new App({
signingSecret: process.env.SLACK_SIGNING_SECRET,
token,
});
// Users who should be notified when certain messages are heard
let userzArray = ["D010Q34TQL9", "UVBBD8989"];
// Conversation IDs corresponding to the users in the array above. This variable will be set automatically when the app starts.
let conversationsToNotify;
// Match messages that include the text 'jira' or '#rdu_qa'
app.message(/jira|#rdu_qa/, async ({ message, client }) => {
// Match the messages that are in the specified channel
if (message.channel === 'C0109KMQCFQ') {
try {
// Get a permalink to this message
const permalinkResult = await client.chat.getPermalink({
channel: message.channel,
message_ts: message.ts,
});
// Send a message to each user containing the permalink for this message
await Promise.all(conversationsToNotify.map((conversationId) => {
return client.chat.postMessage({
channel: conversationId,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `>>> ${payload.text}`,
},
fields: [
{
type: 'mrkdwn',
text: `posted by <#${message.user}>`,
},
{
type: 'mrkdwn',
text: `in <#${message.channel}>`,
},
{
type:'mrkdwn',
text: `<Original|${permalinkResult.permalink}>`,
},
],
},
{
type: 'divider'
},
],
});
}));
} catch (error) {
console.error(error);
}
}
});
async function convertUsersToConversations(input) {
return Promise.all(input.map((id) => {
// For all IDs that seem like user IDs, convert them to a DM conversation ID
if (id.startsWith('U')) {
return app.client.conversations.open({
token,
users: id,
})
.then((result) => result.channel.id);
}
// For all IDs that don't seem to belong to a user, return them as is
return id;
}));
});
(async () => {
// Start the app
conversationsToNotify = await convertUsersToConversations(userzArray);
await app.start(process.env.PORT || 3000);
console.log('⚡️ Bolt app is running!');
})();
I've removed the initialization of a new WebClient object. In Bolt v1.6.0 and later, there is a client argument available in listeners and middleware which you can use to call Web API methods instead. The advantage of using the client argument is that you don't need to read the token from the context and pass it as an argument for each method call on your own, Bolt will find the right token for you.
Instead of using the app.event('message', ...) method to listen for message events, I've changed to using app.message(...). The latter works mostly the same, but has one more advantage: you can pass a pattern to match the text of a message as the first argument (before the listener function): app.message(pattern, ...). That helps remove some of the conditions inside the listener. Instead of using just the two string variables jira_text and #rdu_qa, I've combined them in a single regular expression that matches when either of those values is seen in the text: /jira|#rdu_qa/.
Instead of using middleware to find the permalink of a message, I've moved that code into the listener. Middleware should be used to reuse code across multiple listeners (or global middleware to reuse code across all listeners). In your example, it doesn't seem like the code to find the permalink is being reused, but if you do use this in many listeners, it should be relatively easy to extract. Another advantage is now the logic only runs after the pattern was matched, so you're not making these calls for every single message that the bot sees in all channels that it is a member of (this is much better for performance).
Use Promise.all() to collect the Promises of each call to chat.postMessage into one promise. Currently, you're using userzArray.forEach(...), which doesn't return anything. So then using await on that value will immediately resolve, and doesn't really do anything useful. What we need to do is collect each of the Promises and wait for them to all complete. This is what Promise.all() does. We just need an array of Promises to pass in, which we can get by simply changing userzArray.forEach() to userzArray.map().
There's a problem with the way you're calling chat.postMessage. You're trying to use Slackbot to send those messages, but that's not recommended because users are less likely to understand where that message is coming from. Instead, you should send this message as a DM from your bot user. In order to do that, you need a conversation ID, not a user ID, for each user you want to send this notification to. One of the items in userzArray is already a DM conversation ID (it starts with a D), but the other is not. In order to make this work consistently, I've created the conversationsToNotify array which contains the conversation IDs for each user after calling conversations.open to create a DM. Now in the code, you'll see conversationsToNotify.map() instead of userzArray.map(). Your Slack app will now need the im:write and chat:write permission scopes (don't forget to reinstall once you add scopes). Looking up the conversation IDs will slow down your app from starting up if the number of users in the array gets larger. My recommendation would be to save the conversation IDs in your code (or in a database) instead of the user IDs. This will ensure a consistently fast start up time for your app.
There's an opportunity to do even better. What happens when the first call to chat.postMessage fails? The way I've written the code above, the error would be logged to the console, but later if the second call fails, there's no way to know. That's because Promise.all() returns a promise that will reject as soon as any one of the promises rejects, and then ignores what happens afterwards. If you're using Node v12.9.0 or greater, I would recommend using Promise.allSettled() instead (which would require a few changes in your catch clause as well).
General cleanup:
Use message argument in the listener everywhere instead of payload argument. These are actually the same value when dealing with message events. payload is mostly only useful in middleware that handle several kinds of events (action, event, view, shortcut, etc) so that there's one way to refer to all of their payloads.
Move userzArray outside the listener, and make it a constant. There's no point in redeclaring it inside the listener each time it runs, and it doesn't change.
I added a function to convert from user IDs to conversation IDs (convertUsersToConversations). This function is called before the app is started to avoid a race condition where the an incoming message is handled before the app knows which channels to notify.
Formatted the text content of the message as quoted text, formatted the channel mention, and formatted the permalink. One improvement I'd also recommend is to use a context block to show the message author's name and avatar image.