RXJS filter on another observable - javascript

In my code I am getting a list of users from MS Graph, but this lists all users. I want to emit only those users that are a member of a specified group. So I have set up a filter:
private loadUsers() {
console.log(`Loading users...`);
this.graph.get$(`users`)
.map(resp => resp.json().value)
.flatMap(user => user)
.do(user => {
console.log(`Got user: ${user['displayName']}`);
})
.filter(user => {
// we only want users who are members of the TrackerContact group
const userId: string = user['id'];
console.log(`Checking user ${userId}`);
return this.userIsTrackerContact(user); // <<= PROBLEM HERE!
})
.subscribe(_ => {
console.log(`Completed user fetch!`);
});
}
The problem is in order to find out what groups a user is a member of, I must make another REST call back to MS Graph:
private userIsTrackerContact(user): Observable<boolean> {
// get the groups this user is a member of
return this.graph.get$(`users/${user['id']}/memberOf`)
.map(resp => resp.json().value)
.do(groups => {
console.log(`${user['displayName']} is a member of ${groups.length} groups:`);
groups.forEach((group, idx) => {
console.log(` [${idx+1}]: ${group['displayName']}`);
})
})
.map(groups => {
const found = groups.find((group) => group['id'] === this.authConfig.azureGroups.trackerContact)
return !isUndefined(found);
})
}
It does not seem to me that it's possible to filter an Observable based on another observable. The predicate test must be a simple true/false test.
How can I filter an Observable based on the results of another Observable?

.concatAll() might be better:
this.graph.get$(`users`)
.map(resp => resp.json().value)
.flatMap(user => user)
.do(user => {
console.log(`Got user: ${user['displayName']}`);
})
.filter(user => {
// we only want users who are members of the TrackerContact group
const userId: string = user['id'];
console.log(`Checking user ${userId}`);
return this.userIsTrackerContact(user); // <<= PROBLEM HERE!
})
.concatAll()
.subscribe(...)
I believe you should be able to merge the async observables then filter.
Rx.Observable.merge(this.graph.get$(`users`)
.map(resp => resp.json().value)
.flatMap(user => user)
.do(user => {
console.log(`Got user: ${user['displayName']}`);
})
.map(user => {
// we only want users who are members of the TrackerContact group
const userId: string = user['id'];
console.log(`Checking user ${userId}`);
return this.userIsTrackerContact(user); // <<= PROBLEM HERE!
}))
.filter(x => x)
.subscribe(_ => console.info('success'));
You can change your predicate set a property on user isGroupMember. Then filter(x => x.isGroupMember). Then you have the user objects in the subscribe method.

Related

How to define if #mentioned is subscriber? TMI.js

