deleting from array in firebase - javascript

Im currently having trouble deleting an item from my array in my firebase firestore. I'm able to delete it locally but when I refresh it shows up again. I know I'm supposed to use the actual value. THis is my related code and this is what the items look like in my firestore
const removeGoalHandler = async (goalId) => {
let goalToDel = {}
for(let i =0; i < courseGoals.length; i++){
if(courseGoals[i].id == goalId){
console.log(courseGoals[i])
goalToDel = courseGoals[i]
}
}
const removeGoal = await loansRef.doc(userId).update({
goals: firebase.firestore.FieldValue.arrayRemove(goalToDel)
})
setCourseGoals((currentGoals)=> {
return currentGoals.filter((goal)=> goal.id !== goalId)
})
setGoalCounter(goalCounter-1)
};
const addToFB = async (goalTitle, interestRate, years, paidOff,id) => {
//adding data to firebase, takes into account if doc exists already
if(id==undefined){
id = goalCounter
}
console.log('add to firebase')
const loadDoc = await loansRef.doc(userId).get()
.then((docSnapshot)=> {
if(docSnapshot.exists){
loansRef.doc(userId).onSnapshot((docu)=>{
console.log('num2: '+ (goalCounter+id).toString())
const updateLoansArr = loansRef.doc(userId).update({
goals: firebase.firestore.FieldValue.arrayUnion({
id: userId+(goalCounter+id).toString(),
value: goalTitle,
interest: interestRate,
years: years,
paidOff: paidOff
})
})
})
}
else{
console.log('num3: '+ (goalCounter+id).toString())
const addDoc = loansRef.doc(userId).set({
goals: firebase.firestore.FieldValue.arrayUnion({
id: userId+(goalCounter+id).toString(),
value: goalTitle,
interest: interestRate,
years: years,
paidOff: paidOff
})
})
}})
}
this is my code where I actually add a loan; in here it calls addToFB() which adds it to firebase haha
const addGoalHandler = (goalTitle, interestRate, years, paidOff,id) => {
console.log('add goal handler')
if(id==undefined){
id = 0
}
console.log('num1: '+ (goalCounter+id).toString())
//console.log(goalCounter)
setGoalCounter(goalCounter+1)
setCourseGoals((courseGoals) => [
...courseGoals,
{
id: userId + (goalCounter+id).toString(),
value: goalTitle,
interest: interestRate,
years: years,
paidOff: paidOff
}
]);
//console.log(goalCounter)
addToFB(goalTitle, interestRate,years,paidOff,id)
setIsAddMode(false);
}

