I have an application where a user can create lists.
The user can share a list with other users. I already manage to create the sharing part, the one I'm having issues is with the delete part. I want that when a user deletes a list that is shared , this list is also deleted from the other users.
This delete will be made only by list owner.
So a scenario will be:
User A creates a List with pushID = 1.
This list is added in the following firebase ref: /userList/$userAID/$pushID.
User A shares list with User B and User C.
This list is added in the following firebase ref: /userList/$userBID/$pushID and /userList/$userCID/$pushID.
User A deletes list with pushID = 1.
So in my
So i have this schema:
userList: {
2xYnKcZFEdPYWfUJ3E63yQEDShe2: {
-Kt7lXiY0Yt-oDcV38L5
}
KtQHkXMSwKSByZ1rmTRwjDmSYnE3: {
-Kt7lXiY0Yt-oDcV38L5: {}
-Kt9XP91hjwcwgcBSgbc: {}
}
XHpMVoRqcCdzwTP70L29Lza1ibD3: {
-Kt7lXiY0Yt-oDcV38L5: {}
}
}
In high level this will be:
userList: {
userID: (A) {
-listID : (1) {}
}
userID: (B) {
-listID: (1) {}
-listID: (2) {}
}
userID: (C) {
-listID: (1) {}
-listID: (3) {}
-listID: (4) {}
}
}
The current code I have to do this is the following:
const ref = firebase.database().ref('userList');
ref.once('value')
.then((snapshot) => {
snapshot.forEach((childSnapshot) => {
ref.child(childSnapshot.key)
.once('value')
.then((snapshot2) => {
snapshot2.forEach((childSnapshot2) => {
if (childSnapshot2.key === uid) {
ref.child(childSnapshot2.key)
.remove();
}
});
})
.catch(() => {
console.log('error2');
});
});
})
.catch((error) => {
console.log(error);
});
What I'm doing in this code is, first fetching ALL the list inside userList, by getting the key I manage to jump to the userID child. Inside this node once again I manage to jump inside the pushID where I make a validation of checking if current key is equal to the UID of the list i want to delete, if so I do a remove().
I feel there must be a better way of telling Firebase to go directly to pushID and find all of those that are equal to the UID of the list I want to delete and then do it.
There is no way to do a server-side delete of multiple items based on a condition with the Firebase Database. You must first retrieve the items (or just their IDs) matching the condition and then delete those.
However, you can delete the list itself and all references to it in one go by using a multi-path update.
I'd also recommend keeping a list of all the UIDs you've shared a specific list with, so that you don't have to loop over all users. Keeping many-to-many relations in both directions is quite common in Firebase and other NoSQL databases.
For more see:
the blog post introducing multi-path updates
the blog post describing client-side fan-out using multi-path updates
my answer on strategies for updating denormalized data
my answer on modeling many-to-many relationships
Related
// Update the other user that current user has asked for revision
export async function updateOtherUserForContractRevision(contractID : string, comments : any) {
// getting userDetails
let currentUser : string | any = localStorage.getItem("userDetails");
currentUser = JSON.parse(currentUser);
// update fields in contractRecieved
const contractsRecievedRef : any = doc(db, "contractsRecieved", currentUser.uid);
// const queryContractsRecieved = query(contractsRecievedRef,
// where('contractDetails.contract.contractID','array-contains',contractID)
// );
// console.log(".////////updateOtherUserForContractRevision", queryContractsRecieved ,contractID)
onSnapshot(contractsRecievedRef, (doc: any) => {
doc.data().contract.map((contract: any) => {
if(contract.contractDetails.contract.contractID === contractID) {
// I reach my desired array Index and want to update the msg field to my comments parametre
console.log(".////////contract", contract);
}
})
})
}
I want to update my msg field as well as content field in contract object, which in turn is present in contractDetails object in the contract array(0th index). I have searched and reached to my desired array value using onSnapShot, how can I update these fields in the onSnapShot method? or I should use another approach for searching and updating fields in objects contained by the array.
JUST A THOUGHT: If I could get the reference to the array index and then use it to update the object, maybe It'll work
for example (I'll update sentForRevision)
onSnapshot(contractsRecievedRef, async (doc: any) => {
doc.data().contract.map(async (contract: any) => {
if(contract.contractDetails.contract.contractID === contractID) {
console.log(".////////contract", doc.ref);
// If this contract.ref exists and points to the index present in the document
await updateDoc(contract.ref,{
"contractDetails.sentForRevision": true,
});
}
})
})
There is no way you can query a Firestore collection based on a value that exists in an object that is contained in an array. This kind of filtering cannot be achieved using partial data. I have even written an article regarding this topic called:
How to update an array of objects in Firestore?
If you need that, you can duplicate the data on which you want to perform the filtering and add it to a separate array. In this way, you can query the collection using array-contains operator. Once you get the desired documents, you can get the array, perform the updates and then write the documents back to Firestore.
It seems i have misunderstood sequelize .hasMany() and .belongsTo() associations and how to use them in service. I have two models:
const User = db.sequelize.define("user", {
uid: { /*...*/ },
createdQuestions: {
type: db.DataTypes.ARRAY(db.DataTypes.UUID),
unique: true,
allowNull: true,
},
});
const Question = db.sequelize.define("question", {
qid: { /*...*/ },
uid: {
type: db.DataTypes.TEXT,
},
});
Given that one user can have many questions and each question belongs to only one user I have the following associatons:
User.hasMany(Question, {
sourceKey: "createdQuestions",
foreignKey: "uid",
constraints: false,
});
Question.belongsTo(User, {
foreignKey: "uid",
targetKey: "createdQuestions",
constraints: false,
});
What I want to achieve is this: After creation of a question object, the qid should reside in the user object under "createdQuestions" - just as the uid resides in the question object under uid. What I thought sequelize associations would do for me is to save individual calling and updating the user object. Is there a corresponding method? What I have so far is:
const create_question = async (question_data) => {
const question = { /*... question body containing uid and so forth*/ };
return new Promise((resolve, rejected) => {
Question.sync({ alter: true }).then(
async () =>
await db.sequelize
.transaction(async (t) => {
const created_question = await Question.create(question, {
transaction: t,
});
})
.then(() => resolve())
.catch((e) => rejected(e))
);
});
};
This however only creates a question object but does not update the user. What am I missing here?
Modelling a One-to-many relationship in SQL
SQL vs NoSQL
In SQL, contrary to how it is in NoSQL, every attribute has a fixed data type with a fixed limit of bits. That's manifested by the SQL command when creating a new table:
CREATE TABLE teachers (
name VARCHAR(32),
department VARCHAR(64),
age INTEGER
);
The reason behind this is to allow us to easily access any attribute from the database by knowing the length of each row. In our case, each row will need the space needed to store:
32 bytes (name) + 64 bytes (department) + 4 bytes (age) = 100 byes
This is a very powerful feature in Relation Databases as it minimizes the time needed to retrieve data to Constant time since we knew where each piece of data is located in the memory.
One-to-Many Relationship: Case Study
Now, let's consider we have these 3 tables
Let's say we want to create a one-to-many relation between classes and teachers where a Teacher can give many classes.
We can think of it this way. But, this model is not possible for 2 main reasons:
It will make us lose our constant-time retrieval since we don't know the size of the list anymore
We fear that the amount of space given to the list attribute won't be enough for future data. Let's say we allocate space needed for 10 classes and we end up with a teacher giving 11 classes. This will push us to recreate our database to increase the column size.
Another way would be this:
While this approach will fix the limited column size problem, we no longer have a single source of truth. The same data is duplicated and stored multiple times.
That's why for this one-to-many relationship, we'll need to store the Id of the teacher inside this class table.
This way, we still can find all the classes a teacher can teach by running
SELECT *
FROM classes
WHERE teacherID = teacher_id
And we'll avoid all the problems discussed earlier.
Your relation is a oneToMany relation. One User can have multiple Questions. In SQL, this kind of relation is modelled by adding an attribute to Question called userId or Uid as you did. In Sequelize, this would be achieved through a hasMany or BelongsTo like this:
User.hasMany(Question)
Question.belongsTo(User, {
foreignKey: 'userId',
constraints: false
})
In other words, I don't think you need the CreatedQuestions attribute under User. Only one foreign key is needed to model the oneToMany relation.
Now, when creating a new question, you just need to add the userId this way
createNewQuestion = async (userId, title, body) => {
const question = await Question.create({
userId: userId, // or just userId
title: title, // or just title
body: body // or just body
})
return question
}
Remember, we do not store arrays in SQL. Even if we can find a way to do it, it is not what we need. There must be always a better way.
I'm wondering if there's any consensus out there with regard to how best to handle GraphQL field arguments when using Dataloader. The batchFn batch function that Dataloader needs expects to receive Array<key> and returns an Array<Promise>, and usually one would just call load( parent.id ) where parent is the first parameter of the resolver for a given field. In most cases, this is fine, but what if you need to provide arguments to a nested field?
For example, say I have a SQL database with tables for Users, Books, and a relationship table called BooksRead that represent a 1:many relationship between Users:Books.
I might run the following query to see, for all users, what books they have read:
query {
users {
id
first_name
books_read {
title
author {
name
}
year_published
}
}
}
Let's say that there's a BooksReadLoader available within the context, such that the resolver for books_read might look like this:
const UserResolvers = {
books_read: async function getBooksRead( user, args, context ) {
return await context.loaders.booksRead.load( user.id );
}
};
The batch load function for the BooksReadLoader would make an async call to a data access layer method, which would run some SQL like:
SELECT B.* FROM Books B INNER JOIN BooksRead BR ON B.id = BR.book_id WHERE BR.user_id IN(?);
We would create some Book instances from the resulting rows, group by user_id, then return keys.map(fn) to make sure we assign the right books to each user_id key in the loader's cache.
Now suppose I add an argument to books_read, asking for all the books a user has read that were published before 1950:
query {
users {
id
first_name
books_read(published_before: 1950) {
title
author {
name
}
year_published
}
}
}
In theory, we could run the same SQL statement, and handle the argument in the resolver:
const UserResolvers = {
books_read: async function getBooksRead( user, args, context ) {
const books_read = await context.loaders.booksRead.load( user.id );
return books_read.filter( function ( book ) {
return book.year_published < args.published_before;
});
}
};
But, this isn't ideal, because we're still fetching a potentially huge number of rows from the Books table, when maybe only a handful of rows actually satisfy the argument. Much better to execute this SQL statement instead:
SELECT B.* FROM Books B INNER JOIN BooksRead BR ON B.id = BR.book_id WHERE BR.user_id IN(?) AND B.year_published < ?;
My question is, does the cacheKeyFn option available via new DataLoader( batchFn[, options] ) allow the field's argument to be passed down to construct a dynamic SQL statement in the data access layer? I've reviewed https://github.com/graphql/dataloader/issues/75 but I'm still unclear if cacheKeyFn is the way to go. I'm using apollo-server-express. There is this other SO question: Passing down arguments using Facebook's DataLoader but it has no answers and I'm having a hard time finding other sources that get into this.
Thanks!
Pass the id and params as a single object to the load function, something like this:
const UserResolvers = {
books_read: async function getBooksRead( user, args, context ) {
return context.loaders.booksRead.load({id: user.id, ...args});
}
};
Then let the batch load function figure out how to satisfy it in an optimal way.
You'll also want to do some memoisation for the construction of the object, because otherwise dataloader's caching won't work properly (I think it works based on identity rather than deep equality).
Firestore as the backend. I've managed to get through by simply using basic crud methods. However, I wanted to find out how do I determine the changes to a list of items that are returned after the initial subscription.
What I'm ultimately looking to do is :
- miminise the amount of documents that are read each time
- animate a list of items (entry animation, exit animation, change animamtion)
In the following example I have the basic crud method along with the initial subscription:
posts:post [] = [];
constructor(private db: AngularFirestore){}
ngOnInit(){
//The initial subscription to the posts
this.db.collection("Posts").valuechanges().subscribe( _posts => {
this.posts = _posts;
});
async addItem(_post:post)
{
_post.id = this.db.createId();
await this.db.collection("Posts").doc(_post.id).set(_post);
}
async update(_post:post)
{
await this.db.collection("Posts").doc(_post.id).update(_post);
}
delete (_post:post)
{
await this.db.collection("Posts").doc(_post.id).delete();
}
With the above methods, I'm subscribing to the documents in the Posts collection. Initially I'm receiving an arrray of type Post, and whenever another item is added, updated, removed i'm receiving an updated array of of type post.
How do I differentiate what has happened to the item so I can animate the changes (i.e animate the entry of the item etc...) ?
It would really help me out if you could show a sample code ?
Thanks
The valueChanges observable only exposes the actual data in the document. It has no other metadata about the document, nor the kind of change.
If you need more information, listen for documentChanges instead. That exposes a stream of DocumentChangeAction objects, which amongst others contain a type property that is the DocumentChangeType.
See https://github.com/angular/angularfire2/blob/master/docs/firestore/documents.md#the-documentchangeaction-type
Is it possible to update firestore using the previous state?
So for example I have an address document which has a users field which holds an array of users associated with the address.
whenever I want to add a new user to this array I need the previous array otherwise I will end up overwriting the current data with the new data.
So I end up with something like.
firestore()
.collection("addresses")
.doc(addressId)
.get()
.then(doc => {
this.db
.collection("addresses")
.doc(addressId)
.update({
users: [...doc.data().users, id]
})
});
Is there a way to access the previous data without having to nest calls?
if not
Is there a better way to manage relationships?
If you need the previous value to determine the new value, you should use a transaction. This is the only way to ensure that different clients aren't accidentally overwriting each other's actions.
Unfortunately transactions also need nested calls, since that is the only way to get the current value, and even have one extra wrapper (for the transaction.
var docRef = firestore()
.collection("addresses")
.doc(addressId);
return db.runTransaction(function(transaction) {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(docRef).then(function(doc) {
transaction.update(docRef, { users: [...doc.data().users, id ]});
});
}).then(function() {
console.log("Transaction successfully committed!");
}).catch(function(error) {
console.log("Transaction failed: ", error);
});
The optimal solution is to use a data structure that doesn't require the current value to add new values. This is one of the reasons Firebase recommends against using arrays: they're inherently hard to scale when multiple users may be adding items to the array. If there is no need for maintaining order between the users, I'd recommend using a set-like structure for the users:
users: {
id1: true,
id2: true
}
This is a collection with two users (id1 and id2). The true values are just markers, since you can't have a field without a value.
With this structure, adding a user is as easy as:
firestore()
.collection("addresses")
.doc(addressId)
.update({ "users.id3": true })
Also see the Firestore documentation on
Working with Arrays, Lists, and Sets