Pushing an array of objects into Firebase Collection Angular 8 - arrays

I am trying to add a document into an array studyList in my users collection.
So i have a collection users where i have name, etc.. and studyList.
When i click on a button buy into a DocumentItemComponent i want to add that document into this studyList array.
My code works partially because it adds the document into the array but when i click on another document it changes the first one, it doesn't add another document.
This is my code for the adding function:
addToStudyList(user) {
const userRef: AngularFirestoreDocument<any> = this.afs.doc(`users/${user.id}`);
const data: UserInterface = {
studyList: [{
title: this.document.title,
language: this.document.language,
description: this.document.description,
cover: this.document.cover,
category: this.document.category,
field: this.document.field,
id: this.document.id,
author: this.document.userUid,
urlDocument: this.document.urlDocument
}]
}
return userRef.set(data, {merge: true});
}
Can you help me, please?
Thank you! Have a good day!

There is no direct way to update an array inside a document, but if you are using Firestore, it provides arrayUnion and arrayRemove functions which you can use for adding/removing unique items in the array.
From firestore documentation https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array :
Try this:
userRef.update({
studyList: firebase.firestore.FieldValue.arrayUnion(data)
});

This is because when you declare:
studyList: [{
title: this.document.title,
language: this.document.language,
description: this.document.description,
cover: this.document.cover,
category: this.document.category,
field: this.document.field,
id: this.document.id,
author: this.document.userUid,
urlDocument: this.document.urlDocument
}]
in this piece of code you are assigning just one object to the the studyList array which overwrites the existing array, instead you should utilize the existing user studyList array and push your new object into it, something like this:
addToStudyList(user) {
const userRef: AngularFirestoreDocument<any> = this.afs.doc(`users/${user.id}`);
user.studyList.push({
title: this.document.title,
language: this.document.language,
description: this.document.description,
cover: this.document.cover,
category: this.document.category,
field: this.document.field,
id: this.document.id,
author: this.document.userUid,
urlDocument: this.document.urlDocument
});
const data: UserInterface = {
studyList: user.studyList
}
return userRef.update(data);
}

Related

Merge and sort arrays of objects JS/TS/Svelte - conceptual understanding

