How to use .hasChild and retrieve child using Firebase? - firebase

I'm working on a project with the following Firebase structure:
user {
score: 0,
messages : {
key1 { name: name, text: text }
key2 { name: name, text: text }
key...
}
}
I currently have two problems. The first is determining if the user has a "messages" child, if not, then give it one (along with a score), here's the code I came up with so far:
ref.once('value', function (snapshot) {
if (!snapshot.hasChild("messages")) {
ref.set({
score: 0,
messages: 0
});
}
});
The next is retrieving and displaying the messages from the child once the data has been pushed to it like so:
ref.child("messages").on('child_added', function (snapshot) {
var message = snapshot.val();
$('#messagesDiv').prepend(message.text ": " + message.name);
});
but that doesn't seem like it's working either.
Here is the fiddle I made.
I hope you guys can help me fix this problem! The syntax looks right and I read over the docs to find most of the current code.
Thanks in advance!

Setting the initial data
Your code with hasChild seems is executed fine. It just doesn't make a lot of sense. The structure that you're adding leads to:
user {
score: 0,
messages: 0
}
Which is not the same as the structure you've drawn in your question: messages here is just a number, while you want it to be a collection of messages. In addition this change will not trigger your child_added handler, since... you're not adding a child to messages.
You've done the right thing by starting with designing a data structure. The next step is to ensure that you stick to that data structure. So if you want to add an initial message, add the message in the correct structure:
ref.once('value', function (snapshot) {
if (!snapshot.hasChild("messages")) {
ref.set({
score: 0,
messages: { 0: { name: 'puf', text: 'welcome' }}
});
}
});
If you modify the fiddle you will see that the welcome message does show up in your #messagesDiv.
I think this approach is still flawed though. Unless you are really looking to add a welcome message, there is no need to add a messages node. I would just set the score to 0 and the messages node will be added once the user enters their first message:
ref.once('value', function (snapshot) {
if (!snapshot.hasChild("messages")) {
ref.set({ score: 0 });
}
});
Adding new messages
I noticed that you also have the following code in your fiddle:
$('#messageInput').keypress(function (e) {
if (e.keyCode == 13) {
var name = user;
var text = $('#messageInput').val();
// POST
ref.child("messages").set({
name: name,
text: text
});
$('#messageInput').val('');
}
});
The input handling is fine, but once again your code that modifies the Firebase data structure does not follow along with the data structure you started your question with. If we execute this code, the data structure will be:
user {
score: 0,
messages: {
name: 'NotToBrag',
text: 'asked 10 hours ago'
}
}
In case it's not obvious: this structure is missing the crucial key1 or your structure. Oh... and it has also overwritten the welcome message.
When you're adding a child node to a Firebase list, you almost always want to use push:
ref.child("messages").push({
name: name,
text: text
});
With that tiny change, the data structure becomes:
user {
score: 0,
messages: {
0: {
name: 'puf',
text: 'welcome'
},
'-Jh-aFN42nWef-FvgcfS': {
name: 'NotToBrag',
text: 'asked 10 hours ago'
}
}
}
All of these are (as usual) pretty small changes. But together they ensured that your scenario was pretty badly broken. The tricks I used to troubleshoot are incredibly basic and you'd do well to add them to your arsenal and learn to use them.
Debugging trick 1: console.log the data structure
Whenever I first get an MCVE of somebody's problem, I immediately log their data structure:
new Firebase('https://your.firebaseio.com/').once('value', function(s) {
console.log(s.val());
})
As times I might stringify the JSON:
new Firebase('https://your.firebaseio.com/').once('value', function(s) {
console.log(JSON.stringify(s.val()));
})
That last snippet is for example a great way to get the data structure for use in your question.
The snippet only shows the data structure once, so keep running this snippet every time something changes.
Debugging trick 2: remove your data
Your whole hasChild snippet seems aimed to set up your initial data structure for a user. To aid in testing, I frequently removed the data:
new Firebase('https://your.firebaseio.com/myName').remove()
And then when you run the fiddle again, you can see what your hasChild-using code does.
I often put code to clean out (or otherwise reset) my test data either at the start of my fiddles or simply run a snippet from the browser's JavaScript console.

