Delete item from subcollection array in Firestore Firebase - javascript

I am trying to delete item from array in profile collection named items subcollection. Function should be triggered whenever item is deleted from main items collection. Problem with below function is that when triggered it deletes all items from profile instead of deleted one only. How I could iterate over items array and perform check of deleted item id.
exports.updateDeletedItemOnProfile = functions.firestore
.document('items/{itemId}')
.onDelete((snap, context) => {
const { itemId } = context.params
const deletedItem = snap.data()
if(deletedItem){
db.collection('profiles')
.doc(deletedItem.user.id)
.update({
items: admin.firestore.FieldValue.delete({
id: itemId,
title: deletedItem.title,
price: deletedItem.price,
image: deletedItem.image
})
})
}
return true
})

onDelete triggers only fire when an entire document was deleted. By this time, it's too late - the document is simply gone. Something chose to delete it, and you won't be able to tell what did that.
If you want a function to trigger when any part of a document is modified, you should use an onUpdate trigger instead. That will give you the entire contents of the document before and after the change. You will have to compare them to figure out what specifically changed.

I 've also tried onUpdate trigger for deleting data along with onWrite trigger to listen for any change when data is changed, however in both cases it does not give me any result for part referring to deleting data. Items subcollection in profiles collection remains unchanged. I am not sure where error in function lies, function execution does not show any error, finishes with status Ok....
exports.updateDeletedItemOnProfile = functions.firestore
.document('items/{itemId}')
.onUpdate((change, context) => {
const { itemId } = context.params
const beforeItem = change.before.data()
const updatedItem = change.after.data()
if (updatedItem === null) {
return db.collection('profiles')
.doc(beforeItem.user.id)
.delete({
items: admin.firestore.FieldValue.arrayRemove({
id: itemId,
title: beforeItem.title,
price: beforeItem.price,
image: beforeItem.image,
status: beforeItem.status,
})
})
}
return true
})
exports.updateDeletedItemOnProfile = functions.firestore
.document('items/{itemId}')
.onUpdate((change, context) => {
const { itemId } = context.params
const beforeItem = change.before.data()
const updatedItem = change.after.data()
if (updatedItem === null) {
return db.collection('profiles')
.doc(beforeItem.user.id)
.update({
items: admin.firestore.FieldValue.delete({
id: itemId,
title: beforeItem.title,
price: beforeItem.price,
image: beforeItem.image,
status: beforeItem.status,
})
})
}
return true
})

Related

Can we listen specific key value changes in firestore angular

getUserEventSummaryE(userId) {
return docData(doc(this.firestore, `/user/${userId}/event_summary/current/`), {idField : 'playing_summary'}).
pipe(distinctUntilChanged((prev, curr) => _.isEqual(prev, curr)));
}
getUserEventSummaryE should only return document when playing_summary changes but current it returning document data when other than playing_summary key value changes.
Example Document data
{
id: 1,
playing_summary: 'playing',
userbalance: 222,
dob: '2021-05-02'
}
When I change Other than playing_summary then currently changes triggering.
You can't listen to any key-values in the documents, but you can do that on document. It's only triggered when a document is updated.
exports.dbEventsOnUpdate = functions.firestore
.document('events/{eventId}').onUpdate(async (change,context) => {
const eventID = context.params.eventId;
const newValue = change.after.data();
const previousValue = change.before.data();
const titleIsUpdated = newValue.title !== previousValue.title;
})
You can check this link for more details.

React hook useEffect returns empty array at first

