Enrich RxJS observable result array with data from another request - rxjs

Considering the following simplified data structure:
Teacher
{
id: number,
name: string
students?: Student[] // filled with an inner second request
}
Student
{
name: string
}
TeachersResult (response of the first api request with no students inside)
{
teachers: Teacher[]
pagesCount: number // metadata for the pagination
}
My main question is how use RxJS to fill the students-property for every teacher with these two api endpoints:
GET http://localhost:1337/api/teachers
GET http://localhost:1337/api/students/{teacherId}
The first idea was to start with something like this:
getTeachersWithStudents(): Observable<TeachersResult> {
return this.apiService.getTeachers().pipe(
concatMap(teachersResult => {
const studentsObservables$: Observable<Student[]>[] = [];
teachersResult.teachers.foreach(teacher =>
studentsObservables$.push(this.apiService.getStudents(teacher.id)));
// Add students to associated teacher here? Is forkJoin() the right choice?
});
);
}
It feels complicated for me to add every students-result from the second api request to the associated teacher. This is the end result for me I want to achieve:
{
teachers: [
{
id: 1,
name: 'Uncle Bob',
students: [
{ name: 'Alice' },
{ name: 'Caren' }
]
},
{
...
}
],
pagesCount: 42
}

You may try something like this
getTeachersWithStudents(): Observable<TeachersResult> {
return this.apiService.getTeachers().pipe(
// concatMap here is right since you want to continue only after upstream
// has notifies the list of teachers
concatMap(teachersResult => {
const teachers = teachersResult.teachers;
const secondCalls = teachers.map(teacher => {
return this.apiService.getStudents(teacher.id).pipe(
// set the students into the teacher object and return the teacher
// filled with the students
map(students => {
teacher.students = students;
return teacher
})
);
})
// now you have an array of Observables for the second calls, you
// can therefore use forkJoin to execute them in parallel
return forkJoin(secondCalls).pipe(
// add the page number
map(teachersWithStudents => {
return {
teachers: teachersWithStudents,
pagesCount: teachersResult.pagesCount
}
})
)
})
);
}
In this way you are executing all the calls to get the students concurrently.
If you want to limit the concurrency rate, then you can use mergeMap in a slightly more complex stream, something like this
getTeachersWithStudents(): Observable<TeachersResult> {
return this.apiService.getTeachers().pipe(
concatMap(teachersResult => {
// transform the teachers array into a stream of teachers
const teachers = teachersResult.teachers;
return from(teachers).pipe(
// here you use mergeMap with the rate of concurrency desired
// in this example I set it to 10, which means there will be at
// most 10 requests for students on flight at the same time
mergeMap(teacher => {
return this.apiService.getStudents(teacher.id).pipe(
// set the students into the teacher object and return the teacher
// filled with the students
map(students => {
teacher.students = students;
return teacher
})
)
}, 10),
// now we want to repack all the teachers in one array using toArray
toArray(),
// here we create the response desired and return it
map(teachersWithStudents => {
return {
teachers: teachersWithStudents,
pagesCount: teachersResult.pagesCount
}
})
)
})
);
}
This stackblitz shows an example of the above 2 implementations.

Related

Nested query in `#nestjs/graphql`, in more efficient `ResolverProperty` for all previous query result

I'm using #nestjs/graphql and my code is ↓
#Resolver(() => Course)
export default class CourseResolver {
#Query(() => [Course])
course(#Args('where', { nullable: true }) where: CourseSearchDto) {
return Course.find(where);
}
#ResolveProperty(() => [Profession])
async professions(#Parent() course: Course) {
return await Profession.find({ id: course.id });
}
}
When query like this below, profession method is called for each Course but I want to run just once like a left join in DB.
{
course {
key,
id,
professions{
id, key
}
}
}
Is there any solution to do this once and then join each profession for each course ??

Multiple graphql queries in Gatsby component

I need to run multiple graphQL queries within a component and within the gatsby-node.js file. (Because Prismic is limited to 20 entries per answer...🙄)
I tried the following, just to see if I could create the graphql loop in the default function:
export default () => {
async function allPosts() {
let data
await graphql(`
query allDitherImages {
prismic {
allProjects(sortBy: meta_firstPublicationDate_DESC) {
totalCount
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
edges {
node {
cover_image
cover_imageSharp {
name
}
}
}
}
}
}
`).then(initialRes => {
data = initialRes
})
return data
}
allPosts().then(result => {
console.log(result)
})
return null
}
But then Gatsby tells me that Gatsby related 'graphql' calls are supposed to only be evaluated at compile time, and then compiled away. Unfortunately, something went wrong and the query was left in the compiled code.
How can I run multiple graphql queries?
Thank you in advance :)
Michael
The gatsby-source-prismic-graphql package will create pages for all of your Prismic items (more than just the first 20), as it iterates over all items under the hood, so I'd advise looking into using that if you are looking to generate pages for all of those items.
But if you need to get all items and pass them in the pageContext or something, you'll need to do the recursion yourself in the gatsby-node.
In the gatsby-node, after you have defined the query, you can use something like this to iterate over the results and push to an array.
let documents = [];
async function getAllDocumentsRecursively (query, prop, endCursor = '') {
const results = await graphql(query, { after: endCursor })
const hasNextPage = results.data.prismic[prop].pageInfo.hasNextPage
endCursor = results.data.prismic[prop].pageInfo.endCursor
results.data.prismic[prop].edges.forEach(({node}) => {
documents.push(node)
});
if (hasNextPage) {
await getAllDocumentsRecursively(query, 'allDitherImages ', endCursor)
}
}
await getAllDocumentsRecursively(documentsQuery, 'allDitherImages ');
Then in your createPage, pass the array into the context:
createPage({
path: `/`+ node._meta.uid,
component: allDitherTempate,
context: {
documents: documents
}
})

