FireStore: How to merge timestamp data to existing document? - google-cloud-firestore

I am new to FireStore and and building an app where users can bookmark photo documents and show them on their personal feed. This works fine. Now, I want to be able to sort the bookmarks by bookmarked date when the user is viewing their personal feed (orderBy method). Thus, to make this happen, I figured I'd add a timestamp value at the moment the user bookmarks the document.
Here's my attempt. I wanted to verify with the community whether this is a good way to do it. I am concerned about redundancy and extra writes.
async addDocToFeed({state}, doc) {
try {
const feedRef = this.$fireStore
.collection(`users/${state.userProfile.uid}/feed`)
.doc(doc.id)
await feedRef.set(doc) < --- copy record to user's feed collection (see json sample below)
const bookmark = this.$fireStore
.collection(`users/${state.userProfile.uid}/feed`)
.doc(doc.id)
bookmark.update({
bookmarked: this.$fireStoreObj.FieldValue.serverTimestamp()
})
// })
console.log('doc bookmarked')
} catch (error) {
console.error('error updating doc', error)
}
}
Example JSON of doc before adding the timestamp:
{"id":"1KecNCqYlcVRjq4BLCbZ","comments":"__vue_devtool_nan__","url":"https://firebasestorage.googleapis.com/v0/b/vue-photoapp-api.appspot.com/o/photos%2F0.jpg?alt=media&token=ee23b95b-b5d8-4abe-b1b9-e335d591b413","tags":["router","Texas"],"filename":"0.jpg","description":"test with new router setup","createdAt":{"seconds":1596020630,"nanoseconds":473000000},"title":"test with new router setup","status":"Unsolved","userId":"SvuTxDtHXJdBHImNQWByqnO3F2U2","displayName":"MrRouter"}
I tried to do:
await feedRef.set({doc, bookmarked: this.$fireStoreObj.FieldValue.serverTimestamp()}, {merge: true})
but that erased all the data and only added the bookmarked timestamp.
Thanks for any advice or assurances I'm on the right track (or not)

this
.$fireStore
.collection(users/${state.userProfile.uid}/feed)
.doc(doc.id);
.set({
bookmarked: Date.now()
}, {merge: true})
.then(() => {
resolve(true);
}).catch((error) => {
reject(error)
})
This should work.

Related

How to pass data for sync function using watermelondb

Good day everyone, I am working with watermelondb and I have the code below, but I don't know how to actually use it. I am new in watermelondb and I don't know how to pass data as props to the pullChanges and pushChanges objects. How do I pass necessary data like changes and lastPulledAt from the database into the sync function when I call it. And I need more explanation on the migrationsEnabledAtVersion: 1 too. Thanks in advance for your gracious answers.
import { synchronize } from '#nozbe/watermelondb/sync'
async function mySync() {
await synchronize({
database,
pullChanges: async ({ lastPulledAt, schemaVersion, migration }) => {
const urlParams = `last_pulled_at=${lastPulledAt}&schema_version=${schemaVersion}&migration=${encodeURIComponent(JSON.stringify(migration))}`
const response = await fetch(`https://my.backend/sync?${urlParams}`)
if (!response.ok) {
throw new Error(await response.text())
}
const { changes, timestamp } = await response.json()
return { changes, timestamp }
},
pushChanges: async ({ changes, lastPulledAt }) => {
const response = await fetch(`https://my.backend/sync?last_pulled_at=${lastPulledAt}`, {
method: 'POST',
body: JSON.stringify(changes)
})
if (!response.ok) {
throw new Error(await response.text())
}
},
migrationsEnabledAtVersion: 1,
})
}
Watermelondb's documentation is terrible and its link to typescript even worse.
I spent almost a week to get 100% synchronization with a simple table, now I'm having the same problems to solve the synchronization with associations.
Well, the object you need to return in pullChanges is of the following form:
return {
changes: {
//person is the name of the table in the models
person: {
created: [
{
// in created you need to send null in the id, if you don't send the id it doesn't work
id: null,
// other fields of your schema, not model
}
],
updated: [
{
// the fields of your schema, not model
}
],
deleted: [
// is a string[] consisting of the watermelondb id of the records that were deleted in the remote database
],
}
},
timestamp: new Date().getTime() / 1000
}
In my case, the remote database is not a watermelondb, it's a mySQL, and I don't have an endpoint in my API that returns everything in the watermelon format. For each table I do a search with deletedAt, updatedAt or createdAt > lastPulledAt and do the necessary filtering and preparations so that the data from the remote database is in the schema format of the local database.
In pushChanges I do the reverse data preparation process by calling the appropriate creation, update or deletion endpoints for each of the tables.
It's costly and annoying to do, but in the end it works fine, the biggest problem is watermelon's documentation which is terrible.

