Firebase / Firestore Add Document to Sub collection Version 9 - javascript

Some reason, I just am not getting it.
I want to add a new document to a sub-collection. Here is my layout as follows:
Users----------- Collection
UID----------- Document
Lists------- Collection
Category-- Document
Category-- Document
...--
For the documents in the "Lists", I want to add a Doc to Lists.
The Doc is custom named.
I've tried the following:
async function AddCategory (category) {
return new Promise((resolve, reject) => {
const uid = auth.currentUser.uid
console.log(`UID: ${uid}`)
setDoc(doc(db, 'users', uid, 'lists', category), {
name: 'Johnny Doey'
}).then((res) => {
resolve(res)
}).catch((err) => {
reject(err)
})
})
}
This does not seem to work. The error I am receiving is 'Undefined'.
I almost feel like there is something simple I am missing.... I've checked my auth rules. Everything checks out. Tried to test with hard strings in place of my variables, still no luck...
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId}/{documents=**} {
allow read, write: if request.auth != null && request.auth.uid == userId
}
}
}
now, the firestore data shows the initial user (UID) document to be italicized,
Even though non-existent ancestor documents appear in the console,
they do not appear in queries and snapshots. You must create the
document to include it in query results.
What in the heck...
Could someone please overlook this? Thanks!

