how to search a document where members map contains a particular map_id
chatRoom: {
MVyMPi78DOwVQ5w5GnYe: {
...
members: {
<member_1_id>: {
id: xxxxx,
unreadmessagecount: xx
},
<member_2_id>: {
id: xxxxx,
unreadmessagecount: xx
}
},
La234Pi78DOwVQ5w5GnYe: {
...
members: {
<member_1_id>: {
id: xxxxx,
unreadmessagecount: xx
},
<member_2_id>: {
id: xxxxx,
unreadmessagecount: xx
}
}
}
like for above structure get all chatrooms where members map contains member_id assume member_id is known
You can use dot notation to query documents based on nested field as shown below:
const q = query(collection(db, 'chatRoom'), where('members.user1', '==', {<obj>}))
This won't work for your use-case unless you know the exact object i.e. { uid: 'user1', unreadMessageCount: 1 }.
As a workaround, you can use orderBy() clause that'll only return documents where the field exists like this:
const q = query(collection(db, 'chatRoom'), orderBy('members.user1'))
However, this is actually querying the whole collection and so writing security rules will be a bit difficult. For example, if you use allow read: if request.auth.uid in resource.data.members, the rule will fail as there will be many documents where this returns false.
I would recommend using storing all members' UIDs in an array as follows:
{
...
group: false,
memberIds: ['user_1_uid', 'user_3_uid']
}
Then you can use array-contains operator to query all chat rooms of a user like this:
const q = query(collection(db, 'chatRoom'), where('memberIds', 'array-contains', 'some_uid'))
Then you can use the following security rules to ensure only a chat member can read/write the document:
allow read, write: if request.auth.uid in resource.data.memberIds
Related
I have two Entities, Organization and Applications. One organization can have many Applications.
const organization = mongoose.Schema(
{
name: {
type: String,
required: [true, 'Organization name is required.']
}, // min 1 max 255
// administrators get it from user table with org and role.
applications: [
// use populate in query
{
type: mongoose.Types.ObjectId,
ref: 'Application'
}
]
I am trying to query Organization with two applications and its returning a blank array
const organizations = await Organization.find({
'applications': {
$all: [
'636bdf70bcd2d24005061023',
'6373ba91f53f95ca187809d6'
]
}
}).populate('applications');
I tried running the same expression in MongoDB compass and it works. What am I doing wrong here ?
Based on your schema model, the applications field is an array of ObjectIDs, not an array of objects where each object would have _id property.
So instead of applications._id, it should be applications:
const organizations = await Organization.find({
applications: {
$all: ['636bdf70bcd2d24005061023', '6373ba91f53f95ca187809d6']
}
})
.populate('applications');
I have a Collection of Player Documents in firestore. I want to mark some of those Documents as private, so that they can't be queried against. A JSON dump of my data looks like this:
[
{
"id": "H0ycPIqXB5pX5VmdYlmY",
"name": "Tim",
},
{
"id": "VICMGdutgIN7PUjG571h",
"name": "Zoe",
},
{
"id": "query-blocker",
"name": "Don't look here",
"private": true
},
{
"id": "zYkhO5f7gYPe2VgqQQXe",
"name": "Bob"
}
]
Now apply this security rule, intended to protect any document with a field labelled private:
match /players/{document=**} {
allow read: if !('private' in resource.data);
}
Results:
A query to read a single document that contains a field private, correctly returns a permission denied error.
A query to read all the documents in the collection successfully returns all documents in the collection, including all of the ones marked private.
It seems like the query for all documents should also fail (I understand that security rules are not filters). Is there something I am misunderstanding here?
Here is a working example of the issue using the emulator: https://github.com/Keltin42/firebase-it-rulestest
Here is a simplified example you can run from the command line:
'use strict';
const firebase = require('firebase');
require('firebase/firestore');
firebase.initializeApp({
apiKey: 'your api key here',
projectId: 'your project id here'
});
const db = firebase.firestore();
async function doTest() {
const playersCollection = db.collection('players');
await playersCollection.add({ name: 'Sue' });
await playersCollection.add({ name: 'Bob' });
await playersCollection.doc('good').set({ name: 'Fred' });
await playersCollection.doc('query-blocker').set({ name: 'Tim', private: true });
// Read a good document.
await playersCollection.doc('good').get().then(doc => {
console.log('The good document: ', JSON.stringify(doc.data()));
});
// Read all the documents
await playersCollection.get().then(querySnapshot => {
console.log('All documents: ');
querySnapshot.forEach(doc => {
console.log('\t', doc.id, ' => ', doc.data());
});
});
// Read the query-block document
await playersCollection.doc('query-blocker').get().then(doc => {
console.log('The query-blocker document: ', JSON.stringify(doc.data()));
}).catch(error => {
console.error('Error retrieving query-blocker document: ', error);
});
}
doTest();
with the security rules:
service cloud.firestore {
match /databases/{database}/documents {
match /players/{document=**} {
allow write;
allow read: if !('private' in resource.data);
}
}
}
You've created a highly complex scenario here that is hidden behind your proposed solution (see what is the XY problem). A reasonable answer based on what we can discern from your use case is to simplify your data and the required query. Also recognize that security rules are not filters and that you can't do a get() on the collection and expect that to filter the results on your behalf.
Start by setting private: false for all existing records which don't have a value (you can't query for absence of things or missing values).
Then set up your rules like this:
match /players/{playerId} {
allow read: if resource.data.private == false;
}
And your queries like this:
playersCollection.where("private", "==", false).get()
When performing queries, the query must match the rule for it to succeed (the data isn't actually examined as this can't scale). So your goal is to match those two.
Generally speaking, avoid the use of globs (e.g. document=**) as they introduce potential security issues (implicit allow is usually discouraged in security; explicit allow is preferred).
// my db structure now
rcv : {
visible: 'all',
ids: [
[0] : userId,
[1] : user2Id ]
}
this is how i query to get the data it works.
//service.ts
getAlbumByUserId(userId) {
return this.afs.collection('albums', ref => ref.where('rcv.visible', '==', 'all').where('rcv.ids', 'array-contains', userId)).valueChanges();
}
//component.ts
this.service.getAlbumByUserId(this.userId);
but i want to set the structure like this but i don't know how to query nested objects in firebase
// database structure
rcv : {
visible: 'all',
ids: {
userId: {
id: userId
}
user2Id: {
id: user2Id
}
}
}
You're looking for the array-contains operator, which can check if a field that is an array contains a certain value.
You're already using the correct array-contains operator, but not with the correct syntax. The array-contains operator checks whether any element of your array is exactly the same as the value you pass in. So you need to pass in the complete value that exists in the array:
ref.where('rcv.visible', '==', 'all').where('rcv.ids', 'array-contains', { id: userId })
As you add more data to the array, it may become unfeasible to reproduce the entire array element for the query. In that case, the common approach is to add an additional field where you keep just the IDs.
So you'd end up with one field (say rcv.users) where you keep all details about the receiving users, and one field (say rcv.ids) where you just keep their IDs, and that you use for querying.
I have a structure like this:
{
...
_id: <projectId>
en-GB: [{
_id: <entryId>,
key: 'some key',
value: 'some value',
}]
}
And I've tried updating it with Mongoose (and raw mongodb too) like this:
const result = await Project
.update({
_id: projectId,
'en-GB._id': entryId,
}, {
$set: {
'en-GB.$.key': 'g000gle!!',
},
})
.exec();
I've checked that the IDs are correct. But it doesn't update anything:
{ n: 0, nModified: 0, ok: 1 }
What am I doing wrong? Thanks
As discussed in the comments on the question, the issue is directly related to passing in a string representation of an id in the query as opposed to using an ObjectId. In general, it's good practice to treat the use of ObjectIds as the rule and the use of string representations as special exceptions (e.g. in methods like findByIdAndUpdate) in order to avoid this issue.
const { ObjectId } = require('mongodb');
.update({
_id: ObjectId(projectId),
'en-GB._id': ObjectId(entryId),
})
I have the following schemas for the document Folder:
var permissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
var folderSchema = new Schema({
name: { type: string },
permissions: [ permissionSchema ]
});
So, for each Page I can have many permissions. In my CMS there's a panel where I list all the folders and their permissions. The admin can edit a single permission and save it.
I could easily save the whole Folder document with its permissions array, where only one permission was modified. But I don't want to save all the document (the real schema has much more fields) so I did this:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }, function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
but the problem is that perm is always undefined! I tried to "statically" fetch the permission in this way:
var perm = data.permissions[0];
and it works great, so the problem is that Underscore library is not able to query the permissions array. So I guess that there's a better (and workgin) way to get the subdocument of a fetched document.
Any idea?
P.S.: I solved checking each item in the data.permission array using a "for" loop and checking data.permissions[i]._id == permission._id but I'd like a smarter solution, I know there's one!
So as you note, the default in mongoose is that when you "embed" data in an array like this you get an _id value for each array entry as part of it's own sub-document properties. You can actually use this value in order to determine the index of the item which you intend to update. The MongoDB way of doing this is the positional $ operator variable, which holds the "matched" position in the array:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$": permission
}
},
function(err,doc) {
}
);
That .findOneAndUpdate() method will return the modified document or otherwise you can just use .update() as a method if you don't need the document returned. The main parts are "matching" the element of the array to update and "identifying" that match with the positional $ as mentioned earlier.
Then of course you are using the $set operator so that only the elements you specify are actually sent "over the wire" to the server. You can take this further with "dot notation" and just specify the elements you actually want to update. As in:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$.role": permission.role
}
},
function(err,doc) {
}
);
So this is the flexibility that MongoDB provides, where you can be very "targeted" in how you actually update a document.
What this does do however is "bypass" any logic you might have built into your "mongoose" schema, such as "validation" or other "pre-save hooks". That is because the "optimal" way is a MongoDB "feature" and how it is designed. Mongoose itself tries to be a "convenience" wrapper over this logic. But if you are prepared to take some control yourself, then the updates can be made in the most optimal way.
So where possible to do so, keep your data "embedded" and don't use referenced models. It allows the atomic update of both "parent" and "child" items in simple updates where you don't need to worry about concurrency. Probably is one of the reasons you should have selected MongoDB in the first place.
In order to validate subdocuments when updating in Mongoose, you have to 'load' it as a Schema object, and then Mongoose will automatically trigger validation and hooks.
const userSchema = new mongoose.Schema({
// ...
addresses: [addressSchema],
});
If you have an array of subdocuments, you can fetch the desired one with the id() method provided by Mongoose. Then you can update its fields individually, or if you want to update multiple fields at once then use the set() method.
User.findById(userId)
.then((user) => {
const address = user.addresses.id(addressId); // returns a matching subdocument
address.set(req.body); // updates the address while keeping its schema
// address.zipCode = req.body.zipCode; // individual fields can be set directly
return user.save(); // saves document with subdocuments and triggers validation
})
.then((user) => {
res.send({ user });
})
.catch(e => res.status(400).send(e));
Note that you don't really need the userId to find the User document, you can get it by searching for the one that has an address subdocument that matches addressId as follows:
User.findOne({
'addresses._id': addressId,
})
// .then() ... the same as the example above
Remember that in MongoDB the subdocument is saved only when the parent document is saved.
Read more on the topic on the official documentation.
If you don't want separate collection, just embed the permissionSchema into the folderSchema.
var folderSchema = new Schema({
name: { type: string },
permissions: [ {
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
} ]
});
If you need separate collections, this is the best approach:
You could have a Permission model:
var mongoose = require('mongoose');
var PermissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
module.exports = mongoose.model('Permission', PermissionSchema);
And a Folder model with a reference to the permission document.
You can reference another schema like this:
var mongoose = require('mongoose');
var FolderSchema = new Schema({
name: { type: string },
permissions: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Permission' } ]
});
module.exports = mongoose.model('Folder', FolderSchema);
And then call Folder.findOne().populate('permissions') to ask mongoose to populate the field permissions.
Now, the following:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }).populate('permissions').exec(function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
The perm field will not be undefined (if the permission._id is actually in the permissions array), since it's been populated by Mongoose.
just try
let doc = await Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{ "permissions.$": permission},
);