Firestore - multiple add and update operations in one transaction [closed] - firebase

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 4 years ago.
Improve this question
I have a Firestore document representing a day with a subcollection containing reservations for this day in a Firestore database.
Here is JSON example of my data structure:
{
"Day":{
"ReservationsCount":2,
"Reservations":[
{
"Order":1
},
{
"Order":2
}
]
}
}
I need to add a set of documents, set their ordinal number in collection and update the ReservationsCount in one transaction.
I tried to use firestore transactions and batched writes, but as far I understand, they do not support adding document to a collection in transaction (according to documentation only combination of set(), update(), or delete() operations).
I tried to do update the values using cloud functions, but they are in beta and there are known issues with performance and reliability, so I got sometimes wrong results.
Is there any way to update existing document and add documents to its subcollection within one transaction?

The following should do the trick. You have to pass to the updateRes() function the ref of the 'day" doc, the ref of the sub-collection and an array containing an object for each document to add to the sub-collection.
Just open the HTML file in a browser.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://www.gstatic.com/firebasejs/5.0.4/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/5.0.4/firebase-firestore.js"></script>
</head>
<body>
<script>
var config = {
apiKey: "...",
authDomain: "...",
databaseURL: "...",
....
};
firebase.initializeApp(config);
var firestoredb = firebase.firestore();
function updateRes(dayDocRef, orderCollectionRef, refAndDataArray) {
return firestoredb.runTransaction(function (transaction) {
return transaction.get(dayDocRef)
.then(function (dayDoc) {
if (!dayDoc.exists) {
throw "Document Day does not exist!";
}
newResCount = dayDoc.data().ReservationsCount + refAndDataArray.length;
return transaction.update(dayDocRef, { ReservationsCount: newResCount });
})
.then(function () {
var t = transaction;
refAndDataArray.forEach(function (element) {
t = t.set(orderCollectionRef.doc(element.ref), element.data);
});
return t;
});
}).then(function () {
console.log("Transaction successfully committed!");
}).catch(function (error) {
console.log("Transaction failed: ", error);
});
};
var dayDocRef = firestoredb.collection("Days").doc("Day");
var orderCollectionRef = dayDocRef.collection("Reservations"); //The sub-collection is called "Reservations"
var refAndDataArray = [{ ref: "3", data: { Order: 3, otherData: "foo" } }, { ref: "4", data: { Order: 4, otherData: "bar" } }];
updateRes(dayDocRef, orderCollectionRef, refAndDataArray);
</script>
</body>
</html>

Related

Data function not working react native firestore