The problem you're having is that arrayRemove() uses strict equality to compare array elements and determine which to remove, it doesn't compare the "ids" like you are doing in your code. Unfortunately, this means that every object would be deemed different from every other object (whether different id or same id) irrespective of how identical they are, ({} === {} //false), so it doesn't find the element to remove. arrayRemove() would work better with an array containing primitive types: (number, string, etc).
As it stands, your best option is fetch the existing document, use your "id" logic to remove the desired element and write it back. Like so:
const removeGoalHandler = async (goalId) => {
const existingDoc = await loansRef.doc(userId).get();
const goals = existingDoc.data().goals.filter(goal => goal.id !== goalId);
await loansRef.doc(userId).update({ goals });
setCourseGoals(goals);
...
};

Related

How can i do the pagination without modifying the limit?

How can I get the same elements what the user asked?
for example if I do this, the user asked 30
query = query.limit(30)
const qSnap = await query.get(); // 30 objects
qSnap.docs.forEach((doc) => { // i will get fewer than 30
const item = doc.data().data.tools.find(t => t.trait === 'color1' && t.value == 'red');
console.log(item)
})
i need to filter because i have this structure:
{
name:carla
data: "{
sign: "carly",
tools: [{trait:color1, value:red}, {trait:color2, value:white}] }"
},{
name:dany
data: "{
sign: "dan",
tools: "[{trait:color1, value:blue}, {trait:color2, value:black}]
}"
}
or how can i enhacement my structure to dont have this problem?
Taking Stewart's answer and changing it a bit (I couldn't do that in a comment, sorry)
const toolsFilter = {
trait: 'color1',
value:'red'
}
const qSnap = await query.where('tools','array-contains-any', toolsFilter)
.limit(30)
.get();
qSnap.docs.forEach((doc) => {
const item = doc.data().data;
console.log(item)
}))
The array-contains operations checks if an array, contains a specific (complete) value. It can't check if an array of objects, contains an item with a specific value for a property. The only way is to query the entire object inside the array.
In this example structure:
data: {
sign: "carly",
tools: [{trait:color1, value:red}, {trait:color2, value:white}] }
}
You want to query objects inside a map of an array. See Firestore screenshot below for better visualization:
To be able to query objects inside a map of an array, you must query the whole object inside of it. See example query below:
// As you can see here, you need to be able to jump inside the `data.tools`
// Then query the `tools` array by using objects.
const toolsRef = db.collection("someCollection").where("data.tools", "array-contains", {trait: "color1", value: "red"})
Here's a complete code for reference:
const toolsRef = db.collection("someCollection").where("data.tools", "array-contains", {trait: "color1", value: "red"})
query = toolsRef.limit(30)
query.get()
.then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// Do anything with the result.
console.log(doc.id, " => ", doc.data());
});
})
.catch((error) => {
console.log("Error getting documents: ", error);
});
The result should be something like this:
5nwzwpxr7BmctvznEypl => {
name: 'carla',
data: { tools: [ [Object], [Object] ], sign: 'carly' }
}
For more information, See Array Memberships.
UPDATE:
Firestore does not have a way to search from the document's fields which have a JSON object encoded inside. You should parse the JSON object first to get/filter the necessary data. e.g.:
query = query.limit(30);
const qSnap = await query.get();
qSnap.docs.forEach((doc) => {
const data = JSON.parse(doc.data().data);
const items = data.tools.filter(t => t.trait === "color1" && t.value === "red");
console.log(items);
})
However, the above snippet that is similar to yours could lead into a problem which is not the same as the limit() you set on your query. To enhance your structure, I would suggest to put it in document fields like I gave on my original answer above.
document
(fields)
|- name(string): "carla"
|- data(map)
|- sign(string): "carly"
|- tools(array)
|- (map)
| - trait(string): "color1"
| - value(string): "red"
|- (map)
- trait(string): "color2"
- value(string): "white"
This structure is the same as your JSON object encoded inside the data field. The advantage of using this structure is you can now query using Firestore which I showed to you on my original post. This would result in 30 documents without using a client side filtering. It will be only fewer than 30 if the query can't find matched documents.
To do this, you just need to construct your JSON object and set the data to the document. See e.g. below:
db.collection("someCollection").add({
data: {
sign: "carly",
tools: [{trait: "color1", value:"red"}, {trait:"color2", value:"white"}]
},
name: "carla"
})
.then((docRef) => {
console.log("Document written with ID: ", docRef.id);
})
.catch((error) => {
console.error("Error adding document: ", error);
});
You need to filter the data first, them limit it. Using this synatax.
const toolsFilter = {
trait: 'color1',
value:'red'
}
const qSnap = await query.where('tools','array-contains',toolsFilter)
.limit(30)
.get();
qSnap.docs.forEach((doc) => {
const item = doc.data().data;
console.log(item)
}))
See the Firebase online documentation for more info on query syntax etc.. https://firebase.google.com/docs/firestore/query-data/queries
Also this explains pagination using query cursors https://firebase.google.com/docs/firestore/query-data/query-cursors

Filtering data after fetching in React

