I want to implement a counter function to add up number of likes on a post, here is the code (taken from the firebase counter function example) I have modified it a little, how do I get the placeholder values specificed in the database ref (cid, coid)?
exports.countCommentChange = functions.database.ref('/clipComments/{cid}/{coid}').onWrite(event => {
const db = admin.database();
const clipRef = db.ref(`/clips/${cid}`); // <- how do I get CID?
const countRef = clipRef.child('comments');
return countRef.transaction(current => {
if (event.data.exists() && !event.data.previous.exists()) {
return (current || 0) + 1;
}
else if (!event.data.exists() && event.data.previous.exists()) {
return (current || 0) - 1;
}
}).then(() => {
console.log('Counter updated.');
});
});
my database structure is as follows:
clips: {
clipId: {
name: "my awesome clip",
likes: 0,
comments: 0
}
},
clipComments: {
clipId:
{
commentTimestamp: {
comment: "awesome video!"
}
}
}
If You console.log your event from the onWrite listener, You will able to see the whole data stored in this object at the dashboard console.
You should notice a object at the beginning named params. This Object will store all placeholder variables in the exported function.
In Your case, You should be able to access your placeholder with event.params.cid.
I hope it helps!
Related
The final solution is at the bottom of this post.
I have a nodeJS server application that listens to a rather big collection:
//here was old code
This works perfectly fine: these are lots of documents and the server can serve them from cache instead of database, which saves me tons of document reads (and is a lot faster).
I want to make sure, this collection is staying alive forever, this means reconnecting if a change is not coming trough.
Is there any way to create this certainty? This server might be online for years.
Final solution:
database listener that saves the timestamp on a change
export const lastRolesChange = functions.firestore
.document(`${COLLECTIONS.ROLES}/{id}`)
.onWrite(async (_change, context) => {
return firebase()
.admin.firestore()
.collection('syncstatus')
.doc(COLLECTIONS.ROLES)
.set({
lastModified: context.timestamp,
docId: context.params.id
});
});
logic that checks if the server has the same updated timesta.mp as the database. If it is still listening, it should have, otherwise refresh listener because it might have stalled.
import { firebase } from '../google/auth';
import { COLLECTIONS } from '../../../configs/collections.enum';
class DataObjectTemplate {
constructor() {
for (const key in COLLECTIONS) {
if (key) {
this[COLLECTIONS[key]] = [] as { id: string; data: any }[];
}
}
}
}
const dataObject = new DataObjectTemplate();
const timestamps: {
[key in COLLECTIONS]?: Date;
} = {};
let unsubscribe: Function;
export const getCachedData = async (type: COLLECTIONS) => {
return firebase()
.admin.firestore()
.collection(COLLECTIONS.SYNCSTATUS)
.doc(type)
.get()
.then(async snap => {
const lastUpdate = snap.data();
/* we compare the last update of the roles collection with the last update we
* got from the listener. If the listener would have failed to sync, we
* will find out here and reset the listener.
*/
// first check if we already have a timestamp, otherwise, we set it in the past.
let timestamp = timestamps[type];
if (!timestamp) {
timestamp = new Date(2020, 0, 1);
}
// if we don't have a last update for some reason, there is something wrong
if (!lastUpdate) {
throw new Error('Missing sync data for ' + type);
}
const lastModified = new Date(lastUpdate.lastModified);
if (lastModified.getTime() > timestamp.getTime()) {
console.warn('Out of sync: refresh!');
console.warn('Resetting listener');
if (unsubscribe) {
unsubscribe();
}
await startCache(type);
return dataObject[type] as { id: string; data: any }[];
}
return dataObject[type] as { id: string; data: any }[];
});
};
export const startCache = async (type: COLLECTIONS) => {
// tslint:disable-next-line:no-console
console.warn('Building ' + type + ' cache.');
const timeStamps: number[] = [];
// start with clean array
dataObject[type] = [];
return new Promise(resolve => {
unsubscribe = firebase()
.admin.firestore()
.collection(type)
.onSnapshot(querySnapshot => {
querySnapshot.docChanges().map(change => {
timeStamps.push(change.doc.updateTime.toMillis());
if (change.oldIndex !== -1) {
dataObject[type].splice(change.oldIndex, 1);
}
if (change.newIndex !== -1) {
dataObject[type].splice(change.newIndex, 0, {
id: change.doc.id,
data: change.doc.data()
});
}
});
// tslint:disable-next-line:no-console
console.log(dataObject[type].length + ' ' + type + ' in cache.');
timestamps[type] = new Date(Math.max(...timeStamps));
resolve(true);
});
});
};
If you want to be sure you have all changes, you'll have to:
keep a lastModified type field in each document,
use a query to get documents that we modified since you last looked,
store the last time you queried on your server.
Unrelated to that, you might also be interested in the recently launched ability to serve bundled Firestore content as it's another way to reduce the number of charged reads you have to do against the Firestore server.
In loopback I need to handle Patch in loop.
Whenever I try to patch a property, I may have to update the property in one or all the other records in the table/collection. I've been using before save hook and calling updateInLoop as shown below to update the other records.
module.exports = function(Model) {
Model.observe('before save', async(ctx, next) => {
if(ctx.Data) {
updateInLoop(ctx.Data, Model);
}
});
}
const updateInLoop = async function(data, Model) {
var modelDbData = await Model.find();
for(let entity in modelDbData) {
if(entity.property > threshold){
entity.property = entity.property + 1;
await Model.upsert(model);
}
}
}
This seems to be working fine if loop in updateInloop doesn't run more than once. Whenever the loop need to be run more than once, the code doesn't seem to be working as expected.
I found that the unexpected behavior is due to the before save hook being executed for every update action in loop. How can this be resolved?
This behavior is definitely expected, if you make your increment statement easily recognizable you can prevent it from calling itself.
module.exports = function(Model) {
Model.observe('before save', async(ctx) => {
// if(ctx.Data) {
let keys = ctx.Data ? Object.keys(ctx.Data) : undefined;
// It is an increment if the length of its keys is 1 and that the key is property
let isIncrement = (keys && keys.length === 1 && keys[0] === 'property');
if (ctx.Data && !isIncrement) {
updateInLoop(ctx.Data, Model);
}
});
}
const updateInLoop = async function(data, Model) {
var modelDbData = await Model.find();
for(let entity in modelDbData) {
if(entity.property > threshold){
// entity.property = entity.property + 1;
// await Model.upsert(model);
await Model.updateById(entity.id, {property: entity.property + 1});
}
}
}
In my discord bot, I have some code that looks something like this (with portions impertinent to the question omitted):
run(message) {
[...]
mongoClient.connect(config.mongodb.url, function(err, db) {
[...]
var roleMessage = `\`\`\`Roles on ${message.guild.name} (use !role:add <role> to assign them):`
var isNotificationsEnabled = false
db.collection('roles').find({"sid": message.guild.id}).each(function (err, doc) {
[...]
if(doc) {
if(doc.rolename != '$notify') {
roleMessage += `${doc.rolename}\n`
} else {
isNotificationsEnabled = true
}
} else {
[...]
}
})
[...]
})
}
My problem is that variables from the mongoClient.connect() callback are inaccessible from within the inner callback (specifically roleMessage and isNotificationsEnabled) - I'm trying to build a string out of database elements in a MongoDB database. Is there any way I can make these variables accessible, or is there a better way of doing things?
Thanks in advance for anyone who can help.
There is nothing wrong with javascript you can see the example below related to scope access to variables,
let message = { some: 'thing' }
let items = [ 'item1', 'item2' ]
let something = message => {
let newvar = 'scope1';
items.forEach(function(item) {
items.forEach(function(item2) {
console.log(item2 + ' ' + item + ' ' + newvar + ' ' + message.some)
})
})
}
something(message)
db.collection('roles').find({"sid": message.guild.id}).each(function (err, doc) {
May not be returning any data from the collection.
Here is the deal.
I am using VueFire and would like to retrieve data from a Firebase database I have set up. In there I have a node with users and each one is defined by a unique id. Also for every user I have an array with a cart full of items they would to purchase. To make it dynamic I am calling the reference in the firebase hook like so:
export default {
firebase:{
cart: app.database().ref('users')
}, //and so on and so on
}
Instead of .ref('users') I want to retrieve the current user, using his unique id like so: .ref('users/' + user.uid + '/cart')
To get the current user uid I do the observer that is firebase.auth().onAuthStataChanged(user=>{//code here})
Problem is, since this is asynchronous, the firebase hook activates before the current user is retrieved. I have tried to simply call firebase.auth().currentUser, but this is also slow and unreliable to use.
I am open to any kinds of suggestions!
I made a solution that worked for my case and might work for yours.
What I did is attach value listeners when the user gets authenticated and then I turn them off when the user loses authentication. With this method I'm only asking to retrieve user data if the user is actually authenticated. Here's how I did it:
this.$root.firebase.auth().onAuthStateChanged(user => {
if (user) {
this.$store.dispatch('setUserUID', user.uid)
// Listen for User Data
let dataRef = this.$root.db.ref('/userdata/' + user.uid)
let self = this
dataRef.on('value', function(snap) {
let value = snap.val()
self.$store.dispatch('setUserData', value)
})
}
else {
// Stop Listening for User Data
this.$root.db.ref('/userdata/' + this.$store.state.user.uid).off()
this.$store.dispatch('setUserUID', null)
}
})
Okay I got it. Thanks to #Daniel D for his tip on binding as an array or object. So as it turns out I don't have to do it in the firebase reference hook, I just have to bind it as array in the mounted() hook for example. I just declare an empty cart: [] in data and then fill it like so:
<script>
import changeColorMixin from '../mixins/changeColorMixin'
import animateFunction from '../mixins/animationMixin'
import Firebase from 'firebase'
import app from '../firebaseApp'
export default {
data(){
return{
isLoggedIn: '',
cart: []
}
},
methods:{
changeColor: changeColorMixin,
animateEntrance: animateFunction,
promptLogin: function(){
console.log('you need to login!');
},
chooseSize: function($event){
$($event.target).parents().eq(2).addClass('chosen');
},
closeOrder: function($event) {
$($event.target).parents().eq(2).removeClass('chosen');
},
makeOrder: function($event){
var $this = this;
Firebase.auth().onAuthStateChanged(function(user){
if(user){
var cartRef = app.database().ref('users/' + user.uid + '/cart');
var currentCart;
cartRef.once('value', function(result) {
currentCart = result.val();
var size = $($event.target).html();
var name = $($event.target).parents().eq(2).attr('id');
app.database().ref('users/' + user.uid + '/cart/' + name + '/count').once('value', function(snap){
var count = snap.val();
if(count > 0){
count = count + 1;
}else{
count = 1;
}
cartRef.child(name).set({
size: size,
count: count,
name: name
});
});
});
}else{
console.log('gotta login mate');
}
});
}
},
mounted(){
Firebase.auth().onAuthStateChanged(user => {
if(user){
this.isLoggedIn = true;
var cartRef = app.database().ref('users/' + user.uid + '/cart');
this.$bindAsArray('cart', cartRef);
}else{
this.isLoggedIn = false;
}
});
this.animateEntrance();
this.changeColor();
}
}
</script>
As I said big thanks to #Daniel D
It is 2016, Node has had nearly full ES6 support since v4, and Promises have been around since 0.12. It's time to leave callbacks in the dust IMO.
I'm working on a commander.js-based CLI util which leverages a lot of async operations - http requests and user input. I want to wrap the Commander actions in async functions so that they can be treated as promises, and also to support generators (useful for the co-prompt library I'm using for user input).
I've tried wrapping the CB with co in two ways:
1)
program.command('myCmd')
.action(program => co(function* (program) {...})
.catch(err => console.log(err.stack)) );
and
2) program.command('myCmd').action(co.wrap(function* (program) { .. }));
The problem with 1) is that the program parameter isn't passed
The problem with 2) is that errors are swallowed...
I'd really like to get this working as it yields much nicer code in my use case - involving a lot of http requests and also waiting for user input using the co-prompt library..
Is it a better option altogether perhaps to wrap program.Command.prototype.action somehow?
thanks!
I've used a bespoke version of something like co to get a db.exec function which uses yield to do database request. You can pass parameters into a generator function (I pass in a connection object - see the comment where I do it).
Here is by db.exec function that is very similar to what co does
exec(generator) {
var self = this;
var it;
debug('In db.exec iterator');
return new Promise((accept,reject) => {
debug('In db.exec Promise');
var myConnection;
var onResult = lastPromiseResult => {
debug('In db.exec onResult');
var obj = it.next(lastPromiseResult);
if (!obj.done) {
debug('db.exec Iterator NOT done yet');
obj.value.then(onResult,reject);
} else {
if (myConnection) {
myConnection.release();
debug('db.exec released connection');
}
accept(obj.value);
debug('db.exec Promise Resolved with value %d',obj.value);
}
};
self._connection().then(connection => {
debug('db.exec got a connection');
myConnection = connection;
it = generator(connection); //This passes it into the generator
onResult(); //starts the generator
}).catch(error => {
logger('database', 'Exec Function Error: ' + error.message);
reject(error);
});
});
}
the connection object also wraps by database connection object and provides a generator function ability to process the rows of the results from the database, but I won't post that here (although the example below is using it to process the rows).
Here is an example of using the exec function to run a sequence of sql
db.exec(function*(connection) {
if (params.name === ADMIN_USER) {
debug('Admin Logon');
user.name = ADMIN_DISPLAY;
user.keys = 'A';
user.uid = 0;
let sql = 'SELECT passwordsalt FROM Admin WHERE AdminID = 0';
connection.request(sql);
yield connection.execSql(function*() {
let row = yield;
if (row) {
user.nopass = (row[0].value === null);
} else {
user.nopass = false;
}
debug('Admin Password bypass ' + user.nopass.toString());
});
} else {
debug('Normal User Logon');
let sql = `SELECT u.UserID,PasswordSalt,DisplayName,AccessKey,l.LogID FROM Users u
LEFT JOIN UserLog l ON u.userID = l.userID AND DATEDIFF(D,l.LogDate,GETDATE()) = 0
WHERE u.UserName = #username`;
let request = connection.request(sql);
request.addParameter('username',db.TYPES.NVarChar,params.name);
let count = yield connection.execSql(function*() {
let row = yield;
if (row) {
user.uid = row[0].value;
user.name = row[2].value;
user.keys = (row[3].value === null) ? '' : row[3].value;
user.nopass = (row[1].value === null) ;
user.lid = (row[4].value === null) ? 0 : row[4].value;
debug('Found User with uid = %d and lid = %d, keys = %s',
user.uid, user.lid, user.keys);
}
});
if (count === 0) {
debug('Not Found User');
// couldn't find name in database
reply(false,false);
return;
}
}
if (!user.nopass) {
debug('Need a Password');
//user has a password so we must check it
passGood = false; //assume false as we go into this
let request = connection.request('CheckPassword');
request.addParameter('UserID',db.TYPES.Int,user.uid);
request.addParameter('password',db.TYPES.VarChar,params.password);
yield connection.callProcedure(function*() {
let row = yield;
if (row) {
//got a valid row means we have a valid password
passGood = true;
}
});
} else {
passGood = true;
}
if (!passGood) {
debug('Not a Good Pasword');
reply(false,true);
} else {
if (user.uid !== 0 && user.lid === 0) {
let sql = `INSERT INTO UserLog(UserID,LogDate,TimeOn,UserName) OUTPUT INSERTED.logID
VALUES(#uid,GETDATE(),GETDATE(),#username)`;
let request = connection.request(sql);
request.addParameter('uid',db.TYPES.Int,user.uid);
request.addParameter('username',db.TYPES.NVarChar,user.name);
yield connection.execSql(function*() {
let row = yield;
if (row) {
user.lid = row[0].value;
debug('Users Log Entry = %d',user.lid);
}
});
}
reply(true,user);
}
})
.catch((err) => {
logger('database','Error on logon: ' + err.message);
reply(false,false);
});
});
There is a quite simple way to do async function in Commander.js
async function run() {
/* code goes here */
}
program
.command('gettime')
.action(run);
program.parse(process.argv);