src/app/features/template-export/template-export-api/template-export-api.service.ts

Description

Interact with the PDF Template Generation API that uses File Templates to generate custom pdf documents.

Extends

FileService

Metadata

Relationships

Index

Properties
Methods

Methods

generateBatchFromTemplate
generateBatchFromTemplate(template: TemplateExport, dataList: Object[], mode: "zip" | "combined")

Generate output from one template for many records in a single request.

Both modes go through the template engine's native batch rendering — the backend just forwards the request:

  • mode: "zip" (default): N independent files packaged in a ZIP archive.
  • mode: "combined": N rendered files merged into a single multi-page PDF.
Parameters :
Name Type Optional Default value Description
template TemplateExport No

The TemplateExport entity to render

dataList Object[] No

The array of data objects (typically entities) to apply to the template

mode "zip" | "combined" No "zip"

How the backend should aggregate the output

The generated file (ZIP or PDF) and a derived filename

generatePdfFromTemplate
generatePdfFromTemplate(template: TemplateExport, data: Object)

Generate a PDF applying actual data to an existing template.

Parameters :
Name Type Optional Description
template TemplateExport No

The TemplateExport entity to render

data Object No

The data object (typically an entity) to be applied to the template

An array buffer of the generated PDF

Protected getShowFileUrl
getShowFileUrl(entity: TemplateExport, property: string)
Inherited from FileService
Parameters :
Name Type Optional
entity TemplateExport No
property string No
Returns : string
loadFile
loadFile(entity: Entity, property: string)
Inherited from FileService
Parameters :
Name Type Optional
entity Entity No
property string No
Returns : Observable<SafeUrl>
removeAllFiles
removeAllFiles(entity: Entity)
Inherited from FileService
Parameters :
Name Type Optional
entity Entity No
Returns : Observable<any>
removeFile
removeFile(entity: Entity, property: string)
Inherited from FileService
Parameters :
Name Type Optional
entity Entity No
property string No
Returns : Observable<any>
uploadFile
uploadFile(file: File, entity: TemplateExport, property: string)
Inherited from FileService

Upload a new template file to the API.

Parameters :
Name Type Optional Description
file File No

to be uploaded to the API as a template

entity TemplateExport No
property string No
Returns : Observable<string>

The template ID generated by the API

Protected reportProgress
reportProgress(message: string, obs: Observable)
Inherited from FileService
Parameters :
Name Type Optional
message string No
obs Observable<HttpEvent | any> No
Returns : void
showFile
showFile(entity: Entity, property: string)
Inherited from FileService

If a file is available, downloads this file and shows it in a new tab.

Parameters :
Name Type Optional Description
entity Entity No
property string No

where a file previously has been uploaded

Returns : void

Properties

Readonly API_URL
Type : unknown
Default value : environment.API_PROXY_PREFIX + "/v1/export"
Protected alertService
Type : unknown
Default value : inject(AlertService)
Inherited from FileService
Protected dialog
Type : MatDialog
Default value : inject(MatDialog)
Inherited from FileService
Protected entities
Type : unknown
Default value : inject(EntityRegistry)
Inherited from FileService
Protected entityMapper
Type : unknown
Default value : inject(EntityMapperService)
Inherited from FileService
Protected httpClient
Type : HttpClient
Default value : inject(HttpClient, { optional: true, })
Inherited from FileService
Protected schemaService
Type : unknown
Default value : inject(EntitySchemaService)
Inherited from FileService
Protected snackbar
Type : MatSnackBar
Default value : inject(MatSnackBar)
Inherited from FileService
Protected syncState
Type : unknown
Default value : inject(SyncStateSubject)
Inherited from FileService
import { inject, Injectable } from "@angular/core";
import { FileService } from "../../file/file.service";
import { SafeUrl } from "@angular/platform-browser";
import { Entity } from "app/core/entity/model/entity";
import { from, Observable, of, throwError } from "rxjs";
import { HttpResponse } from "@angular/common/http";
import { NotAvailableOfflineError } from "../../../core/session/not-available-offline.error";
import { NAVIGATOR_TOKEN } from "../../../utils/di-tokens";
import { switchMap } from "rxjs/operators";
import { TemplateExport } from "../template-export.entity";
import { resolveLocaleText } from "../../../core/language/active-locale";
import { Logging } from "../../../core/logging/logging.service";
import { environment } from "../../../../environments/environment";
import {
  TemplateExportComplement,
  TemplateExportContextService,
} from "../template-export-context/template-export-context.service";
import { EntityRelationResolverService } from "../../../core/entity/entity-relation-resolver/entity-relation-resolver.service";

/**
 * Format of API response body upon uploading a new template file.
 */
interface TemplateUploadResponseDto {
  templateId: string;
}

/**
 * Format of API request body to render one or more files from a template.
 * TemplateId is provided via URL path.
 *
 * `data` is a single record for `/render`, or an array of records for `/render-batch`
 * (the backend renders each one and returns either a ZIP or a combined PDF).
 */
interface TemplateRenderRequestDto {
  /**
   * target file type (e.g. "pdf")
   */
  convertTo: string;

  /**
   * The data used to fill placeholders in the template.
   */
  data: Object | Object[];

  /**
   * Additional context data available in the template under the `{c.…}` prefix.
   */
  complement?: TemplateExportComplement;
}

