how to query for key exists in object field in firestore document (firebase web sdk) - google-cloud-firestore

I have subjects collection. In this collection every document has tutors field that is object where key is id of tutors( from tutors collection)
tutors: {
"tutor_id_1": {
"name": "jonas",
"email": "jonas#gmail.com"
},
"tutor_id_2":{
"name": "stephen",
"email": "stephen#gmail.com"
},
"tutor_id_3":{
"name": "maria",
"email":"maria#gmail.com"
}
}
So how to query subjects where tutors field contain tutor id equal to "tutor_id_1" ?
I found one way
if I have two variables in client side
const tutorToFindId = "xxx"
const tutorToFindEmail = "YYY"
query(
collection(db, 'subjects'),
where(`tutors.${tutorToFindId}.email`, '==', `${tutorToFindEmail}`)
),
Is there any other way ??

As I understand, "tutor_id_1" is being used as a unique id. Considering that, you may structure your data model with "tutors" as a subcollection instead of a field and you will be able to get the content of that specific document as follows:
const docRef = db.collection('subjects').doc('subjectX').collection('tutors').doc(tutorToFindId);
const tutor = await docRef.get();
if (!tutor.exists) {
console.log('No such document!');
} else {
console.log('Document data:', tutor.data());
}

db.getBy({ where: { [`tutors.${tutorToFindId}.email`]: tutorToFindEmail } });
Take a look at the getBy function in https://www.npmjs.com/package/firebase-firestore-helper
Disclaimer: I am the creator of this library. It helps to manipulate objects in Firebase Firestore (and adds Cache)
Enjoy!

Related

MongoDB Atlas triggers - access different collection

