I need to give anonymous users the ability to update a specific value inside a nested architecture without giving permission on the rest of the document.
events is a top level collection and I update my document like so
update(`events/${event.id}`, {
[`boardgames.${boardgameId}.votes.${uid}`]: true,
})
I have tried this solution
allow update: if ((request.writeFields.size() == 1) && ('boardgames' in request.writeFields));
as mentioned here but it fails (both halves fails independently too).
What am I missing? Am I not updating only one field when 'going inside' a nested object?
It would be super helpful to be able to console.log the request and the request.writeFields.
Note that ideally I would like to only allow modification on the deepest property, the current user's uid inside the votes.
Actually, request.writeFields captures the full path of the modified fields, so in your case it will be equal to boardgames.${boardgameId}.votes.${uid}.
You could use a regex with String.matches
allow update: if request.auth.uid != null
&& request.writeFields.size() == 1
&& request.writeFields[0].matches("boardgames\.[^.]{5,25}\.votes\." + request.auth.uid)
Careful: request.writeFields is deprecated but unfortunately there is no good replacement for your problem
Related
In my scenario a user can "like" the profile of another user. As a result I have a subcollection called "likedBy" for each user, where I create a document for a specific user, e.g.:
users(col) -> User A(doc) -> likedBy(col) -> User B (doc), User C (doc)
So in the rare scenario of a user being deleted, I want to delete all the likes issues by that user.
I am just not sure if this is even possible (a workaround could be to just save the userId again in said document and query for that).
What I am basically looking for is something like this:
db.collectionGroup('likedBy').where(firebase.firestore.FieldPath.documentId(), '==', "User A").get();
The problem is, that I can not create an index for the documentId in the Firestore console.
UPDATE 2020-01-23:
For updates on this, see a conversation with Sam Stern on the group board:https://groups.google.com/d/msgid/google-cloud-firestore-discuss/e1b47358-b106-43a0-91fb-83c97d6244de%40googlegroups.com
Much of the discussion comes from the apparent fact that there is a SINGLE "master index" of ALL records in a database based on the FULLY QUALIFIED path to the document (hence only needing to be unique "within a collection").
To "accelerate" document references, the JS SDK actually "pre-pends" the collection path onto whatever info is passed in .doc to "conveniently" use that master index
i.e. all of these are exactly equivalent
db.doc('collection/docId/collection/docId/collection/docId")
db.collection('collection").doc("docId/collection/docId/collection/docId")
db.collection('collection").doc("docId").collection("collection").doc("docId/collection/docId")
db.collection('collection").doc("docId").collection("collection").doc("docId").collection("collection").doc("docId")
db.doc("collection/docId").collection("collection").doc("docId").collection("collection").doc("docId")
db.collection("collection/docId/collection").doc("docId").collection("collection").doc("docId")
db.doc("collection/docId/collection/docId").collection("collection").doc("docId")
db.collection("collection/docId/collection/docId/collection").doc("docId")
--- they ALL create the same index reference 'collection/docId/collection/docId/collection/docId" to find the document in the "master index".
(in my not-at-all-humble-opinion) FieldPath.documentId() was implemented (incorrectly) to "conveniently" match this behavior thus requiring the fully-qualified path, not the docId, when it should have been implemented like any other query, and required creating and maintaining a NEW INDEX to accommodate the query.
The code for this behavior was written BEFORE collectionGroups were implemented - and never documented the hack used didn't match the METHOD NAME used.
The work around is to require the Coder to copy the docId as a field in each document, and write your queries on that. I already wrote my own layer between Firestore.js and my application to abstract the behavior, and will probably simply implement this as a basic feature of the library.
But this is clearly a MISTAKE, and so far everybody keeps trying to tell me it makes sense, and that they'll change the documentation to match the existing behavior (but not the method name).
As I wrote previously, I keep getting handed a ratty bunch of daisies, and being told "See? these are roses!! The documentation calls them roses!! Roses by any other name smell as sweet, and all that!!"
No Update Expected Unless They Get Embarrassed Enough
UPDATE 2020-01-10: I have built a demo app showing the exact bug, and have sent it to Firebase support as requested. For some dang reason, the support critter considers it a "feature request", in spite of it clearly a bug. When a URL is called in the form "/ShowInfo/showID", the App signs in to Firebase Auth anonymously; then calls a query on the collectionGroup (3 levels deep) using FieldPath.documentId() "==" showID
It makes the query 3 ways:
1) Once with only the showID- which fails with the familiar "Invalid query. When querying a collection group by FieldPath.documentId(), the value provided must result in a valid document path, but 'pqIPV5I7UWne9QjQMm72'(the actual showID) is not because it has an odd number of segments (1)."
2) Once with a "Relative Path" (/Shows/showID), which doesn't have the error, but returns no document.
3) Finally with the "Full Path" (/Artists/ArtistID/Tour/tourID/Shows/showID). This doesn't have an error, and does return a document - but if I have the full path, why do I need the query on the collectionGroup? And I don't have the full path - the showID (above) comes in as part of a URL (a link to the show data, obviously) - I hand-faked it for the test.
Waiting for response.
UPDATE 2019-12-02: Firebase support reached out to ask if I still wanted this solved. Duh.
UPDATE 2019-09-27: Firebase Support has acknowledged this is a bug. No word on when it will be fixed. documentId() should, indeed, be able to be used directly against only the document Id.
documentID can be used as part of a query but, as #greg-ennis notes above, it needs an even number of segments. As it turns out, if you truly need a collectionGroup (I do), then the "Id" to be compared needs to redundantly add the collectionGroup ID/name as a segment:
db.collectionGroup('likedBy')
.where(firebase.firestore.FieldPath.documentId(), '==', "likedBy" + "/" + "User A")
.get();
I've used it and it (*sorta) works. (admittedly, it took me 2 hours to figure it out, and the above question helped focus the search)
On the other hand, this specific case is not where you want to use collectionGroup. Remember what collection group is - a way to refer to a a set of separate collections as if they were one. In this case the "collection" that holds "User A"s likes exists as a collection under "User A"s doc. Simply delete that single collection before deleting "User A"s doc. No need to bring everybody else's likes into it.
Sorta: the field path compared apparently has to be the complete path to the document. If you know the documentId, but for "reasons" you do not know the complete path, including which document the sub-collection is a part of (kinda why you were using the collectionGroup approach in the first place), this now seems a tadly dead-end. Continuing working on it.
Verified and Bug Report filed: FieldPath.documentID() does not compare against the documentId; it compares against the fully segmented document path, exactly as you would have to provide to .doc(path):
To wit: to find a document at "TopCollection/document_this/NextCollection/document_that/Travesty/document_Id_I_want"
using the collectionGroup "Travesty"
db.collectionGroup("Travesty")
.where(firebase.firestore.FieldPath.documentId(), '==', "document_id_I_want")
...will fail, but...
db.collectionGroup("Travesty")
.where(firebase.firestore.FieldPath.documentId(), '==', "TopCollection/document_this/NextCollection/document_that/Travesty/document_Id_I_want")
.get()
...will succeed. Which makes this useless, since if we had all that info, we would just use:
db.doc("TopCollection/document_this/NextCollection/document_that/Travesty/document_Id_I_want")
.get()
There is no way you can use the following query:
db.collectionGroup('likedBy').where(firebase.firestore.FieldPath.documentId(), '==', "User A").get();
And this is because collection group queries work only on document properties and not on document ids. According to the official documentation regarding collection group queries:
db.collectionGroup('landmarks').where('type', '==', 'museum');
You query the landmarks subcollection where the type property holds the value of museum.
A workaround could be to store the id of the user in an array and use array-contains but remember, for each collection group query you use, you need an index and unfortunately you cannot create such an index programmatically. Even if you can create an index in the Firebase console, it won't help you since you get those ids dynamically. So is not an option to create an index for each user separately because you'll reach the maximim number of indexes very quickly.
Maximum number of composite indexes for a database: 200
To solve this kind of problems, you should consider adding an array under each user object and use a query like this:
usersRef.where("usersWhoLikedMe", "array-contains", "someUserId")
Where usersWhoLikedMe is a property of type array.
If you add User A and B ids to the doc itself:
users(col) -> User A(doc) -> likedBy(col) -> User B ({user_id: B, profile_liked_id: A})
Then you can query using:
db.collectionGroup('likedBy').where('user_id', '==', 'B');
if the reference is a collection, the value you compare to needs to be document id (the last segment of a full path)
this is a error message if you violated the rule
Invalid query. When querying a collection by documentId(), you must
provide a plain document ID, but 'a/b/c' contains a '/' character
if the reference is a collectonGroup, the value you compare to needs to be a full document path <-- which is very redundant in my opinion
Invalid query. When querying a collection group by documentId(), the
value provided must result in a valid document path, but
'a/b/c' is not because it has an odd number of
segments (3)
tested recently
I have this huge form that only gets pre-filled data if data already exists in a database. Otherwise, none of the text boxes should have the value parameter in them. I thought it would just ignore the value parameters if the variable I get data from does not exist. But instead, I get an error.
How to I handle this case? Will I have to do an if check before each text box in Jade like the following?
if (typeof(prefilled_data) !== 'undefined')
input.form-control#prevalence(type="text", name="prevalence")
else
input.form-control#prevalence(type="text", name="prevalence", value=prefilled_data.tb_burden_estimates.prevalence)
While I don't mind doing this (Sublime Text will help with all the copy-pasting), there are quite a few form fields, so it might get ugly. Is there a way to consolidate this into one check somewhere?
you seemed to be suggesting that the if statement's were going to be bulky/make the code hard to read if they were there.. my suggestion would be to programmatically create the inputs, there by reducing the if statements to a more manageable number and answering your question about being able to "consolidate this into one check somewhere"
EDIT
If you are looking to access data in js.. I have been known to use something like:
script(type='text/javascript').
window.prefilled_data = !{ JSON.stringify(prefilled_data) };
This will allow you then to access the global window.prefilled_data variable to get front end version of your data
you can do this:
- if (typeof(prefilled_data) === 'undefined'){
- prefilled_data = '';
- }
input.form-control#prevalence(type="text", value=#{prefilled_data})
if prefilled_data is undefined you just set a '' value
This theoretically seems kind of easy but I am not 100% on the appropriate code to write.
Inside a view if I am defining a max variable that returns an attribute points in a collection, I can pass that variable as an object into my template like maxPoints.toJSON() and get that object's attributes.
var maxPoints = this.collection.max(function(player){
return player.get('team') == home || player.get('team') == away ? player.get('points') : 0;
});
//Pass to template
this.$el.find('.top-preformer').append(this.template(maxPoints.toJSON()));
//Inside the template using underscore.js I can get the `name` of the object returned
<%= name %>
//Easy, but now I am passing two objects, how do I access `name` for top points
//or `name` for top assists?
This is fine, but what if I want to retrieve multiple max values and interact with them inside the template?
var maxAssists = this.collection.max(function(player){
return player.get('team') == home || player.get('team') == away ? player.get('assists') : 0;
});
Now I have an object maxAssists that I could pass into my template, but I am not sure how this would work. How could I interact with each specific object?
I'm sorry if this answer isn't appropriate now that you've deleted your previous question and asked another one. This is the answer I was about to send when you deleted your question:
I hope I've correctly understood your scenario. Let me sum it up just in case:
There's a Model, let's call it Player, which represents a player with a name, a team, points and assists (at least). There's a Collection of Players which is held by the View you're referring to. This View shows stuff about players and also highglights the player with most points and the one with most assists.
I'm assuming too much about your needs, but let's say you want to show a list of players with their info and then the info of those highlighted. Check this JSFiddle I prepared to show my approach.
As a rule of thumb, you shouldn't set data in the DOM unless it's extrictly necessary, that's why you're using JavaScript after all. Since you have a View which holds a Collection, that View should be the one to hold references to both most points player and most assists player. Using your max functions, you may do it this way:
getMaxPointsPlayer: function () {
return this.getMax("points");
},
getMaxAssistsPlayer: function () {
return this.getMax("assists");
},
getMax : function (attribute) {
return this.collection.max(function (player) {
return player.get("team") === this.homeTeam || player.get("team") === this.awayTeam ? player.get(attribute) : 0;
}, this);
},
Note two things:
You should always use === instead of == where possible. Search this site for more info on it.
Since I don't know where those home and away variables are in your code and what are them, I placed them as member variables in the view (and they're just strings).
So, when you want to reference the player with most points/assists, you'd call one of those functions which are part of the View. Then you may store it in a variable (so you can get it without further filtering) or use it straight away from the call to the function.
If you study the code in the JSFiddle, you'll see more stuff than you asked for. It's there to show you a nice way to keep your application clean and tidy (obviously, it's just a JSFiddle). Try to keep Views on their own business (may it be present and deal with a Model, have a list of sub-views and manage them because they're part of a Collection or just be a manager of other sub-views). That way, those views have the logic they need without dealing with data in the DOM.
I'm designing a grid in ExtJs 4 that will represent a Map data structure. I have a custom cell editor for the key column in the grid. I want to enforce the fact that the key must be unique among all keys. How do I go about doing this?
At the moment I'm trying to set up a special textfield editor with a custom validator that checks if the user-submitted key value is unique by comparing it to the dataStore's values. Unfortunately, this runs into the issue that the dataStore does not know what is the "current" record and so does not know which record to exclude when checking for duplicates.
^Confusing, right? That's why I think I'm doing it wrong.
It's a very good question, and I ended up doing pretty much exactly what you're doing. In order to handle current record so I can exclude it from the lookup I added something like that to the vtype validation function:
unique: function (val, field) {
// If field has not been changed - don't care about anything else
if (field.originalValue === undefined || val === field.originalValue) {
// _trace('unique - original value has not changed');
return true;
}
if (!field.isDirty()) {
// _trace('unique - field is not dirty');
return true;
}
// Check you store here...
},
uniqueText: 'Duplicate'
One more note - if you will try to use same validator in the dialogs (as oppose to grid roweditor) - you will need to add more checks because originalValue will not be properly set always (at least I had to do this in 4.0.7 in my application).
I was remembering that there was a jquery method which provide unique number for dom elements. It just may for only animated dom objects. Now I couldn't find that method. What is that method ? Is there any another way to provide unique number for elements ?
I think you may be thinking about the concept of jQuery.expando. There is an attribute called jQuery.expando that exists on every page that has jQuery running. It is defined like this:
expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
So for me, on the current page, it is jQuery15209244967177268291. Any element that has any data stored on it (including event handlers, which are stored as data) has a property with that name. This contains a unique number, which is the key for that element in the global data cache.
For instance, with the global StackExchange inbox on the top left of the screen:
$('.genu')[0].jQuery15209244967177268291 === 29
You can mimic this with $('.genu')[0][jQuery.expando]; I'm not sure whether you'll get the same number. (Edit: it's not even the same number for me every time.)
Note, however, that not every element has a unique number, only those with data attached to them. This may or may not fit your purposes...
The only thing that looks remotely like what you might be after is the jQuery.unique(), but even that doesn't do what you're suggesting. I would encourage you to update your question to state the purpose for this, as there may be a better way to solve the problem that is prompting you to get unique numbers for each element.