dashpresshq/dashpress

View on GitHub
src/backend/data/data.service.ts

Summary

Maintainability
C
1 day
Test Coverage
A
97%
import { runFormAction } from "@/backend/form-actions/run-form-action";
import { NotFoundError, progammingError } from "@/backend/lib/errors";
import { compileTemplateString } from "@/shared/lib/strings/templates";
import type { PaginatedData, QueryFilterSchema } from "@/shared/types/data";
import { DataEventActions, FilterOperators } from "@/shared/types/data";
import type { IEntityField } from "@/shared/types/db";
import type { IAccountProfile } from "@/shared/types/user";

import type { ConfigurationApiService } from "../configuration/configuration.service";
import { configurationApiService } from "../configuration/configuration.service";
import type { EntitiesApiService } from "../entities/entities.service";
import { entitiesApiService } from "../entities/entities.service";
import type { RDBMSDataApiService } from "./data-access/RDBMS";
import { rDBMSDataApiService } from "./data-access/RDBMS";
import { PortalDataHooksService, PortalQueryImplementation } from "./portal";
import type { IDataApiService, IPaginationFilters } from "./types";
import { makeTableData } from "./utils";

const DEFAULT_LIST_LIMIT = 50;

const GOOD_FIELD_TYPES_FOR_LIST: IEntityField["type"][] = ["enum", "string"];

export class DataApiService implements IDataApiService {
  constructor(
    private _rDBMSApiDataService: RDBMSDataApiService,
    private _entitiesApiService: EntitiesApiService,
    private _configurationApiService: ConfigurationApiService
  ) {}

  async runOnLoad() {
    await this.getDataAccessInstance().bootstrap();
  }

  private getDataAccessInstance() {
    return this._rDBMSApiDataService;
  }

  async fetchData(
    entity: string,
    select: string[],
    queryFilter: QueryFilterSchema,
    paginationFilters: IPaginationFilters
  ): Promise<Record<string, unknown>[]> {
    return await this.getDataAccessInstance().list(
      entity,
      select,
      await this.appendPersistentQuery(
        entity,
        await PortalQueryImplementation.query(queryFilter, entity)
      ),
      paginationFilters
    );
  }

  async countData(
    entity: string,
    queryFilter: QueryFilterSchema
  ): Promise<number> {
    return await this.getDataAccessInstance().count(
      entity,
      await this.appendPersistentQuery(
        entity,
        await PortalQueryImplementation.query(queryFilter, entity)
      )
    );
  }

  async readData<T>(
    entity: string,
    select: string[],
    queryFilter: QueryFilterSchema
  ): Promise<T> {
    progammingError(
      "We dont do that here, Please define the fields you want to select",
      select.length === 0
    );
    return await this.getDataAccessInstance().read<T>(
      entity,
      select,
      queryFilter
    );
  }

  async referenceData(entity: string, id: string): Promise<string> {
    const [relationshipSettings, primaryField] = await Promise.all([
      this.getRelationshipSettings(entity),
      this._entitiesApiService.getEntityPrimaryField(entity),
    ]);

    const data = await this.readData<Record<string, unknown>>(
      entity,
      relationshipSettings.fields,
      {
        operator: "and",
        children: [
          {
            id: primaryField,
            value: {
              operator: FilterOperators.EQUAL_TO,
              value: id,
            },
          },
        ],
      }
    );

    return compileTemplateString(relationshipSettings.format, data);
  }

  async showData(
    entity: string,
    id: string | number,
    column?: string
  ): Promise<Record<string, unknown>> {
    const [fieldsToShow, columnField] = await Promise.all([
      this._entitiesApiService.getAllowedCrudsFieldsToShow(entity, "details"),
      column || this._entitiesApiService.getEntityPrimaryField(entity),
    ]);
    const data = await this.readData<Record<string, unknown>>(
      entity,
      fieldsToShow,
      await this.appendPersistentQuery(entity, {
        operator: "and",
        children: [
          {
            id: columnField,
            value: {
              operator: FilterOperators.EQUAL_TO,
              value: id,
            },
          },
        ],
      })
    );

    if (!data) {
      throw new NotFoundError(
        `Entity '${entity}' with '${columnField}' '${id}' does not exist`
      );
    }
    return data;
  }

  async create(
    entity: string,
    data: Record<string, unknown>,
    accountProfile: IAccountProfile
  ): Promise<string | number> {
    const [allowedFields, primaryField] = await Promise.all([
      this._entitiesApiService.getAllowedCrudsFieldsToShow(entity, "create"),
      this._entitiesApiService.getEntityPrimaryField(entity),
    ]);

    await PortalDataHooksService.beforeCreate({
      dataApiService: this,
      entity,
      data,
    });

    const id = await this.getDataAccessInstance().create(
      entity,
      this.returnOnlyDataThatAreAllowed(data, allowedFields),
      primaryField
    );

    await PortalDataHooksService.afterCreate({
      dataApiService: this,
      entity,
      data,
      insertId: id,
    });

    await runFormAction(
      entity,
      DataEventActions.Create,
      async () => await this.showData(entity, id),
      accountProfile
    );

    return id;
  }

