I am using firebase firestore for my chat room application.
I have a function to send messages to firebase as
const sendMessageHandler = message => {
if (message) {
firestore()
.collection(`ChatRooms/${roomId}/messages`)
.doc(moment().format('YYYY-MM-DD-HH-mm-sssss'))
.set({
message: Encrypt(message),
userId: userId,
});
}
};
and I am fetching messages into flatlist only from firestore as
// this messages const is getting data only from firestore
const [messages, setMessage] = useState([]);
firestore()
.collection(`ChatRooms/${roomId}/messages`)
.onSnapshot(
querySnapshot => {
const messages = [];
querySnapshot.forEach(dataSnapshot => {
messages.push({
id: dataSnapshot.id,
message: dataSnapshot.data().message,
userId: dataSnapshot.data().userId,
});
});
setMessage(messages.reverse());
setLoading(false);
},
error => {
console.log('error : ', error);
},
);
// And now I am using this messages somewhere in return function of a component as :
<FlatList
data={messages}
renderItem={flatListItemRenderer}
inverted={true}
/>
Notice I am fetching only from firestore and not locally.
Now I turned off the internet and tried sending messages so this happens
The data has not been updated to firestore (of course because of no internet), but the flatlist has been updated with the new message !!
The only possibility I can think of is that the setter methods of firestore is storing data in both local and remote database and the getter method is first getting snapshot from local database and then remote database.
So the question is does #react-native-firebase/firestore keep a local snapshot also and updates both local and remote snapshot of data whenever we change something?
Github Link
Edit :
Firestore docs says
Firestore provides out of the box support for offline capabilities. When reading and writing data, Firestore uses a local database which synchronizes automatically with the server.This functionality is enabled by default, however it can be disabled if you need it to be disabled
I tried turning off persistence, but this property is related to storing data offline not the state. i.e, now when my app loads it fetch all the data directly from server(previously fetching from storage and server both), but the flatlist still updates with the new message(maybe it is storing some state like useState also???)
The Firestore SDK keeps a local copy of:
All data that you have an active listener for.
All pending writes.
In addition, if offline persistence is enabled, it also keeps a local copy of data that your code has recently read.
When you make a write operation, the SDK:
Writes your operation to its queue of pending writes.
Fires an event for any local listeners.
Tries to synchronize the pending write with the server.
Steps 1 and 2 always happen, regardless of whether you are online or offline. Only step 3 won't complete immediately when you are offline.
Related
I'm following this tutorial on building in-app presence using cloud firestore and they recommend using Firebase realtime database. The relevant bit of code for this question is:
database.onValue(database.ref(db, '.info/connected'), function (snapshot) {
if (snapshot.val() == false) {
// Instead of simply returning, we'll also set Firestore's state
// to 'offline'. This ensures that our Firestore cache is aware
// of the switch to 'offline.'
console.log('not connected to rtdb');
firestore.setDoc(userStatusFirestoreRef, isOfflineForFirestore);
return;
};
userStatusDatabaseRef.onDisconnect().set(isOfflineForDatabase).then(function () {
console.log('connected to rtdb');
userStatusDatabaseRef.set(isOnlineForDatabase);
// We'll also add Firestore set here for when we come online.
firestore.setDoc(userStatusFirestoreRef, isOnlineForFirestore);
});
});
The line not connected to rtdb consistently prints to my console, but I haven't seen connected to rtdb once. Is there an additional step I need to do to establish a connection to the realtime database?
I am using Firebase JavaScript Modular Web Version 9 SDK with my Vue 3 / TypeScript app.
My understanding is that when using Firestore real-time listeners with offline persistence it should work like this:
When the listener is started the callback fires with data read from the local cache, and then immediately after it also tries to read from the server to make sure the local cache has up to date values. If the server data matches the local cache the callback listener should only fire once with data read from the local cache.
When data changes, the callback listener fires with data read from the server. It uses that data to update the local cache.
When data doesn't change, all subsequent calls to the listener trigger a callback with data read from the local cache.
But I have setup offline persistence, created a listener for my Firestore data, and monitored where the reads were coming from...
And in my app I see an initial read from the local cache (expected), and then a second immediate read from the server (unexpected). And after that all subsequent reads are coming from the server (also unexpected).
During this testing none of my data has changed. So I expected all reads from the callback listener to be coming from the local cache, not the server.
And actually the only time I see a read from the local cache is when the listener is first started, but this was to be expected.
What could be the problem?
P.S. To make those "subsequent calls" I am navigating to a different page of my SPA and then coming back to the page where my component lives to trigger it again.
src/composables/database.ts
export const useLoadWebsite = () => {
const q = query(
collection(db, 'websites'),
where('userId', '==', 'NoLTI3rDlrZtzWCbsZpPVtPgzOE3')
);
const firestoreWebsite = ref<DocumentData>();
onSnapshot(q, { includeMetadataChanges: true }, (querySnapshot) => {
const source = querySnapshot.metadata.fromCache ? 'local cache' : 'server';
console.log('Data came from ' + source);
const colArray: DocumentData[] = [];
querySnapshot.docs.forEach((doc) => {
colArray.push({ ...doc.data(), id: doc.id });
});
firestoreWebsite.value = colArray[0];
});
return firestoreWebsite;
};
src/components/websiteUrl.vue
<template>
<div v-if="website?.url">{{ website.url }}</div>
</template>
<script setup lang="ts">
import { useLoadWebsite } from '../composables/database';
const website = useLoadWebsite();
</script>
Nothing is wrong. What you're describing is working exactly the way I would expect.
Firestore local persistence is not meant to be a full replacement for the backend. By default, It's meant to be a temporary data source in the case that the backend is not available. If the backend is available, then the SDK will prefer to ensure that the client app is fully synchronized with it, and serve all updates from that backend as long as it's available.
If you want to force a query to use only the cache and not the backend, you can programmatically specify the cache as the source for that query.
If you don't want any updates at all from the server for whatever reason, then you can disable network access entirely.
See also:
Firestore clients: To cache, or not to cache? (or both?)
I figured out why I was getting a result different than expected.
The culprit was { includeMetadataChanges: true }.
As explained here in the docs, that option will trigger a listener event for metadata changes.
So the listener callback was also triggering on each metadata change, instead of just data reads and writes, causing me to see strange results.
After removing that it started to work as expected, and I verified it by checking it against the Usage graphs in Firebase console which show the number of reads and snapshot listeners.
Here is the full code with that option removed:
export const useLoadWebsite = () => {
const q = query(
collection(db, 'websites'),
where('userId', '==', 'NoLTI3rDlrZtzWCbsZpPVtPgzOE3')
);
const firestoreWebsite = ref<DocumentData>();
onSnapshot(q, (querySnapshot) => {
const source = querySnapshot.metadata.fromCache ? 'local cache' : 'server';
console.log('Data came from ' + source);
const colArray: DocumentData[] = [];
querySnapshot.docs.forEach((doc) => {
colArray.push({ ...doc.data(), id: doc.id });
});
firestoreWebsite.value = colArray[0];
});
return firestoreWebsite;
};
Is there a way to get the user session and profile at the same time? The way I did it would be get the user session first after login then fetch the user profile using the id.
const [authsession, setSession] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const userSession = supabase.auth.session();
setSession(userSession);
if (userSession) {
getProfile(userSession.user.id);
} else {
setSession((s) => ({ ...s, profile: null }));
}
supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
if (session) {
getProfile(session.user.id);
} else {
setSession((s) => ({ ...s, profile: null }));
}
});
}, []);
const getProfile = async (id) => {
setLoading(true);
setError(false);
try {
const { data } = await supabase
.from("profiles")
.select("*")
.eq("id", id)
.single();
setSession((s) => ({ ...s, profile: data }));
} catch (error) {
setError(true);
} finally {
setLoading(false);
}
};
(In support of #chipilov's answer, but too long for a comment.)
Solution
When you get the user session at login time, Supabase will additionally return the JSON stored in auth.users.raw_user_meta_data. So, save the profile info there at signup time wit supabase.auth.signUp() or later with supabase.auth.updateUser(), you are all set.
Should you really store profile data in auth.users?
People seem to freak out a bit at the prospect of writing into auth.users, probably fearing that they might mess with Supabase internals. However, raw_user_meta_data is meant for this purpose: Supabase itself does not save anything into that column, only the additional user metadata that you may provide at signup or when updating a user.
Supabase maintainers do not recommend to write with own server-side routines to auth.users (source). But we don't do that here, relying only on the Supabase-provided functions supabase.auth.signUp() and supabase.auth.updateUser().
In the Supabase docs, they even provide an example where this "additional user metadata" is used for profile information:
const { data, error } = await supabase.auth.signUp(
{
email: 'example#email.com',
password: 'example-password',
options: {
data: {
first_name: 'John',
age: 27,
}
}
}
)
(Source: Supabase Documentation: JavaScript [Client Library] v2.0: AUTH: Create a new user, example "Sign up with additional user metadata")
How to access this profile data server-side?
The OP uses a table public.profiles to maintain profile information. For additional profile information that you generate and write to server-side, this is the recommended practice. Such a table is also recommended to make user data from auth.users accessible through the API:
GaryAustin1 (Supabase maintainer): "You could use the user meta data [field to store your additional metadata] and update it server side. The recommendation though normally is to have your own copy of key user info from the auth.users table […] [in] your own user table. This table is updated with a trigger function on auth.users inserts/updates/deletes." (source)
Instead of a set of trigger functions, you may also opt for a VIEW with SECURITY definer privileges to make auth.users data available in the public schema and thus via the API. Compared to triggers, it does not introduce redundancy, and I also think it's simpler. Such a view can also include your auth.users.raw_user_meta_data JSON split nicely into columns, as explored in this question. For security, be sure to include row access filtering via the WHERE clause of this view (instructions), because VIEWs cannot have their own row-level security (RLS) policies.
How to modify this profile data server-side?
Your own user table in the public schema can be used to store that additional profile data. It would be connected to auth.users with a foreign key relation. In the VIEW proposed above, you can then include both data from this own table and columns from auth.users.
Of course, user information from your own table will then not be returned automatically on login. If you cannot live with that, then I propose to alternatively use auth.users.raw_user_meta_data to save your additional metadata. I know I disagree with a Supabase maintainer here, but really, you're not messing with Supabase internals, just writing into a field that nothing in Supabase depends on.
You would use PostgreSQL functions (with SECURITY definer set) to provide access to specific JSON properties of auth.users.raw_user_meta_data from within the public schema. These functions can even be exposed to the API automatically (see).
If however you also need to prevent write access by users to some of your user metadata, then the own table solution from above is usually better. You could use triggers on auth.users.raw_user_meta_data, but it seems rather complex.
The session.user object contains a property called user_metadata - this property contains the data which is present in the raw_user_meta_data column of the of auth.users table.
Hence, you can setup a DB trigger on your custom user profile table which copies the data from that table to the raw_user_meta_data column of the of auth.users table in JSON format anytime the data in the user profile table changes (i.e. you will need the trigger to be run on INSERT/UPDATE and probably DELETE statements). This way the profile data will be automatically delivered to the client with the sign-in or token refresh events.
IMPORTANT: This approach has potential drawbacks:
this data will also be part of the JWT sent to the user. This might be a problem because the JWT is NOT easy to revoke BEFORE its expiration time and you might end up in a situation where a JWT which expires in 1 hour (and will be refreshed in 1 hour) still contains profile data which has already been updated on the server. The implications of this really depends on what you put in your profile data and how you use it in the client and/or the backend;
since this data is in the JWT, the data will be re-sent to the client with each refresh of the session (which is every 1 hour by default). So, (a) if the profile data is big, you would be sending this large piece of data on every token refresh even if it has NOT changed and (b) if the data changes often and you need to ensure that the client is up to date in (near) real time you will need to increase the token refresh rate;
I'm attempting to make a Discord.js command that uploads data to Cloud Firestore. It works, just if I attempt to reuse the command it reruns the firebase.initializeApp() line, which throws an error. Is there any way to disconnect after I have uploaded the data?
It is not recommended to disconnect - the time and processing cycle cost of .initializeApp() is prohibitive unless these calls are quite rare.
There are a few of valid approaches:
=> Keep a state value that indicates that the app is already initialized
=> initialize firebase at a "higher point" in your code to avoid reloading the module
=> I believe you can actually "ask" firebase if there is a running app, in which case don't re-initialize (const app = firebase.app())
If for some reason you do need to remove a firebase app instance:
const app = firebase.app(); //retrieves the default instance
app.delete()
.then(function() {
console.log("App deleted successfully");
})
.catch(function(error) {
console.log("Error deleting app:", error);
});
The database structure looks like this
-LGw89Lx5CA9mOe1fSRQ {
uid: "FzobH6xDhHhtjbfqxlHR5nTobL62"
image: "https://pbs.twimg.com/profile_images/8950378298..."
location: "Lorem ipsum, lorem ipsum"
name: "Lorem ipsum"
provider: "twitter.com"
}
How can I delete everything, including the -LGw89Lx5CA9mOe1fSRQ key programmatically?
I looked at this, but it's outdated and deprecated Firebase: removeUser() but need to remove data stored under that uid
I've also looked at this, but this requires for user to constantly sign in (I'm saving the user ID in localStorage) and it returns null on refresh if I write firebase.auth().currentUser. Data records and user accounts are created through social network providers and I can see the data both on Authentication and Database tab in the Firebase console.
I've tried with these piece of code but it does nothing.
// currentUser has a value of UID from Firebase
// The value is stored in localStorage
databaseChild.child(currentUser).remove()
.then(res => {
// res returns 'undefined'
console.log('Deleted', res);
})
.catch(err => console.error(err));
The bottom line is, I need to delete the user (with a specific UID) from the Authentication tab and from the Database at the same time with one click.
I know that there is a Firebase Admin SDK but I'm creating a Single Page Application and I don't have any back end code. Everything is being done on the front end.
Any kind of help is appreciated.
With suggestions from #jeremyw and #peter-haddad I was able to get exactly what I want. Here is the code that is hosted on Firebase Cloud Functions
const functions = require('firebase-functions'),
admin = require('firebase-admin');
admin.initializeApp();
exports.deleteUser = functions.https.onRequest((request, response) => {
const data = JSON.parse(request.body),
user = data.uid;
// Delete user record from Authentication
admin.auth().deleteUser(user)
.then(() => {
console.log('User Authentication record deleted');
return;
})
.catch(() => console.error('Error while trying to delete the user', err));
// Delete user record from Real Time Database
admin.database().ref().child('people').orderByChild('uid').equalTo(user).once('value', snap => {
let userData = snap.val();
for (let key of Object.keys(userData)) {
admin.database().ref().child('people').child(key).remove();
}
});
response.send(200);
});
Also, if you are facing CORS errors, add the mode: 'no-cors' option to your fetch() function and it will work without any problems.
The link you already found for deleting the user-login-account client-side is your only option if you want to keep the action on the client. Usually you want to keep most of the actions for things like account creation/deletion on the server for security reasons, and Firebase forces the issue. You can only delete your account if you were recently logged in, you can't have client-side start deleting old/random accounts.
The better option is to create your own Cloud Function to handle everything related to deleting a user. You would have to use the Admin SDK that you already found for this... but you could have that Cloud Function perform as many actions as you want - it will have to delete the user from the Auth tab, and delete the matching data in the Database.