I get the data from my database like so:
const clicked = props.clicked;
const [allEmployees, setAllEmployees] = useState([]);
const [list, setList] = useState([]);
useEffect(()=>{ //getting employees
Axios.get(
PATH + `/${employees}`
).then((data) => {
setAllEmployees(data.data);
});
},[]);
useEffect(()=>{ //getting list of employees who should be selected
Axios.get(
PATH + `/${list}`
).then((data) => {
setList(data.data);
setList(
allEmployees.map(t=>{
if(list.includes(t.id)){
return{
select: true,
id: t.id,
name: t.name
}
}else{
return{
select: false,
id: t.id,
name: t.name
}
}
})
)
console.log(allEmployees);// <<
console.log(list);// <<
});
},[clicked]);
My problem is that the fist time I click on the buttons, activating clicked.props, both console.log() show empty arrays. After the second click and on, they work and show the arrays. I'm guessing I need to update them in a better way, but don't know how. (I'm trying to show the data but it really shows nothing on the first click of the button).
this is because they actually are empty arrays at first. Axios sends an async request to the server, fetching data. The state hooks are rendered right after the first page render, so basically at the same time. The data from the server will return when it returns. (as it is a promise). Once the data returns as a promise, you can resolve it and add the resolved data to the state.
It also seems like you're trying to set the list state twice with different data in the second useEffect. You are using the fetched data from axios first, and using the the data from the first useEffect after (allEmployees), it's kind of hard for me to really understand your thought process here.
the reason why you get the console.logs return empty on first click is because of the way useState works , when you set state you'll have to wait until the component reaload to see the new values of your state . so when you first click the state was empty so you get empty logs but on the second click you get none empty values but actually their just the new values from your first click :
const [value,setValue] = useState(0)
useEffect(()=>{ //getting list of employees who should be selected
Axios.get(PATH + `/${list}` )
.then((data) => {
setValue(value+1 )
});
console.log(value);//this will output 1
},[]);
useEffect(()=>{ //getting list of employees who should be selected
console.log(value);//this will output 2 on the next component render
},[value]);
so in your case you can check if list and employes are not empty if so return null :
const clicked = props.clicked;
const [allEmployees, setAllEmployees] = useState([]);
const [list, setList] = useState([]);
useEffect(()=>{ //getting employees
Axios.get(
PATH + `/${employees}`
).then((data) => {
setAllEmployees(data.data);
});
},[]);
useEffect(()=>{ //getting list of employees who should be selected
Axios.get(
PATH + `/${list}`
).then((data) => {
setList(
allEmployees.map(t=>{
if(list.includes(t.id)){
return{
select: true,
id: t.id,
name: t.name
}
}else{
return{
select: false,
id: t.id,
name: t.name
}
}
})
)
console.log(allEmployees);// <<
console.log(list);// <<
});
},[clicked]);
if(!allEmployees.length || !list.length) return null
// after this line you're garanted that allEmployees and list are not empty
render <div>{list.map(your implemntation)}<div>

How to get out a search result from forEach in an Object?

I have a React app with some JSON data. I have a drop down menu to set a status to a job in my app. I have a state called status with loaded statuses with their ids as follows.
this.state.status: [
{id: 1, name: 'To-do'},
{id: 2, name: 'In-progress'},
{id: 3, name: 'Completed'},
]
When change the status by selecting a menu item in the drop down list, I need to send the status id to the API. So I'm setting the value of the drop down list with selected status and find the status id by status name in the status Object and get the particular id. Here what I have tried so far. The problem is, API call can't catch the status id. Console says "status_id is not defined"
statusHandleChange = event => {
const project_id = this.props.projectData.id;
const job_id = this.props.projectData.currentJobId;
this.setState({ currentStatus: event.target.value }, () =>
Object.keys(this.state.status).forEach((key) => {
if (this.state.status[key].name === event.target.value) {
const status_id = (this.state.status[key].id)
}
}),
API.post('job/change_current_status', { project_id, job_id, status_id })
.then(({ data }) => {
console.log("success!", data)
})
.catch((err) => {
console.log("AXIOS ERROR: ", err);
})
);
};
ERROR : 'status_id' is not defined!
Because you defined status_id as a block-scoped constant inside an if statement inside a function - it's not accessible outside of that if statement. You can use find and Object.values with destructuring instead (because forEach doesn't return anything):
const { id: status_id } = Object.values(this.state.status).find(({ name }) => name === event.target.value);

What is Firebase Firestore 'Reference' data type good for?

