My Flutter app uses Firbase Cloudfirestore as its backend. Later I'll want to add new features to my app which would require new fields in a Firestore document. How can I check whether the field exists in the document and return a default value if it doesn't?
Stream<List<Car>> streamCars() {
return _carsCollection.snapshots().map((snapshot) => snapshot.docs.map((document) => Car.fromDocumentSnapshot(document)).toList());
}
static Car fromDocumentSnapshot(DocumentSnapshot snapshot) {
return Car(
id: snapshot.id,
date: snapshot['date'] ?? Timestamp.now(),
seats: snapshot['seats'] ?? 0,
newFeature: snapshot['newFeature'] ?? '', // This field does not exist yet and throws error
);
}
This throws the error:
Bad state: field does not exist within the DocumentSnapshotPlatform
You can do it like so.
newFeature: (snapshot.data() as Map)['newFeature'] ?? ''
static Car fromDocumentSnapshot(DocumentSnapshot snapshot) {
return Car(
id: snapshot.id,
date: snapshot['date'] ?? Timestamp.now(),
seats: snapshot['seats'] ?? 0,
newFeature: snapshot['newFeature']==null ?'tjhjhjh': '', // check it if is null will solve the error
);
}
Related
I have a MessageModel in which there are a few fields. But the field "edited" does not exist in the document of each message. It is a new field that I want to add later in the future. When I get all messages using stream it throws the error.
Unhandled Exception: Bad state: field does not exist within the DocumentSnapshotPlatform
Is there any way I can check if the field "edited" exists in the model or ignore it?
This is my MessageModel:
factory MessageModel.fromJson(DocumentSnapshot snapshot) => MessageModel(
chatId: snapshot["chat_id"],
messageId: snapshot["message_id"],
userId: snapshot["user_id"],
you: snapshot["you"],
time: snapshot["time"],
seen: snapshot["seen"],
type: snapshot["type"],
message: snapshot["message"],
fileURL: snapshot["file_url"] ?? "" ,
thumbnail: snapshot["thumbnail"] ?? "" ,
isUploading: RxBool(false),
isPlaying: RxBool(false),
file: File("").obs,
thumb: Uint8List(5).obs,
edited: snapshot["edited"]
);
I am using this stream to get all my messages from firestore. Or can I check here if the field exists?
Flutter code:
Stream<List<MessageModel>> getMessages() {
Stream<QuerySnapshot> stream =
CollectionReferences.chatRef.doc(userID.value).collection("messages").orderBy("time", descending: true).snapshots();
return stream.map((data) => data.docs.map((e) => MessageModel.fromJson(e)).toList());
}
If you capture the data as a map, you can check if the map contains a key:
edited: (snapshot.data() as Map<String, dynamic>).containsKey("edited") ?
snapshot["edited"] : null
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.
Firebase error (unhandled rejection)
React error on the browser:
Unhandled Rejection (FirebaseError): Function addDoc() called with invalid data. Unsupported field value: undefined (found in field username in document posts/MkHzQWzXyayty0KfQQgP)
Most probably the problem is in this code :
// complete function
storage
.ref("images")
.child(image.name)
.getDownloadURL()
.then(url => {
// Post image on db
db.collection("posts").add({
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
caption: caption,
imageUrl: url,
username: username
});
setProgress(0);
setCaption("");
setImage(null);
});
}
);
}; ```
In the error, it says your variable username is undefined. You should make sure that it has a value. Meanwhile, you can do a quick and dirty fix like this:
db.collection("posts").add({
timestamp: firebase.firestore.FieldValue.serverTimestamp(),
caption: caption,
imageUrl: url,
username: username || null,
});
This code will assign null to username when the variable username is undefined.
My question is actually twofold, so I m not sure I should ask both in one post or create another post. Anyway, here it is:
I am creating users in firestore database. I do not want to put all details in a single document because it will be requested a lot, and all details will be retrieved, even if not needed. So I decided to create a collection members_full with all details of users I may not need often, and another collection called members_header to keep the few most important details. On creation of a new user, I want reference ID in both collections to be the same for a specific user.
- members_full -+
|
+ --- abnGMbre --- +
|
+ --- mother : 'His mom'
+ --- Father: 'daddy'
- members_header+
|
+ ---- abnGMbre -- +
|
+ ---- fullname: 'john Doe'
+ ---- pictURL: 'path to his profile pic'
I want something looking like the above.
So this is what I did in the cloud function:
/** Create / Update a member
* ------------------------- */
exports.updateMember = functions.https.onCall( (data, context) =>{
// root member and secretaries are allowed to update members
const authParams:any = {
uid: context.auth.uid,
email: context.auth.token.email,
};
// Check if user is allowed to perform operation
return checkPermission(authParams, ['root', 'secretary']).then(res => {
if(res==false){
return { // Permission denied
status: STATUS.permission_denied,
}
}
// set object to add/ update
const member:any = data;
// Check if uid of member object is present (true:update, false: create)
var fullRef : admin.firestore.DocumentReference;
var headRef : admin.firestore.DocumentReference;
var countRef: admin.firestore.DocumentReference;
var createNewMember = false;
if(member.uid!==undefined && member.uid!==null){ // update
fullRef = fsDB.collection('members_full').doc(member.uid);
headRef = fsDB.collection('members_header').doc(member.uid);
} else {
fullRef = fsDB.collection('members_full').doc();
headRef = fsDB.collection('members_header').doc(fullRef.id);
countRef = fsDB.collection('counters').doc('members');
createNewMember = true;
}
return fsDB.runTransaction(t => {
return t.get(fullRef).then(doc => {
// Update full details
t.set(fullRef, {
surname : member.surname ,
firstName : member.firstName ,
birthDate : member.birthDate ,
birthPlace : member.birthPlace ,
email : member.email ,
phone : member.phone ,
occupation : member.occupation ,
father : member.father ,
mother : member.mother ,
spouse : member.spouse ,
children : member.children ,
addressHome : member.addressHome ,
addressLocal: member.addressLocal,
contactHome : member.contactHome ,
contactLocal: member.contactLocal,
comment : member.comment ,
regDate : member.regDate ,
});
// Update header details
t.set(headRef, {
fullName : member.fullName ,
gender : member.gender ,
active : member.active ,
picURL : member.picURL ,
});
// Increment number of members
if(createNewMember ){
t.update(countRef, {count: admin.firestore.FieldValue.increment(1)});
}
}).then(() => {
return { status : STATUS.ok }
}).catch(err => {
return {
status: STATUS.fail,
message: err.message,
error: err
}
});
}).then(() => {
return { status : STATUS.ok }
}).catch(error =>{
return {
status: STATUS.fail,
message: error.message,
debug: 'run transaction err',
error: error
}
});
}).catch(err => {
return {
status: STATUS.fail,
message: err.message,
debug: 'check permission err',
error: err
}
});
});
/** Check if authenticated user's roles are among the ones allowed
* --------------------------------------------------------------- */
function checkPermission(authParams:any, allowedRoles:any[]):Promise<boolean>{
// Check if authenticated user as any of the roles in array 'allowedRoles'
return new Promise((resolve, reject) => {
// If one of allowed roles is root, check against global variables
if(allowedRoles.indexOf('root')>=0 &&
( root_auth.email.localeCompare(authParams.email)==0 ||
root_auth.uid.localeCompare(authParams.uid)==0)){
resolve(true);
}
// Get autID
const uid = authParams.uid;
// Get corresponding user in collection roles
admin.firestore().collection('userRoles').doc(uid).get().then(snap => {
// Get roles of user and compare against all roles in array 'allowedRoles'
const memRoles = snap.data().roles;
var found = false;
var zz = memRoles.length;
for(let z=0; z<zz; z++){
if(allowedRoles.indexOf(memRoles[z])){
found = true;
break;
}
}
resolve(found);
}).catch(err => {
reject(err);
});
});
}
When I call this cloud function, it only writes in document members_full, and increment number of members. It does not create entry in members_header.
My first question: where did I go wrong? the way I' m getting ID from the first document to create second document, isn't it valid?
The second question, will it be better to create subcollections rather than having 2 collections? if yes, how to do I do that in a transaction?
Help much appreciated
You need to chain the method calls in the Transaction. It is not extremely clear in the documentation, but if you look at the reference document for a Transaction (https://firebase.google.com/docs/reference/node/firebase.firestore.Transaction) you will see that the update() and set() methods return a Transaction, which is
the "Transaction instance. [and is] used for chaining method calls".
So you should adapt your code along these lines:
return fsDB.runTransaction(t => {
return t.get(fullRef)
.then(doc => {
t.set(fullRef, {
surname : member.surname ,
firstName : member.firstName
//....
})
.set(headRef, {
//....
gender : member.gender
//....
})
.update(countRef, {count: admin.firestore.FieldValue.increment(1)});
});
});
You also need to correctly chain all the different promises, as follows:
return checkPermission(authParams, ['root', 'secretary'])
.then(res => {
//...
return fsDB.runTransaction(t => {
//.....
});
.then(t => {
return { status : STATUS.ok }
})
.catch(error => {...})
However, you may use a batched write instead of a transaction, since it appears that you don't use the document returned by t.get(fullRef) in the transaction.
For your second question, IMHO there is no reason to use sub-collections instead of two (root) collections.
I'm using Meteor methods to update documents so I can share them easier and have more control. However i've ran into a problem with checking ownership.
How should I check to make sure the user calling the update method is the owner of the document? Currently i'm grabbing the document first then running the update.
Is there a better pattern to accomplish this?
Meteor.methods({
'Listing.update': function(docId, data) {
var doc = db.listings.findOne({_id: docId}) || {};
if (doc.userId !== this.userId) {
throw new Meteor.Error(504, "You don't own post");
}
// ensure data is the type we expect
check(data, {
title: String,
desc: String
});
return db.listings.update(docId, {$set: data});
}
});
You don't need the additional db call to fetch the original doc, just make the userId an additional criteria in the update selector. If no doc exists with the correct _id and userId no update will be done. update returns the number of docs updated so it will return 1 on success and 0 on failure.
like this:
'Listing.update': function(docId, data) {
var self = this;
check(data, {
title: String,
desc: String
});
if ( ! self.userId )
throw new Meteor.Error(500, 'Must be logged in to update listing');
res = db.listings.update({_id: docId, userId: self.userId}, {$set: data});
if ( res === 0 )
throw new Meteor.Error( 504, "You do not own a post with that id" );
return res;
}
Also, if you use findOne to check a document's existence, use the fields option to limit what you return from the db. Usually just {fields: {_id:1}}.