I have a trigger on new documents in collection A, in which I want to get a related record from the users collections.
I (think) I followed the documentation to the "t", but both the collection itself comes out empty and the actual user I'm trying to fetch. And ideas what I'm doing wrong?
exports = async function trigger(changeEvent) {
const toolRecord = changeEvent.fullDocument;
const usersCollection = await context.services
.get("Cluster0")
.db("mind-tools")
.collection("users");
const user = await usersCollection.find({ _id: toolRecord.userId });
const users = await usersCollection.find({});
const text = [
"New tool record - " + toolRecord._id,
toolRecord.toolKey,
"users",
JSON.stringify(usersCollection),
JSON.stringify(users[0]),
"user",
toolRecord.userId,
JSON.stringify(user),
user.name,
user.email,
]
.filter(Boolean)
.join("\n");
}
Try to use findOne instead of find for the user.
Replace:
const user = await usersCollection.find({ _id: toolRecord.userId });
with:
const user = await usersCollection.findOne({ _id: toolRecord.userId });
Is it possible to console.log() different variables in your script to see what value they got in run time?
Also in the example provided in Atlas tutorial, the first line is
exports = async function (changeEvent) { ...
Without the keyword "trigger"

Strapi : populate result from a relation collection

I'm using strapi 3.4.4 with mongodb.
I've got a location collection with a reservation linked collection (a location can have many reservations).
Location Model
text field : title of the location
Reservations Model
linked collection : has one author
My goal is to populate the author name when I made a get request to /location
But right now, my request is :
{
"reservations": [
{
"_id": "60be41626a15ab675c91f417",
"author": "60be3d40ea289028ccbd4d5a",
"id": "60be41626a15ab675c91f417"
}
],
"_id": "60be40a26a15ab675c91f414",
"title": "New York Plaza",
},
As you can see, the author is an id, so I try to edit location/controllers/location.js to
const { sanitizeEntity } = require('strapi-utils');
module.exports = {
/**
* Retrieve records.
*
* #return {Array}
*/
async find(ctx) {
let entities;
if (ctx.query._q) {
entities = await strapi.services.location.search(ctx.query);
} else {
entities = await strapi.services.location.find(ctx.query, ['reservations', 'reservations.author']);
}
return entities.map(entity => sanitizeEntity(entity, { model: strapi.models.location }));
},
};
I followed this guide : https://www.youtube.com/watch?v=VBNjCgUokLk&list=PL7Q0DQYATmvhlHxHqfKHsr-zFls2mIVTi&index=27
After further research, it seems that the syntax ['reservations', 'reservations.author'] doesn't work to populate with mongodb.
await strapi.services.post.find(ctx.query, {path:'reservations', populate:{path:'author'}});
works
You need to provide the API key you're using "Full access" for what ever reason, "Read only" won't return relations.
On top of that you will still need to use the query parameter populate.
http://localhost:1337/api/locations?populate=*

how can I set the value of objectId to another property different that _id when creating a document?

I'm trying to create an object that looks like this:
const userSettingsSchema = extendSchema(HistorySchema, {
user: //ObjectId here,
key:{type: String},
value:{type: String}
});
this is the post method declared in the router
app.post(
"/api/user/settings/:key",
userSettingsController.create
);
and this is the method "create":
async create(request, response) {
try {
const param = request.params.key;
const body = request.body;
console.log('body', body)
switch (param) {
case 'theme':
var userSettings = await UserSettings.create(body) // user:objecId -> missing...
response.status(201).send(userSettings);
break;
}
} catch (e) {
return response.status(400).send({ msg: e.message });
}
}
I don't know how to assign the value of ObjectId to the user property, because ObjectId is generate when the doc is created, thus, I can not do this: userSettings.user = userSettings._id, because the objectr is already. I only manage to get something like this created:
{
"_id": "60c77565f1ac494e445cccfe",
"key": "theme",
"value": "dark",
}
But it should be:
{
"user": "60c77565f1ac494e445cccfe",
"key": "theme",
"value": "dark",
}
_id is the only mandatory property of a document. It is unique identifier of the document and you cannot remove it.
If you provide document without _id the driver will generate one.
You can generate ObjectId as
let id = mongoose.Types.ObjectId();
and assign it to as many properties as you want.
You can even generate multiple different ObjectIds for different properties of the same document.
Now, you don't really need to assign ObjectId to "user" property. _id will do just fine. In fact it is most likely you don't need user's settings in separate collection and especially as multiple documents with single key-value pair.
You should be good by embedding "settings" property to your "user" collection as a key-value json.

Firebase firestore rules don't always work on web

Consider the following rule:
match /triplogs/{anyDocument} {
allow create: if request.auth != null;
allow read, update, delete: if request.auth.uid == resource.data.userId;
}
If I hit that with my logged in user, I get the standard:
Error: FirebaseError: [code=permission-denied]: Missing or insufficient permissions.
A few points to know:
I am indeed logged in, I can output the UID of the current user and it is correct.
I can hit this same route via the mobile version and it works.
If I remove the resource.data.userId check, and just check to see if the user is authenticated (even hard code a uid), it works (so the rule seems to get checked against appropriately)
The simulator says "resources" is null. I don't understand how the resources object could be null, maybe it's something with the simulator?
Any help would be appreciated, I've troubleshot and googled around for the last few hours with no success.
Query in question:
// ... My firebase wrapper:
import { firebaseApp } from '../utils/firebase';
// ...
const collectionName = 'triplogs';
export const fetchTrips = async (filters?: Filter[]) => {
let query;
const ref = db?.collection(collectionName);
const trips: TripLog[] = [];
if (filters) {
filters.forEach((filter) => {
query = ref?.where(filter.field, filter.operator, filter.value);
});
}
try {
const obj = query || ref;
console.log(firebaseApp()?.auth().currentUser?.uid); // <-- this is populated with my logged in UID, FWIW
const docs = await obj?.get();
docs?.forEach((doc) => {
const data = doc.data() as TripLog;
trips.push({
...data,
id: doc.id
});
});
return trips;
} catch (err) {
throw new Error(err);
}
};
Here's the info when I try to run a sample request in the simulator, against those auth rules:
{
"path": "/databases/%28default%29/documents/triplogs/%7BanyDocument%7D"
"method": "get"
"auth": {
"uid": "5ompaySrXQcL9veWr3QlSujwlDS2"
"token": {
"sub": "5ompaySrXQcL9veWr3QlSujwlDS2"
"aud": "....omitted"
"firebase": {
"sign_in_provider": "google.com"
}
"email": "...omitted..."
"email_verified": true
"phone_number": ""
"name": "...."
}
}
"time": "2020-10-30T16:48:39.601Z"
}
The error in the sim is:
Error: simulator.rules line [32], column [58]. Null value error.
^^ Which is the resource object
If I change the auth rules to be the following, then I will get back data
allow read, update, delete: if request.auth.uid != null;
Data like:
[{
"createdAt": 1597527979495,
"name": "Foo Bar Trip",
"date": 1597527979495,
"userId": "5ompaySrXQcL9veWr3QlSujwlDS2",
"id": "FnH2E9WfDkpRHPLXxlDy"
}]
But as soon as I check against a field, the "resources" object is null
allow read, update, delete: if request.auth.uid == resource.data.userId;
I'll note you seem to be querying a collection, but your rules seem written for a document... as stated here , rules are not filters - they do not separate out individual documents in a collection. Are you doing that in your filter[array]?
Don't know what else might be going on, but your processing of the filter Array (I do a similar thing) won't work - you are REPLACING the query on each loop, not extending it. You might try
query = ref;
if (filters) {
filters.forEach((filter) => {
query = query?.where(filter.field, filter.operator, filter.value);
});
}
to extend the query.
Also, unless your wrapper does more than it shows, your line:
const docs = await obj?.get();
"gets" the QuerySnapshot, not the array of docs. You'd have to do
const docs = await obj?.get().docs;
to get the array of actual docs.
Finally, we have no idea (from your above snippets) whether the userID field is even present in your Firestore documents
Solution
I figured it out. I incorrectly assumed the query I was making was auto-filtered by the auth rule (in this case request.auth.id === userId) ... but the auth middleware to firestore doesn't do that. Makes sense. So I just added my filter ... something like this:
fetchTrips([{
field: 'userId',
operator: '==',
value: user.uid,
}]);
This works and returns the correct results. Trying to change the userID correctly throws the auth error too. So that was it.

How to guarantee unique primary key with one update query

In my Movie schema, I have a field "release_date" who can contain nested subdocuments.
These subdocuments contains three fields :
country_code
date
details
I need to guarantee the first two fields are unique (primary key).
I first tried to set a unique index. But I finally realized that MongoDB does not support unique indexes on subdocuments.
Index is created, but validation does not trigger, and I can still add duplicates.
Then, I tried to modify my update function to prevent duplicates, as explained in this article (see Workarounds) : http://joegornick.com/2012/10/25/mongodb-unique-indexes-on-single-embedded-documents/
$ne works well but in my case, I have a combination of two fields, and it's a way more complicated...
$addToSet is nice, but not exactly what I am searching for, because "details" field can be not unique.
I also tried plugin like mongoose-unique-validator, but it does not work with subdocuments ...
I finally ended up with two queries. One for searching existing subdocument, another to add a subdocument if the previous query returns no document.
insertReleaseDate: async(root, args) => {
const { movieId, fields } = args
// Searching for an existing primary key
const document = await Movie.find(
{
_id: movieId,
release_date: {
$elemMatch: {
country_code: fields.country_code,
date: fields.date
}
}
}
)
if (document.length > 0) {
throw new Error('Duplicate error')
}
// Updating the document
const response = await Movie.updateOne(
{ _id: movieId },
{ $push: { release_date: fields } }
)
return response
}
This code works fine, but I would have preferred to use only one query.
Any idea ? I don't understand why it's so complicated as it should be a common usage.
Thanks RichieK for your answer ! It's working great.
Just take care to put the field name before "$not" like this :
insertReleaseDate: async(root, args) => {
const { movieId, fields } = args
const response = await Movie.updateOne(
{
_id: movieId,
release_date: {
$not: {
$elemMatch: {
country_code: fields.country_code,
date: fields.date
}
}
}
},
{ $push: { release_date: fields } }
)
return formatResponse(response, movieId)
}
Thanks a lot !