I'm just exploring the new Firebase Firestore and it contains a data type called reference. It is not clear to me what this does.
Is it like foreign key?
Can it be used to point to a collection that is located somewhere else?
If reference is an actual reference, can I use it for queries? For example can I have a reference that points directly to the user, instead of storing the userId in a text field? And can I use this user reference for querying?
Adding below what worked for me using references in Firestore.
As the other answers say, it's like a foreign key. The reference attribute doesn't return the data of the reference doc though. For example, I have a list of products, with a userRef reference as one of the attributes on the product. Getting the list of products, gives me the reference of the user that created that product. But it doesn't give me the details of the user in that reference. I've used other back end as a services with pointers before that have a "populate: true" flag that gives the user details back instead of just the reference id of the user, which would be great to have here (hopefully a future improvement).
Below is some example code that I used to set the reference as well as get the collection of products list then get the user details from the user reference id given.
Set a reference on a collection:
let data = {
name: 'productName',
size: 'medium',
userRef: db.doc('users/' + firebase.auth().currentUser.uid)
};
db.collection('products').add(data);
Get a collection (products) and all references on each document (user details):
db.collection('products').get()
.then(res => {
vm.mainListItems = [];
res.forEach(doc => {
let newItem = doc.data();
newItem.id = doc.id;
if (newItem.userRef) {
newItem.userRef.get()
.then(res => {
newItem.userData = res.data()
vm.mainListItems.push(newItem);
})
.catch(err => console.error(err));
} else {
vm.mainListItems.push(newItem);
}
});
})
.catch(err => { console.error(err) });
References are very much like foreign keys.
The currently released SDKs cannot store references to other projects. Within a project, references can point to any other document in any other collection.
You can use references in queries like any other value: for filtering, ordering, and for paging (startAt/startAfter).
Unlike foreign keys in a SQL database, references are not useful for performing joins in a single query. You can use them for dependent lookups (which seem join like), but be careful because each hop will result in another round trip to the server.
For those looking for a Javascript solution to querying by reference - the concept is that, you need to use a 'document reference' object in the query statement
teamDbRef = db.collection('teams').doc('CnbasS9cZQ2SfvGY2r3b'); /* CnbasS9cZQ2SfvGY2r3b being the collection ID */
//
//
db.collection("squad").where('team', '==', teamDbRef).get().then((querySnapshot) => {
//
}).catch(function(error) {
//
});
(Kudos to the answer here: https://stackoverflow.com/a/53141199/1487867)
According to the #AskFirebase https://youtu.be/Elg2zDVIcLo?t=276
the primary use-case for now is a link in Firebase console UI
A lot of answers mentioned it is just a reference to another document but does not return data for that reference but we can use it to fetch data separately.
Here is an example of how you could use it in the firebase JavaScript SDK 9, modular version.
let's assume your firestore have a collection called products and it contains the following document.
{
name: 'productName',
size: 'medium',
userRef: 'user/dfjalskerijfs'
}
here users have a reference to a document in the users collection. we can use the following code segment to get the product and then retrieve the user from the reference.
import { collection, getDocs, getDoc, query, where } from "firebase/firestore";
import { db } from "./main"; // firestore db object
let productsWithUser = []
const querySnaphot = await getDocs(collection(db, 'products'));
querySnapshot.forEach(async (doc) => {
let newItem = {id: doc.id, ...doc.data()};
if(newItem.userRef) {
let userData = await getDoc(newItem.userRef);
if(userData.exists()) {
newItem.userData = {userID: userData.id, ...userData.data()}
}
productwithUser.push(newItem);
} else {
productwithUser.push(newItem);
}
});
here collection, getDocs, getDoc, query, where are firestore related modules we can use to get data whenever necessary. we use user reference returned from the products document directly to fetch the user document for that reference using the following code,
let userData = await getDoc(newItem.userRef);
to read more on how to use modular ver SDK refer to official documentation to learn more.
If you don't use Reference data type, you need to update every document.
For example, you have 2 collections "categories" and "products" and you stored the category name "Fruits" in categories to every document of "Apple" and "Lemon" in products as shown below. But, if you update the category name "Fruits" in categories, you also need to update the category name "Fruits" in every document of "Apple" and "Lemon" in products:
collection | document | field
categories > 67f60ad3 > name: "Fruits"
collection | document | field
products > 32d410a7 > name: "Apple", category: "Fruits"
58d16c57 > name: "Lemon", category: "Fruits"
But, if you store the reference of "Fruits" in categories to every document of "Apple" and "Lemon" in products, you don't need to update every document of "Apple" and "Lemon" when you update the category name "Fruits" in categories:
collection | document | field
products > 32d410a7 > name: "Apple", category: categories/67f60ad3
58d16c57 > name: "Lemon", category: categories/67f60ad3
This is the goodness of Reference data type.
Belatedly, there are two advantages from this blog:
if I expect that I'll want to order restaurant reviews by rating, or publish date, or most upvotes, I can do that within a reviews subcollection without needing a composite index. In the larger top level collection, I'd need to create a separate composite index for each one of those, and I also have a limit of 200 composite indexes.
I wouldn't have 200 composite indices but there are some constraints.
Also, from a security rules standpoint, it's fairly common to restrict child documents based on some data that exists in their parent, and that's significantly easier to do when you have data set up in subcollections.
One example would be restricting to insert a child collection if the user doesn't have the privilege in the parent's field.
2022 UPDATE
let coursesArray = [];
const coursesCollection = async () => {
const queryCourse = query(
collection(db, "course"),
where("status", "==", "active")
)
onSnapshot(queryCourse, (querySnapshot) => {
querySnapshot.forEach(async (courseDoc) => {
if (courseDoc.data().userId) {
const userRef = courseDoc.data().userId;
getDoc(userRef)
.then((res) => {
console.log(res.data());
})
}
coursesArray.push(courseDoc.data());
});
setCourses(coursesArray);
});
}
UPDATE 12/18/22 - I put this in a package.
Original Blog Post
What this package does is use RXJS to loop through each field in a document. If that document type is a Reference type, then it grabs that foreign document type. The collection version grabs the foreign key value for each reference field in all documents in your collection. You can also input the fields manually that you wish to parse to speed up the searching (see my post). This is definitely not as efficient as doing manual aggregations with Firebase Functions, as you will get charged for lots of reads for each document you read, but it could come in handy for people that want a quick way to join data on the frontend.
This could also come in handy if you cache data and really only need to do this once.
J
install
npm i j-firebase
import
import { expandRef, expandRefs } from 'j-firebase';
https://github.com/jdgamble555/j-firebase
Original Post
Automatic JOINS:
DOC
expandRef<T>(obs: Observable<T>, fields: any[] = []): Observable<T> {
return obs.pipe(
switchMap((doc: any) => doc ? combineLatest(
(fields.length === 0 ? Object.keys(doc).filter(
(k: any) => {
const p = doc[k] instanceof DocumentReference;
if (p) fields.push(k);
return p;
}
) : fields).map((f: any) => docData<any>(doc[f]))
).pipe(
map((r: any) => fields.reduce(
(prev: any, curr: any) =>
({ ...prev, [curr]: r.shift() })
, doc)
)
) : of(doc))
);
}
COLLECTION
expandRefs<T>(
obs: Observable<T[]>,
fields: any[] = []
): Observable<T[]> {
return obs.pipe(
switchMap((col: any[]) =>
col.length !== 0 ? combineLatest(col.map((doc: any) =>
(fields.length === 0 ? Object.keys(doc).filter(
(k: any) => {
const p = doc[k] instanceof DocumentReference;
if (p) fields.push(k);
return p;
}
) : fields).map((f: any) => docData<any>(doc[f]))
).reduce((acc: any, val: any) => [].concat(acc, val)))
.pipe(
map((h: any) =>
col.map((doc2: any) =>
fields.reduce(
(prev: any, curr: any) =>
({ ...prev, [curr]: h.shift() })
, doc2
)
)
)
) : of(col)
)
);
}
Simply put this function around your observable and it will automatically expand all reference data types providing automatic joins.
Usage
this.posts = expandRefs(
collectionData(
query(
collection(this.afs, 'posts'),
where('published', '==', true),
orderBy(fieldSort)
), { idField: 'id' }
)
);
Note: You can also now input the fields you want to expand as a second argument in an array.
['imageDoc', 'authorDoc']
This will increase the speed!
Add .pipe(take(1)).toPromise(); at the end for a promise version!
See here for more info. Works in Firebase 8 or 9!
Simple!
J

Updating Apollo store when mutating list filter

I have a graphql schema that looks like this :
type User {
entries(status: EntryStatus): [Entry]
}
type Mutation {
updateEntry(status: EntryStatus): Entry
}
I can filter the list of entries by status (which is an enum) and I can update the status of an entry. I'd like to update the store when the status is updated so that the entry appears in the right list (entries(status: FOO) to entries(status: BAR)).
I know I can update the store with the update method.
const withMutation = graphql(updateEntryMutation, {
props: ({ mutate }) => ({
updateEntry: updateEntryInput =>
mutate({
variables: { updateEntryInput },
update: (proxy, newData) => {
// update data here
// Which would involve removing the entry from its previous filtered "list", and adding it to the one with the new status
})
})
});
But how can I know from which list to remove the entry since I don't have access to the old data (previous entry.status) from update ?
(apart from enumerating all lists by status and removing the updated entry if I find it...)
You need to store.readQuery() first, then write the new data to the store.
Kinda like this:
const EntriesQuery = 'yourQueryHere';
const withMutation = graphql(updateEntryMutation, {
props: ({ mutate }) => ({
updateEntry: updateEntryInput =>
mutate({
variables: { updateEntryInput },
update: (store, { data: { updateEntry }}) => {
const data = store.readQuery({ query: EntriesQuery });
data.entries.push(updateEntry)
store.writeQuery({ query: EntriesQuery, data })
})
})
});

Categories