I need to make a list of objects based on combined data from 2 arrays, one comes from a localStorage and the second one from Django backend. First of all objects from localStorage are displayed by showCart() function
export const showCart = () => {
if (typeof window !== undefined) {
if (localStorage.getItem("cart")) {
return JSON.parse(localStorage.getItem("cart"));
};
};
};
it returns data in this format: FE: { id: 1, amount: 7, size: "L", product: 1 }. product is the Foreign Key needed to match data from other array.
The second array comes form a backend and it is feched by getAllProducts() function
export const getAllProducts = () => {
return fetch(`${url}/products/`, {method: "GET"})
.then((response) => {
return response.json();
})
.catch((error) => console.log(error))
};
It returns data in this format: FE { name: "Red", id: 3, price: 33, image:"some-url"}
​​
Now I need to create another list of objects by merging then by product of an object in first array with id of an object from the second one. The objects in the third array need to contain amount and size from first array as well as name, price and image from the second one. In the end I want to store it in useState().
This is what I came up with, I guess my code stops working arter first for loop:
const [cart, setCart] = useState([]);
const CheckAnonymousCart = () => {
getAllProducts()
.then((data) => {
const localCart = showCart();
var products = [];
for (let i = 0; i < localCart.lenght; i++) {
for (let y = 0; y < data.lenght; y++) {
if (localCart[i].product === data[y].id) {
console.log(localCart[i].product, data[y].id)
const item = {
name: data[y].name,
price: data[y].price,
image: data[y].image,
amount: localCart[i].amount,
size: localCart[i].size,
}
products.push(item)
break;
}
}
}
setCart(products);
})
.catch((error) => console.log(error))
};
​​Any thoughts?
In addition to Jacob's comment, you probably want to avoid FETCH'ing all products from the DB, because it requires more DB resources, most of the info is not required, and it makes the for-loop take longer to JOIN both lists.
Ideally, you would use a parameterized query like so:
return fetch(`${url}/products/?id=1&id=2&id=3`, {method: "GET"})
Where ?id=1&id=2&id=3 are a subset of the product IDs that you're retrieving.
Note: You will also want to sanitize/validate the product IDs in localStorage, because the data can be modified by the end-user, which is a potential attack vector by malicious users.
The problem could simply be the typo from the for loop conditions, but you can also accomplish this more succinctly using the JS ES6 methods:
const products = localCart.map(item => {
const match = data.find(x => x.id === item.product);
return {
amount,
size,
name: match?.name,
price: match?.price,
image: match?.image
}
});

How to populate an array inside a map function in js and send it to the server?

This is my ObjectIds array -
obj_ids = [
"5ee71cc94be8d0180c1b63db",
"5ee71c884be8d0180c1b63d9",
"5ee71c494be8d0180c1b63d6",
"5ee71bfd4be8d0180c1b63d4"
]
I am using these objectids to serach whether they exist in the db or not and based on that I want to send the response to server.
This is the code I am trying but I dont know how to populate the array and send it to the server.
var msg = [];
obj_ids.map((ele) => {
Lead.find({ _id: ele._id }, async function (error, docs) {
if (docs.length) {
msg.push(
`Lead already exist for Lead id - ${ele._id} assgined to ${docs[0].salesPerson}`
);
} else {
msg.push(`Lead doesn't exist for Lead id: ${ele._id}`);
const newDuty = new AssignedDuty({
duty: ele._id,
salesPerson: req.body.salesPerson,
});
await newDuty.save();
}
});
});
res.json(msg);
By doing this approach I am getting an empty array. I cannot put res.json(msg) inside the loop. If it is possible by using async-await, please guide me through.
You don't need to make multiple queries to find whether given object ids exist in the database.
Using $in operator, you can make one query that will return all the documents where the _id is equal to one of the object id in the list.
const docs = await Lead.find({
_id: {
$in: [
"5ee71cc94be8d0180c1b63db",
"5ee71c884be8d0180c1b63d9",
"5ee71c494be8d0180c1b63d6",
"5ee71bfd4be8d0180c1b63d4"
]
}
});
After this query, you can check which object id is present in the docs array and which is absent.
For details on $in operator, see $in comparison operator
Your code can be simplified as shown below:
const obj_ids = [
"5ee71cc94be8d0180c1b63db",
"5ee71c884be8d0180c1b63d9",
"5ee71c494be8d0180c1b63d6",
"5ee71bfd4be8d0180c1b63d4"
];
const docs = await Lead.find({
_id: { $in: obj_ids }
});
const msg = [];
obj_ids.forEach(async (id) => {
const doc = docs.find(d => d._id == id);
if (doc) {
msg.push(
`Lead already exist for Lead id - ${doc._id} assgined to ${doc.salesPerson}`
);
}
else {
msg.push(`Lead doesn't exist for Lead id: ${id}`);
const newDuty = new AssignedDuty({
duty: id,
salesPerson: req.body.salesPerson
});
await newDuty.save();
}
});
res.json(msg);

