Typescript Angular, how do I call - as part of a chain of Observables - an operation on each member of an array and just return a count of successes?

Viewed 122

I have a parent record I need to create, and then a series of child records to create (order not important). Then I need to do a follow-up logging action.

I know how to create a chain of single actions on an observable by mapping them together, so, I could do:

-- create one parent record, return its id:

createParentRecord(parentInfo: parentType): Observable<number>

-- create one child record, return its id

createChildRecord(childInfo: childType): Observable<number>

-- log the entry

logStuff(parentId: number, childId: number)

-- composite function does all the work, returning the parentId

doTheThing(): Observable<number> {
   /* ... */
   let parentId: number;
   let childId: number;

   return createParent(parentInfo).pipe(
     tap(id => (parentId = id)),
     mergeMap(id => { childInfo.parentId = parentId; return createChildRecord(childInfo);} ),
     mergeMap(childId => { childInfo.childId = childId; return logStuff(parentId, childId); ),
     map( () => parentId)
   ); 
}

Question

This works great.

BUT if I have childInfo[] rather than childInfo, how do I do this? how do I handle an array of childInfo[] to subscribe to and return inner observable results? I just want a number back of how many succeeded - or just whatever results come back from each observable? Do I use forkJoin()?

I can't for the life of me figure this out. Help?

2 Answers

If I understand right your question, I would proceed like this. Comments are inline

function doTheThing(): Observable<any> {
   /* ... */
   let parentId: number;
   // somehow there is an array of children info to be saved
   const childInfos = new Array(3).fill({})

   return createParent('Parent Info').pipe(
     tap(id => (parentId = id)),
     concatMap(id => { 
         // each childInfo is filled with the result of the createParent operation
         childInfos.forEach(c => c.parentId = parentId); 
         // an array of Observable is created - each one represents a createChildRecord operation
         const childInfosObs = childInfos.map(ci => createChildRecord(ci))
         // all child record operations are executed in parallel via forkJoin
         return forkJoin(childInfosObs);
      }),
     concatMap(childIds => { 
       // forkJoin returns an array of results with the same order as the array of Observables passed to forkJoin
       // therefore we can loop through the array and set each childInfo with its result
       childInfos.forEach((ci, i) => ci.childId = childIds[i])
       // last thing we do is to log and then return the results of the forkJoin
       return logStuff(parentId, childIds).pipe(
         map(() => childIds)
       );
     } )
   ); 
}

Note that I use concatMap instead of mergeMap. Unless there is a specific reason, anytime I do DB or Rest operations I prefer concatMap just to keep the sequence of operations.

You can also enrich every observable contained in childInfosObs with some error handling logic if required, considering that forkJoin would error if any of the Observables it tries to execute errors.

Here a stackblitz with an example of the proposed solution.

Blockquote

You could map your ChildInfo[] to an observable array and then merge it like this:

return createParent(parentInfo).pipe(
  tap(newParentId => parentId = newParentId),
  mergeMap(newParentId => forkJoin(
   childInfos.map(childInfo => { // this is an Array map.
     childInfo.parentId = parentId;
     return createChildRecord(childInfo); // this should return childInfo, since it sounds like a CRUD operation.
   })
  )),

Note that this only works ifcreateChildRecord completes because forkJoin won't emit otherwise. I assumed this was a single call in my solution. If you make createChildRecord return the updated version of your childInfo, your life should become much easier in the following operations as well.

createChildRecord(childInfo: ChildInfo): Observable<ChildInfo> {

  const childId = /** TBD **/
  return of({...childInfo, childId});
}

For readability, consider putting the inner observable in some kind of method.

Related