Mongoose findOneAndUpdate() only updates if document already exists, doesn't create new doc if not?

I ran into a problem with my javascript bot, my custom prefixes don't get saved if there isn't yet a custom prefix for that server, if there is though, it does get updated correctly.
await mongo().then(async (mongoose) => {
try {
let newprefix = content.replace(`${prefix}setprefix `, '')
await prefixSchema.findOneAndUpdate({_id: guild.id}, {_id: guild.id, prefix: newprefix})
.then(async () => {
console.log(`updated prefix for guild: ${guild.id}`)
await channel.send(`Succesfully updated prefix for this server to '${newprefix}'`)
message.guild.me.setNickname(`[${newprefix}] - Helix`)
})
.catch(async (err) => {
console.error(`failed to update prefix for guild: ${guild.id}\n${err}`)
await channel.send(`Failed to update prefix.`)
})
console.log("saved to db")
} catch {
console.log("Something went wrong while saving new prefix for a server.")
} finally {
mongoose.connection.close()
}
The bot does print and send that it succesfully updated the prefix, but if there isn't already a document for the guild.id, nothing is saved. What did I do wrong and how can I solve it?
Thanks for reading!
Model.updateOne()
Parameters
[options.upsert=false] «Boolean» if true, and no documents found, insert a new document
MongoDB will update only the first document that matches filter regardless of the value of the multi option.
Use replaceOne() if you want to overwrite an entire document rather than using atomic operators like $set.
Example:
const res = await Person.updateOne({ name: 'Jean-Luc Picard' }, { ship: 'USS Enterprise' });
res.n; // Number of documents matched
res.nModified; // Number of documents modified
please visit https://mongoosejs.com/docs/api.html#model_Model.updateOne for more information.

Firestore cloud functions - can I send emails to users every time an document is added to a different collection (not 'users')?

I'm very new to cloud functions but have set up a couple of firestore cloud functions & got them working sending emails to individuals when their user document is created or updates but I really want to send emails to each user when a document is added to another collection (it's a react app displaying videos - I want to update all subscribed users when a new video is added). I can restructure the db if necessary but it currently has users and videos as the only 2 root level collections.
I've tried using .get() to the users collection to collect all their email addresses to put in the 'to' field of the email, but I just get an error saying 'db.get() is not a function'. After researching I found a couple of things to try but both got the same error:
functions.firestore
.document('users/{uid}').get()
.then((querySnapshot) => {
and
const admin = require('firebase-admin');
var db = admin.firestore();
db.document('users/{uid}').get()
.then((querySnapshot) => {
Can anyone help? Is it possible to do this? It seems that in theory the new Email Trigger Extension might do this but tbh I'd rather code it myself and learn how it works as I go - especially having 'cracked' the first two! But I can't find any way to access the contents of two collections within one function & I've spend days looking in all the usual places for any info so I'm beginning to think maybe cloud functions can only access one collection per function - but I also can't find anything that actually says that...?
Here is the whole function using the format I have working for the other 2 functions (apart from trying to access the users):
const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const admin = require('firebase-admin');
var db = admin.firestore();
//google account credentials used to send email
var transporter = nodemailer.createTransport({
host: process.env.HOST,
port: 465,
secure: true,
auth: {
user: process.env.USER_EMAIL,
pass: process.env.USER_PASSWORD,
}
});
//Creating a Firebase Cloud Function
exports.sendNewVidEmail = functions.firestore
.document('videos{uid}')
.onCreate(async (snap, context) => {
const newValue = snap.data();
// access title & description
const newVideoTitle = newValue.title;
const newVideoDescription = newValue.description;
//try to access users
db.document('users/{uid}').get()
.then((querySnapshot) => {
let users = [];
querySnapshot.forEach((doc) => {
// check for data
console.log(doc.id, " => ", doc.data());
users.push(doc.data().subscriberEmail)
//check for 'users' array
console.log('users = ', users)
});
})
.catch((error) => {
console.log("Error getting documents: ", error);
});
// perform desired operations ...
if (newVideoTitle) {
const mailOptions = {
from: process.env.USER_EMAIL,
to: users,
subject: 'new video!',
html: `
<h2> xxx has a new video called ${newVideoTitle}</h2>
<p> xxx says this about ${newVideoTitle}: ${newVideoDescription}.
</p></ br>
<h4>Watch ${newVideoTitle} here.and please tick 'like' if you like it!</h4>
<p>Yours,</p>
<p>xxx :-) </p>`
};
return transporter.sendMail(mailOptions)
.then(() => {
console.log('sent')
})
.catch(error => {
console.log(error)
return
})
}
});
Well, I fixed it!! The code I was using was almost there, and thanks to a great youtube tutorial from Jeff Delaney (fireship) [here][1] I got the code I needed. 2 lines and so simple, and now I'm kicking myself, but in case anyone else gets stuck on this, my error was to try & use .forEach() (from the docs) and .push() to get the users' emails array when just using .map() on the snapshots creates the users array perfectly and then it worked!
const userSnapshots = await admin.firestore().collection('users').get();
const emails = userSnapshots.docs.map(snap => snap.data().subscriberEmail);
Hope it helps someone down the line:
https://www.youtube.com/watch?v=vThujL5-fZQ

Insert into relationship table using id created at user registration

I have two tables as seen below
The first table is for users and is populated via a registration form on the client side. When a new user is created, I need the second 'quotas' table to be populated with date, amount, and linked with the user id. The 'user_id' is used to pull the quotas information in a GET and display client side. I am having issues using the 'id' to populate the second table at the time of creation. I am using knex to make all queries. Would I be using join to link them in knex?
server
hydrateRouter // get all users
.route('/api/user')
.get((req, res) => {
knexInstance
.select('*')
.from('hydrate_users')
.then(results => {
res.json(results)
})
})
.post(jsonParser, (req, res, next) => { // register new users
const { username, glasses } = req.body;
const password = bcrypt.hashSync(req.body.password, 8);
const newUser = { username, password, glasses };
knexInstance
.insert(newUser)
.into('hydrate_users')
.then(user => {
res.status(201).json(user);
})
.catch(next);
})
client
export default class Register extends React.Component {
constructor(props) {
super(props);
this.state = {
username: '',
password: '',
glasses: 0
}
}
handleSubmit(event) {
event.preventDefault();
fetch('http://localhost:8000/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.state)
})
.then(response => response.json())
.then(responseJSON => {
this.props.history.push('/login');
})
}
server side route for displaying the water amount
hydrateRouter
.route('/api/user/waterconsumed/:user_id') // display water consumed/day
.all(requireAuth)
.get((req, res, next) => {
const {user_id} = req.params;
knexInstance
.from('hydrate_quotas')
.select('amount')
.where('user_id', user_id)
.first()
.then(water => {
res.json(water)
})
.catch(next)
})
Thank you!
Getting the id of an inserted row
So this is a common pattern in relational databases, where you can't create the egg until you have the unique id of the chicken that lays it! Clearly, the database needs to tell you how it wants to refer to the chicken.
In Postgres, you can simply use Knex's .returning function to make it explicit that you want the new row's id column returned to you after a successful insert. That'll make the first part of your query look like this:
knexInstance
.insert(newUser)
.into('users')
.returning('id')
Note: not all databases support this in the same way. In particular, if you happen to be developing locally using SQLite, it will return the number of rows affected by the query, not the id, since SQLite doesn't support SQL's RETURNING. Best is just to develop locally using Postgres to avoid nasty surprises.
Ok, so we know which chicken we're after. Now we need to make sure we've waited for the right id, then go ahead and use it:
.then(([ userId ]) => knexInstance
.insert({ user_id: userId,
date: knex.fn.now(),
amount: userConstants.INITIAL_QUOTA_AMOUNT })
.into('quotas')
)
Or however you choose to populate that second table.
Note: DATE is a SQL keyword. For that reason, it doesn't make a great column name. How about created or updated instead?
Responding with sensible data
So that's basic "I have the ID, let's insert to another table" strategy. However, you actually want to be able to respond with the user that was created... this seems like sensible API behaviour for a 201 response.
What you don't want to do is respond with the entire user record from the database, which will expose the password hash (as you're doing in your first code block from your question). Ideally, you'd probably like to respond with some UI-friendly combination of both tables.
Luckily, .returning also accepts an array argument. This allows us to pass a list of columns we'd like to respond with, reducing the risk of accidentally exposing something to the API surface that we'd rather not transmit.
const userColumns = [ 'id', 'username', 'glasses' ]
const quotaColumns = [ 'amount' ]
knexInstance
.insert(newUser)
.into('users')
.returning(userColumns)
.then(([ user]) => knexInstance
.insert({
user_id: user.id,
date: knex.fn.now(),
amount: userConstants.INITIAL_QUOTA_AMOUNT
})
.into('quotas')
.returning(quotaColumns)
.then(([ quota ]) => res.status(201)
.json({
...user,
...quota
})
)
)
Async/await for readability
These days, I'd probably avoid a promise chain like that in favour of the syntactic sugar that await provides us.
try {
const [ user ] = await knexInstance
.insert(newUser)
.into('users')
.returning(userColumns)
const [ quota ] = await knexInstance
.insert({
user_id: userId,
date: knex.fn.now(),
amount: userConstants.INITIAL_QUOTA_AMOUNT
})
.into('quotas')
.returning(quotaColumns)
res
.status(201)
.json({
...user,
...quota
})
} catch (e) {
next(Error("Something went wrong while inserting a user!"))
}
A note on transactions
There are a few assumptions here, but one big one: we assume that both inserts will be successful. Sure, we provide some error handling, but there's still the possibility that the first insert will succeed, and the second fail or time out for some reason.
Typically, we'd do multiple insertions in a transaction block. Here's how Knex handles this:
try {
const userResponse = await knexInstance.transaction(async tx => {
const [ user ] = await tx.insert(...)
const [ quota ] = await tx.insert(...)
return {
...user,
...quota
}
})
res
.status(201)
.json(userResponse)
} catch (e) {
next(Error('...'))
}
This is good general practice for multiple inserts that depend on each other, since it sets up an "all or nothing" approach: if something fails, the database will go back to its previous state.

Should I use information stored in a cookie for indexing purposes?

I am creating my first major app, and I thought of a way to optimize query performance. I am not sure though whether I should go through with it.
Here is a description of my approach.
Every time a user account is created, that user is assigned a random number between 1 and 10, which is stored in their user document. The number is called a number-Id. Here is how the user schema will look like:
let User = new Schema({
/// I left out all the other fields for clairty sake
numberId: {
type: Number,
default: Math.floor(Math.random() * 11),
index: true
}
}
Every time a user creates a blogpost and post, their number-Id is referenced inside the document of that blogpost and post. This is to make querying much faster by indexing the users number-id. Here is how the document of a blogpost would look like in MongoD:
{
"title": "my Blog Post",
"_id": "ObjectId("594824b2828d7b15ecd7b6a5")",
/// Here is the numberId of the user who posted the blogpost, it is added
/// to the document of the blogpost when it is created.
"postersNumberId": 2
/// the Id of the user who posted the blogpost
"postersId": "59481f901f0c7d249cf6b050"
}
Let's say I want to get all the blogposts made by a specific user. I can optimize my query much faster by using the number-Id of the user in question as an index, given that their number-Id is referenced in all the blogposts and comment posts they make.
BlogPost.find({postersId: user_id, postersNumberId: user.numberId});
It seems like this approach warrants that I store the users number-id in req.user in order for it to be readily available whenever I need it to optimize queries. So that means I would have to store the users data in a cookie via passport:
passport.serializeUser(function(user, done){
done(null, user._id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function (err, user){
if (err || !user) return done(err, null);
done(null, user);
});
});
Given this approach, I could now use all the information stored in the cookie, particularly the numberId, to optimize queries that retrieve the comments and blogposts a user makes:
BlogPost.find({postersId: req.user_id, postersNumberId: req.user.numberId});
However, I am using json-web-tokens to authenticate the user rather than cookies. So I will have to use a cookie to store the number-Id for indexing purposes in addition to using JWT for authentication. I've heard, however, that having cookies is bad for scalability, so I am worried that storing the users number-Id in req.user will eventually impact performance.
Should I continue with this approach, or no? What are the performance implications?
In addition to authentication JWT has a payload, which can be used to store additional information within the generated token itself:
var jwt = require('jsonwebtoken');
var token = jwt.sign({
data: {
numberId: 7
}
}, 'jwtSecret', {
expiresIn: '1h'
});
For retrieval:
jwt.verify(token, 'jwtSecret', function(err, decoded) {
if (err) {
console.log(err)
} else {
console.log(decoded);
//{ data: { numberId: 7 }, iat: 1498350787, exp: 1498354387 }
}
});