dashpresshq/dashpress

View on GitHub
src/backend/dashboard-widgets/dashboard-widgets.service.ts

Summary

Maintainability
A
1 hr
Test Coverage
A
96%
import { nanoid } from "nanoid";

import {
  RDBMSDataApiService,
  rDBMSDataApiService,
} from "@/backend/data/data-access/RDBMS";
import { relativeDateNotationToActualDate } from "@/backend/data/data-access/time.constants";
import type { EntitiesApiService } from "@/backend/entities/entities.service";
import { entitiesApiService } from "@/backend/entities/entities.service";
import type { AbstractConfigDataPersistenceService } from "@/backend/lib/config-persistence";
import { createConfigDomainPersistenceService } from "@/backend/lib/config-persistence";
import { BadRequestError } from "@/backend/lib/errors";
import type { ListOrderApiService } from "@/backend/list-order/list-order.service";
import { listOrderApiService } from "@/backend/list-order/list-order.service";
import type { RolesApiService } from "@/backend/roles/roles.service";
import { rolesApiService } from "@/backend/roles/roles.service";
import { SPECTRUM_COLORS } from "@/components/ui/spectrum";
import { SystemIconsList } from "@/shared/constants/Icons";
import { sortListByOrder } from "@/shared/lib/array/sort";
import { userFriendlyCase } from "@/shared/lib/strings/friendly-case";
import type { IWidgetConfig } from "@/shared/types/dashboard";
import {
  HOME_DASHBOARD_KEY,
  WIDGET_SCRIPT_RELATIVE_TIME_MARKER,
} from "@/shared/types/dashboard";
import { DATA_SOURCES_CONFIG } from "@/shared/types/data-sources";
import type { ILabelValue } from "@/shared/types/options";
import type { IAccountProfile } from "@/shared/types/user";
import { GranularEntityPermissions } from "@/shared/types/user";

import {
  mutateGeneratedDashboardWidgets,
  PORTAL_DASHBOARD_PERMISSION,
} from "./portal";

const runAsyncJavascriptString = async (
  javascriptString: string,
  context: Record<string, unknown>
) => {
  // eslint-disable-next-line @typescript-eslint/no-empty-function
  const AsyncFunction = async function X() {}.constructor;
  try {
    return await AsyncFunction("$", javascriptString)(context);
  } catch (error) {
    return {
      message: error.message,
      error,
      context,
      expression: javascriptString,
    };
  }
};

export class DashboardWidgetsApiService {
  constructor(
    private readonly _dashboardWidgetsPersistenceService: AbstractConfigDataPersistenceService<IWidgetConfig>,
    private readonly _entitiesApiService: EntitiesApiService,
    private readonly _listOrderApiService: ListOrderApiService,
    private readonly _rolesApiService: RolesApiService,
    private readonly _rDBMSApiDataService: RDBMSDataApiService
  ) {}

  async runScript(
    script$1: string,
    currentUser: IAccountProfile,
    relativeDate?: string
  ) {
    if (!script$1) {
      return "{}";
    }

    const script = script$1.replaceAll(
      `$.${WIDGET_SCRIPT_RELATIVE_TIME_MARKER}`,
      relativeDateNotationToActualDate(relativeDate).toISOString()
    );

    return (
      (await runAsyncJavascriptString(script, {
        currentUser,
        query: async (sql: string) => {
          return await this._rDBMSApiDataService.runQuery(sql);
        },
      })) || "{}"
    );
  }

  async runWidgetScript(
    widgetId: string,
    currentUser: IAccountProfile,
    relativeDate: string
  ) {
    const widget = await this._dashboardWidgetsPersistenceService.getItemOrFail(
      widgetId
    );
    return await this.runScript(widget.script, currentUser, relativeDate);
  }

  private async generateDefaultDashboardWidgets(dashboardId: string) {
    const entitiesToShow = await this._entitiesApiService.getActiveEntities();

    const defaultWidgets = await mutateGeneratedDashboardWidgets(
      await this.generateDashboardWidgets(entitiesToShow),
      entitiesToShow
    );

    for (const widget of defaultWidgets) {
      await this._dashboardWidgetsPersistenceService.createItem(
        widget.id,
        widget
      );
    }

    const widgetList = defaultWidgets.map(({ id }) => id);

    await this._listOrderApiService.upsertOrder(dashboardId, widgetList);

    return defaultWidgets;
  }