The goal is to display a recent activity overview.
As an example: I would like it to display posts, comments, users.
A post, comment and user object live in its corresponding arrays. All of the objects have a timestamp (below createdAt), but also keys that the objects from the different arrays don't have. The recent activites should be sorted by the timestamp.
(Ultimately it should be sortable by different values, but first I would like to get a better general understanding behind merging and sorting arrays / objects and not making it to complicated)
I thought of somehow merging the arrays into something like an activity array, then sorting it and looping over it and conditionally output an object with its keys depending on what kind of object it is?
If someone is willing to deal with this by giving an example, it would make my day. The best thing I could imagine would be a svelte REPL that solves this scenario. Anyway I'm thankful for every hint. There probably already are good examples and resources for this (I think common) use case that I didn't find. If someone could refer to these, this would also be superb.
The example I'm intending to use to get this conceptual understanding:
const users = [
{ id: 'a', name: 'michael', createdAt: 1 },
{ id: 'b', name: 'john', createdAt: 2 },
{ id: 'c', name: 'caren', createdAt: 3 }
]
const posts = [
{ id: 'd', topic: 'food', content: 'nonomnom' createdAt: 4 },
{ id: 'e', name: 'drink', content: 'water is the best' createdAt: 5 },
{ id: 'f', name: 'sleep', content: 'i miss it' createdAt: 6 }
]
const comments = [
{ id: 'g', parent: 'd', content: 'sounds yummy' createdAt: 7 },
{ id: 'h', parent: 'e', content: 'pure life' createdAt: 8 },
{ id: 'i', parent: 'f', content: 'me too' createdAt: 9 }
]
Edit: it would have been a bit better example with more descriptive id keys, like userId and when a post and comment object contains the userId. However, the answers below make it very understandable and applicable for "real world" use cases.
This is fun to think about and it's great that you're putting thought into the architecture of the activity feed.
I'd say you're on the right track with how you're thinking of approaching it.
Think about:
How you want to model the data for use in your application
How you process that model
Then think about how you display it
You have 3 different types of data and you have an overall activity feed you want to create. Each type has createdAt in common.
There's a couple of ways you could do this:
Simply merge them all into one array and then sort by createdAt
const activities = [...users, ...posts, ...comments];
activities.sort((a,b) => b.createdAt - a.createdAt); // Sort whichever way you want
The tricky part here is when you're outputting it, you'll need a way of telling what type of object each element in the array is. For users, you can look for a name key, for posts you could look for the topic key, for comments you could look for the parent/content keys to confirm object type but this is a bit of a brittle approach.
Let's try to see if we can do better.
Give each activity object an explicit type variable.
const activities = [
...users.map((u) => ({...u, type: 'user'})),
...posts.map((u) => ({...u, type: 'post'})),
...comments.map((u) => ({...u, type: 'comment'}))
];
Now you can easily tell what any given element in the whole activities array is based on its type field.
As a bonus, this type field can also let you easily add a feature to filter the activity feed down to just certain types! And it also makes it much simpler to add new types of activities in the future.
Here's a typescript playground showing it and logging the output.
As a typesafe bonus, you can add types in typescript to reinforce the expected data types:
eg.
type User = {
type: 'user';
name: string;
} & Common;
type Post = {
type: 'post';
topic: string;
content: string;
} & Common;
type UserComment = {
type: 'comment';
parent: string;
content: string;
} & Common;
type Activity = User | Post | UserComment;
To expand on the other answers, eventually you will want to show each element also differently, while you could do this with an if block testing the type that has been added to the object, this is not very scalable as a new type of block would require at least two changes, one to add the type to the activities array and one to add this new type to the if blocks.
Instead if we change our activities array as follows:
const activities = [
...users.map((u) => ({...u, component: UserCompomnent})),
...posts.map((u) => ({...u, component: PostComponent})),
...comments.map((u) => ({...u, component: CommentComponent}))
];
where UserComponent, PostComponent and CommentComponent are the different ways of presenting this data.
Then when you loop over your data to display them, we can use svelte:component and leverage that we already defined which component should be shown:
{#each acitivities as activity}
<svelte:component this={activity.component} {...activity} />
{/each}
Here's an approach using simple 'helper classes' so that the different objects can be distinguished when displayed REPL
<script>
class User {
constructor(obj){
Object.assign(this, obj)
}
}
class Post {
constructor(obj){
Object.assign(this, obj)
}
}
class Comment {
constructor(obj){
Object.assign(this, obj)
}
}
const users = [
{ id: 'a', name: 'michael', createdAt: 1652012110220 },
{ id: 'b', name: 'john', createdAt: 1652006110121 },
{ id: 'c', name: 'caren', createdAt: 1652018110220 }
].map(user => new User(user))
const posts = [
{ id: 'd', topic: 'food', content: 'nonomnom', createdAt: 1652016900220 },
{ id: 'e', topic: 'drink', content: 'water is the best', createdAt: 1652016910220 },
{ id: 'f', topic: 'sleep', content: 'i miss it', createdAt: 1652016960220 }
].map(post => new Post(post))
const comments = [
{ id: 'g', parent: 'd', content: 'sounds yummy', createdAt: 1652116910220 },
{ id: 'h', parent: 'e', content: 'pure life', createdAt: 1652016913220 },
{ id: 'i', parent: 'f', content: 'me too', createdAt: 1652016510220 }
].map(comment => new Comment(comment))
const recentActivities = users.concat(posts).concat(comments).sort((a,b) => b.createdAt - a.createdAt)
</script>
<ul>
{#each recentActivities as activity}
<li>
{new Date(activity.createdAt).toLocaleString()} -
{#if activity instanceof User}
User - {activity.name}
{:else if activity instanceof Post}
Post - {activity.topic}
{:else if activity instanceof Comment}
Comment - {activity.content}
{/if}
</li>
{/each}
</ul>

How do I select and update an object from a larger group of objects in Recoil?

My situation is the following:
I have an array of game objects stored as an atom, each game in the array is of the same type and structure.
I have another atom which allows me to store the id of a game in the array that has been "targeted".
I have a selector which I can use to get the targeted game object by searching the array for a match between the game ids and the targeted game id I have stored.
Elsewhere in the application the game is rendered as a DOM element and calculations are made which I want to use to update the data in the game object in the global state.
It's this last step that's throwing me off. Should my selector be writable so I can update the game object? How do I do this?
This is a rough outline of the code I have:
export const gamesAtom = atom<GameData[]>({
key: 'games',
default: [
{
id: 1,
name: 'Bingo',
difficulty: 'easy',
},
{
id: 21,
name: 'Yahtzee',
difficulty: 'moderate',
},
{
id: 3,
name: 'Twister',
difficulty: 'hard',
},
],
});
export const targetGameIdAtom = atom<number | null>({
key: 'targetGameId',
default: null,
});
export const targetGameSelector = selector<GameData | undefined>({
key: 'targetGame',
get: ({ get }) => {
return get(gamesAtom).find(
(game: GameData) => game.id === get(selectedGameIdAtom)
);
},
// This is where I'm getting tripped up. Is this the place to do this? What would I write in here?
set: ({ set, get }, newValue) => {},
});
// Elsewhere in the application the data for the targetGame is pulled down and new values are provided for it. For example, perhaps I want to change the difficulty of Twister to "extreme" by sending up the newValue of {...targetGame, difficulty: 'extreme'}
Any help or being pointed in the right direction will be appreciated. Thanks!

Is there any way to .populate() a mongoDB document with mongoose and save the populated object back into the model? [duplicate]

This question already has answers here:
Mongoose populate vs object nesting
(1 answer)
MongoDB relationships: embed or reference?
(10 answers)
Closed 5 years ago.
I have a question, wondering if there is any way to persist the returned document when using the Mongoose .populate() function by saving it back to the model. Also some questions on how to structure things. Here are my schemas:
var clientSchema = new Schema({
phone: String,
email: String
},
);
var menuSchema = new Schema({
itemName: String,
itemPrice: Number,
});
var transactionSchema = new Schema ({
createdBy: { type: Schema.ObjectId, ref: 'Client'},
items: [{ type: Schema.ObjectId, ref: 'Menu' }],
});
var Menu = mongoose.model('Menu', menuSchema);
var Client = mongoose.model('Client', clientSchema);
var Transaction = mongoose.model('Transaction', transactionSchema);
When I create a new Transaction with a POST request, I populate it and return the populated Transaction as a response:
{
"_id": "5a0bde94f4434c0a604341d2",
"createdBy": {
"_id": "5a0a8a3f9c348f0998ba8c2c",
"phone": "1234567890",
"email": "some#thing.com"
},
"__v": 0,
"items": [{ Many Menu objects }]
}
However, when I query the DB again with GET, I get this:
{
"_id": "5a0bde94f4434c0a604341d2",
"createdBy": "5a0a8a3f9c348f0998ba8c2c",
"__v": 0,
"items": [Array of ObjectIds]
}
I can't use .save() because the original schema only accepts ObjectId, not an entire Object.
I noticed that when I made my schema include SubDocuments, I did not really need to use the .populate() function. I simply pushed an object into the array, and it would be there when queried.
var transactionSchema = new Schema ({
createdBy: { type: Schema.ObjectId, ref: 'Client'},
items: [menuSchema], // Sub Doc
});
MongoDB Docs say creating large mutable arrays is a bad design. I could see some transactions having 50 or 100 objects. I can see this being more of a problem if I use subDocuments because file size , but I could also imagine that doing a .populate() on an array of 100 object IDs may be expensive.
I need to update the items array in the transaction schema every time the client registers an onclick function. I need to re-render that to the client, which involves a single PUT request per click. But I have to parse that array with each click, one by one. If I'm doing a .populate() on every single item in the array...that's not great. But using subDocuments would increase the filesize of the database.
I previously had a simpler schema, but thought that passing by reference would increase the integrity of the prices being rendered. Is it better to just have an array of Objects and push into that?
var transactionSchema = new Schema ({
createdBy: { type: Schema.ObjectId, ref: 'Client'},
items: [{
name: {type: String},
price: {type: Number}
}]
});

How to combine two collections in Mongoose and sort them using createdDate

I am developing application using MEAN.js in which I have two documents as below:
var RingtoneSchema = new Schema({
language: {
label: String,
code : String
},
deliver:{
type:Boolean,
default:false
},
artists: [RingtoneArtist],
digital_booklet:{
title: {
type: String,
trim:true
},
copyright:{
type: String,
trim:true
}
}
title: {
type: String,
default: '',
required: 'Please fill Album title',
trim: true
},
created: {
type: Date,
default: Date.now
}
});
var AlbumSchema = new Schema({
language: {
label: String,
code : String
},
deliver:{
type:Boolean,
default:false
},
artists: [artist],
title: {
type: String,
default: '',
required: 'Please fill Album title',
trim: true
},
created: {
type: Date,
default: Date.now
});
I want to get all the records from both schema and order them by created date to display them in client side. I don't know how to do this using mongoose.
Any help will be appreciated.
Mongodb itself is not relational database and any kind of "join" operation is not possible. I see two ways to go:
Easy way, but not the best way:
If you, say, need to show 30 items on a page you need:
Load 30 items from Ringtone, load 30 items from AlbumSchema
Put them together in one array
Do final sort in memory.
Downside of this approach is you need to load more data into memory than needed and also perform in memory sort manually.
A better way. If you need to show data from two collections it can be a good sign to have one collection instead if two. Your two collections schema looks very similar. Later you can use sparse indexes to optimize queries which require only one of two entities.
Implementation details if you pick first way: you can load data from two queries in parallel using async or using promises and do final in memory sort using underscore
Hope this helps!

sencha touch routing extraparams kitchensink

I am trying to extend the routing in the Sencha Touch Kitchensink app as follows:
My data (in List store) are as follows:
{ category: 'fruit', str: 'tomato'},
{ category: 'fruit', str: 'green bean'},
{ category: 'vegetable', str: 'celery'},
{ category: 'vegetable', str: 'sprouts'},
{ category: 'notAVegetable', str: 'ketchup'},
{ category: 'notAVegetable', str: 'prune'}
I would like to show only those data selected by a particular category, such as "fruit"
In the Main.js controller, I am trying to do this by grabbing another parameter from the "List" node in the Demos TreeStore
routes: {
'demo/:id/:category': 'showViewById',
'menu/:id': 'showMenuById'
},
Where the showViewById action adds the extra parameter for use later
showViewById: function (id, category) {
var nav = this.getNav(),
view = nav.getStore().getNodeById(id);
console.log('view ' + id);
this.showView(view);
this.setCurrentDemo(view);
this.hideSheets();
// do stuff with category
},
I am trying to add and access 'category' as an extraParameter in my Demos.js store in the "List" tree node as follows:
{
text: 'List',
leaf: true,
id: 'list',
extraParams: {
category: 'fruit'
}
},
A few questions: Can I use an extraParameter to add this attribute to the Store? If so, how can I access it to use for my routing? I thought it would be available as metadata for my Demos store, but have not been able to access it.
Any alternatives short of creating multiple stores (one for "fruit", "vegetable", "notAVegetable," etc.) with filters on them to achieve the same thing?
TIA!

Resources