Trying to make little bot for my friend's twitch and want to make it define if "#mentioned" is a sub.
I decided to test if it define mods first but it always says "2"
client.on("chat", function (channel, user, message, self) {
if (message.toLowerCase().startsWith("!love")) {
var loved = message.split(' ')[1];
if (loved.mod || loved['user-type'] === 'mod'){
client.action(channel, "1 ");  //says sth
      }
else {
client.action(channel, "2 ");//says sth different
      }
}
user.mod
Is only available for user objects that are returned by the message handler. If you want to check if a mentioned user is a mod you have to request the chat list
function getAllChatters(channel, _attempts) {
return rp({
uri: `https://tmi.twitch.tv/group/user/${channel.replace('#','')}/chatters`,
json: true
})
.then(data => {
return Object.entries(data.chatters)
.reduce((p, [ type, list ]) => p.concat(list.map(name => {
return {name, type};
})), []);
})
.catch(err => {
if(_attempts < 3) {
return getChatters(channel, _attempts + 1);
}
throw err;
})
}
Then filter it for only mods
function getModsOnline(channel) {
return getAllChatters(channel)
.then(data => {
var mods = data.filter(chatter => chatter.type == 'moderators' || chatter.type == 'broadcaster').map(chatter => chatter.name);
return mods;
})
}
And then see if the user is in the list
function isModOnline(channel, username) {
// This will only return true if the mod is online in chat aka in the chat list
return getMods(channel)
.then(mods => {
return mods.includes(username);
})
}
Keep in mind that
This is for getting mods, not subscribers
It will only work for mods that are currently in chat, that list tends to update rather slowly
You need to work with promises here so
const rp = require('request-promise');
and to use the value
```
isModOnline(channel, username)
.then(value => {
// Code to work with the value here
console.log(`User is mod: ${value}`);
})

Is there a way to use a observable returning function for each element of another observable array?

I get an Observable<Group[]> from my Firebase collection.
In this Group class is an id which I wanna use to retrieve another dataset array from Firebase, which would be messages for each unique group Observable<Message[]>.(each group has its own chat: Message[])
And it want to return an observable which hold an array of a new Type:
return { ...group, messages: Message[] } as GroupWithMessages
the final goal should be Observable<GroupWithMessages[]>
getGroupWithChat(): Observable<GroupWithMessages[]> {
const groupColl = this.getGroups(); // Observable<Group[]>
const messages = groupColl.pipe(
map(groups => {
return groups.map(meet => {
const messages = this.getMessagesFor(group.uid);
return { messages:messages, ...group} as GroupWithMessages
});
})
);
return messages;
}
}
and here the Message function
getMessagesFor(id: string): Observable<Message[]> {
return this.afs.collection<Message>(`meets/${id} /messages`).valueChanges();
}
sadly that doesnt work because when i create the new Obj I cannot bind messages:messages because messages ist vom typ Observable<Message[]>
I hope that cleares things
UPDATE:
my main problem now comes down to this:
getGroupsWithMessages() {
this.getJoinedGroups()
.pipe(
mergeMap(groups =>
from(groups).pipe(
mergeMap(group => {
return this.getMessagesFor(group.uid).pipe(
map(messages => {
return { ...group, messages } as GroupIdMess;
})
);
}),
tap(x => console.log('reaching here: ', x)),
toArray(),
tap(x => console.log('not reaching here = completed: ', x))
)
),
tap(x => console.log('not reaching here: ', x))
)
.subscribe(x => console.log('not reaching here: ', x));
}
when i call that function my console.log is as follows:
Not sure if I follow what you're doing here but the logic look like you'd want:
getGroupWithChat() {
return this.getGroups.pipe(map(groups=> {
return groups.map(group => this.getMessagesFor(group.uid));
})).subscribe(); // trigger "hot" observable
}
Let me know if I can help further after you clarify.
UPDATE:
So it looks like you need to get the UID of the group before making the call to get the GroupMessages[]?
get Group: Observable
call getMessagesFor(Group.uid)
this example gets groups result$ then
concatMap uses groups result$ to make the messages query
this.getGroups().pipe(
concatMap((group: Group) => this.getMessagesFor(group.uid))
).subscribe((messages: GroupWithMessages[]) => {
console.log(messages);
});
You may still want to map them together but it seems like you know how to do that. concatMap waits for the first to finish, then makes the second call which you need.
Is this closer?
Use forkJoin to wait for messages to be received for all groups. Then map the result of forkJoin to an array of GroupWithMessages like this -
getGroupWithChat(): Observable<GroupWithMessages[]> {
return this.getGroups()
.pipe(
switchMap(groups => {
const messagesForAllGroups$ = groups.map(group => this.getMessagesFor(group.uid));
return forkJoin(messagesForAllGroups$)
.pipe(
map(joined => {
//joined has response like -
//[messagesArrayForGroup0, messagesArrayForGroup1, messagesArrayForGroup2....];
const messagesByGroup = Array<GroupWithMessages>();
groups.forEach((group, index) => {
//assuming that GroupWithMessages has group and messages properties.
const gm = new GroupWithMessages();
gm.group = group;
gm.messages = joined[index];
messagesByGroup.push(gm);
});
return messagesByGroup;
})
)
})
)
}
I usually do that by splitting Observable<any[]> to Observable<any> and then mergeMap the results to inner Observable.
Something like this should work:
getMessagesFor(id: string): Observable<number> {
return of(1);
}
getGroups(): Observable<string[]> {
return of(["1", "2"]);
}
getGroupWithChat() {
this.getGroups().pipe(
mergeMap(groups => from(groups)), // Split the stream into individual group elements instead of an array
mergeMap(group => {
return this.getMessagesFor(group).pipe(
map(messages => {
return Object.assign(group, messages);
})
);
})
);
}
Edit:
Consider BehaviorSubject. It doesn't complete at all:
const behSub: BehaviorSubject<number[]> = new BehaviorSubject([1, 2, 3]);
setTimeout(() => {
behSub.next([4, 5, 6]);
}, 5000);
behSub
.pipe(
mergeMap(arr =>
from(arr).pipe(
tap(), // Do something with individual items, like mergeMap to messages
toArray() // Go back to array
)
)
)
.subscribe(console.log, null, () => {
console.log('Complete');
});

Sequelize Update Returning Before and After Results

I'm trying to create an Update API route using Sequelize that will:
Capture the record before the update (beforeRec)
Perform the update
Capture the updated record (updatedRec)
Return both the beforeRec and updatedRec
I'm having trouble with my promise chain, which is executing the before and after select queries before executing the update. I've tried several different ways of chaining and capturing results, but here's the latest code:
router.put('/:id', (req, res) => {
const pk = req.params.id;
const getBeforeRec = Master.findByPk(pk)
.then(rec => {return rec})
const updateRec = getBeforeRec
.then(
Master.update(
req.body,
{ where: {id: pk} }
)
)
const getUpdatedRec = updateRec
.then(
Master.findByPk(pk)
.then(rec => {return rec})
);
return Promise.all([getBeforeRec, updateRec, getUpdatedRec])
.then( ([beforeRec, updateRes, afterRec]) => {
return res.json({beforeRec, afterRec})
})
.catch(err => {
return res.status(400).json({'error': err});
});
});
Here's a sanitized example of how the results look:
{
"beforeRec": {
"id": 100,
"updated_col_name": false,
},
"afterRec": {
"id": 100,
"updated_col_name": false,
}
}
In the console, I can see that the update is executing last:
Executing (default): SELECT [id], [updated_col_name] FROM [master] WHERE [master].[id] = N'100';
Executing (default): SELECT [id], [updated_col_name] FROM [master] WHERE [master].[id] = N'100';
Executing (default): UPDATE [master] SET [updated_col_name]=1 WHERE [id] = N'106'
What's the best way to make the second select statement wait for the update?
Any help in clarifying how to chain promises while capturing results along the way will be greatly appreciated! Thanks.
After trying a number of ways, it finally works with nesting:
router.put('/:id', (req, res) => {
const pk = req.params.id;
let beforeRec;
Master.findByPk(pk)
.then(rec => { beforeRec = rec; })
.then(() => {
Master.update(
req.body,
{ where: {id: pk} }
)
.then(() => {
Master.findByPk(pk)
.then(rec => { return rec; })
.then((afterRec) => {
return res.json({beforeRec, afterRec})
})
})
})
.catch(err => {
return res.status(400).json({'error': err});
});
});
If I don't nest the second Master.findByPk, then Master.update() ends up executing last. Also, while I can set beforeRec outside of the promise chain, it didn't work for afterRec.
I don't love it, since I'm still confused by promises, but it's returning the desired results. However, with this nesting mess, I'm not sure where the catch() belongs. Will it catch errors within the nested then()s? Only further testing will tell.
You can do that with , previous method of the instance that returned by update query :
Master.update( req.body , { where: {id: pk} }).then(master => {
console.log(master.get()); // <---- Will give you latest values
console.log(master.previous()); // <---- returns the previous values for all values which have changed
})
For More Detail :
http://docs.sequelizejs.com/class/lib/model.js~Model.html#instance-method-previous
https://github.com/sequelize/sequelize/issues/1814
Give this a shot:
router.put('/:id', (req, res) => {
const pk = req.params.id;
let beforeRec, afterRec;
Master.findByPk(pk)
.then(rec => { beforeRec = rec; })
.then(() => {
Master.update(
req.body,
{ where: {id: pk} }
)
})
.then(() => {
Master.findByPk(pk)
.then(rec => { afterRec = rec; })
})
.then(() => {
res.json({beforeRec, afterRec})
})
.catch(errror => {
res.status(400).json({error});
});
});
Resurrecting an old question to help people in the future...
I've been using sequelize v6 with MySQL. I can't speak to other variances but assuming you just want the snapshot of the "previous" values, you can use the following method to create a copy the properties and their values before updating them
// then catch method
router.put('/:id', (req, res) => {
const pk = req.params.id;
let beforeRecord;
const updateRec = Master.findByPk(pk).then(rec => {
// .get() method is synchronous
beforeRecord = rec.get({ plain: true });
// calling .update on the model instance will also
// call .reload on the instance as well.
// Same thing happens when calling .save on the instance
return rec.update(req.body);
});
updateRec.then(rec => {
const afterRec = rec.get({ plain: true });
return res.json({beforeRec, afterRec})
}).catch(err => {
return res.status(400).json({'error': err});
});
});
// Async await method
router.put('/:id', async (req, res) => {
const pk = req.params.id;
try {
/** #type{import('sequelize').Model} */ // rec equals a sequelize model instance
const rec = await Master.findByPk(pk)
// .get() method is synchronous and returns an object (NOT a sequelize model instance)
const beforeRecord = rec.get({ plain: true });
// calling .update on the model instance will also
// call .reload on the instance as well.
// Same thing happens when calling .save on the instance
await rec.update(req.body); // after this call, rec contains the new updated values
const afterRec = rec.get({ plain: true });
return res.json({beforeRec, afterRec})
} catch (err) {
return res.status(400).json({'error': err});
}
});

javascript array contains values called empty

Not sure what is going on but when I update my store state by deleting items, it's saying that the items are empty.
see screenshot
and therefore the array is still of the same length?
these are my functions:
export const deleteUserFromFirebase = ({ id, name, username, email }) => {
return firebase
.database()
.ref("/users/" + id)
.set({
id: null
});
};
export const deleteUserFromStoreThenUpdateFirebase = user => {
return (dispatch, getState) => {
return dispatch(deleteUser(user)).then(() => {
return deleteUserFromFirebase(user);
});
};
};
export const deleteUser = user => {
return async dispatch => {
dispatch({ type: DELETE_USER, user: user });
};
};
and I call deleteUserFromStoreThenUpdateFirebase from a button click
why would it be doing this??
also my reducer does this:
case DELETE_USER: {
return {
...state,
users: state.users.filter(u => u.id !== action.user.id)
};
}
well confused right now
also firebase is showing the correct amount of nodes and when i refresh it's suppose to be pulling from firebase but it pulls the empty ones too

how to create only one subscribe() of this code

I've made this code that do its work, using RxJS:
from(dateQuery.first())
.subscribe((result) => {
query.greaterThanOrEqualTo('createdAt', result.createdAt);
const dateFormat = moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss');
from(query.find())
.map(el => el.map((e) => {
return {
id: e.id,
username: e.get('username')
}
}))
.mergeMap((array) => Observable.of({
names: array,
lastUpdate: dateFormat
}))
.subscribe(
(next) => res.send(serialize(next)),
(error) => res.send(serialize(error)),
() => console.log('completed'));
},
(error) => console.log(error)
);
My question: is it possible to create only one subscription instead of this two? Because I need to use result of the first subscription also in the other mergeMap before and I don't know how to store it if I try to do only one subscription.
Thank you for help.
You can do something like that
from(dateQuery.first()).pipe(
tap(() => query.greaterThanOrEqualTo('createdAt', result.createdAt)),
map(result => moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss')),
mergeMap(dateFormat => from(query.find()).pipe(
map(el => el.map((e) => ({
id: e.id,
username: e.get('username')
}))),
mergeMap((array) => Observable.of({
names: array,
lastUpdate: dateFormat
})),
tap(() => res.send(serialize(next))),
catchError(error => of('Find error'))
))
).subscribe();
Here's my first idea from what I understood.
I have exported the first subscription in an observable and I reuse it to construct the second observable (find$) where I do the second subscription.
I don't know what query.greaterThanOrEqualTo('createdAt', result.createdAt) does, so I put it in a .do() operator as a side effect. If you need to execute this method before executing the remaining code, return an observable or a promise from it and use a mergeMap as well.
const dataQueryFirst$ = dateQuery.first();
const find$ = dataQueryFirst$
.do((result) => query.greaterThanOrEqualTo('createdAt', result.createdAt))
.mergeMap(result => {
const dateFormat = moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss');
return from(query.find())
.map(el => el.map((e) => {
return {
id: e.id,
username: e.get('username')
}}))
.mergeMap((array) => Observable.of({
names: array,
lastUpdate: dateFormat
}))
});
find$.subscribe(
(next) => res.send(serialize(next)),
(error) => res.send(serialize(error)),
() => console.log('completed')
);
I would implement it with a "switchMap" .
Plus i would extract some logic in functions to make it more readable. And i prefere the new "pipe" way of rxjs since v5.5.
from(dateQuery.first()).pipe(
// TAP because it seems to be a sideeffect. If this could stop your stream, "filter" would be appropriate
tap(result =>query.greaterThanOrEqualTo('createdAt', result.createdAt)),
// MAP because we can discard "result" and only need the Moment-Object in the rest of the stream
map(result => getMoment(result.createdAt),
// switchMap to switch to the second stream, because it is only one command (in a long pipe) you don´t need a explicit return-command
switchMap( createdAt =>
from(query.find()).pipe(
// MAP because we now want a UserObjects-Array in our stream
map(el => createUserObjects(el)),
// MAP because we want our NamesUpdateList. Depending on the usage you may not need "Observable.of()" here
map(userObjects => createNamesUpdateList(userObjects, createdAt ))
)
)
).subscribe(
(next) => res.send(serialize(next)),
(error) => res.send(serialize(error)),
() => console.log('completed')
)
getMoment(date){
return moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss')
}
createUserObjects(el){
el.map((e) => {
return {
id: e.id,
username: e.get('username')
}
}
}
createNamesUpdateList(array, dateFormat){
return {
names: array,
lastUpdate: dateFormat
}
}
warm regards

Categories