Usando RxJS para construir dados de várias chamadas de API

Oct 09 2020

Estou tentando entender melhor como usar os operadores RxJS para resolver um problema específico que estou tendo. Na verdade, tenho dois problemas, mas eles são semelhantes o suficiente.

Estou pegando um monte de documentos de um endpoint de API /api/v3/folders/${folderId}/documentse configurei serviços com funções para fazer isso e lidar com toda a autenticação, etc.

No entanto, essa matriz de objetos de documento não possui o atributo de descrição. Para obter a descrição, preciso recorrer /api/v3/documents/${documentId}/a cada documento da chamada anterior. A interface do meu documento é assim:

export interface Document {
  id: number;
  name: string;
  description: string;
}

Estou pensando que preciso mergeMapesperar e obter os documentos de alguma forma para adicionar a descrição à minha Documentinterface e retornar tudo, no entanto, estou tendo problemas para obter o resultado final.

getDocuments(folderId: number) {
    return this.docService.getDocuments(folderId, this.cookie).pipe(
      map(document => {
        document; // not entirely sure what to do here, does this 'document' carry over to mergeMap?
      }),
      mergeMap(document => {
        this.docService.getDocument(documentId, this.cookie)
      }) // possibly another map here to finalize the array? 
    ).subscribe(res => console.log(res));
  }

Pode parecer um pouco duplicado, mas qualquer postagem que encontrei não esclareceu 100% as coisas para mim.

Qualquer ajuda para entender como usar corretamente os dados na segunda chamada e agrupar tudo é muito apreciada. Obrigado.

Graças a @BizzyBob, aqui está a solução final com uma edição e explicação:

  getDocuments(folderId: number) {
    const headers = new HttpHeaders({
      'Content-Type': 'application/json',
      'Context': this.cookie$ }); return this.docService.getDocuments(folderId, this.cookie$).pipe(
      mergeMap(documents => from(documents)),
      mergeMap(document => this.http.get<IDocument>(
        this.lcmsService.getBaseUrl() + `/api/v3/documents/${document.id}`,
        { headers: headers }
      ).pipe(
        map(completeDoc => ({...document, description: completeDoc.description}))
      )),
      toArray()
    ).subscribe(docs => {
      this.documents = docs;
      console.log(this.documents);
    }
    )
  }

Por algum motivo, não consegui pipe()na segunda assinatura do serviço, então acabei tendo que fazer a http.getligação para lá. O erro era "Não é possível usar pipe () na assinatura do tipo", o que é um pouco confuso, pois estou usando pipe()na primeira assinatura. Apliquei essa mudança à minha função de serviço que atualiza um assunto de comportamento e está funcionando perfeitamente. Obrigado!

Respostas

1 BizzyBob Oct 10 2020 at 05:00

Existem algumas maneiras diferentes de compor dados de várias chamadas de API.

Poderíamos:

  • use frompara emitir cada item no fluxo individualmente
  • use mergeMappara assinar a chamada de serviço secundária
  • use toArraypara emitir uma matriz quando todas as chamadas individuais forem concluídas
  getDocuments() {
    return this.docService.getDocuments().pipe(
        mergeMap(basicDocs => from(basicDocs)),
        mergeMap(basicDoc => this.docService.getDocument(basicDoc.id).pipe(
          map(fullDoc => ({...basicDoc, description: fullDoc.description}))
        )),
        toArray()
    );
  } 

Também podemos utilizar forkJoin:

  getDocuments() {
    return this.docService.getDocuments().pipe(
      switchMap(basicDocs => forkJoin(
        basicDocs.map(doc => this.docService.getDocument(doc.id))
      ).pipe(
        map(fullDocs => fullDocs.map((fullDoc, i) => ({...basicDocs[i], description: fullDoc.description})))
      )),
    );
  }

Aqui está um StackBlitz funcional

Além disso, você pode considerar a definição de seu fluxo como uma variável documents$em oposição a um método getDocuments().

  documents$ = this.docService.getDocuments().pipe(
    mergeMap(basicDocs => from(basicDocs))
    mergeMap(basicDoc => this.docService.getDocument(basicDoc.id).pipe(
      map(fullDoc => ({...basicDoc, description: fullDoc.description}))
    ))
    toArray()
  );
1 coderrr22 Oct 09 2020 at 19:18

Como você deve chamar o endpoint document / {id} para obter a descrição, para cada documento você fará chamadas restantes como o número de documentos que possui.

mergeMapretorna um observável e o inscreve na emissão observável externa. (Mantém as assinaturas internas ativas em contraste com o switchMap)https://www.learnrxjs.io/learn-rxjs/operators/transformation/mergemap

Rxjs frompega uma matriz e retorna um observável, emitindo cada item da matriz em sequência.https://www.learnrxjs.io/learn-rxjs/operators/creation/from

Portanto, podemos usar ambos para atingir seu objetivo, como:

( Desculpe pela má formatação, estou usando meu telefone )

Presumi que docService.getDocuments retorna uma matriz de documentos.

getDocuments(folderId: number) { 
    return this.docService.getDocuments(folderId, this.cookie).pipe(
      mergeMap((allDocumentsArray)=> {
             return from(allDocumentsArray).pipe(
               mergeMap((document) =>        
                   this.docservice.getDocument(document).pipe(
                        map(doc) => {...document,description})),//maps the document with document.description
     toArray(),//combines the returned values to an array
    ).subscribe(res => console.log(res));

Eu recomendo ler sobre mergemap em sua documentação. E teste isso antes de usar porque eu não testei.