File

src/app/core/export/data-transformation-service/data-transformation.service.ts

Description

Prepare data for export or analysis

Index

Methods

Constructor

constructor(queryService: QueryService)
Parameters :
Name Type Optional
queryService QueryService No

Methods

Async queryAndTransformData
queryAndTransformData(config: ExportColumnConfig[], from?: Date, to?: Date)
Parameters :
Name Type Optional
config ExportColumnConfig[] No
from Date Yes
to Date Yes
Async transformData
transformData(data: any[], config: ExportColumnConfig[], from?: Date, to?: Date, groupByProperty?: literal type)

Creates a dataset with the provided values that can be used for a simple table or export.

Parameters :
Name Type Optional Description
data any[] No

an array of elements. If not provided, the first query in config will be used to get the data.

config ExportColumnConfig[] No

(Optional) config specifying how export should look

from Date Yes

(Optional) limits the data which is fetched from the database and is also available inside the query. If not provided, all data is fetched.

to Date Yes

(Optional) same as from.If not provided, today is used.

groupByProperty literal type Yes

(optional) groups the data using the value at the given property and adds a column to the final table.

array with the result of the queries and sub queries

import { Injectable } from "@angular/core";
import {
  getReadableValue,
  transformToReadableFormat,
} from "../../common-components/entities-table/value-accessor/value-accessor";
import { ExportColumnConfig } from "./export-column-config";
import { QueryService } from "../query.service";
import { groupBy } from "../../../utils/utils";

/**
 * Prepare data for export or analysis
 */
@Injectable({
  providedIn: "root",
})
export class DataTransformationService {
  constructor(private queryService: QueryService) {}

  async queryAndTransformData(
    config: ExportColumnConfig[],
    from?: Date,
    to?: Date,
  ): Promise<ExportRow[]> {
    const combinedResults: ExportRow[] = [];
    const totalRow: ExportRow = {};
    for (const c of config) {
      await this.queryService.cacheRequiredData(c.query, from, to);
      const baseData = this.queryService.queryData(c.query, from, to);
      const result: ExportRow[] = [];
      if (c.subQueries) {
        result.push(
          ...(await this.transformData(
            baseData,
            c.subQueries,
            from,
            to,
            c.groupBy,
          )),
        );
      } else {
        totalRow[c.label] = baseData;
      }

      combinedResults.push(...result);
    }
    return Object.keys(totalRow).length > 0
      ? [totalRow, ...combinedResults]
      : combinedResults;
  }

  private concatQueries(config: ExportColumnConfig) {
    return (config.subQueries ?? []).reduce(
      (query, c) => query + this.concatQueries(c),
      config.query,
    );
  }

  /**
   * Creates a dataset with the provided values that can be used for a simple table or export.
   * @param data an array of elements. If not provided, the first query in `config` will be used to get the data.
   * @param config (Optional) config specifying how export should look
   * @param from (Optional) limits the data which is fetched from the database and is also available inside the query. If not provided, all data is fetched.
   * @param to (Optional) same as from.If not provided, today is used.
   * @param groupByProperty (optional) groups the data using the value at the given property and adds a column to the final table.
   * @returns array with the result of the queries and sub queries
   */
  async transformData(
    data: any[],
    config: ExportColumnConfig[],
    from?: Date,
    to?: Date,
    groupByProperty?: { label: string; property: string },
  ): Promise<ExportRow[]> {
    const fullQuery = config.map((c) => this.concatQueries(c)).join("");
    await this.queryService.cacheRequiredData(fullQuery, from, to);
    return this.generateRows(data, config, from, to, groupByProperty).map(
      transformToReadableFormat,
    );
  }

  private generateRows(
    data: any[],
    config: ExportColumnConfig[],
    from: Date,
    to: Date,
    groupByProperty?: { label: string; property: string },
  ) {
    const result: ExportRow[] = [];
    if (groupByProperty) {
      const groups = groupBy(data, groupByProperty.property);
      for (const [group, values] of groups) {
        const groupColumn: ExportColumnConfig = {
          label: groupByProperty.label,
          query: `:setString(${getReadableValue(group)})`,
        };
        const rows = this.generateColumnsForRow(
          values,
          [groupColumn].concat(...config),
          from,
          to,
        );
        result.push(...rows);
      }
    } else {
      for (const dataRow of data) {
        const rows = this.generateColumnsForRow(dataRow, config, from, to);
        result.push(...rows);
      }
    }
    return result;
  }

  /**
   * Generate one or more export row objects from the given data data and config.
   * @param data A data to be exported as one or more export row objects
   * @param config
   * @param from
   * @param to
   * @returns array of one or more export row objects (as simple {key: value})
   * @private
   */
  private generateColumnsForRow(
    data: any | any[],
    config: ExportColumnConfig[],
    from: Date,
    to: Date,
  ): ExportRow[] {
    let exportRows: ExportRow[] = [{}];
    for (const exportColumnConfig of config) {
      const partialExportObjects = this.buildValueRecursively(
        data,
        exportColumnConfig,
        from,
        to,
      );

      exportRows = this.mergePartialExportRows(
        exportRows,
        partialExportObjects,
      );
    }
    return exportRows;
  }

  /**
   * Generate one or more (partial) export row objects from a single property of the data
   * @param data one single data item
   * @param exportColumnConfig
   * @param from
   * @param to
   * @private
   */
  private buildValueRecursively(
    data: any | any[],
    exportColumnConfig: ExportColumnConfig,
    from: Date,
    to: Date,
  ): ExportRow[] {
    const label =
      exportColumnConfig.label ?? exportColumnConfig.query.replace(".", "");
    const value = this.getValueForQuery(exportColumnConfig, data, from, to);

    if (!exportColumnConfig.subQueries) {
      return [{ [label]: value }];
    } else if (value.length === 0) {
      return this.generateColumnsForRow(
        {},
        exportColumnConfig.subQueries,
        from,
        to,
      );
    } else {
      return this.generateRows(
        value,
        exportColumnConfig.subQueries,
        from,
        to,
        exportColumnConfig.groupBy,
      );
    }
  }

  private getValueForQuery(
    exportColumnConfig: ExportColumnConfig,
    data: any | any[],
    from: Date,
    to: Date,
  ): any {
    const value = this.queryService.queryData(
      exportColumnConfig.query,
      from,
      to,
      Array.isArray(data) ? data : [data],
    );

    if (!Array.isArray(value)) {
      return value;
    } else if (!exportColumnConfig.subQueries && value.length === 1) {
      return value[0];
    } else {
      return value.filter((val) => val !== undefined);
    }
  }

  /**
   * Combine two arrays of export row objects.
   * Every additional row is merged with every row of the first array (combining properties),
   * resulting in n*m export rows.
   *
   * @param exportRows
   * @param additionalExportRows
   * @private
   */
  private mergePartialExportRows(
    exportRows: ExportRow[],
    additionalExportRows: ExportRow[],
  ): ExportRow[] {
    const rowsOfRows: ExportRow[][] = additionalExportRows.map((addRow) =>
      exportRows.map((row) => Object.assign({}, row, addRow)),
    );
    // return flattened array
    return rowsOfRows.reduce((acc, rowOfRows) => acc.concat(rowOfRows), []);
  }
}

interface ExportRow {
  [key: string]: any;
}

results matching ""

    No results matching ""