Convert all MirageJS ids to integers

MirageJS provides all model ids as strings. Our backend uses integers, which are convenient for sorting and so on. After reading around MirageJS does not support integer IDs out of the box. From the conversations I've read the best solution would be to convert Ids in a serializer.
Output:
{
id: "1",
title: "Some title",
otherValue: "Some other value"
}
But what I want is:
Expected Output:
{
id: 1,
title: "Some title",
otherValue: "Some other value"
}
I really want to convert ALL ids. This would included nested objects, and serialized Ids.
I think you should be able to use a custom IdentityManager for this. Here's a REPL example. (Note: REPL is a work in progress + currently only works on Chrome).
Here's the code:
import { Server, Model } from "miragejs";
class IntegerIDManager {
constructor() {
this.ids = new Set();
this.nextId = 1;
}
// Returns a new unused unique identifier.
fetch() {
let id = this.nextId++;
this.ids.add(id);
return id;
}
// Registers an identifier as used. Must throw if identifier is already used.
set(id) {
if (this.ids.has(id)) {
throw new Error('ID ' + id + 'has already been used.');
}
this.ids.add(id);
}
// Resets all used identifiers to unused.
reset() {
this.ids.clear();
}
}
export default new Server({
identityManagers: {
application: IntegerIDManager,
},
models: {
user: Model,
},
seeds(server) {
server.createList("user", 3);
},
routes() {
this.resource("user");
},
});
When I make a GET request to /users with this server I get integer IDs back.
My solution is to traverse the data and recursively convert all Ids. It's working pretty well.
I have a number of other requirements, like removing the data key and embedding or serializing Ids.
const ApplicationSerializer = Serializer.extend({
root: true,
serialize(resource, request) {
// required to serializedIds
// handle removing root key
const json = Serializer.prototype.serialize.apply(this, arguments)
const root = resource.models
? this.keyForCollection(resource.modelName)
: this.keyForModel(resource.modelName)
const keyedItem = json[root]
// convert single string id to integer
const idToInt = id => Number(id)
// convert array of ids to integers
const idsToInt = ids => ids.map(id => idToInt(id))
// check if the data being passed is a collection or model
const isCollection = data => Array.isArray(data)
// check if data should be traversed
const shouldTraverse = entry =>
Array.isArray(entry) || entry instanceof Object
// check if the entry is an id
const isIdKey = key => key === 'id'
// check for serialized Ids
// don't be stupid and create an array of values with a key like `arachnIds`
const isIdArray = (key, value) =>
key.slice(key.length - 3, key.length) === 'Ids' && Array.isArray(value)
// traverse the passed model and update Ids where required, keeping other entries as is
const traverseModel = model =>
Object.entries(model).reduce(
(a, c) =>
isIdKey(c[0])
? // convert id to int
{ ...a, [c[0]]: idToInt(c[1]) }
: // convert id array to int
isIdArray(c[0], c[1])
? { ...a, [c[0]]: idsToInt(c[1]) }
: // traverse nested entries
shouldTraverse(c[1])
? { ...a, [c[0]]: applyFuncToModels(c[1]) }
: // keep regular entries
{ ...a, [c[0]]: c[1] },
{}
)
// start traversal of data
const applyFuncToModels = data =>
isCollection(data)
? data.map(model =>
// confirm we're working with a model, and not a value
model instance of Object ? traverseModel(model) : model)
: traverseModel(data)
return applyFuncToModels(keyedItem)
}
})
I had to solve this problem as well (fingers crossed that this gets included into the library) and my use case is simpler than the first answer.
function convertIdsToNumbers(o) {
Object.keys(o).forEach((k) => {
const v = o[k]
if (Array.isArray(v) || v instanceof Object) convertIdsToNumbers(v)
if (k === 'id' || /.*Id$/.test(k)) {
o[k] = Number(v)
}
})
}
const ApplicationSerializer = RestSerializer.extend({
root: false,
embed: true,
serialize(object, request) {
let json = Serializer.prototype.serialize.apply(this, arguments)
convertIdsToNumbers(json)
return {
status: request.status,
payload: json,
}
},
})