export interface TemplateExportResult {
  filename: string;
  file: ArrayBuffer;
}

/**
 * Interact with the PDF Template Generation API that uses File Templates to generate custom pdf documents.
 */
@Injectable({
  providedIn: "root",
})
export class TemplateExportApiService extends FileService {
  private navigator = inject<Navigator>(NAVIGATOR_TOKEN);
  private readonly exportContext = inject(TemplateExportContextService);
  private readonly relationResolver = inject(EntityRelationResolverService);

  readonly API_URL = environment.API_PROXY_PREFIX + "/v1/export";

  /*
  --- FileService methods ---
  */

  /**
   * Upload a new template file to the API.
   * @param file to be uploaded to the API as a template
   * @param entity
   * @param property
   * @return The template ID generated by the API
   */
  uploadFile(
    file: File,
    entity: TemplateExport,
    property: string,
  ): Observable<string> {
    if (!this.navigator.onLine) {
      return throwError(() => new NotAvailableOfflineError("File Attachments"));
    }

    const formData = new FormData();
    formData.append("template", file, file.name);

    return this.httpClient.post(this.API_URL + "/template", formData).pipe(
      switchMap(async (res: TemplateUploadResponseDto) => {
        entity.templateId = res.templateId;
        await this.entityMapper.save(entity);
        return res.templateId;
      }),
    );
  }

  protected override getShowFileUrl(
    entity: TemplateExport,
    property: string,
  ): string {
    return this.API_URL + "/template/" + entity.getId();
  }

  loadFile(entity: Entity, property: string): Observable<SafeUrl> {
    // should not be required for our use cases of the Template Export API
    throw new Error("Method not implemented.");
  }

  removeFile(entity: Entity, property: string): Observable<any> {
    // we do not do explicit file removal due to the design of the API
    Logging.debug("skipping file removal for Template Export API");
    return of(true);
  }

  removeAllFiles(entity: Entity): Observable<any> {
    Logging.debug("skipping file removal for Template Export API");
    return of(true);
  }

  /*
  --- PDF Generation API methods ---
  */

  /**
   * POST a render request to the given endpoint and resolve the response's Content-Disposition
   * filename, falling back to `fallbackFilename` if the header is missing or cannot be parsed.
   *
   * Shared by the single-record and batch render methods below, which only differ in the
   * endpoint URL, the shape of `data`, and the fallback filename.
   */
  private renderTemplate(
    url: string,
    data: Object | Object[],
    fallbackFilename: string,
  ): Observable<TemplateExportResult> {
    const complement = this.exportContext.getComplement();

    // give the template access to details of linked records (e.g. a Note's related
    // Child) instead of just their id - resolved one level deep, see the service.
    return from(this.relationResolver.resolveRelations(data)).pipe(
      switchMap((resolvedData) =>
        this.httpClient.post(
          url,
          {
            convertTo: "pdf",
            data: resolvedData,
            ...(complement ? { complement } : {}),
          } as TemplateRenderRequestDto,
          { observe: "response", responseType: "arraybuffer" },
        ),
      ),
      switchMap(async (res: HttpResponse<ArrayBuffer>) => {
        // the API returns the filename in the Content-Disposition header as a URL-encoded string with special delimiters
        const disposition = res.headers.get("Content-Disposition");
        let filenameMatch: RegExpMatchArray | null = null;
        if (disposition) {
          try {
            filenameMatch = decodeURIComponent(disposition).match(
              /filename="?([^";]+)"?/,
            );
          } catch (err) {
            // malformed percent-encoding must not fail an otherwise successful
            // download - fall back to fallbackFilename below
            Logging.warn(
              "TemplateExportApiService: could not decode Content-Disposition filename",
              err,
            );
          }
        }

        return {
          filename: filenameMatch?.[1] ?? fallbackFilename,
          file: res.body,
        };
      }),
    );
  }

  /**
   * Generate a PDF applying actual data to an existing template.
   * @param template The TemplateExport entity to render
   * @param data The data object (typically an entity) to be applied to the template
   * @return An array buffer of the generated PDF
   */
  generatePdfFromTemplate(
    template: TemplateExport,
    data: Object,
  ): Observable<TemplateExportResult> {
    return this.renderTemplate(
      this.API_URL + "/render/" + template.getId(),
      data,
      resolveLocaleText(template.title),
    );
  }

  /**
   * Generate output from one template for many records in a single request.
   *
   * Both modes go through the template engine's native batch rendering — the backend
   * just forwards the request:
   * - `mode: "zip"` (default): N independent files packaged in a ZIP archive.
   * - `mode: "combined"`: N rendered files merged into a single multi-page PDF.
   *
   * @param template The TemplateExport entity to render
   * @param dataList The array of data objects (typically entities) to apply to the template
   * @param mode How the backend should aggregate the output
   * @return The generated file (ZIP or PDF) and a derived filename
   */
  generateBatchFromTemplate(
    template: TemplateExport,
    dataList: Object[],
    mode: "zip" | "combined" = "zip",
  ): Observable<TemplateExportResult> {
    return this.renderTemplate(
      this.API_URL + "/render-batch/" + template.getId() + "?mode=" + mode,
      dataList,
      resolveLocaleText(template.title) +
        (mode === "combined" ? ".pdf" : ".zip"),
    );
  }
}

results matching ""

    No results matching ""