Related

AWS Amplify AppSync Subscription: data returning null

I was working on my Amplify App and I had subscriptions working fine with this:
graphql:
type Item #model(subscriptions: null)
#auth(rules: [
{allow: owner},
{allow: groups, groups: ["Admin"], operations: [create, update, read, delete] }
]) {
id: ID!
name: String
files: String
}
type Subscription {
itemUpdated(id: ID): Item #aws_subscribe(mutations: ["updateItem"])
}
js:
const handleSubscription = (data) => {
if (data.value.data.itemUpdated) {
setItemObj(data.value.data.itemUpdated);
}
};
useEffect(() => {
const subscription = API.graphql(
graphqlOperation(subscriptions.itemUpdated, {
id,
}),
).subscribe({
next: handleSubscription,
});
return () => subscription.unsubscribe();
}, []);
In the handleSubscription method, when the app made a mutation call to the Item, the return data (data.value.data.itemUpdated) would have the correct data.
Now, for reasons I am obviously unclear about, I can still see the subscription event fire when a mutation occurs, but the return data (data.value.data.itemUpdated) is consistently null.
I have tried to remove the {allow: owner} rule from the graphql schema's auth field as This Question suggests - which did not work (aside: I am still curious as to why that would work in the first place, but I do not have enough rep to comment).
While writing this, my thoughts were that I am going to try to create a new Item without the {allow: owner} rule and try again, if that works I will report back, but my question will pivot to asking why and asking then how can I ensure Items are private to the owner still? Lastly, I am almost positive I had the {allow: owner} rule in there when it was working too, but I could be mistaken.
I have also tried:
tested with updating different Item fields
let amplify cli rebuild my graphql js files
changed code around, i.e
removed the return () => subscription.unsubscribe();
made the input more specific API.graphql(graphqlOperation(subscriptions.itemUpdated, {input: { id: id },}) (which I am sure does not matter, but I wanted to try.)
I am just not sure what is going on here. This all seems so simple and it must be something dumb I am missing...I know I will figure it out eventually, but I wanted to tap anyone here in case.
Versions:
"aws-amplify": "^3.0.24"
"#aws-amplify/ui-react": "^0.2.15"
"react": "^16.13.1"
amplify-cli: 4.29.0
Please let me know if I left any important information out. Thanks in advance for any help.
Ok.. just a dumb thing like I thought. My bad for wasting anyone's time!
API.graphql(
graphqlOperation(subscriptions.itemUpdated, {
id: Id,
}),
).subscribe({
next: handleSubscription,
});
it was the id: Id, parameter. I had the Id var before named as id and js allows for shorting {name: name} to { name } - I must have changed the id var and went right to {input: { id: Id },} which is the incorrect syntax for subscriptions.
Real bonehead move and I am appropriately embarrassed. Good lesson in bad naming even during testing.

Best way to make ReactiveAggregate reactive when data changes on a user

I am currently using ReactiveAggregate to find a subset of Product data, like this:
ReactiveAggregate(this, Products, [
{ $match: {}},
{ $project: {
title: true,
image: true,
variants: {
$filter: {
input: "$variants",
as: "variant",
cond: {
$setIsSubset: [['$$variant.id'], user.variantFollowing]
}
}
}
}}
], { clientCollection: 'aggregateVariants' }
As you can see, a variant is returned if user.variantFollowing matches. When a user 'follows' a product, the ID is added to their object. However, if I understand correctly, this is not triggering ReactiveAggregate to get the new subset when this happens. Only on a full page refresh do I get the correct (latest) data.
Is this the correct way to approach this?
I could store the user's ID as part of the Product object, but the way this would be stored would be nested two places, and I think I would need the Mongo 3.5 updates to then be able to accurately update this. So i'm looking for how to do this in Meteor 1.5+ / Mongo 3.2.12
So, I've been able to get there by adding autorun to the subscription of the aggregate collection, like this:
Template.followedProducts.onCreated(function() {
Meteor.subscribe('products');
this.autorun(() => {
Meteor.subscribe('productsFollowed');
});
... rest of function
For context, productsFollowed is the subscription to retrieve aggregateVariants from the original question.
Thanks to robfallows in this post: https://forums.meteor.com/t/when-and-how-to-use-this-autorun/26075/6

How to make a function that uses counters to compute values reactively

I'm new to meteor and I tried to use the tmeasday:publish-counts package to publish some counts. Reactivity works fine out of the box when I just read the counts, but when I use the counts in a Template.helper function, the function doesn't get updated when the counts change.
Here is what I have :
On the server :
Meteor.publish('counters', function() {
Counts.publish(this, 'searches-thisWeek', UserActions.find({
$and: [
{ action: SEARCHES_REGEX },
{ date : { $gte : moment().startOf('week').toDate()} },
{ date : { $lt : moment().toDate()} }
]
}));
Counts.publish(this, 'searches-lastWeek', UserActions.find({
$and: [
{ action: SEARCHES_REGEX },
{ date : { $gte : moment().subtract(1, 'week').startOf('week').toDate()} },
{ date : { $lt : moment().subtract(1, 'week').endOf('week').toDate()} }
]
}));
});
On the client :
Template.dashboard.helpers({
nbSearchesThisWeek : function() {
var variation = Counts.get('searches-thisWeek') - Counts.get('searches-lastWeek');
return {
currentValue : Counts.get('searches-thisWeek'),
orientation : {
sign: (variation > 0) ? '+' : '',
class: (variation > 0) ? 'up' : 'down'
},
variation : variation
};
}
});
In my template I have a :
<td>{{getPublishedCount 'searches'}}</td>
<td>{{#with nbSearchesThisWeek}}{{>variations }}{{/with}}</td>
It uses this subtemplate :
<template name="variations">
<div class="variation col-lg-12">
<span class="variationValue {{orientation.class}} col-lg-6">{{orientation.sign}}{{variation}}</span>
<span class="currentValue col-lg-6">{{currentValue}}</span>
</div>
</template>
The {{getPublishedCount 'searches'}} updates fine. It just gets my "searches" counter and updates anytime the counter changes.
However, the counters in the subtemplate execute fine at startup but never update when any of the dependent counters change.
So my question is, how and where do I make my nbSearchesThisWeek helper react to the changes on the dependent counters values ? I'm very confused when I read the documentation about Deps, Tracking, and ReactiveVars... I didn't really understand where those things should be used to make my use case work...
Thanks for your help.
Philippe
I have created a MeteorPad with code which is as far as possible the same as yours and it is all reactive, without any changes to your code.
Please note that instead of trying to recreate your UserActions collection, I have created two separate collections, UserActions and Players (the names have no significance) to create two counters just like yours. I think once you have seen the code you will agree that for the purpose of checking your code it will do.
Go to this page: http://app-cmrd2ous.meteorpad.com/ open the Chrome or Firefox console and insert the following two records and watch the counters:
UserActions.insert({name: 'bloggs', date: new Date()});
Players.insert({name: 'bloggs again', date: new Date()});
The counters are all reactive.
The meteorpad is here: http://meteorpad.com/pad/n4GwwtPesEZ9rTSuq/Dashboard
All I can suggest is that you look at where I have put the code (whether on the client or the server) - maybe that has something to do with it.
But maybe more likely is that you were running the wrong tests. In other words perhaps you got your dates wrong - you thought you were inserting a record with a date for last week, whereas actually it was two weeks ago or something like that.

Extend a meteor collection server and client side

I am currently developping an app with the amazing Meteor platform. I would like to do something with my collections but I couldn't really find how to do it from the examples I have seen so far.
Basically I would like to display a list of items which contains their own countdown. Each items core data come from a collection. Each countdown starting times must be computed server side and not saved anywhere. Each countdown are computed client side and not saved anywhere.
I have a collection named "items" coming from my MongoDb db. At the beginning document in my collections could look like:
{ name: "My countdown"}
1) I would like to "extend" the documents server side in adding a computed property "startTime". A documents could look like then:
{ name: "My countdown", startTime: 40 }
I guess I need to use the publish method, but I don't really get how to extend existing documents that way.
2) I would like to "extend" the documents client side in adding a local property "currentTime", that i will update with a setInterval. A document could look like then:
{ name: "My countdown", startTime: 40, currentTime: 5 }
Maybe using a transform there but once again I don't really get how to extend existing documents.
3) I would likethoses 2 new properties reactives and so trigger some updates in the UI if they change.
So if i could get any starting points and good pratices it will be really appreciated :)
Many thanks for your help!!
You can update a document of a Collection: Best practice is to do this on the server.
client.js
Meteor.call('setStartTime',
[your_document_id],
[new_start_time],
function(err, val) {
if (err) {
console.error(err);
} else {
// Successful.
}
});
server.js
Meteor.methods({
'setStartTime': function(itemId, newStartTime) {
Items.update(itemId, {
$set: { startTime: newStartTime }
});
}
});
This will set or update the startTime of your item. (Be cautious, as anyone with access to your JavaScript will be able to see your setStartTime call on the client. This is functional, but not secure.)

how to discard initial data in a Firebase DB

I'm making a simple app that informs a client that other clients clicked a button. I'm storing the clicks in a Firebase (db) using:
db.push({msg:data});
All clients get notified of other user's clicks with an on, such as
db.on('child_added',function(snapshot) {
var msg = snapshot.val().msg;
});
However, when the page first loads I want to discard any existing data on the stack. My strategy is to call db.once() before I define the db.on('child_added',...) in order to get the initial number of children, and then use that to discard that number of calls to db.on('child_added',...).
Unfortunately, though, all of the calls to db.on('child_added',...) are happening before I'm able to get the initial count, so it fails.
How can I effectively and simply discard the initial data?
For larger data sets, Firebase now offers (as of 2.0) some query methods that can make this simpler.
If we add a timestamp field on each record, we can construct a query that only looks at new values. Consider this contrived data:
{
"messages": {
"$messageid": {
"sender": "kato",
"message": "hello world"
"created": 123456 // Firebase.ServerValue.TIMESTAMP
}
}
}
We could find messages only after "now" using something like this:
var ref = new Firebase('https://<your instance>.firebaseio.com/messages');
var queryRef = ref.orderBy('created').startAt(Firebase.ServerValue.TIMESTAMP);
queryRef.on('child_added', function(snap) {
console.log(snap.val());
});
If I understand your question correctly, it sounds like you only want data that has been added since the user visited the page. In Firebase, the behavior you describe is by design, as the data is always changing and there isn't a notion of "old" data vs "new" data.
However, if you only want to display data added after the page has loaded, try ignoring all events prior until the complete set of children has loaded at least once. For example:
var ignoreItems = true;
var ref = new Firebase('https://<your-Firebase>.firebaseio.com');
ref.on('child_added', function(snapshot) {
if (!ignoreItems) {
var msg = snapshot.val().msg;
// do something here
}
});
ref.once('value', function(snapshot) {
ignoreItems = false;
});
The alternative to this approach would be to write your new items with a priority as well, where the priority is Firebase.ServerValue.TIMESTAMP (the current server time), and then use a .startAt(...) query using the current timestamp. However, this is more complex than the approach described above.

Resources