  async listData(
    entity: string,
    searchValue?: string
  ): Promise<{ value: string; label: string }[]> {
    const [relationshipSettings, primaryField] = await Promise.all([
      this.getRelationshipSettings(entity),
      this._entitiesApiService.getEntityPrimaryField(entity),
    ]);

    const data = await this.fetchData(
      entity,
      [...relationshipSettings.fields, primaryField],
      {
        operator: "or",
        children: relationshipSettings.fields.map((field) => ({
          id: field,
          value: {
            operator: FilterOperators.CONTAINS,
            value: searchValue,
          },
        })),
      },
      {
        take: DEFAULT_LIST_LIMIT,
        page: 1,
      }
    );

    return data.map((datum: Record<string, unknown>) => {
      return {
        value: datum[primaryField] as string,
        label: compileTemplateString(relationshipSettings.format, datum),
      };
    });
  }

  async tableData(
    entity: string,
    queryFilters: QueryFilterSchema,
    paginationFilters: IPaginationFilters
  ): Promise<PaginatedData<Record<string, unknown>>> {
    return makeTableData(
      await Promise.all([
        this.fetchData(
          entity,
          await this._entitiesApiService.getAllowedCrudsFieldsToShow(
            entity,
            "table"
          ),
          queryFilters,
          paginationFilters
        ),
        this.countData(entity, queryFilters),
      ]),
      paginationFilters
    );
  }

  async update(
    entity: string,
    id: string,
    data: Record<string, unknown>,
    accountProfile: IAccountProfile,
    options: {
      skipDataEvents?: boolean;
    } = {}
  ): Promise<void> {
    const [allowedFields, primaryField, metadataColumns] = await Promise.all([
      this._entitiesApiService.getAllowedCrudsFieldsToShow(entity, "update"),
      this._entitiesApiService.getEntityPrimaryField(entity),
      this._configurationApiService.show("metadata_columns"),
    ]);

    const beforeData = await PortalDataHooksService.beforeUpdate({
      dataApiService: this,
      entity,
      data,
      dataId: id,
    });

    const valueToUpdate = this.returnOnlyDataThatAreAllowed(
      data,
      allowedFields
    );

    if (allowedFields.includes(metadataColumns.updatedAt)) {
      valueToUpdate[metadataColumns.updatedAt] = new Date();
    }

    await this.getDataAccessInstance().update(
      entity,
      await this.appendPersistentQuery(
        entity,
        rDBMSDataApiService.whereEqualQueryFilterSchema(primaryField, id)
      ),
      valueToUpdate
    );

    await PortalDataHooksService.afterUpdate({
      dataApiService: this,
      entity,
      beforeData,
      data,
      dataId: id,
      options,
    });

    await runFormAction(
      entity,
      DataEventActions.Update,
      async () => await this.showData(entity, id),
      accountProfile
    );
  }

  async delete(
    entity: string,
    id: string,
    accountProfile: IAccountProfile
  ): Promise<void> {
    await runFormAction(
      entity,
      DataEventActions.Delete,
      async () => await this.showData(entity, id),
      accountProfile
    );

    const beforeData = await PortalDataHooksService.beforeDelete({
      dataApiService: this,
      entity,
      dataId: id,
    });

    const queryFilter = await this.appendPersistentQuery(
      entity,
      this._rDBMSApiDataService.whereEqualQueryFilterSchema(
        await this._entitiesApiService.getEntityPrimaryField(entity),
        id
      )
    );

    await PortalQueryImplementation.delete({
      entity,
      queryFilter,
      implementation: async () => {
        await this.getDataAccessInstance().delete(entity, queryFilter);
      },
    });

    await PortalDataHooksService.afterDelete({
      beforeData,
      dataApiService: this,
      entity,
      dataId: id,
    });
  }

  private async appendPersistentQuery(
    entity: string,
    filterSchema: QueryFilterSchema
  ): Promise<QueryFilterSchema> {
    const persistentFilter = await this._configurationApiService.show(
      "persistent_query",
      entity
    );

    if (persistentFilter.children.length === 0) {
      // TODO compile all the values
      return filterSchema;
    }

    return {
      operator: "and",
      children: [filterSchema, persistentFilter],
    };
  }

  async getRelationshipSettings(entity: string): Promise<{
    format: string;
    fields: string[];
  }> {
    const relationshipSettings = await this._configurationApiService.show(
      "entity_relation_template",
      entity
    );

    if (relationshipSettings.fields.length > 0) {
      return relationshipSettings;
    }
    const [hiddenColumns, primaryField, entityFields] = await Promise.all([
      this._configurationApiService.show("hidden_entity_table_columns", entity),
      this._entitiesApiService.getEntityPrimaryField(entity),
      this._entitiesApiService.getEntityFields(entity),
    ]);
    const displayField =
      entityFields.filter((field) => {
        return (
          field.name !== primaryField &&
          !hiddenColumns.includes(field.name) &&
          GOOD_FIELD_TYPES_FOR_LIST.includes(field.type)
        );
      })[0]?.name || primaryField;

    const configuration = {
      fields: [displayField],
      format: `{{ ${displayField} }}`,
    };

    await this._configurationApiService.upsert(
      "entity_relation_template",
      configuration,
      entity
    );
    return configuration;
  }

  private returnOnlyDataThatAreAllowed(
    data: Record<string, unknown>,
    allowedFields: string[]
  ) {
    return Object.fromEntries(
      allowedFields.map((field) => [field, data[field]])
    );
  }
}

export const dataApiService = new DataApiService(
  rDBMSDataApiService,
  entitiesApiService,
  configurationApiService
);