According to this post:
"When you create a reference to a subcollection using a document id like this:
db.collection('coll').doc('doc').collection('subcoll')
If document id doc doesn't already exist, that's not a problem at all. In fact, after adding documents to subcoll, doc will appear in the Firebase console in italics, indicating that it doesn't exist."
What you can do is: First, create a setDoc() for collection "users" with its own document (whether auto-generated or manually coded), second, you can input your setDoc() query: setDoc(doc(db, 'users', uid, 'lists', category)....
For a better visualisation, here's a sample code:
setDoc(doc(db, 'users', uid), {
name: 'Johnny Doey'
}).then(() => {
setDoc(doc(db, 'users', uid, 'lists', category), {
// some additional inputs here...
})
}).catch((e) => {
console.log(e)
})
For additional reference, you can check, Non-existent ancestor documents.

Well - it works. I think one of the issues I was facing was altering the generated users.
My fix was to delete the users, and build it from scratch. That seemed to solve it for me. Not sure what exact constraints the generated user collection/doc has, but it seems it does have some restrictions tied into it.

Related

cannot get fields from SnapShot in firebase

I'm trying to get the real-time document fields (text and timeStamp) to be displayed from the "first" collection in firestore collection, with the use of onSnapshot. I can verify that the snapshot realtime updation is working, on addition of a new document, it does shows an update. But I cannot access the text and timeStamp in the document.
onSnapshot(collection(db, 'first'), (snapshot) => {
console.log(snapshot.text, snapshot.timeStamp);
});
It just shows undefined to me. Also, I just want to access this database, db only when the user is authenticated. So is there a way to check if the user is authenticated?
To get the data from a document snapshot, you need to call data() on it. In addition, since you're reading an entire collection, you get back a query snapshot that can contain multiple documents, which you also need to handle.
So:
onSnapshot(collection(db, 'first'), (querySnapshot) => {
querySnapshot.docs.forEach((docSnapshot) => {
console.log(docSnapshot.data().text, docSnapshot.data().timeStamp);
})
});
See the documentation on getting data from Firestore and on reading all documents from a collection for more examples like this.

How to write data in Firebase Realtime Database as [custom key : value] in React?

What I'm trying to achieve is having random IDs in a dedicated node in Firebase Realtime Database, while being able to check if an ID already exists in the node before writing it to prevent duplicity.
I'm working with web React (JavaScript).
The database currently looks like this:
What I'm aiming for is to have another node with IDs in the following format:
Note: Generated IDs in 'users' node are different from those in 'ids', they're not connected.
ID keys in [key : value] pairs, such as 'abcd', shouldn't be generated by Firebase (as push() method does).
Instead, IDs are generated separately before writing data.
So far, I've found a few resources with similar questions, but they were either related to a different platform with another syntax or outdated.
My current code to read IDs from the 'ids' node appears to be working, if the node with IDs is manually created in database console, but it appears that I still need help with the code to write data in the desired format.
This is the code for reading data:
function checkIdDuplicity(id)
{
const database = getDatabase();
get(child(ref(database), 'ids/'))
.then((snapshot) => {
if (snapshot.exists()) {
// Snapshot exists, find possible duplicate
if (snapshot.hasChild(id)) {
// Such ID is already in database
console.log("ID already exists");
return true;
}
else {
// ID is unique
return false;
}
}
else {
// No IDs in database, node shall be created
return false;
}
})
.catch((error) => {
console.log(error);
});
}
Would anyone have any ideas? 🙂
To add a node with custom key, you need to use set() instead of push().
const nodeRef = child(ref(database), "ids/" + id); // id = custom ID you want to specify
await set(nodeRef, { ...data })
Also, you are downloading entire ids/ node to check existence of a given ID. Instead you can just download that particular ID:
// specify the custom ID to check for in ref
get(child(ref(database), 'ids/' + id)) .then((snapshot) => {
if (snapshot.exists()) {
console.log("ID already exists in database")
} else {
console.log("ID does not exist in database")
}
})

Firestore document query allowed with get() but denied using where()

I am unit testing the firestore security rules below which allows a member of a group to read the groups collection if their userId is a key in the roles map.
function isMemberOfGroup(userId, groupId) {
return userId in get(/databases/$(database)/documents/groups/$(groupId)).data.roles.keys();
}
match /groups/{groupId} {
allow read: if isMemberOfGroup(request.auth.uid, groupId);
}
Sample groups collection
{
name: "Group1"
roles: {
uid: "member"
}
}
My unit test performs a get() of a single document which succeeds.
It then performs a get() using a where() query which fails.
it("[groups] any member of a group can read", async () => {
const admin = adminApp({ uid: "admin" });
const alice = authedApp({ uid: "alice" });
// Groups collection initialisation
await firebase.assertSucceeds(
admin.collection("groups")
.doc("group1")
.set({ name: "Group1", roles: { alice: "member" } })
);
// This succeeds
await firebase.assertSucceeds(
alice.collection("groups")
.doc("group1")
.get()
);
// This fails
await firebase.assertSucceeds(
alice.collection("groups")
.where(new firebase.firestore.FieldPath('roles', 'alice'), '==', "member")
.get()
);
});
It fails with the error:
FirebaseError:
Null value error. for 'list' # L30
where L30 points to the allow read security rule
I know that firestore rules are not responsible for filtering and will deny any request that could contain documents outside of the rule.
However from what I understand, my where() should be limiting this correctly.
Is there a problem with my where() query or my rules?
The problem is still that security rules are not filters. Since your rule depends on a specific document ID (in your case, groupId), that value can never be used in the rule as part of a comparison. Since rules are not filters, the rule must work for any possible value of groupId without knowing it an advance, since the rule will not get() and check each individual group document.
The security check you have now will simply not work for queries for this reason. Instead, consider storing a list of per-user group access in custom claims or a single document that the user can read, so they can then iterate the list and get() only the specific groups they've been told they have access to.

How to notify the front end that it needs to refresh after setting a custom claim with Firebase cloud functions onCreate listener

I'm trying to initialize a user upon registration with a isUSer role using custom claims and the onCreate listener. I've got it to set the correct custom claim but the front end is aware of it only after a full page refresh.
I've been following this article, https://firebase.google.com/docs/auth/admin/custom-claims?authuser=0#logic, to notify the front end that it needs to refresh the token in order to get the latest changes on the custom claims object, but to be honest I don't quite fully understand what's going on in the article.
Would someone be able to help me successfully do this with the firestore database ?
This is my current cloud function:
exports.initializeUserRole = functions.auth.user().onCreate(user => {
return admin.auth().setCustomUserClaims(user.uid, {
isUser: true
}).then(() => {
return null;
});
});
I've tried adapting the real-time database example provided in the article above to the firestore database but I've been unsuccessful.
exports.initializeUserRole = functions.auth.user().onCreate(user => {
return admin.auth().setCustomUserClaims(user.uid, {
isUser: true
}).then(() => {
// get the user with the updated claims
return admin.auth().getUser(user.uid);
}).then(user => {
user.metadata.set({
refreshTime: new Date().getTime()
});
return null;
})
});
I thought I could simply set refreshTime on the user metadata but there's no such property on the metadata object.
In the linked article, does the metadataRef example provided not actually live on the user object but instead somewhere else in the database ?
const metadataRef = admin.database().ref("metadata/" + user.uid);
If anyone could at least point me in the right direction on how to adapt the real-time database example in the article to work with the firestore database that would be of immense help.
If my description doesn't make sense or is missing vital information let me know and I'll amend it.
Thanks.
The example is using data stored in the Realtime Database at a path of the form metadata/[userID]/refreshTime.
To do the same thing in Firestore you will need to create a Collection named metadata and add a Document for each user. The Document ID will be the value of user.uid. Those documents will need a timestamp field named refreshTime.
After that, all you need to do is update that field on the corresponding Document after the custom claim has been set for the user. On the client side, you will subscribe to changes for the user's metadata Document and update in response to that.
Here is an example of how I did it in one of my projects. My equivalent of the metadata collection is named userTokens. I use a transaction to prevent partial database changes in the case that any of the steps fail.
Note: My function uses some modern JavaScript syntax that is being transpiled with Babel before uploading.
exports.initializeUserData = functions.auth.user().onCreate(async user => {
await firestore.collection('userTokens').doc(user.uid).set({ accountStatus: 'pending' })
const tokenRef = firestore.collection('userTokens').doc(user.uid)
const userRef = firestore.collection('users').doc(user.uid)
const permissionsRef = firestore.collection('userPermissions').doc(user.email)
await firestore.runTransaction(async transaction => {
const permissionsDoc = await transaction.get(permissionsRef)
const permissions = permissionsDoc.data();
const customClaims = {
admin: permissions ? permissions.admin : false,
hasAccess: permissions ? permissions.hasAccess : false,
};
transaction.set(userRef, { name: user.displayName, email: user.email, getEmails: customClaims.hasAccess })
await admin.auth().setCustomUserClaims(user.uid, customClaims)
transaction.update(tokenRef, { accountStatus: 'ready', refreshTime: admin.firestore.FieldValue.serverTimestamp() })
});
})

Authentication and Access Control with Relay

The official line from Facebook is that Relay is "intentionally agnostic about authentication mechanisms." In all the examples in the Relay repository, authentication and access control are a separate concern. In practice, I have not found a simple way to implement this separation.
The examples provided in the Relay repository all have root schemas with a viewer field that assumes there is one user. And that user has access to everything.
However, in reality, an application has has many users and each user has different degrees of access to each node.
Suppose I have this schema in JavaScript:
export const Schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: () => ({
node: nodeField,
user: {
type: new GraphQLObjectType({
name: 'User',
args: {
// The `id` of the user being queried for
id: { type: new GraphQLNonNull(GraphQLID) },
// Identity the user who is querying
session: { type: new GraphQLInputObjectType({ ... }) },
},
resolve: (_, { id, session }) => {
// Given `session, get user with `id`
return data.getUser({ id, session });
}
fields: () => ({
name: {
type: GraphQLString,
resolve: user => {
// Does `session` have access to this user's
// name?
user.name
}
}
})
})
}
})
})
});
Some users are entirely private from the perspective of the querying user. Other users might only expose certain fields to the querying user. So to get a user, the client must not only provide the user ID they are querying for, but they must also identify themselves so that access control can occur.
This seems to quickly get complicated as the need to control access trickles down the graph.
Furthermore, I need to control access for every root query, like nodeField. I need to make sure that every node implementing nodeInterface.
All of this seems like a lot of repetitive work. Are there any known patterns for simplifying this? Am I thinking about this incorrectly?
Different applications have very different requirements for the form of access control, so baking something into the basic Relay framework or GraphQL reference implementation probably doesn't make sense.
An approach that I have seen pretty successful is to bake the privacy/access control into the data model/data loader framework. Every time you load an object, you wouldn't just load it by id, but also provide the context of the viewer. If the viewer cannot see the object, it would fail to load as if it doesn't exist to prevent even leaking the existence of the object. The object also retains the viewer context and certain fields might have restricted access that are checked before being returned from the object. Baking this in the lower level data loading mechanism helps to ensure that bugs in higher level product / GraphQL code doesn't leak private data.
In a concrete example, I might not be allowed to see some User, because he has blocked me. You might be allowed to see him in general, but no his email, since you're not friends with him.
In code something like this:
var viewer = new Viewer(getLoggedInUser());
User.load(id, viewer).then(
(user) => console.log("User name:", user.name),
(error) => console.log("User does not exist or you don't have access.")
)
Trying to implement the visibility on GraphQL level has lots of potential to leak information. Think of the many way to access a user in GraphQL implementation for Facebook:
node($userID) { name }
node($postID) { author { name } }
node($postID) { likers { name } }
node($otherUserID) { friends { name } }
All of these queries could load a user's name and if the user has blocked you, none of them should return the user or it's name. Having the access control on all these fields and not forgetting the check anywhere is a recipe for missing the check somewhere.
I found that handling authentication is easy if you make use of the GraphQL rootValue, which is passed to the execution engine when the query is executed against the schema. This value is available at all levels of execution and is useful for storing an access token or whatever identifies the current user.
If you're using the express-graphql middleware, you can load the session in a middleware preceding the GraphQL middleware and then configure the GraphQL middleware to place that session into the root value:
function getSession(req, res, next) {
loadSession(req).then(session => {
req.session = session;
next();
}).catch(
res.sendStatus(400);
);
}
app.use('/graphql', getSession, graphqlHTTP(({ session }) => ({
schema: schema,
rootValue: { session }
})));
This session is then available at any depth in the schema:
new GraphQLObjectType({
name: 'MyType',
fields: {
myField: {
type: GraphQLString,
resolve(parentValue, _, { rootValue: { session } }) {
// use `session` here
}
}
}
});
You can pair this with "viewer-oriented" data loading to achieve access control. Check out https://github.com/facebook/dataloader which helps create this kind of data loading object and provides batching and caching.
function createLoaders(authToken) {
return {
users: new DataLoader(ids => genUsers(authToken, ids)),
cdnUrls: new DataLoader(rawUrls => genCdnUrls(authToken, rawUrls)),
stories: new DataLoader(keys => genStories(authToken, keys)),
};
}
If anyone has problems with this topic: I made an example repo for Relay/GraphQL/express authentication based on dimadima's answer. It saves session data (userId and role) in a cookie using express middleware and a GraphQL Mutation

Categories