How to implement subscriptions in graphql.js?

This is a pretty simple question.
How to implement subscriptions in graphql?
I'm asking specifically for when using graphql.js constructors like below ?
I could not find a clean/simple implementation.
There is another question here, but it deals with relay.js - i don't want to unnecessarily increase the nr of external dependencies in my app.
What i have:
module.exports = function (database){
return new GraphQLSchema(
{ query: RootQuery(database)
, mutation: RootMutation(database)
, subscription: RootSubscription(database) -- i can see this in graphiql - see below
}
);
}
function RootSubscription(database){
return new GraphQLObjectType(
{ name: "RootSubscriptionType"
, fields:
{ getCounterEvery2Seconds:
{ type: new GraphQLNonNull(GraphQLInt)
, args :
{ id: { type: GraphQLString }
}
, subscribe(parent, args, context){
// this subscribe function is never called .. why?
const iterator = simpleIterator()
return iterator
}
}
}
}
)
}
I learned that i need a subscribe() which must return an iterator from this github issue.
And here is a simple async iterator. All this iterator does - is to increase and return the counter every 2 seconds. When it reaches 10 it stops.
function simpleIterator(){
return {
[ Symbol.asyncIterator ]: () => {
let i = 0
return {
next: async function(){
i++
await delay(2000)
if(i > 10){
return { done: true }
}
return {
value: i,
done: false
}
}
}
}
}
}
When i run the graphiql subscription, it returns null for some reason:
I'm piecing together code from multiple sources - wasting time and hacking it basically. Can you help me figure this one out?
Subscriptions are such a big feature, where are they properly documented? Where is that snippet of code which you just copy paste - like queries are for example - look here.
Also, i can't use an example where the schema is separate - as a string/from a file. I already created my schema as javascript constructors. Now since im trying to add subscriptions i can't just move back to using a schema as a string. Requires rewriting the entire project. Or can i actually have both? Thanks :)

Perform sequential api calls with RxJs?

Is there a way in RxJs to perform two api calls where the second requires data from the first and return a combined result as a stream? What I'm trying to do is call the facebook API to get a list of groups and the cover image in various sizes. Facebook returns something like this:
// call to facebook /1234 to get the group 1234, cover object has an
// image in it, but only one size
{ id: '1234', cover: { id: '9999' } }
// call to facebook /9999 to get the image 9999 with an array
// with multiple sizes, omitted for simplicity
{ images: [ <image1>, <image2>, ... ] }
// desired result:
{ id: '1234', images: [ <image1>, <image2>, ... ] }
So I have this:
var result = undefined;
rxGroup = fbService.observe('/1234');
rxGroup.subscribe(group => {
rxImage = fbService.observe(`/${group.cover.id}`);
rxImage.subscribe(images => {
group.images = y;
result = group;
}
}
I want to create a method that accepts a group id and returns an Observable that will have the combined group + images (result here) in the stream. I know I can create my own observable and call the next() function in there where I set 'result' above, but I'm thinking there has to be an rx-way to do this. select/map lets me transform, but I don't know how to shoe-in the results from another call. when/and/then seems promising, but also doesn't look like it supports something like that. I could map and return an observable, but the caller would then have to do two subscribes.
Looks like flatMap is the way to go (fiddle). It is called like subscribe and gives you a value from a stream. You return an observable from that and it outputs the values from all the created observables (one for for each element in the base stream) into the resulting stream.
var sourceGroup = { // result of calling api /1234
id: '1234',
cover: {
id: '9999'
}
};
var sourceCover = { // result of calling api /9999
id: '9999',
images: [{
src: 'image1x80.png'
}, {
src: 'image1x320.png'
}]
};
var rxGroup = Rx.Observable.just(sourceGroup);
var rxCombined = rxGroup.flatMap(group =>
Rx.Observable.just(sourceCover)
.map(images => ({
id: group.id,
images: images.images
}))
)
rxCombined.subscribe(x =>
console.log(JSON.stringify(x, null, 2)));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.all.min.js"></script>
Result:
{
"id": "1234",
"images": [
{
"src": "image1x80.png"
},
{
"src": "image1x320.png"
}
]
}
You should use concatMap instead of flatMap, it will preserve the order of the source emissions.

Chaining queries in rethinkdb

Lets say I have a "category" table, each category has associated data in the "data" table and it has associated data in other tables "associated" and I want to remove a category with all it's associated data.
What I'm currently doing is something like this:
getAllDataIdsFromCategory()
.then(removeAllAssociated)
.then(handleChanges)
.then(removeDatas)
.then(handleChanges)
.then(removeCategory)
.then(handleChanges);
Is there a way to chain these queries on the db-side?
my functions currently look like this:
var getAllDataIdsFromCategory = () => {
return r
.table('data')
.getAll(categoryId, { index: 'categoryId' })
.pluck('id').map(r.row('id')).run();
}
var removeAllAssociated = (_dataIds: string[]) => {
dataIds = _dataIds;
return r
.table('associated')
.getAll(dataIds, { index: 'dataId' })
.delete()
.run()
}
var removeDatas = () => {
return r
.table('data')
.getAll(dataIds)
.delete()
.run()
}
notice that I cannot use r.expr() or r.do() since I want to do queries based on the result of the previous query.
The problem with my approach is that it won't work for large amounts of "data" since I have to bring all of the ids to the client side, and doing paging for it in the client side in a loop seems like a workaround.
You can use forEach for this:
r.table('data').getAll(categoryID, {index: 'categoryId'})('id').forEach(function(id) {
return r.table('associated').getAll(id, {index: 'dataId'}).delete().merge(
r.table('data').get(id).delete())
});

Resources