I got stuck in this puzzle which doesn't seem to wanna be solved, I am kinda sure I am forgetting something since I just started learning react-native.
I have this code :
async componentDidMount() {
let user = await UserRepository.getUserRef(firebase.auth().currentUser.uid);
await firebase
.firestore()
.collection("reminder")
.where("user", "==", user)
.get()
.then((remindersRecord) => {
remindersRecord.forEach((reminderDoc) => {
console.log(reminderDoc.data());
});
});
I am trying to get the "reminders" data of the connected user, the query works since we got reminderDoc which contain a bunch of objects, and inside there is the data I want but when I call data() nothing changes, I don't get the document it returns the same object.
Reminder collection :
Any help is much appreciated!
I tried to replicate this on my side and I think this is working fine. I think that result that you get is related with fields boss and user which I guess are reference type in firestore. If you log to console such fields give results like this:
{
reference: DocumentReference {
_firestore: Firestore {
_settings: [Object],
_settingsFrozen: true,
_serializer: [Serializer],
_projectId: <PROJECT_ID>,
registeredListenersCount: 0,
bulkWritersCount: 0,
_backoffSettings: [Object],
_clientPool: [ClientPool]
},
_path: ResourcePath { segments: [Array] },
_converter: {
toFirestore: [Function: toFirestore],
fromFirestore: [Function: fromFirestore]
}
},
text_field: 'test',
...
}
So for presented example you will get 2 such fields and for those fields you will not see as a string. BTW the timestamp field will not be shown properly as well.
To avoid this issue you can use example path property of document reference or when it comes to timestamp you can use toDate() method. I have created small example to show the fields properly (looping over all the object fields):
remindersRecord.forEach((reminderDoc) => {
for (const [key, value] of Object.entries(reminderDoc.data())) {
if (key == 'boss' || key == 'user') console.log(`${key}: ${value.path}`)
else if (key == 'startAt') console.log(`${key}: ${value.toDate()}`)
else console.log(`${key}: ${value}`)
});
I tested this in nodejs directly, but it should work in componentDidMount as well.

How do I add/delete array elements on a Firestore document via REST?

I want to use an array to store temperature readings from an Arduino on a Firestore database. My (probably terrible) way of thinking it out so far is to read the document, do my array operations on the Arduino, and send the whole array back to Firestore. I don't know how to write to Firestore via REST at all so I haven't implemented it yet. Here is my code:
void writeTemp(String url, int temperature) {
// writeTemp() appends the given temperature to an array. temperature[0]
// holds the oldest temperature while temperature[9] holds the first.
// When a new temperature is put in, the last one is taken out.
HTTPClient http;
http.begin(url);
int httpCode = http.GET();
// Gets the current temperature array from the provided URL.
String payload = http.getString();
Serial.println(httpCode); // Prints HTTP response code.
// Calculates the size of the JSON buffer. This is big enough for 11
// temperature values that are all 3 digits so as long as you're not using
// this on the Sun you're probably fine.
const size_t capacity = JSON_ARRAY_SIZE(11) + 14 * JSON_OBJECT_SIZE(1) +
JSON_OBJECT_SIZE(4) + 440;
DynamicJsonDocument doc(capacity); // Makes the JSON document
DeserializationError err = deserializeJson(doc, payload);
// Prints out the deserialization error if an error occurred
if (err) {
Serial.print("JSON DESERIALIZE ERROR: ");
Serial.println(err.c_str());
}
// Sets up the array from the JSON
JsonArray temperatureArray =
doc["fields"]["Temperature"]["arrayValue"]["values"];
// Creates a new array object to store the new temperature
JsonObject newTemp = temperatureArray.createNestedObject();
// Puts the new temperature in the new array object. For some reason,
// Firestore stores numbers as strings so the temperature is converted into
// a string.
newTemp["integerValue"] = String(temperature);
// Removes the first (oldest) array object.
temperatureArray.remove(0);
// Removes irrelevant data that we got from the Firestore request
doc.remove("name");
doc.remove("createTime");
doc.remove("updateTime");
String newJson;
serializeJson(doc, newJson);
Serial.println(newJson);
}
How would I send this new JSON back to Firestore? Am I even doing this right? I've heard of transactions, which sounds like the theoretically better way to do what I'm trying to do but I can't find any guides or readable documentation on how to do it. My database is in test mode right now so no need to worry about authentication.
The documentation for the Firestore REST API is here.
To create a document, you need to issue a POST Request to an URL with the following format:
https://firestore.googleapis.com/v1/{parent=projects/*/databases/*/documents/**}/{collectionId}
With an instance of a Document in the Request body.
To be more concrete, below is an example in a simple HTML page (using the Axios library to issue the HTTP Request). This code will create a new document in the collection1 Firestore collection.
Just save this file on your local disk, adapt the value of <yourprojectID> and opens this page in a browser, directly from your local disk.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<script>
var firebaseProjectId = '<yourprojectID>';
var collectionId = 'collection1';
var url =
'https://firestore.googleapis.com/v1/projects/' +
firebaseProjectId +
'/databases/(default)/documents/' +
collectionId;
var writeObj = {
fields: {
name: {
stringValue: 'theName'
},
initialBudget: {
doubleValue: 1200
}
}
};
axios.post(url, writeObj).catch(function(error) {
console.log(error);
});
</script>
</body>
</html>
In order to update an Array in an existing document, you have to use a FieldTransform with appendMissingElements elements.
Excerpt of this doc on appendMissingElements elements:
appendMissingElements: Append the given elements in order if they are not already present in the current field value. If the field is not an array, or if the field does not yet exist, it is first set to the empty array.
You will find below an example of FieldTransform value containing appendMissingElements elements.
{
"transform": {
"document": "projects/" + firebaseProjectId + "/databases/(default)/documents/....,
"fieldTransforms": [
{
"setToServerValue": "REQUEST_TIME",
"fieldPath": "lastUpdate"
},
{
"appendMissingElements": {
"values": [
{
"stringValue": "...."
}
]
},
"fieldPath": "fieldName"
}
]
}
}
UPDATE FOLLOWING YOUR COMMENT
The following should work (tested positively):
var collectionId = 'SensorData';
var url =
'https://firestore.googleapis.com/v1/projects/' +
firebaseProjectId +
'/databases/(default)/documents:commit';
var writeObj = {
writes: {
transform: {
document:
'projects/' +
firebaseProjectId +
'/databases/(default)/documents/' +
collectionId +
'/Temperature',
fieldTransforms: [
{
setToServerValue: 'REQUEST_TIME',
fieldPath: 'lastUpdate'
},
{
appendMissingElements: {
values: [
{
integerValue: 25
}
]
},
fieldPath: 'Temperature'
}
]
}
}
};

Vuejs & Firestore - How to Update when Data Changes in Firestore

I've gone through a bunch of tutorials and docs but cannot seem to be able to update on page when data changes in Firestore (NOTE: not Firebase)
Heres what I have currently which is working fine except if data changes in the DB it is not reflected on the page itself unless I refresh. Code below is within script tags:
import { recipeRef } from '../../firebase';
export default {
data() {
return {
recipes: []
}
},
firestore: {
recipes: recipeRef
},
created() {
db.collection('recipes').get().then((onSnapshot) => {
this.loading = false
onSnapshot.forEach((doc) => {
let data = {
'id': doc.id,
'name': doc.data().name
}
this.recipes.push(data)
})
})
}
I'm not using Vuex. Adding data, editing and reading works fine. Just not reflecting changes once data has changed. Maybe there is a life cycle hook Im supposed to be using? For "onSnapshot" - Ive tried "snap", "querySnapshot" etc. No luck.
Thanks in advance.
Remove the get() and just replace with snapshot - like so
created() {
db.collection('recipes').onSnapshot(snap => {
let foo = [];
snap.forEach(doc => {
foo.push({id: doc.id, name: doc.data().name})
});
}
});
I am not familiar with the firestore API, but glancing through the docs, it looks like calling get() is how you query a single time. Where you have onSnapshot should really be querySnapshot -- that is, the results of a one query. See:
https://firebase.google.com/docs/firestore/query-data/get-data
versus:
https://firebase.google.com/docs/firestore/query-data/listen
So to get live updates, it looks like you need to create a listener, like so:
db.collection('recipes')
.onSnapshot(function(snapshot) {
snapshot.forEach(function(doc) {
// Find existing recipe in this.recipes
// and swap in the new data
});
}, function(error) {
// handle errors
});
I think you will need to add that listener in addition to the get() query you are currently doing. Hope this helps!

Can't store user details in local-storage without using an observer (Firebase & Polymer authentication)

I created a simple login with Polymer & Firebase (google authentication). I came across a problem which got me thinking that my chosen approach might not be the cleanest. So this question is more about the general pattern.
In my code below I combined the basic login from the latest Polycast video and some code I found in the Firebase Guide.
The Problem
Basically I am unable to call a function inside the login that would then store the values, neither can I store the returned user value's inside the login method directly.
My solution
To work around that problem I created a listener which fires if a user exists and then calls the modification method to store the user details inside my localstorgae userDetails object.
So I am wondering if my approach is okey and what I could improve to skip the observer and store the user details inside the login method. In addition I found in the Firebase guidelines a special observer but was not sure how to implement it.
<dom-module id="my-app">
<template>
<style>
:host {
display: block;
}
</style>
<firebase-app auth-domain="...firebaseapp.com"
database-url="...firebaseio.com"
api-key="..."></firebase-app>
<iron-localstorage id="localstorage" name="my-app-storage"
value="{{userDetails}}"></iron-localstorage>
<section class="wrapper">
<paper-material id="loginContainer" elevation="1">
<firebase-auth id="auth"
user="{{user}}"
status-known="{{statusKnown}}"
provider="{{provider}}"
on-error="handleError"></firebase-auth>
<template is="dom-if" if="[[user]]">
<h1>Welcome [[user.displayName]]</h1>
</template>
<paper-button raised on-tap="login" hidden$="[[user]]">Sign in</paper-button>
<paper-button raised on-tap="logout" hidden$="[[!user]]">Sign out</paper-button>
</paper-material>
</section>
</template>
<script>
Polymer({
is: 'my-app',
properties: {
userDetails: {
type: Object,
},
user: {
type: Object,
observer: '_changed'
},
statusKnown: {
type: Object
},
provider: {
type: String,
value: 'google'
}
},
login: function() {
this.$.auth.signInWithPopup(this.provider).then(function(result) {
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
//UNABLE TO CALL A METHOD OR TO STORE VALUES DIRECTLY IN MY OBJECT
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
var email = error.email;
var credential = error.credential;
});
},
//OBSERVER THAT CALLS THE MODIFICATION FOR THE LOCALSTORAGE
_changed: function() {
if (this.user != null) {
var user = firebase.auth().currentUser;
this.makeModifications(user);
}
},
// set localstorage
makeModifications: function(user) {
this.set('userDetails.name', user.displayName);
this.set('userDetails.email', user.email);
this.set('userDetails.photoUrl', user.photoURL);
console.log(this.userDetails.name + ' is singed in');
},
logout: function() {
this.$.localstorage.reload();
this.set('userDetails.name', null);
this.set('userDetails.email', null);
this.set('userDetails.photoUrl', null);
return this.$.auth.signOut();
},
});
</script>
</dom-module>
You have a problem with scope?. When inside this.$.auth.signInWithPopup().then(function you cant use Polymers this.set because your within a new scope. Theres also nothing wrong with what you've done, asuming its works for you.
this.$.auth.signInWithPopup().then(function(user) {
// notice bind(this), binds your polymer element to the scope of the
// function, this allows you to use its API, thus enabling
// the use of set within the Promise
this.set('userDetails.name', user.displayName);
}.bind(this));
this can also be done via
login: function() {
// create a local reference to the Polymer element
// assuming its within a closure like this login function it will be
// available to any function called from it.
var that = this;
this.$.auth.signInWithPopup().then(function(result) {
var user = result.user
that.set('userDetails.name', user.displayName);
});
}
I havent tested this code while writing so copy with caution.
Closures Example
The way I've been doing it recently is
login: function() {
this.$.auth.signInWithPopup()
.then(this._setUserDetails.bind(this))
.then(this._anotherFunctionCall.bind(this));
},
_setUserDetails: function(user) {
this.set('userDetails.name', user.displayName);
return Promise.resolve();
}
Its not doing anything extra just feels cleaner

Firebase on(child_added) some field 'undefined'

I am working on a real time application and i am using firebase with pure html and javascript (not angularJS).
I am having a problem where i saved user's data to firebase with the given code by firebase :
var isNewUser = true;
ref.onAuth(function(authData) {
if (authData && isNewUser) {
authData['status'] = 'active';
authData['role'] = 'member';
ref.child("users").child(authData.uid).set(authData);
}
});
This will add the authData to the /users/ node. As you can see that i also appended some custom fields to the authData, status and role.
Now i am using this code to get the user's data from firebase and display them.
ref4.on("value", function(snapshot) {
var snapshotData = snapshot.val();
console.log('username: '+snapshotData.status);
});
If i use on('value'), the status get printed out on the console but if i do it this way,
ref4.on("child_added", function(snapshot) {
var snapshotData = snapshot.val();
console.log('status: '+snapshotData.status);
});
It is showing undefined for the status. May i know what's wrong and how to fix this problem. Thank you.
Since value is returning the path provided by ref4, and child_added is returning each child of that path, it's unlikely both are going to have a key status.
Consider this data structure:
{
"users": {
"brucelee": {
"status": "awesome"
},
"chucknorris": {
"status": "awesomerest"
}
}
}
If I now query for this according to your incomplete example:
var ref = new Firebase('https://<instance>firebaseio.com/users/brucelee');
ref.on('value', function(snap) {
// requests the brucelee record
console.log(snap.name(), ':', snap.val().status); // "brucelee: awesome"
});
ref.on('child_added', function(snap) {
// iterates children of the brucelee path (i.e. status)
console.log(snap.name(), ':', snap.val().status); // THROWS AN ERROR, because status is a string
});
So to do this on child_added with a data structure like this (and presumably somewhat like yours), it would look as follows:
ref.on('child_added', function(snap) {
// iterates children of the brucelee path (i.e. status)
console.log(snap.name(), ':', snap.val()); // "status: awesome"
});

Resources