Update an array of objects with or without mutation dynamically - javascript

I am currently working a todo list project using plain JavaScript and have to create a function that updates a projects title,description, priority, etc. Is there a way to update the array of objects with or without mutation ?
An example would be something along the lines of
project1.tasks[2].title = newTitle
// Project Manager object
const PM = {
// Empty projects array
projects: [],
// Project method pushing to projects array
addProject(project) {
this.projects.push(project)
},
deleteProject(project) {
const projectIndex = this.projects.indexOf(project)
if (projectIndex !== -1) {
this.projects.splice(projectIndex, 1)
return true
}
return false
},
updateProject() {
// Update projects title, description, priority
}
}
const projectFactory = (title) => ({
title,
tasks: [],
addTask(task) {
this.tasks.push(task)
},
deleteTask(task) {
const taskIndex = this.tasks.indexOf(task)
if (taskIndex !== -1) {
this.tasks.splice(taskIndex, 1)
return true
}
return false
}
})
const createProject = (title) => {
const project = projectFactory(title)
return project
}
const taskFactory = (title, description, dueDate, priority) => ({ title, description, dueDate, priority })
const createTask = (title, description, dueDate, priority) => {
const task = taskFactory(title, description, dueDate, priority)
return task`
}
const project1 = projectFactory("Clean")
const task1 = taskFactory("Clean kitchen", "Wash dishes", "1/19/2023", "Medium")
PM.addProject(project1)
project1.addTask(task1)
I have tried to follow along on similiar problems that people have come across on StackOverflow but they're using ids or their todo list is made in React.

Related

How to query a firestore search for a name within a document?

What i have set up for my firestore database is one collection called 'funkoPops'. That has documents that are genres of funkoPops, with an array of funkoData that holds all pops for that genre. it looks like this below
I should also note, that the collection funkoPops has hundreds of documents of 'genres' which is basically the funko pop series with the sub collections of funkoData that I web scraped and now need to be able to search through the array field of 'funkoData' to match the name field with the given search parameter.
collection: funkoPops => document: 2014 Funko Pop Marvel Thor Series => fields: funkoData: [
{
image: "string to hold image",
name: "Loki - with helmet",
number: "36"
},
{
image: "string to hold image",
name: "Black and White Loki with Helmet - hot topic exsclusive",
number: "36"
},
{
etc...
}
So how could i run a query in firestore to be able to search in collection('funkoPops'), search through the document fields for name.
I have the ability to search for genres like so, which gives the genre back and the document with the array of data below:
const getFunkoPopGenre = async (req, res, next) => {
try {
console.log(req.params);
const genre = req.params.genre;
const funkoPop = await firestore.collection("funkoPops").doc(genre);
const data = await funkoPop.get();
if (!data.exists) {
res.status(404).send("No Funko Pop found with that search parameter");
} else {
res.send(data.data());
}
} catch (error) {
res.status(400).send(error.message);
}
};
what i am trying to use to search by the field name is below and returns an empty obj:
const getFunkoPopName = async (req, res, next) => {
try {
const name = req.params.name;
console.log({ name });
const funkoPop = await firestore
.collection("funkoPops")
.whereEqualTo("genre", name);
const data = await funkoPop.get();
console.log(data);
res.send(data.data());
} catch (error) {
res.status(400).send(error);
}
};
Any help would be great, thanks!
So the way i went about answering this as it seems from top comment and researching a little more on firebase, you do you have to match a full string to search using firebase queries. Instead, I query all docs in the collection, add that to an array and then forEach() each funkoData. From there i then create a matchArray and go forEach() thru the new funkoData array i got from the first query. Then inside that forEach() I have a new variable in matches which is filter of the array of data, to match up the data field name with .inlcudes(search param) and then push all the matches into the matchArr and res.send(matchArr). Works for partial of the string as well as .includes() matches full and substring. Not sure if that is the best and most efficient way but I am able to query thru over probably 20k data in 1-2 seconds and find all the matches. Code looks like this
try {
const query = req.params.name.trim().toLowerCase();
console.log({ query });
const funkoPops = await firestore.collection("test");
const data = await funkoPops.get();
const funkoArray = [];
if (data.empty) {
res.status(404).send("No Funko Pop records found");
} else {
data.forEach((doc) => {
const funkoObj = new FunkoPop(doc.data().genre, doc.data().funkoData);
funkoArray.push(funkoObj);
});
const matchArr = [];
funkoArray.forEach((funko) => {
const genre = funko.genre;
const funkoData = funko.funkoData;
const matches = funkoData.filter((data) =>
data.name.toLowerCase().includes(query)
);
if (Object.keys(matches).length > 0) {
matchArr.push({
matches,
genre,
});
}
});
if (matchArr.length === 0) {
res.status(404).send(`No Funko Pops found for search: ${query}`);
} else {
res.send(matchArr);
}
}
} catch (error) {
res.status(400).send(error.message);
}
with a little bit of tweaking, i am able to search for any field in my database and match it with full string and substring as well.
update
ended up just combining genre, name, and number searches into one function so that whenver someone searches, the query param is used for all 3 searches at once and will give back data on all 3 searches as an object so that we can do whatever we like in front end:
const getFunkoPopQuery = async (req, res) => {
try {
console.log(req.params);
const query = req.params.query.trim().toLowerCase();
const funkoPops = await firestore.collection("test");
const data = await funkoPops.get();
const funkoArr = [];
if (data.empty) {
res.status(404).send("No Funko Pop records exsist");
} else {
data.forEach((doc) => {
const funkoObj = new FunkoPop(doc.data().genre, doc.data().funkoData);
funkoArr.push(funkoObj);
});
// genre matching if query is not a number
let genreMatches = [];
if (isNaN(query)) {
genreMatches = funkoArr.filter((funko) =>
funko.genre.toLowerCase().includes(query)
);
}
if (genreMatches.length === 0) {
genreMatches = `No funko pop genres with search: ${query}`;
}
// name & number matching
const objToSearch = {
notNullNameArr: [],
notNullNumbArr: [],
nameMatches: [],
numbMatches: [],
};
funkoArr.forEach((funko) => {
const genre = funko.genre;
if (funko.funkoData) {
const funkoDataArr = funko.funkoData;
funkoDataArr.forEach((data) => {
if (data.name) {
objToSearch.notNullNameArr.push({
funkoData: [data],
genre: genre,
});
}
if (data.number) {
objToSearch.notNullNumbArr.push({
funkoData: [data],
genre: genre,
});
}
});
}
});
// find name that includes query
objToSearch.notNullNameArr.forEach((funko) => {
const genre = funko.genre;
const name = funko.funkoData.filter((data) =>
data.name.toLowerCase().includes(query)
);
if (Object.keys(name).length > 0) {
objToSearch.nameMatches.push({
genre,
name,
});
}
});
// find number that matches query
objToSearch.notNullNumbArr.forEach((funko) => {
const genre = funko.genre;
const number = funko.funkoData.filter((data) => data.number === query);
if (Object.keys(number).length > 0) {
objToSearch.numbMatches.push({
genre,
number,
});
}
});
if (objToSearch.nameMatches.length === 0) {
objToSearch.nameMatches = `No funko pops found with search name: ${query}`;
}
if (objToSearch.numbMatches.length === 0) {
objToSearch.numbMatches = `No funko pop numbers found with search: ${query}`;
}
const searchFinds = {
genre: genreMatches,
name: objToSearch.nameMatches,
number: objToSearch.numbMatches,
};
res.send(searchFinds);
}
} catch (error) {
res.status(400).send(error.message);
}
};
If anyone is well suited in backend and knows more about firestore querying, please let me know!

storing array state objects in asyncStorage

I want to store an array state using async storage. but everytime i reload the app, it comes up blank. below is a sample code, and I have shown only the functions for better clarity.
componentDidMount() {
this.getDataSync();
}
getDataSync = async () => {
try {
const list = await AsyncStorage.getItem(LIST_STORAGE_KEY);
const parsedList = JSON.parse(list);
const obj = Object.keys(parsedList);
this.setState({ isDataReady: true, list: obj || [] });
} catch (e) {
Alert.alert('Failed to load list.');
}
}
handleAdd() {
const { firstname, lastname, email, phone} = this.state;
const ID = uuid();
const newItemObject = {
key: ID,
firstname: firstname,
lastname: lastname,
email: email,
phone: phone,
image: null,
};
this.setState(prevState => ({
list: [...prevState.list, newItemObject]
}));
this.saveItems(this.state.list);
}
saveItems = list => {
AsyncStorage.setItem(LIST_STORAGE_KEY, JSON.stringify(list));
};
You are not saving your list but getting keys from the list. const obj = Object.keys(parsedList); you are saving array indexes to state.
getDataSync = async () => {
try {
const list = await AsyncStorage.getItem(LIST_STORAGE_KEY);
const parsedList = JSON.parse(list);
this.setState({
isDataReady: true,
list: Array.isArray(parsedList) && parsedList.length && parsedList || []
});
} catch (e) {
Alert.alert('Failed to load list.');
}
}
Also pass saveItems as a callback to save the correct data.
this.setState(prevState => ({
list: [...prevState.list, newItemObject]
}), () => this.saveItems(this.state.list));
The .setState() method is may be asynchronous, so the result of setting the state, cannot be used immediately after setting it. If you want to use the results of setting the state, you should use the callback (2nd param), which is called after the state is actually set:
this.setState(
prevState => ({
list: [...prevState.list, newItemObject]
}),
() => this.saveItems(this.state.list)
);

How to querying and aggregate multiple collection in Firebase

I need to retrieve a monthly work orders collection by project and list of users (in the month I could have some project's OdL hours not included in the users list and the users could have some OdL of other projects).
The OdL model contains the strings for project (projectCode) and user (userId).
I send to the service the dates in Timestamp format, the code of the project and an array with users list.
this.loadGantt(from: Timestamp, to: Timestamp, projectCode: string, users: string[]){...}
I tried to execute single queries, one for project code and one for single user.
These are the queries of the service:
this.docs = this.afs.collection<OdlModel>('odl', ref => {
return ref.where('projectCode', '==', project)
.where('date', '>=', from)
.where('date', '<=', to)
.orderBy('date', 'asc');
})
.snapshotChanges().pipe(map(coll => {
return coll.map(doc => ({ id: doc.payload.doc.id, ...doc.payload.doc.data()}));
}));
return this.docs;
this.docs = this.afs.collection<OdlModel>('odl', ref => {
return ref.where('userId', '==', user)
.where('date', '>=', from)
.where('date', '<=', to)
.orderBy('date', 'asc');
})
.snapshotChanges().pipe(map(coll => {
return coll.map(doc => ({ id: doc.payload.doc.id, ...doc.payload.doc.data()}));
}));
return this.docs;
This is the solution that I found:
loadGantt() {
const momentDate: Moment = moment(new Date());
this.firstDay = momentDate.startOf('month').toDate();
this.lastDay = momentDate.endOf('month').toDate();
const from = firebase.firestore.Timestamp.fromDate(this.firstDay);
const to = firebase.firestore.Timestamp.fromDate(this.lastDay);
this.odl = [];
const odlArray = [];
const a$ = this.odlService.getProjectOdlCollection(from, to, 'IGNC0954-IMI-19');
odlArray.push(a$);
this.user.forEach(u => {
const b$ = this.odlService.getUserOdlCollection(from, to, u);
odlArray.push(b$);
});
const result$ = combineLatest([odlArray]);
result$.subscribe(res => {
res.map(r => {
r.subscribe(odl => {
odl.forEach(o => {
const index = this.odl.findIndex(x => x.id === o.id);
if (index === -1) {
this.odl.push(o);
}
});
});
});
});
}
How to I get all data in a single Observable object?
Below is a snippet which shows how to combine two or more Firestore collections from AngularFire into a single Observable object.
Link here
Data Structure
const usersRef = this.afs.collection('users');
const fooPosts = usersRef.doc('userFoo').collection('posts').valueChanges();
const barPosts = usersRef.doc('userBar').collection('posts').valueChanges();
Solution
import { combineLatest } from 'rxjs/observable/combineLatest';
const combinedList = combineLatest<any[]>(fooPosts, barPosts).pipe(
map(arr => arr.reduce((acc, cur) => acc.concat(cur) ) ),
)
Also this example may also help with your issue.
Let me know if this was helpful.

Remove an array item nested inside of an object

I'm trying to remove a specific item from an objects array based on the title attribute in the array. I keep running into a problem where I can view the array item, but I'm not able to splice the item out of the array based on the parameters entered in my remove function. I'm just getting the error message back from my else statement in the function.
I've tried using find, forEach, findIndex and match that case in order to test out removing the result based on the index, or the text value of the key 'text'. I commented out all of the functions I tried prior to searching for the answer in the forum recommendations. All of my recipe functions are working, along with my createIngredient function, which adds an object to the recipe array. But the removeIngredient function I've been trying to get to work, isn't because of the problems mentioned above.
let recipes = []
// Read existing recipes from localStorage
const loadRecipes = () => {
const recipesJSON = localStorage.getItem('recipes')
try {
return recipesJSON ? JSON.parse(recipesJSON) : []
} catch (e) {
return []
}
}
// Expose recipes from module
const getRecipes = () => recipes
const createRecipe = () => {
const id = uuidv4()
const timestamp = moment().valueOf()
recipes.push({
id: id,
title: '',
body: '',
createdAt: timestamp,
updatedAt: timestamp,
ingredient: []
})
saveRecipes()
return id
}
// Save the recipes to localStorage
const saveRecipes = () => {
localStorage.setItem('recipes', JSON.stringify(recipes))
}
// Remove a recipe from the list
const removeRecipe = (id) => {
const recipeIndex = recipes.findIndex((recipe) => recipe.id === id)
if (recipeIndex > -1) {
recipes.splice(recipeIndex, 1)
saveRecipes()
}
}
// Remove all recipes from the recipe array
const cleanSlate = () => {
recipes = []
saveRecipes()
}
const updateRecipe = (id, updates) => {
const recipe = recipes.find((recipe) => recipe.id === id)
if (!recipe) {
return
}
if (typeof updates.title === 'string') {
recipe.title = updates.title
recipe.updatedAt = moment().valueOf()
}
if (typeof updates.body === 'string') {
recipe.body = updates.body
recipe.updateAt = moment().valueOf()
}
saveRecipes()
return recipe
}
const createIngredient = (id, text) => {
const recipe = recipes.find((recipe) => recipe.id === id)
const newItem = {
text,
have: false
}
recipe.ingredient.push(newItem)
saveRecipes()
}
const removeIngredient = (id) => {
const ingredient = recipes.find((recipe) => recipe.id === id)
console.log(ingredient)
const allIngredients = ingredient.todo.forEach((ingredient) => console.log(ingredient.text))
// const recipeIndex = recipes.find((recipe) => recipe.id === id)
// for (let text of recipeIndex) {
// console.log(recipdeIndex[text])
// }
// Attempt 3
// if (indexOfIngredient === 0) {
// ingredientIndex.splice(index, 1)
// saveRecipes()
// } else {
// console.log('error')
// }
// Attempt 2
// const recipe = recipes.find((recipe) => recipe.id === id)
// const ingredients = recipe.todo
// // let newItem = ingredients.forEach((item) => item)
// if (ingredients.text === 'breadcrumbs') {
// ingredients.splice(ingredients, 1)
// saveRecipes()
// }
// Attempt 1
// const ingredientName = ingredients.forEach((ingredient, index, array) => console.log(ingredient, index, array))
// console.log(ingredientName)
// const recipeIndex = recipes.findIndex((recipe) => recipe.id === id)
// if (recipeIndex > -1) {
// recipes.splice(recipeIndex, 1)
// saveRecipes()
// }
}
recipes = loadRecipes()
OUTPUT
{id: "ef88e013-9510-4b0e-927f-b9a8fc623450", title: "Spaghetti", body: "", createdAt: 1546878594784, updatedAt: 1546878608896, …}
recipes.js:94 breadcrumbs
recipes.js:94 noodles
recipes.js:94 marinara
recipes.js:94 meat
recipes.js:94 ground beef
recipes.js:94 milk
So I'm able to view the output I printed above and see each item in the ingredients array, but trying to splice the item based on the index number or key is not working for me with the functions I have already tried and the info I have found on Stackoverflow about objects, arrays and the splice method so far.
If I am understanding correctly (after reading the commented out attempts in your code), you are trying to remove the "breadcrumbs" ingredient from the recipe that corresponds to the id passed to the removeIngredient() function.
In that case, perhaps you could take a slightly different approach to removing the ingredient from the recipes todo array, via the Array#filter method?
You could use filter() in the following way to "filter out" (ie remove) the "breadcrumbs" ingredient from the todo array via the following filter logic:
// Keep any ingredients that do not match ingredient (ie if ingredient
// equals "breadcrumbs")
todo.filter(todoIngredient => todoIngredient !== ingredient)
You might consider revising your removeIngredient() function by;
adding an additional ingredient parameter to the function arguments. This allows you to specify the ingredient to be removed from the recipe corresponding to recipeId
and, introducing the filter() idea as described:
const removeIngredient = (recipeId, ingredient) => {
const recipe = recipes.find(recipe => recipe.id === recipeId)
if(recipe) {
// Filter recipe.todo by ingredients that do not match
// ingredient argument, and reassign the filtered array
// back to the recipie object we're working with
recipe.todo = recipe.todo.filter(todoIngredient =>
(todoIngredient !== ingredient));
}
}
Now, when you introduce the "remove" button for each ingredient, you would call the removeIngredient() as follows:
var recipeId = /* got id from somewhere */
var ingredientText = /* got ingredient from somewhere */
removeIngredient( recipeId, ingredientText );
Hope this helps!

Cleaning Unwanted Fields From GraphQL Responses

I have an object that my GraphQL client requests.
It's a reasonably simple object:
type Element {
content: [ElementContent]
elementId: String
name: String
notes: String
type: String
createdAt: String
updatedAt: String
}
With the special type ElementContent, which is tiny and looks like this:
type ElementContent {
content: String
locale: String
}
Now, when I query this on the clientside, both the top level object and the lower level object has additional properties (which interfere with updating the object if I attempt to clone the body exactly-as-is);
Notably, GraphQL seems to supply a __typename property in the parent object, and in the child objects, they have typename and a Symbol(id) property as well.
I'd love to copy this object to state, update in state, then clone the state and ship it to my update mutation. However, I get roadblocked because of unknown properties that GraphQL itself supplies.
I've tried doing:
delete element.__typename to good effect, but then I also need to loop through the children (a dynamic array of objects), and likely have to remove those properties as well.
I'm not sure if I'm missing something during this equation, or I should just struggle through the code and loop + delete (I received errors attempting to do a forEach loop initially). Is there a better strategy for what I'm attempting to do? Or am I on the right path and just need some good loop code to clean unwanted properties?
There are three ways of doing this
First way
Update the client parameter like this it will omit the unwanted fields in graphql.
apollo.create({
link: http,
cache: new InMemoryCache({
addTypename: false
})
});
Second Way
By using the omit-deep package and use it as a middleware
const cleanTypeName = new ApolloLink((operation, forward) => {
if (operation.variables) {
operation.variables = omitDeep(operation.variables,'__typename')
}
return forward(operation).map((data) => {
return data;
});
});
Third Way
Creating a custom middleware and inject in the apollo
const cleanTypeName = new ApolloLink((operation, forward) => {
if (operation.variables) {
const omitTypename = (key, value) => (key === '__typename' ? undefined : value);
operation.variables = JSON.parse(JSON.stringify(operation.variables), omitTypename);
}
return forward(operation).map((data) => {
return data;
});
});
and inject the middleware
const httpLinkWithErrorHandling = ApolloLink.from([
cleanTypeName,
retry,
error,
http,
]);
If you use fragments with the queries/mutations Second Way & Third Way is recommended.
Preferred method is Third Way Because it does not have any third pary dependency and no cache performance issues
If you want to wipe up __typename from GraphQL response (from the root and its children), you can use graphql-anywhere package.
Something like:
const wipedData = filter(inputFragment, rcvData);
inputFragment is a fragment defines the fields (You can see details here)
rcvData is the received data from GraphQL query
By using the filter function, the wipedData includes only required fields you need to pass as mutation input.
Here's what I did, to support file uploads as well. It's a merge of multiple suggestions I found on the Github thread here: Feature idea: Automatically remove __typename from mutations
import { parse, stringify } from 'flatted';
const cleanTypename = new ApolloLink((operation, forward) => {
const omitTypename = (key, value) => (key === '__typename' ? undefined : value);
if ((operation.variables && !operation.getContext().hasUpload)) {
operation.variables = parse(stringify(operation.variables), omitTypename);
}
return forward(operation);
});
Hooking up the rest of my client.tsx file, simplified:
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createUploadLink } from 'apollo-upload-client';
import { ApolloClient } from 'apollo-client';
import { setContext } from 'apollo-link-context';
import { ApolloLink } from 'apollo-link';
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem(AUTH_TOKEN);
return {
headers: {
...headers,
authorization: token ? `Bearer ${ token }` : '',
},
};
});
const httpLink = ApolloLink.from([
cleanTypename,
authLink.concat(upLoadLink),
]);
const client = new ApolloClient({
link: httpLink,
cache,
});
export default client;
Now when I call mutations that are of type upload, I simply set the context hasUpload to true, as shown here:
UpdateStation({variables: { input: station }, context: {hasUpload: true }}).then()
For those looking for a TypeScript solution:
import cloneDeepWith from "lodash/cloneDeepWith";
export const omitTypenameDeep = (
variables: Record<string, unknown>
): Record<string, unknown> =>
cloneDeepWith(variables, (value) => {
if (value && value.__typename) {
const { __typename, ...valWithoutTypename } = value;
return valWithoutTypename;
}
return undefined;
});
const removeTypename = new ApolloLink((operation, forward) => {
const newOperation = operation;
newOperation.variables = omitTypenameDeep(newOperation.variables);
return forward(newOperation);
});
// ...
const client = new ApolloClient({
cache: new InMemoryCache(),
link: ApolloLink.from([removeTypename, httpLink]),
});
I've just published graphql-filter-fragment to help with this use case. I've wrote in a bit more detail about the CRUD use case I had that led me to this approach.
Example:
import {filterGraphQlFragment} from 'graphql-filter-fragment';
import {gql} from '#apollo/client/core';
const result = filterGraphQlFragment(
gql`
fragment museum on Museum {
name
address {
city
}
}
`,
{
__typename: 'Museum',
name: 'Museum of Popular Culture',
address: {
__typename: 'MuseumAddress',
street: '325 5th Ave N',
city: 'Seattle'
}
}
);
expect(result).toEqual({
name: 'Museum of Popular Culture',
address: {
city: 'Seattle'
}
});
Here is my solution. Vanilla JS, recursive, and does not mutate the original object:
const removeAllTypenamesNoMutate = (item) => {
if (!item) return;
const recurse = (source, obj) => {
if (!source) return;
if (Array.isArray(source)) {
for (let i = 0; i < source.length; i++) {
const item = source[i];
if (item !== undefined && item !== null) {
source[i] = recurse(item, item);
}
}
return obj;
} else if (typeof source === 'object') {
for (const key in source) {
if (key === '__typename') continue;
const property = source[key];
if (Array.isArray(property)) {
obj[key] = recurse(property, property);
} else if (!!property && typeof property === 'object') {
const { __typename, ...rest } = property;
obj[key] = recurse(rest, rest);
} else {
obj[key] = property;
}
}
const { __typename, ...rest } = obj;
return rest;
} else {
return obj;
}
};
return recurse(JSON.parse(JSON.stringify(item)), {});
};
Below approach worked for me so far in my use case.
const {
loading,
error,
data,
} = useQuery(gqlRead, {
variables: { id },
fetchPolicy: 'network-only',
onCompleted: (data) => {
const { someNestedData } = data;
const filteredData = removeTypeNameFromGQLResult(someNestedData);
//Do sth with filteredData
},
});
//in helper
export const removeTypeNameFromGQLResult = (result: Record<string, any>) => {
return JSON.parse(
JSON.stringify(result, (key, value) => {
if (key === '__typename') return;
return value;
})
);
};

Categories

Resources