Home > Enterprise >  What is the correct way to obtain an asynchronous resource in the mapping of some data?
What is the correct way to obtain an asynchronous resource in the mapping of some data?

Time:05-21

From the following data set I am trying to replace the value of the logoUrl property which is a protected blob resource in S3 with a value that I can use in the template

[
  {
    id: 2,
    name: "ABC",
    logoUrl: null,
  },
  {
    id: 1,
    name: "DEF",
    logoUrl:
      "https://assets.demo.app.net/bucket/154F460F8FAE344F13C29962931CBD3C1F1384F7A9DF9F548D23CF4AFF5E6811-some-name.jpeg",
  },
  {
    id: 3,
    name: "GHI",
    logoUrl:
      "https://assets.demo.app.net/bucket/4CA2F69627B51F7F07A39642DB3538A3D55564891F456528067B47DCEED61B4F-some-name.png",
  },
  {
    id: 6,
    name: "JKL",
    logoUrl: null,
  },
];

To this end I try to implement the following Resolve

@Injectable({
  providedIn: 'root',
})
export class FetchDataResolver implements Resolve<Model[]> {
  constructor(
    private dataService: DataService,
    private s3Service: S3Service
  ) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<Model[]> {
    return this.dataService
      .getAll()
      .pipe(
        map((data) => data.sort((a, b) => a.name.localeCompare(b.name)))
      );
  }
}

The problem I'm trying to solve is how inside .pipe() Could I map the response of getting the resource from S3 and then replace the record. Because the first action is an asynchronous operation.

The S3 service has two methods, one is responsible for obtaining the resource that is a blob and the second is responsible for obtaining a url that I can use in the template to display the image. The service is as follows

@Injectable({
  providedIn: 'root',
})
export class S3Service {
  constructor(
    private domSanitizer: DomSanitizer,
    private httpClient: HttpClient
  ) {}

  getResource(resource: string): Observable<Blob> {
    return this.httpClient.get(resource, { responseType: 'blob' });
  }

  getSourceFromBlob(blob: Blob): SafeUrl {
    return this.domSanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
  }
}

The expected result is similar to the following

[
  {
    id: 2,
    name: "ABC",
    logoUrl: null,
  },
  {
    id: 1,
    name: "DEF",
    logoUrl:
      "blob:http://localhost:4200/e9113189-d08e-485c-b61a-5a8f5f4a0fdd",
  },
  {
    id: 3,
    name: "GHI",
    logoUrl:
      "blob:http://localhost:4200/e9113189-d08e-485c-b61a-5a8f5f4a0fdd",
  },
  {
    id: 6,
    name: "JKL",
    logoUrl: null,
  },
];

CodePudding user response:

Let's try to get a high level picture of the situation.

You've a got a stream, that gives you an Observable<Model[]>.

From that stream, you want to get a new stream that has the exact same shape but where 1 property has been updated (logoUrl).

In order to do that, you need to make an asynchronous action for each item of the array.

When that's the case, pretty much the first operator to think about it forkJoin. That operator let's you pass an array of observables and will give you back one stream containing all the arrays. So in other words: Pass an Array<Observable<T>> and you'll get back Observable<Array<T>>.

If you're having a hard time thinking about that one and you're familiar with Promise.all it's very much alike.

return this.dataService.getAll().pipe(
  map((data) => data.sort((a, b) => a.name.localeCompare(b.name))),
  switchMap((data) => {
    // for each entry...
    const dataWithSourceLogoUrl$ = data.map((x) => {
      this.s3Service
      // let's fetch the source logo
        .getSourceFromBlob(x.logoUrl)
        // and merge it back to the original object
        .pipe(pipe(map((sourceLogoUrl) => ({ ...x, logoUrl: sourceLogoUrl }))));
    });
    
    // finally, use forkJoin to start all the streams we prepared above
    return forkJoin(dataWithSourceLogoUrl$);
  })
);

Hopefully the comments in the code are enough but if it's not let me know and I'll edit to explain more.

  • Related