I'm trying to implement a Reddit/HackerNews style tree of comments as part of a project, and am trying out Firestore as a database solution. However, I'm unsure as to the correct design reading through the docs. In a SQL database I would use numeric keys like:
0
1.0
1.1
1.1.1
0.0
to represent my tree. However, numeric keys like that seem to be a Firebase antipattern. The other route is using an actual tree in the json where a post is represented like:
{
uid: 'A0000',
content: 'foo',
children: [
{uid:..., content:..., children: []}]
}
but supposedly deep trees are bad in Firestore. As I understand it the reason deep trees are bad is that you have to fetch the whole thing, but in my case I'm not sure if that's a problem. A client fetching a post would fetch the root content node and the first 20 or so child trees. That could be a pretty big fetch, but not insanely so.
Does anyone know of a good standard way to implement this kind of structure?
Extra: Here is the more verbose expression of what the structure should look like once the client processes it.
{
uid: 0,
title: 'Check out this cat!',
body: 'It\'s pretty cute! This **text** is [markdown](link), so it can have ' +
'links and *stuff*. Yay!',
poster: {
uid: 0,
name: 'VivaLaPanda',
aviUrl: 'badlink',
},
posted: '2018-03-28',
children: [{
uid: 0,
body: 'This is a comment, it\'s angry!',
poster: {
uid: 0,
name: 'VivaLaPanda',
aviUrl: 'badlink',
},
posted: '2018-03-20',
children: [{
uid: 0,
body: 'This is a comment, it\'s neutral!',
poster: {
uid: 0,
name: 'Steve',
aviUrl: 'badlink',
},
posted: '2018-03-20',
children: [{
uid: 0,
body: 'This is a comment, it\'s neutral!',
poster: {
uid: 0,
name: 'Craig',
aviUrl: 'badlink',
},
posted: '2018-04-10',
children: []
}, ]
}, ]
},
{
uid: 0,
body: 'This is a comment, it\'s happy!',
poster: {
uid: 0,
name: 'Craig',
aviUrl: 'badlink',
},
posted: '2018-03-28',
children: []
},
]
};
Edit:
While I've marked this as answered because there is an answer, I'm still really interested in seeing something more elegant/efficient.
Edit2:
For posterity: I ended up deciding that any Firebase solution was hopelessly convoluted and just used DGraph for the data, with Firebase sitting in front for Auth.
This is tough since the structure you have is naturally recursive. The obvious options are each comment is a new document in a collection and each reply is a single document in the same collection.
Each comment as a new document could work something like this. Each comment has a "postId" attribute which dictates which post it belongs to. Some comments, those which are replies to other comments, have a "replyToId". These two attribute in conjunction allow your client app to:
Get the top level comments (look for comments with the correct postId and which don't have replyToId). Top level comments allows you to limit the size of payloads if you need to worry about that in the future.
Get all comments (look for comments with the correct postId only). If you don't care about payload sizes you can get everything and figure out the tree structure on the client.
Get replies to a particular comment if you want "see replies" YouTube style comments interaction (looks for comments which have a particular replyToId). This works well in conjunction with 1. for limiting payload sizes.
But the logic here is obviously complex.
Your children approach could be really messy if there are a lot of people commenting on eachother. A nicer approach would be the following structure for every comment:
// single comment
postUid // <- random generated by firebase
{
postedBy: userUid
postedTime: timestamp
postIsChildOfUid: postUid // <- reference to an other post (optional if the comment didn't respond to another comment(top-level comment))
}
This doesn't even require nesting at all :). You can generate easily now a comment tree with this approach, but this has to be client side. But that should be easy!
Related
Files for the scenario
All the files are on same directory.
title-update-request.json
{id: 12, name: 'Old Hello', config:[{username: 'qwe', password: 'tyu'},{username: 'abc', password: 'xyz'}]}
title-update-response.json
{id: 12, name: 'New Hello', config:[{username: 'qwe', password: 'tyu'},{username: 'abc', password: 'xyz'}]}
title-update-error-request.json
{id: 00, name: 'Old Hello', config:[{username: 'qwe', password: 'tyu'},{username: 'abc', password: 'xyz'}]}
title-update-error-response.json
{Error: 'not found', Message: 'The provided Book is not found.'}
book-record.feature
Feature: CRUD operation on the book records.
Background:
* def signIn = call read('classpath:login.feature')
* def accessToken = signIn.accessToken
* url baseUrl
Scenario: Change title of book in the single book-record.
* json ExpResObject = read('classpath:/book-records/title-update-response.json')
* json ReqObject = read('classpath:/book-records/title-update-request.json')
* call read('classpath:/book-records/update.feature') { Token: #(accessToken), ReqObj: #(ReqObject), ResObj: #(ExpResObject), StatusCode: 200 }
Scenario: Change title of book in the non-existing book-record.
* json ExpResObject = read('classpath:/book-records/title-update-error-request.json')
* json ReqObject = read('classpath:/book-records/title-update-error-response.json')
* call read('classpath:/book-records/update.feature') { Token: #(accessToken), ReqObj: #(ReqObject), ResObj: #(ExpResObject), StatusCode: 400 }
update.feature
Feature: Update the book record.
Scenario: Update single book-record.
Given path '/book-record'
And header Authorization = 'Bearer ' + __arg.Token
And header Content-Type = 'application/json'
And request __arg.ReqObj
When method put
Then status __arg.StatusCode
And response == __arg.ExpectedResponse
Actual API response for scenario: 1 is :
{name: 'New Hello', config:[{username: 'abc', password: 'xyz'},{username: 'qwe', password: 'tyu'}]}
Actual API response for scenario: 2 is :
{Error: 'not found', Message: 'The provided Book is not found.'}
Question: How should I validate the response in update.feature file since problem is if I make change s as using #^^config that will not works for scenario :2 and response == _arg.ExpectedResponse is not working for Scenario: 1?
This is classic over-engineering of tests. If someone has told you that "re-use" is needed for tests, please don't listen to that person.
You have two scenarios, one happy path and one negative path. I am providing how you should write the negative path here below, the rest is up to you.
Scenario: Change title of book in the non-existing book-record.
Given path 'book-record'
And header Authorization = 'Bearer ' + accessToken
And request {id: 00, name: 'Old Hello', config:[{username: 'qwe', password: 'tyu'},{username: 'abc', password: 'xyz'}]}
When method put
Then status 400
And response == {Error: 'not found', Message: 'The provided Book is not found.'}
See how clean it is ? There is no need for "extreme" re-use in tests. If you still insist that you want a super-generic re-usable feature file that will handle ALL your edge cases, you are just causing trouble for yourself. See how un-readable your existing tests have become !!
EDIT: Since I refer the question to others often as an example of how NOT to write tests, I wanted to make my point more clear and add a couple of links for reference.
Sometimes it is okay to "repeat yourself" in tests. Tests don't have to be DRY. Karate is a DSL that enables you to make HTTP calls or JSON manipulation in one or two lines. When you start attempting "re-use" like this, it actually leads to more harm than good. For example, you now need to look at multiple files to understand what your test is doing.
If you don't believe me, maybe you will believe the team at Google: https://testing.googleblog.com/2019/12/testing-on-toilet-tests-too-dry-make.html
I am trying to write a Python code so that I can apply a questionnaire to my physics students remotely. They would receive and reply to the quiz via
a Telegram bot. I'm using the python-telegram-bot module. I can already identify which users replied, but I can't get yet their answers.
I am starting with the example code pollbot.py from here: https://raw.githubusercontent.com/python-telegram-bot/python-telegram-bot/master/examples/pollbot.py
The specific methods are these:
def quiz(update: Update, context: CallbackContext) -> None:
"""Send a predefined poll"""
questions = ["1", "2", "4", "20"]
message = update.effective_message.reply_poll(
"How many eggs do you need for a cake?", questions, type=Poll.QUIZ, correct_option_id=2
)
# Save some info about the poll the bot_data for later use in receive_quiz_answer
payload = {
message.poll.id: {"chat_id": update.effective_chat.id, "message_id": message.message_id}
}
context.bot_data.update(payload)
def receive_quiz_answer(update: Update, context: CallbackContext) -> None:
"""Close quiz after three participants took it"""
# the bot can receive closed poll updates we don't care about
if update.poll.is_closed:
return
if update.poll.total_voter_count == 3:
try:
quiz_data = context.bot_data[update.poll.id]
# this means this poll answer update is from an old poll, we can't stop it then
except KeyError:
return
context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"])
The telegram app shows to the user if s/he answered correctly or not. I already know which user answered the quiz. I would like to know programmatically which of the possible options of the quiz the user answered.
I did print message in the quiz method and I found this:
{'message_id': 93, 'date': 1614250320, 'chat': {'id': xxxxxxxx, 'type': 'private', 'first_name': 'user', 'last_name': 'name'}, 'entities': [], 'caption_entities': [], 'photo': [], 'new_chat_members': [], 'new_chat_photo': [], 'delete_chat_photo': False, 'group_chat_created': False, 'supergroup_chat_created': False, 'channel_chat_created': False, 'poll': {'id': '5024004102909067266', 'question': 'How many eggs do you need for a cake?', 'options': [{'text': '1', 'voter_count': 0}, {'text': '2', 'voter_count': 0}, {'text': '4', 'voter_count': 0}, {'text': '20', 'voter_count': 0}], 'total_voter_count': 0, 'is_closed': False, 'is_anonymous': True, 'type': 'quiz', 'allows_multiple_answers': False, 'correct_option_id': 2, 'explanation_entities': [], 'close_date': None}, 'from': {'id': 144******6, 'first_name': 'my_own', 'is_bot': True, 'username': '_my_own_Bot'}}
It seems the info on the user answer is nowhere here.
Also voter_count is not changing, it stays at zero no matter how many
answers I click on at the chat group.
I added these lines in the method receive_quiz_answer:
quiz_data = context.bot_data[update.poll.id]
print(quiz_data)
and I only got this info:
{'chat_id': 15******53, 'message_id': 87}
which is not relevant for what I want. I believe the answer should be in
telegram.PollAnswer, which is not listed in the code above.
I added the following lines:
answer = update.poll_answer
print("answer is {}". format(answer))
in receive_quiz_answer method, but I got the reply:
answer is None
Any help appreciated.
Thanks a lot.
I'm writing a meteor app and I'm trying to add an autocomplete feature to a search box. The data is very large and is on the server, so I can't have it all on the client. It's basically a database of users. If I'm not wrong, the mizzao:autocomplete package should make that possible, but I can't seem to get it to work.
Here's what I have on the server:
Meteor.publish('autocompleteViewers', function(selector, options) {
Autocomplete.publishCursor(viewers.find(selector, options), this);
this.ready();
});
And here are the settings I use for the search box on the client:
getSettings: function() {
return {
position: 'bottom',
limit: 5,
rules: [{
subscription: 'autocompleteViewers',
field: '_id',
matchAll: false,
options: '',
template: Template.vLegend
}],
};
}
But I keep getting this error on the client:
Error: Collection name must be specified as string for server-side search at validateRule
I don't really understand the problem. When I look at the package code, it just seems like it's testing whether the subscription field is a string and not a variable, which it is. Any idea what the problem could be? Otherwise is there a minimum working example I could go from somewhere? I couldn't find one in the docs.
Error: Collection name must be specified as string for server-side search at validateRule
You get this error because you don't specify a Collection name in quotes.
getSettings: function() {
return {
position: 'bottom',
limit: 5,
rules: [{
subscription: 'autocompleteViewers',
field: '_id',
matchAll: false,
collection: 'viewers', // <- specify your collection, in your case it is a "viewers" collection.
options: '',
template: Template.vLegend
}],
};
}
For more information please read here.
Hope this helps!
I have the following Mongo collection:
{
id: '123456',
name: 'GameXYZ',
reviews: [
{createdBy: 'Bob', score: 5}, {createdBy: 'John', score: 8}
]
}
I would like to create a publish function that returns only the review created by Bob:
{
reviews: [
{createdBy: 'Bob', score: 5}
]
}
I've tried this:
return myCollection.find({'reviews.createdBy': 'Bob'}, {'reviews.$': 1});
The problem is Meteor returns the entire document. According to their documents, "Field operators such as $ and $elemMatch are not available on the client side yet."
My function is running on the server, so I don't know why it's not working. It does work on the Mongo Shell.
My question is: could anyone recommend a way to publish only that single object of the array, in Meteor?
This is best done like the comments example from discover meteor. Create new collection reviews. You can put what ever you need in it but it has to have the id of what ever it's a review of. That way you can publish and find it with reviews.find({reviewsId: the id of the collection; in this case 123456})
I am using mrt add accounts-ui-bootstrap-dropdown and mrt add accounts-password to get a simple login page running on my app.
The accounts users gives me a nice hash containing ids, createdAt, emails, etc.
If I wanted to add other fields in this hash so I can make use of them later, how would I do that? For example, I want then to also enter their given name and surname:
"given_name": "John", "surname": "Doe"
Users are special objects in meteor ; you don't want to add fields in the user but in the users profile.
From the doc :
By default the server publishes username, emails, and profile.
If you want to add properties like surname when you create the account, you should use in the Account.onCreateUser server-side hook : http://docs.meteor.com/#accounts_oncreateuser
Accounts.onCreateUser(function(options, user) {
//pass the surname in the options
user.profile['surname'] = options.surname
return user
}
If you want to update a user after, you can do it from the client that way :
Meteor.users.update({_id:Meteor.user()._id}, { $set: {what you want to update} });
By default, the users base will allow that (the current user may update itself). If you don't trust your users and want to ensure that everything is properly update, you can also forbid any updates from the client and make them via a Meteor.call() and proceed to the checkings server-side. But this would be sad.
Edit :
As said in the comments, adding options via the standard account-ui won't be possible. You'll only be able to update the user after the registration. To add options when you subscribe, you'll have to make you own form.
I won't insult you by writing html markup, but here is what you want to have after the submit event (and after the various checking) :
var options = {
username: $('input#username')[0].value,
emails: [{
address: $('input#email')[0].value,
verified: false
}],
password: $('input#password')[0].value,
profile: {
surname: $('input#surname')
},
};
Accounts.createUser( options , function(err){
if( err ) $('div#errors').html( err.message );
});
You only need the account-base package ; not the account-ui.
Login with the social networks is cake :
Meteor.loginWithFacebook({
requestPermissions: ['email', 'user_birthday', 'user_location']
}, function(error){loginCallBack(error);});
About the answer ram1 made :
This is not the way meteor works. You do not "POST" a form. You want all your client / server communication done via the websocket. The equivalent of what you are talking about is making a "Meteor.call('myserverfunction', myarguments, mycallback)" of a server method from the client and you pass the arguments you want the server to use.
But this is not the way you will get the best of meteor. There is the philosophy you want to work with :
you have datas in your local mini mongo you got from the server
you update locally those datas in your base / view
meteor do his magic to transmit those updates to the server
there the server can answer : ok, updates saved, this is seamless for you. Or answer : nop ! reverse the changes (and you can implement an error notification system)
(it can answer no because you don't have the permission to update this field, because this update break a rule you did set up...)
All you do is setting permissions and controls on the databases server-side. That way, when an honest client make an update, he sees the result instantly ; way before it has been pushed to the server and send to the other clients. This is latency compensation, one of the seven principles of meteor.
If you modify a data via Meteor.call, you will do that :
send an update to the server
the server checks and update the base
the server send the update to the clients (including you)
your local base updates and your view update => you see your update
=> this is what you had in yesterday app ; meteor allow you to build a today app. Don't apply the old recipes :)
The accepted answer has the HOW right, but the WHERE is outdated information. (Yes, this would be better as a comment on the answer, but I can't do that yet.)
From the Meteor 1.2 documentation:
The best way to store your custom data onto the Meteor.users collection is to add a new uniquely-named top-level field on the user document.
And regarding using Meteor.user.profile to store custom information:
🔗Don’t use profile
There’s a tempting existing field called profile that is added by
default when a new user registers. This field was historically
intended to be used as a scratch pad for user-specific data - maybe
their image avatar, name, intro text, etc. Because of this, the
profile field on every user is automatically writeable by that user
from the client. It’s also automatically published to the client for
that particular user.
Basically, it's probably fine to store basic information such as name, address, dob, etc in the profile field, but not a good idea to store anything beyond that as it will, by default, be writeable by the client and vulnerable to malicious users.
I had the same problem and managed to do it only with Accounts.createUser:
Accounts.createUser({
email: email,
password: password,
profile: {
givenName: 'John',
surname: 'Doe',
gender: 'M'
}
}
Thats very simple way and it works. Just add your desired variables in the profile section and it should be ready. Hope it helps someone.
I ended up using https://atmospherejs.com/joshowens/accounts-entry which offers an extraSignUpFields config option.
From the documentation (https://github.com/ianmartorell/meteor-accounts-ui-bootstrap-3/blob/master/README.md):
Custom signup options
You can define additional input fields to appear in the signup form, and you can decide wether to save these values to the profile object of the user document or not. Specify an array of fields using Accounts.ui.config like so:
Accounts.ui.config({
requestPermissions: {},
extraSignupFields: [{
fieldName: 'first-name',
fieldLabel: 'First name',
inputType: 'text',
visible: true,
validate: function(value, errorFunction) {
if (!value) {
errorFunction("Please write your first name");
return false;
} else {
return true;
}
}
}, {
fieldName: 'last-name',
fieldLabel: 'Last name',
inputType: 'text',
visible: true,
}, {
fieldName: 'gender',
showFieldLabel: false, // If true, fieldLabel will be shown before radio group
fieldLabel: 'Gender',
inputType: 'radio',
radioLayout: 'vertical', // It can be 'inline' or 'vertical'
data: [{ // Array of radio options, all properties are required
id: 1, // id suffix of the radio element
label: 'Male', // label for the radio element
value: 'm' // value of the radio element, this will be saved.
}, {
id: 2,
label: 'Female',
value: 'f',
checked: 'checked'
}],
visible: true
}, {
fieldName: 'country',
fieldLabel: 'Country',
inputType: 'select',
showFieldLabel: true,
empty: 'Please select your country of residence',
data: [{
id: 1,
label: 'United States',
value: 'us'
}, {
id: 2,
label: 'Spain',
value: 'es',
}],
visible: true
}, {
fieldName: 'terms',
fieldLabel: 'I accept the terms and conditions',
inputType: 'checkbox',
visible: true,
saveToProfile: false,
validate: function(value, errorFunction) {
if (value) {
return true;
} else {
errorFunction('You must accept the terms and conditions.');
return false;
}
}
}]
});
The official Meteor Guide provides a comprehensive answer with an example code:
The best way to store your custom data onto the Meteor.users collection is to add a new uniquely-named top-level field on the user document.
https://guide.meteor.com/accounts.html#custom-user-data