Usando RxJS para construir datos a partir de múltiples llamadas API
Estoy tratando de comprender mejor cómo usar los operadores RxJS para resolver un problema específico que tengo. De hecho, tengo dos problemas, pero son bastante similares.
Estoy tomando un montón de documentos de un punto final de API /api/v3/folders/${folderId}/documentsy he configurado servicios con funciones para hacer eso, y manejar toda la autenticación, etc.
Sin embargo, esta matriz de objetos de documento no tiene el atributo de descripción. Para obtener la descripción, necesito llamar /api/v3/documents/${documentId}/a cada documento de la llamada anterior. La interfaz de mi documento se ve así:
export interface Document {
id: number;
name: string;
description: string;
}
Estoy pensando que necesito usar mergeMappara esperar y obtener los documentos de alguna manera para agregar la descripción a mi Documentinterfaz y devolver todo, sin embargo, tengo problemas para obtener el 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));
}
Esto puede parecer un poco duplicado, pero cualquier publicación que haya encontrado no me ha aclarado al 100%.
Se agradece mucho cualquier ayuda para comprender cómo utilizar correctamente los datos en la segunda llamada y agruparlos todos . Gracias.
Gracias a @BizzyBob, aquí está la solución final con una edición y explicación:
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 alguna razón, no pude hacerlo pipe()desde la segunda suscripción de servicio, así que terminé teniendo que hacer la http.getllamada allí. El error fue "No se puede usar pipe () en la suscripción de tipo", lo cual es un poco confuso, ya que lo estoy usando pipe()en la primera suscripción. Apliqué este cambio a mi función de servicio que actualiza un tema de comportamiento y funciona perfectamente. ¡Gracias!
Respuestas
Hay un par de formas diferentes de componer datos a partir de múltiples llamadas a la API.
Podríamos:
- utilizar
frompara emitir cada elemento en la secuencia de forma individual - utilizar
mergeMappara suscribirse a la llamada de servicio secundaria - usar
toArraypara emitir una matriz una vez que se completen todas las llamadas individuales
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()
);
}
También podríamos 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})))
)),
);
}
Aquí hay un StackBlitz funcional
Además, puede considerar definir su flujo como una variable documents$en lugar de un 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()
);
Dado que debe llamar al punto final del documento / {id} para obtener la descripción, para cada documento eventualmente hará llamadas de descanso como el número de documentos que tiene.
mergeMapdevuelve un observable y se suscribe a él en una emisión observable externa. (Mantiene las suscripciones internas activas en contraste con switchMap)https://www.learnrxjs.io/learn-rxjs/operators/transformation/mergemap
Rxjs fromtoma una matriz y devuelve un observable, emitiendo cada elemento de la matriz en secuencia.https://www.learnrxjs.io/learn-rxjs/operators/creation/from
Entonces podemos usar ambos para lograr su objetivo de esta manera:
( Perdón por el formato incorrecto, estoy usando mi teléfono )
Supuse que docService.getDocuments devuelve una 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));
Recomiendo leer sobre mergemap en su documentación. Y prueba esto antes de usarlo porque no lo he probado.