2

First off: I'm new to Angular so the terms here might be wrong. I hope I can get the understanding through.

I'm consuming an API. Endpoint A exposes Tickets; these Tickets hold an array (0-*) of Attachments. Those Attachments are not complete; in the data model they contain the base64 string of the content of the underlying file but to reduce general load the Attachments are only complete when fetched from Endpoint B. But in order to fetch an Attachment from Endpoint B I need the Attachment's id which I can only get from Endpoint A.

What I need to happen is that I fetch attachment.id from Endpoint A, use that id to fetch the approriate Attachment from Endpoint B, and replace Attachments in the result from Endpoint A with the result from Endpoint B. However since the Attachments are retreived as Observable I can't just simply assign the value.

The following snippet is one of several attempts at solving this and is able to get the Attachments from the API but they aren't assigned the way I wanted.

ngOnInit(): void {
  console.log(`getting ticket for id ${this.id}`);
  this.ticket$ = this.ticketService.getTicket(this.id).pipe(
    map((response:Ticket) => {
      response.attachments.map((attachmentPartly:Attachment) => {
        console.log(`getting attachment for id ${attachmentPartly.id}`);
        let attachmentFilled = this.ticketService.getAttachment(attachmentPartly.id);
        attachmentFilled.subscribe(att => console.log(`base64 content is: ${att.base64Content}`)); //for posterity: this line has been omitted in tests with no change in result
        return attachmentFilled;
      });
      return response;
    })
  );
}

getTicket returns an Observable<Ticket>. getAttachment returns an Observable<Attachment>.

As can be seen I've started to debug by logging (since the angular app is hosted within a larger system that also provides authorization against the endpoints I can't really debug it the right way...) and the base64 value is written to the console, so at least some parts of the code works as I want. For instance for a text/plain type file I can get the value "SGV5IHRoZXJlLCByYW5kb20gcGVyc29uIGZyb20gU3RhY2tPdmVyZmxvdw" written to the console which matches the value found when I execute the query manually via Postman.

I suspect that I'm trying to assign an Observable (attachmentFilled) to the Attachment[] but TS is not warning about illegal type conversions. Looking at the Network tab in Chrome DevTools I can't see the call to getAttachments only the calls to getTickets. The data models for Ticket and Attachment are:

export interface Ticket {
    id?:               string;
    description?:      string;
    assignee?:         string;
    createdAt?:        string;
    resolvedAt?:       string;
    attachments?:      Attachment[];
}

export interface Attachment {
    name?:          string; //contained in result from Endpoint A
    id?:            string; //contained in result from Endpoint A
    relatesTo?:     string; 
    type?:          string; //contained in result from Endpoint A
    sizeBytes?:     string; //contained in result from Endpoint A
    hash?:          string;
    url?:           string;
    base64Content?: string; //need to fetch this
}

I'm using the following template to create the download file links (hugely inspired by angular-file-saver download base64 file using FileSaver):

<div *ngFor="let attachment of ticket.attachments" class="list-group-item">
    <div class="d-flex w-100 justify-content-between">
        <a [href]="'data:' + attachment.type + ';base64,' + attachment.base64Content | safeUrl" target="_blank" download="{{ attachment.name }}">{{ attachment.name }}</a>
        <small>{{ attachment.sizeBytes | filesize }}</small>
    </div>
    <small class="mb-1">{{ attachment.type }}</small>
</div>

which results in the following html (notice how only the data from Endpoint A is populated):

<div _ngcontent-dmp-c28="" class="list-group-item">
    <div _ngcontent-dmp-c28="" class="d-flex w-100 justify-content-between">
        <a _ngcontent-dmp-c28="" target="_blank" download="test.txt" href="data:text/plain;base64,undefined">test.txt</a>
        <small _ngcontent-dmp-c28="">43 B</small>
    </div>
    <small _ngcontent-dmp-c28="" class="mb-1">text/plain</small>
</div>

(Url sanitization, inspired by Angular2 Base64 sanitizing unsafe URL value, appear to force the null base64content to undefined. Before santitization there would be nothing where there now is undefined).

The getAttachment endpoint only accept the attachment.id as an input.

I have tried to implement the solution in Transform data in an Observable this way:

ngOnInit(): void {
    console.log(`getting ticket for id ${this.id}`);
    this.ticket$ = this.ticketService.getTicket(this.id).pipe(
    mergeMap((response: Ticket) => forkJoin([...response.attachments.map((attachment : Attachment) => this.ticketService.getAttachment(attachment.id))])));
}

But to no awail: this.ticket$ throws an error "Type 'Observable<Attachment[]>' is not assignable to type 'Observable'. Type 'Attachment[]' has no properties in common with type 'Ticket'"

Alrekr
  • 300
  • 4
  • 14

1 Answers1

1

I think that is a classical problem asked very often here.

ngOnInit(): void {
  this.ticket$ = this.ticketService.getTicket(this.id).pipe(
    switchMap((response: Ticket) => {
      const filledAttechments$ = response.attachments.map(
        (attachmentPartly: Attachment) => this.ticketService.getAttachment(attachmentPartly.id).pipe(
          map(att => ({ ...attachmentPartly, base64Content: att.base64Content })),
        ),
      );

      return forkJoin(filledAttechments$).pipe(
        map(filledAttachments => ({...response, attachments: filledAttachments})),
      );
    })
  );
}
MoxxiManagarm
  • 8,735
  • 3
  • 14
  • 43
  • If it is asked very often I must be so off in my nomenclature that I couldn't find it. Thank you for answering it yet again - it worked the first time without any issues. There's some interesting syntax going on there, what does "{ ..." do? (A link to documentation is enough). – Alrekr Nov 29 '20 at 18:51
  • 1
    There is no common word to find the other issues I mean. It's just my experience that many questions come down to this solution. ... is the spread operator. It includes all properties of the spread object. Into the new object started with { – MoxxiManagarm Nov 29 '20 at 19:33