React class component methods: is my code imperative?

I'm new to react and as well to the terms of functional, imperative, declarative. And I get to know that pure function is easy to test. I am self taught to program with Javascript. So far, it is working but my goal is to learn to write clean and maintainable code.
my question is the method addProductToSaleList below is bad and untestable because it is imperative? and how can I do it differently.
class SaleComponent extends React.Component {
addProductToSaleList = (values, dispatch, props) => {
//filter product from productList
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
if (productFound) {
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.filter(detail => {
if (productFound.name === detail.product) {
return detail
}
return undefined
})[0]
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
} else {
alert('The product code you add is not exist in product list');
}
}
render() {
// Render saleList
}
}
I belive this question should go to Code Review, but I will give it a shot. Part of the code can be improved
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
First, filter function receives a callback and for each item that callback will be executed. If the callback returns a value interpreted as true, it will return the item in the new array the function will build. Otherwise, it will skip that item. Assuming you're trying to find one item in the code, you could use the function find which will return you that element directly (no need for [0]), or undefined if that item is not found. So your code could be rewrite to
const productFound = props.productList.find(product => values.productCode === product.code.toString());
Note: No IE support.
Then, if the value was not found, you could just alert and do an early return. (You might also want to handle errors differently, with a better format than plain alert).
The code would look like
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// rest of the function
in order to find details, you can use find method as well
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
and then just call the rest of the code
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
Another improvement:
You're receiving a dispatch function as a parameter, but you're not using it. So you could remove it from function's declaration
(values, props) => { ... }
And you could split the last part into two different functions, something like
const getAction = details => `${detailFound ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = detailFound;
return {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
};
}
and then just call
props.dispatcher(getAction(details), getObject(details, productFound));
The end result would look like
addProductToSaleList = (values, props) => {
//filter product from productList
const productFound = props.productList.find(product => values.productCode === product.code.toString());
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
const getAction = details => `${details ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = details;
return {
...rest,
qty: parseInt(details.qty, 10) + 1
};
}
props.dispatcher(getAction(details), getObject(details, productFound));
}
my question is the method addProductToSaleList below is bad and
untestable because it is imperative
Well your code is testable, there are no external dependencies. So you could pass mocked values and props and add unit tests to that. That means, passing a fake values and props (they are just plain js object) and make assertions over that.
For instance:
You could mock dispatcher function and given the fake values in productList and saleItem.details you could see if dispatcher is called with the proper values. You should test different combinations of that
Mock alert function (Again, I would use another UI approach) and verify it is called, and that no other code is called (asserting that your fake dispatcher is not called). Something like this:
let actionToAssert;
let objectToAssert;
let values = { productCode: 'somecode' };
let props = {
productList: // your item listm with id and price, name, etc,
saleItem: {
details: // your details array here
}
dispatcher: (action, newObject) => {
actionToAssert = action;
objectToAssert = newObject;
}
}
addProductToSaleList(values, props); // make here assertions over actionToAssert and objectToAssert

Categories