  private generateDashboardWidgets = async (entitiesToShow: ILabelValue[]) => {
    const DEFAULT_NUMBER_OF_SUMMARY_CARDS = 8;

    const dbCredentials = await RDBMSDataApiService.getDbCredentials();

    const queryQuote =
      DATA_SOURCES_CONFIG[dbCredentials.dataSourceType].scriptQueryDelimiter;

    const defaultWidgets: IWidgetConfig[] = await Promise.all(
      entitiesToShow
        .slice(0, DEFAULT_NUMBER_OF_SUMMARY_CARDS)
        .map(async (entity, index) => {
          const dateField =
            await this._entitiesApiService.getEntityFirstFieldType(
              entity.value,
              "date"
            );

          const plainCountQuery = (await RDBMSDataApiService.getInstance())
            .from(entity.value)
            .count({ count: "*" })
            .toQuery();

          const dateCountQuery = (await RDBMSDataApiService.getInstance())
            .from(entity.value)
            .where(dateField, "<", `$.${WIDGET_SCRIPT_RELATIVE_TIME_MARKER}`)
            .count({ count: "*" })
            .toQuery();

          return {
            id: nanoid(),
            title: userFriendlyCase(`${entity.value}`),
            _type: "summary-card",
            entity: entity.value,
            color: SPECTRUM_COLORS[index % (SPECTRUM_COLORS.length - 1)],
            icon: SystemIconsList[index % (SystemIconsList.length - 1)],
            script: dateField
              ? `const actual = await $.query(${queryQuote}${plainCountQuery}${queryQuote});
const relative = await $.query(${queryQuote}${dateCountQuery}${queryQuote});

return [actual[0], relative[0]];
`
              : `return await $.query(${queryQuote}${plainCountQuery}${queryQuote})`,
          };
        })
    );

    const firstEntity = entitiesToShow[0];

    if (firstEntity) {
      const firstQuery = (await RDBMSDataApiService.getInstance())
        .from(firstEntity.value)
        .limit(5)
        .toQuery();

      defaultWidgets.push({
        id: nanoid(),
        title: userFriendlyCase(`${firstEntity.value}`),
        _type: "table",
        entity: firstEntity.value,
        script: `return await $.query(${queryQuote}${firstQuery}${queryQuote})`,
      });
    }

    return defaultWidgets;
  };

  private async listDashboardWidgetsToShow(dashboardId: string) {
    const widgetList = await this._listOrderApiService.getItemOrder(
      dashboardId
    );
    if (widgetList.length === 0) {
      return await this.generateDefaultDashboardWidgets(dashboardId);
    }

    const widgets = Object.values(
      await this._dashboardWidgetsPersistenceService.getAllItemsIn(widgetList)
    );

    return sortListByOrder(widgetList, widgets, "id");
  }

  async listDashboardWidgets(
    dashboardId: string,
    userRole: string
  ): Promise<IWidgetConfig[]> {
    if (
      dashboardId !== HOME_DASHBOARD_KEY &&
      !(await this._rolesApiService.canRoleDoThis(
        userRole,
        PORTAL_DASHBOARD_PERMISSION(dashboardId, GranularEntityPermissions.Show)
      ))
    ) {
      throw new BadRequestError(
        "You can't view this dashboard or it doesn't exist"
      );
    }

    return await this.listDashboardWidgetsToShow(dashboardId);
  }

  async createWidget(widget: IWidgetConfig, dashboardId: string) {
    await this._dashboardWidgetsPersistenceService.createItem(
      widget.id,
      widget
    );

    await this._listOrderApiService.appendToList(dashboardId, widget.id);
  }

  async updateWidgetList(dashboardId: string, widgetList: string[]) {
    await this._listOrderApiService.upsertOrder(dashboardId, widgetList);
  }

  async updateWidget(widgetId: string, widget: IWidgetConfig) {
    await this._dashboardWidgetsPersistenceService.upsertItem(widgetId, widget);
  }

  async removeWidget({
    dashboardId,
    widgetId,
  }: {
    widgetId: string;
    dashboardId: string;
  }) {
    await this._dashboardWidgetsPersistenceService.removeItem(widgetId);

    await this._listOrderApiService.removeFromList(dashboardId, widgetId);
  }
}

const dashboardWidgetsPersistenceService =
  createConfigDomainPersistenceService<IWidgetConfig>("dashboard-widgets");

export const dashboardWidgetsApiService = new DashboardWidgetsApiService(
  dashboardWidgetsPersistenceService,
  entitiesApiService,
  listOrderApiService,
  rolesApiService,
  rDBMSDataApiService
);