File

lib/common/explorer/basic-explorer.service.ts

Description

Basic implementation of the ExplorerService. Provides functionality for exploring and analyzing the database schema.

Extends

ExplorerService

Index

Methods
Accessors

Constructor

constructor(saveHandlers: EntitySaveHandler[], dataSource: DataSource, targetRep: Repository<ExplorerTargetEntity>, columnRep: Repository<ExplorerColumnEntity>, logger: Logger)
Parameters :
Name Type Optional
saveHandlers EntitySaveHandler[] No
dataSource DataSource No
targetRep Repository<ExplorerTargetEntity> No
columnRep Repository<ExplorerColumnEntity> No
logger Logger No

Methods

Async analyzeDatabase
analyzeDatabase()
Inherited from ExplorerService

Analyzes the database and saves the results to the corresponding entities.

Returns : Promise<void>
Private applyColumnFilter
applyColumnFilter(qb: SelectQueryBuilder, exCol: ExplorerColumn, path: string, value: string)
Type parameters :
  • T

Applies a column filter to a SelectQueryBuilder.

Parameters :
Name Type Optional Description
qb SelectQueryBuilder<T> No
  • The SelectQueryBuilder to apply the column filter to.
exCol ExplorerColumn No
  • The ExplorerColumn data
path string No
  • The column name to filter.
value string No
  • The column filter value.
Returns : void
Private applyDateFilter
applyDateFilter(qb: SelectQueryBuilder, aliasOrEntity: string, column: string, value: string)
Type parameters :
  • T

Applies a date filter to a SelectQueryBuilder.

Parameters :
Name Type Optional Description
qb SelectQueryBuilder<T> No
  • The SelectQueryBuilder to apply the date filter to.
aliasOrEntity string No
  • The alias or entity to apply the filter on.
column string No
  • The column name to filter.
value string No
  • The date filter value.
Returns : void
Private Async applyFilterParams
applyFilterParams(qb: SelectQueryBuilder, targetData: TargetData, filterParams: Record)
Type parameters :
  • T

Applies filter parameters to a SelectQueryBuilder for a specific entity.

Parameters :
Name Type Optional Description
qb SelectQueryBuilder<T> No
  • The SelectQueryBuilder to apply filters to.
targetData TargetData No
  • Information about the target entity.
filterParams Record<string | string> No
  • The filter parameters to apply.
Returns : any
Private Async attachRelations
attachRelations(row: T, targetData: TargetData, selectParams: ExplorerSelectParams, visitedEntities: string[], maxDepth)
Type parameters :
  • T

Attaches relations to the given row recursively up to the specified depth.

Parameters :
Name Type Optional Default value Description
row T No

The row to attach relations to.

targetData TargetData No

The target data of the current row.

selectParams ExplorerSelectParams No {}

Parameters regulating sample width

visitedEntities string[] No []

The array of visited entity names.

maxDepth No Infinity

The maximum depth of relations to fetch. Defaults to Infinity.

Returns : unknown

The row with attached relations.

Async changeTarget
changeTarget(target: ExplorerTarget)
Inherited from ExplorerService

Change target data

Parameters :
Name Type Optional Description
target ExplorerTarget No
  • data of the target entity.
Returns : unknown
Private checkEntityAccess
checkEntityAccess(targetData: TargetData, targetParams: ExplorerTargetParams)

Checks requested user access to entity.

Parameters :
Name Type Optional Description
targetData TargetData No

The target data of the current row.

targetParams ExplorerTargetParams No
  • Fetch and check entity access params
Returns : any
Private Async detectAndMarkNamedColumn
detectAndMarkNamedColumn(target: ExplorerTargetEntity)

Defines a suitable column that can be used for naming.

Parameters :
Name Type Optional Description
target ExplorerTargetEntity No

The target entity with columns

Returns : any
Private getColsForSelect
getColsForSelect(targetData: TargetData, params: ExplorerSelectParams)

Generates a list of columns to extract.

Parameters :
Name Type Optional Default value Description
targetData TargetData No

The target data of the current row.

params ExplorerSelectParams No {}

Parameters regulating sample width

Returns : { colList: {}; referencedCols: {}; }
Private getColumnType
getColumnType(type: string)

Converts a string type name to a corresponding ColumnDataType.

Parameters :
Name Type Optional Description
type string No

The string type name.

Returns : ColumnDataType

The corresponding ColumnDataType.

Async getEntityData
getEntityData(target: string, rowId: string | number, maxDepth, targetParams?: ExplorerTargetParams)
Inherited from ExplorerService

Retrieves entity data for the given target and rowId, with relations attached up to the specified depth.

Parameters :
Name Type Optional Default value Description
target string No

The target entity name.

rowId string | number No

The ID of the row to fetch.

maxDepth No Infinity

The maximum depth of relations to fetch. Defaults to Infinity.

targetParams ExplorerTargetParams Yes
  • Fetch and check entity access params
Returns : unknown

A Promise that resolves to the entity object.

Async getPageableEntityData
getPageableEntityData(target: string, params?: PageableParams, targetParams?: ExplorerTargetParams)
Inherited from ExplorerService

Retrieves paginated entity data with relations.

Parameters :
Name Type Optional Description
target string No
  • The name of the target entity or table.
params PageableParams Yes
  • An optional object containing pageable parameters.
targetParams ExplorerTargetParams Yes
  • Fetch and check entity access params

A Promise that resolves to a PageableData object containing the paginated results.

Async getTargetData
getTargetData(target: string, targetParams: ExplorerTargetParams)
Inherited from ExplorerService

Retrieves target data for the specified target entity name.

Parameters :
Name Type Optional Default value Description
target string No

The target entity name.

targetParams ExplorerTargetParams No {}
  • Fetch and check entity access params

A Promise that resolves to the TargetData object, or null if not found.

Async getTargetList
getTargetList()
Inherited from ExplorerService

Getting all registered targets with count items inside.

Private isColumnUnique
isColumnUnique(md: EntityMetadata, column: ColumnMetadata)

Checks if the given column is unique within the entity metadata.

Parameters :
Name Type Optional Description
md EntityMetadata No

The entity metadata to check.

column ColumnMetadata No

The column metadata to check for uniqueness.

Returns : boolean

True if the column is unique, false otherwise.

Async removeEntity
removeEntity(target: string, id: string | number, targetParams?: ExplorerTargetParams)
Inherited from ExplorerService

Remove an entity by its ID.

Parameters :
Name Type Optional Description
target string No
  • The name of the target entity.
id string | number No
  • The ID of the entity to be removed.
targetParams ExplorerTargetParams Yes
  • Fetch and check entity access params
Returns : Promise<ObjectLiteral>

The removed entity.

Private Async saveColumn
saveColumn(column: ExplorerColumnEntity)

Saves the column entity to the repository, if it does not exist.

Parameters :
Name Type Optional Description
column ExplorerColumnEntity No

The column entity to save.

Returns : any
Async saveEntityData
saveEntityData(target: string, entity: T, targetParams?: ExplorerTargetParams)
Inherited from ExplorerService
Type parameters :
  • T

Save or update an entity including its nested entities.

Parameters :
Name Type Optional Description
target string No
  • The name of the target entity.
entity T No
  • The entity object to be saved or updated.
targetParams ExplorerTargetParams Yes
  • Fetch and check entity access params
Returns : Promise<T>

The saved or updated entity.

Private Async saveNestedEntities
saveNestedEntities(entity: T, targetData: TargetData, repository: Repository)
Type parameters :
  • T

Recursively save or update nested entities.

Parameters :
Name Type Optional Description
entity T No
  • The entity containing nested entities to be saved or updated.
targetData TargetData No
  • Metadata of the target entity.
repository Repository<any> No
  • The repository associated with the target entity.
Returns : Promise<T>

The saved or updated entity with its nested entities.

Private Async saveTarget
saveTarget(target: ExplorerTargetEntity)

Saves the target entity to the repository, if it does not exist.

Parameters :
Name Type Optional Description
target ExplorerTargetEntity No

The target entity to save.

Returns : any
Private Async setColumnProperties
setColumnProperties(c: ExplorerColumnEntity, relation: RelationMetadata, target: ExplorerTargetEntity)

Sets the properties of a column entity based on the given relation and target.

Parameters :
Name Type Optional Description
c ExplorerColumnEntity No

The column entity to set properties for.

relation RelationMetadata No

The relation metadata used to set properties.

target ExplorerTargetEntity No

The target entity to associate the column with.

Returns : any

Accessors

connection
getconnection()

Gets the current database connection.

import {
  ForbiddenException,
  Inject,
  Injectable,
  InternalServerErrorException,
  Logger,
  NotFoundException,
  Optional,
} from "@nestjs/common";
import { ExplorerTargetEntity } from "./entity/explorer-target.entity";
import { ExplorerColumnEntity } from "./entity/explorer-column.entity";
import { InjectDataSource, InjectRepository } from "@nestjs/typeorm";
import {
  Brackets,
  DataSource,
  EntityMetadata,
  ObjectLiteral,
  Repository,
  SelectQueryBuilder,
} from "typeorm";
import {
  ColumnDataType,
  EntitySaveHandler,
  ExplorerColumn,
  ExplorerSelectParams,
  ExplorerService,
  ExplorerTarget,
  ExplorerTargetParams,
  TargetData,
} from "./explorer.types";
import { ColumnMetadata } from "typeorm/metadata/ColumnMetadata";
import { RelationMetadata } from "typeorm/metadata/RelationMetadata";
import { LOGGER } from "../../shared/modules/log/log.constants";
import { UserUtils } from "../../shared/utils/user.utils";
import { TransformUtils } from "../../shared/utils/transform.utils";
import { Explorer } from "./explorer.constants";
import {
  PageableData,
  PageableParams,
  SortOrder,
} from "../../shared/modules/pageable/pageable.types";
import { ObjectUtils } from "../../shared/utils/object.utils";
import { LocalizedStringEntity } from "../../shared/modules/locale/entity/localized-string.entity";
import { Type } from "@nestjs/common/interfaces/type.interface";
import ENTITY_SAVE_HANDLER = Explorer.ENTITY_SAVE_HANDLER;
import parseParamsString = TransformUtils.parseParamsString;
import TARGET_RELATIONS_OBJECT = Explorer.TARGET_RELATIONS_OBJECT;
import TARGET_RELATIONS_SECTION = Explorer.TARGET_RELATIONS_SECTION;
import TARGET_RELATIONS_FULL = Explorer.TARGET_RELATIONS_FULL;
import hasAccessForRoles = UserUtils.hasAccessForRoles;

/**
 * Basic implementation of the ExplorerService.
 * Provides functionality for exploring and analyzing the database schema.
 */
@Injectable()
export class BasicExplorerService extends ExplorerService {
  constructor(
    @Optional()
    @Inject(ENTITY_SAVE_HANDLER)
    private readonly saveHandlers: EntitySaveHandler[] = [],
    @InjectDataSource()
    private readonly dataSource: DataSource,
    @InjectRepository(ExplorerTargetEntity)
    private readonly targetRep: Repository<ExplorerTargetEntity>,
    @InjectRepository(ExplorerColumnEntity)
    private readonly columnRep: Repository<ExplorerColumnEntity>,
    @Inject(LOGGER) private readonly logger: Logger,
  ) {
    super();
  }

  /**
   * Gets the current database connection.
   * @returns The current database connection.
   */
  private get connection() {
    return this.dataSource.manager.connection;
  }

  /**
   * Analyzes the database and saves the results to the corresponding entities.
   */
  async analyzeDatabase(): Promise<void> {
    this.logger.log(`Starting database analysis`);
    for (const md of this.connection.entityMetadatas) {
      if (md.tableType !== "regular") {
        continue;
      }
      const t = new ExplorerTargetEntity();
      t.target = md.targetName;
      t.tableName = md.tableName;
      await this.saveTarget(t);
      t.columns = [];
      for (const column of md.nonVirtualColumns) {
        const c = new ExplorerColumnEntity();
        c.target = t;
        c.id = `${t.tableName}.${column.databasePath}`;
        c.property = column.propertyName;
        c.type = this.getColumnType(column.type as string);
        c.primary =
          md.primaryColumns.find(
            (pc) => pc.propertyName === column.propertyName,
          ) !== undefined;
        c.unique = this.isColumnUnique(md, column);
        c.multiple = false;
        t.columns.push(c);
        await this.saveColumn(c);
      }
      for (const relation of [
        ...md.oneToManyRelations,
        ...md.manyToManyRelations,
      ]) {
        const c = new ExplorerColumnEntity();
        await this.setColumnProperties(c, relation, t);
        c.multiple = true;
        t.columns.push(c);
        await this.saveColumn(c);
      }
      for (const relation of [
        ...md.oneToOneRelations,
        ...md.manyToOneRelations,
      ]) {
        const c = new ExplorerColumnEntity();
        await this.setColumnProperties(c, relation, t);
        c.multiple = false;
        t.columns.push(c);
        await this.saveColumn(c);
      }
      await this.detectAndMarkNamedColumn(t);
    }
    this.logger.log(`Database was analyzed`);
  }

  /**
   * Save or update an entity including its nested entities.
   * @param target - The name of the target entity.
   * @param entity - The entity object to be saved or updated.
   * @param targetParams - Fetch and check entity access params
   * @returns The saved or updated entity.
   * @throws {NotFoundException} If the target entity is not found.
   */
  async saveEntityData<T = any>(
    target: string,
    entity: T,
    targetParams?: ExplorerTargetParams,
  ): Promise<T> {
    for (const handler of this.saveHandlers) {
      entity = handler.handle(target, entity, targetParams?.checkUserAccess);
    }
    const targetData = await this.getTargetData(target, targetParams);
    const repository = this.connection.getRepository(targetData.entity.target);
    if (!entity[targetData.primaryColumn.property]) {
      entity = repository.create(entity) as T;
    }
    return await this.saveNestedEntities(entity, targetData, repository);
  }

  /**
   * Change target data
   * @param target - data of the target entity.
   */
  async changeTarget(target: ExplorerTarget) {
    return await this.targetRep.save(target);
  }

  /**
   * Getting all registered targets with count items inside.
   */
  async getTargetList(): Promise<ExplorerTarget[]> {
    const res: ExplorerTarget[] = await this.targetRep.find({
      relations: Explorer.TARGET_RELATIONS_BASIC,
    });
    for (const v of res) {
      const rep = this.connection.getRepository(v.target);
      v.size = await rep.count();
    }
    return res;
  }

  /**
   * Remove an entity by its ID.
   * @param target - The name of the target entity.
   * @param id - The ID of the entity to be removed.
   * @param targetParams - Fetch and check entity access params
   * @returns The removed entity.
   * @throws {NotFoundException} If the target entity or the entity with the specified ID is not found.
   */
  async removeEntity(
    target: string,
    id: string | number,
    targetParams?: ExplorerTargetParams,
  ): Promise<ObjectLiteral> {
    const targetData = await this.getTargetData(target, targetParams);
    if (
      targetParams?.checkUserAccess &&
      !this.checkEntityAccess(targetData, targetParams)
    ) {
      throw new ForbiddenException(`Can't get access to target: ${target}`);
    }
    const repository = this.connection.getRepository(targetData.entity.target);
    const entity = await repository.findOne({
      where: { [targetData.primaryColumn.property]: id },
    });
    if (!entity) {
      throw new NotFoundException(
        `Entity with ID ${id} not found in table ${target}`,
      );
    }
    return await repository.remove(entity);
  }

  /**
   * Retrieves entity data for the given target and rowId, with relations attached up to the specified depth.
   * @param target The target entity name.
   * @param rowId The ID of the row to fetch.
   * @param maxDepth The maximum depth of relations to fetch. Defaults to Infinity.
   * @param targetParams - Fetch and check entity access params
   * @returns A Promise that resolves to the entity object.
   */
  async getEntityData(
    target: string,
    rowId: string | number,
    maxDepth = Infinity,
    targetParams?: ExplorerTargetParams,
  ) {
    const tParams = targetParams ?? {};
    tParams.object = true;
    const targetData = await this.getTargetData(target, tParams);
    if (!targetData) {
      throw new NotFoundException(`Target entity not found: ${target}`);
    }
    if (
      targetParams?.checkUserAccess &&
      !this.checkEntityAccess(targetData, targetParams)
    ) {
      throw new ForbiddenException(`Can't get access to target: ${target}`);
    }
    const repository = this.connection.getRepository(targetData.entity.target);
    const row = await repository.findOne({
      where: { [targetData.primaryColumn.property]: rowId },
    });
    if (!row) {
      throw new NotFoundException(
        `Row with ID ${rowId} not found in table ${target}`,
      );
    }
    return await this.attachRelations(
      row,
      targetData,
      { object: true },
      [],
      maxDepth,
    );
  }

  /**
   * Retrieves paginated entity data with relations.
   *
   * @param target - The name of the target entity or table.
   * @param params - An optional object containing pageable parameters.
   * @param targetParams - Fetch and check entity access params
   * @returns A Promise that resolves to a PageableData object containing the paginated results.
   * @throws NotFoundException if the target entity is not found.
   */
  async getPageableEntityData(
    target: string,
    params?: PageableParams,
    targetParams?: ExplorerTargetParams,
  ): Promise<PageableData> {
    const tParams = targetParams ?? {};
    tParams.section = true;
    const targetData = await this.getTargetData(target, tParams);
    if (!targetData) {
      throw new NotFoundException(`Target entity not found: ${target}`);
    }
    const repository = this.connection.getRepository(targetData.entity.target);
    const primaryColumnProperty = targetData.primaryColumn.property;
    const limit = params?.limit || 20;
    const page = params?.page || 1;
    const sort = params?.sort || primaryColumnProperty;
    const order = params?.order || SortOrder.DESC;
    const qb = repository.createQueryBuilder("entity");
    if (params?.filter) {
      const filterParams = parseParamsString(params.filter);
      await this.applyFilterParams(qb, targetData, filterParams);
    }
    const colsForSelect = this.getColsForSelect(targetData, {
      section: true,
      prefix: "entity.",
    });
    if (!colsForSelect.referencedCols.length) {
      qb.select(colsForSelect.colList);
    }
    const [items, totalCount] = await qb
      .skip((page - 1) * limit)
      .take(limit)
      .orderBy(`entity.${sort}`, order)
      .getManyAndCount();
    const itemsWithRelations = await Promise.all(
      items.map(
        async (item) =>
          await this.attachRelations(
            item,
            targetData,
            { section: true },
            [],
            3,
          ),
      ),
    );
    return new PageableData(itemsWithRelations, totalCount, page, limit);
  }

  /**
   * Retrieves target data for the specified target entity name.
   * @param target The target entity name.
   * @param targetParams - Fetch and check entity access params
   * @returns A Promise that resolves to the TargetData object, or null if not found.
   */
  async getTargetData(
    target: string,
    targetParams: ExplorerTargetParams = {},
  ): Promise<TargetData> {
    let relations = ["columns", "canRead", "canWrite"];
    if (targetParams.fullRelations) {
      if (!targetParams.section || !targetParams.object) {
        relations = TARGET_RELATIONS_FULL;
      } else {
        relations = targetParams.section
          ? TARGET_RELATIONS_SECTION
          : TARGET_RELATIONS_OBJECT;
      }
    }
    const entity = await this.targetRep.findOne({
      where: [{ target }, { tableName: target }, { alias: target }],
      relations,
    });
    if (!entity) {
      return null;
    }
    if (targetParams.section) {
      entity.columns = entity.columns.filter((c) => c.sectionEnabled);
      ObjectUtils.sort(entity.columns, "sectionPriority");
      entity.actions = entity.actions?.filter((a) => a.type === "section");
    } else if (targetParams.object) {
      entity.columns = entity.columns.filter((c) => c.objectEnabled);
      ObjectUtils.sort(entity.columns, "objectPriority");
      entity.actions = entity.actions?.filter((a) => a.type === "object");
    }
    ObjectUtils.sort(entity.actions, "priority");
    const primaryColumn = entity.columns.find((c) => c.primary === true);
    const namedColumn = entity.columns.find((c) => c.named === true);
    const targetData = { entity, primaryColumn, namedColumn };
    if (!targetData) {
      throw new NotFoundException(`Target entity not found: ${target}`);
    }
    if (
      targetParams?.checkUserAccess &&
      !this.checkEntityAccess(targetData, targetParams)
    ) {
      throw new ForbiddenException(`Can't get access to target: ${target}`);
    }
    return targetData;
  }

  /**
   * Applies filter parameters to a SelectQueryBuilder for a specific entity.
   *
   * @param qb - The SelectQueryBuilder to apply filters to.
   * @param targetData - Information about the target entity.
   * @param filterParams - The filter parameters to apply.
   */
  private async applyFilterParams<T = any>(
    qb: SelectQueryBuilder<T>,
    targetData: TargetData,
    filterParams: Record<string, string>,
  ) {
    for (const key in filterParams) {
      const value = filterParams[key];
      const column = targetData.entity.columns.find((c) => c.property === key);
      if (!column) {
        continue;
      }
      const prop = column.property;
      if (column.type === "reference") {
        const match = value.match(/\{([^}]*)}/);
        const parts = match[1].split(".");
        const targetName = parts[0];
        const colName = parts[1];
        const clearValue = value.replace(/\{[^}]*}/g, "");
        const refTarget = await this.getTargetData(targetName, {
          section: true,
        });
        const refColumn = refTarget.entity.columns.find(
          (c) => c.property === colName,
        );
        const alias = colName + targetName;
        qb.innerJoinAndSelect(`entity.${prop}`, alias);
        if (refColumn.type === "date") {
          this.applyDateFilter(qb, alias, colName, clearValue);
        } else {
          this.applyColumnFilter(
            qb,
            refColumn,
            `${alias}.${colName}`,
            clearValue,
          );
        }
      } else {
        if (column.type === "date") {
          this.applyDateFilter(qb, `entity`, prop, value);
        } else {
          this.applyColumnFilter(qb, column, `entity.${prop}`, value);
        }
      }
    }
  }

  /**
   * Defines a suitable column that can be used for naming.
   * @param target The target entity with columns
   */
  private async detectAndMarkNamedColumn(target: ExplorerTargetEntity) {
    let namedCol = target.columns.find((c) => c.named);
    if (namedCol) {
      return;
    }
    namedCol = target.columns.find(
      (c) => c.referencedEntityName === LocalizedStringEntity.name,
    );
    if (!namedCol) {
      namedCol = target.columns.find((c) => c.unique && c.type === "string");
    }
    if (!namedCol) {
      namedCol = target.columns.find((c) => c.primary);
    }
    namedCol.named = true;
    await this.columnRep.save(namedCol);
  }

  /**
   * Applies a date filter to a SelectQueryBuilder.
   *
   * @param qb - The SelectQueryBuilder to apply the date filter to.
   * @param aliasOrEntity - The alias or entity to apply the filter on.
   * @param column - The column name to filter.
   * @param value - The date filter value.
   */
  private applyDateFilter<T = any>(
    qb: SelectQueryBuilder<T>,
    aliasOrEntity: string,
    column: string,
    value: string,
  ) {
    const match = value.match(/FROM(\d+)TO(\d+)/);
    const fromTimestamp = match[1];
    const toTimestamp = match[2];
    const fromDate = new Date(parseInt(fromTimestamp, 10));
    const toDate = new Date(parseInt(toTimestamp, 10));
    qb.andWhere(
      new Brackets((sqb) => {
        sqb.andWhere(`${aliasOrEntity}.${column} >= :from${column}`, {
          [`from${column}`]: fromDate.toJSON(),
        });
        sqb.andWhere(`${aliasOrEntity}.${column} <= :to${column}`, {
          [`to${column}`]: toDate.toJSON(),
        });
      }),
    );
  }

  /**
   * Applies a column filter to a SelectQueryBuilder.
   *
   * @param qb - The SelectQueryBuilder to apply the column filter to.
   * @param exCol - The ExplorerColumn data
   * @param path - The column name to filter.
   * @param value - The column filter value.
   */
  private applyColumnFilter<T = any>(
    qb: SelectQueryBuilder<T>,
    exCol: ExplorerColumn,
    path: string,
    value: string,
  ) {
    if (exCol.type === "boolean") {
      value = value === "true" ? "1" : "0";
    }
    const exactMatch = !(value.startsWith("%") && value.endsWith("%"));
    qb.andWhere(`${path} ${exactMatch ? "=" : "LIKE"} :${path}`, {
      [path]: value,
    });
  }

  /**
   * Recursively save or update nested entities.
   * @param entity - The entity containing nested entities to be saved or updated.
   * @param targetData - Metadata of the target entity.
   * @param repository - The repository associated with the target entity.
   * @returns The saved or updated entity with its nested entities.
   */
  private async saveNestedEntities<T = any>(
    entity: T,
    targetData: TargetData,
    repository: Repository<any>,
  ): Promise<T> {
    const referencedCols = targetData.entity.columns.filter(
      (c) => c.type === "reference",
    );
    for (const col of referencedCols) {
      const relationProp = col.property;
      if (!entity?.hasOwnProperty(relationProp)) {
        continue;
      }
      const relatedEntityData = entity[relationProp];
      if (relatedEntityData) {
        const currTargetData = await this.getTargetData(
          col.referencedEntityName,
        );
        if (!currTargetData) {
          entity[relationProp] = repository.create();
        } else {
          const relatedRepository = this.connection.getRepository(
            currTargetData.entity.target,
          );
          if (Array.isArray(relatedEntityData) && col.multiple) {
            for (let i = 0; i < relatedEntityData.length; i++) {
              entity[relationProp][i] = await this.saveNestedEntities(
                relatedEntityData[i],
                currTargetData,
                relatedRepository,
              );
            }
          } else {
            entity[relationProp] = await this.saveNestedEntities(
              relatedEntityData,
              currTargetData,
              relatedRepository,
            );
          }
        }
      }
    }
    return await repository.save(entity);
  }

  /**
   * Attaches relations to the given row recursively up to the specified depth.
   * @param row The row to attach relations to.
   * @param targetData The target data of the current row.
   * @param selectParams Parameters regulating sample width
   * @param visitedEntities The array of visited entity names.
   * @param maxDepth The maximum depth of relations to fetch. Defaults to Infinity.
   * @returns The row with attached relations.
   */
  private async attachRelations<T = any>(
    row: T,
    targetData: TargetData,
    selectParams: ExplorerSelectParams = {},
    visitedEntities: string[] = [],
    maxDepth = Infinity,
  ) {
    if (maxDepth < 0) {
      throw new InternalServerErrorException("maxDepth should be non-negative");
    }
    const colsForSelect = this.getColsForSelect(targetData, selectParams);
    const relations = colsForSelect.referencedCols.map((c) => c.property);
    if (!row || !relations.length || maxDepth <= 0) {
      return row;
    }
    const repository = this.connection.getRepository(targetData.entity.target);
    const idProp = targetData.primaryColumn.property;
    const visitedKey = `${targetData.entity.target}_${row[idProp]}`;
    if (visitedEntities.includes(visitedKey)) {
      return undefined;
    }
    visitedEntities.push(visitedKey);
    const newRow = await repository.findOne({
      select: colsForSelect.colList,
      where: { [idProp]: row[idProp] },
      relations,
    });
    const withRelations = new (repository.metadata.target as Type)();
    Object.assign(withRelations, row, newRow);
    for (const k in withRelations) {
      if (relations.indexOf(k) === -1) {
        continue;
      }
      const colData = targetData.entity.columns.find((c) => c.property === k);
      const currTargetData = await this.getTargetData(
        colData.referencedEntityName,
        {
          section: selectParams.section,
          object: selectParams.object,
        },
      );
      if (Array.isArray(withRelations[k]) && colData.multiple) {
        for (const key in withRelations[k]) {
          withRelations[k][key] = await this.attachRelations(
            withRelations[k][key],
            currTargetData,
            selectParams,
            visitedEntities.slice(),
            maxDepth - 1,
          );
        }
      } else {
        withRelations[k] = await this.attachRelations(
          withRelations[k],
          currTargetData,
          selectParams,
          visitedEntities.slice(),
          maxDepth - 1,
        );
      }
    }
    return withRelations;
  }

  /**
   * Generates a list of columns to extract.
   * @param targetData The target data of the current row.
   * @param params Parameters regulating sample width
   */
  private getColsForSelect(
    targetData: TargetData,
    params: ExplorerSelectParams = {},
  ) {
    const colList: string[] = [];
    const referencedCols: ExplorerColumn[] = [];
    const prefix = params.prefix ? params.prefix : "";
    for (const col of targetData.entity.columns) {
      if (col.virtual) {
        continue;
      }
      if (params.section && !col.sectionEnabled) {
        continue;
      }
      if (params.object && !col.objectEnabled) {
        continue;
      }
      if (col.type === "reference") {
        referencedCols.push(col);
      }
      colList.push(prefix + col.property);
    }
    return { colList, referencedCols };
  }

  /**
   * Saves the target entity to the repository, if it does not exist.
   * @param target The target entity to save.
   */
  private async saveTarget(target: ExplorerTargetEntity) {
    const t = await this.targetRep.findOne({
      where: { target: target.target },
    });
    if (t) {
      this.logger.verbose(`Entity ${target.target} already exists, skipping`);
      return;
    }
    await this.targetRep.save(target);
    this.logger.verbose(`Entity ${target.target} was created`);
  }

  /**
   * Saves the column entity to the repository, if it does not exist.
   * @param column The column entity to save.
   */
  private async saveColumn(column: ExplorerColumnEntity) {
    const c = await this.columnRep.findOne({ where: { id: column.id } });
    if (c) {
      this.logger.verbose(`Column ${column.id} already exists, skipping`);
      return;
    }
    await this.columnRep.save(column);
    this.logger.verbose(`Column ${column.id} was created`);
  }

  /**
   * Sets the properties of a column entity based on the given relation and target.
   * @param c The column entity to set properties for.
   * @param relation The relation metadata used to set properties.
   * @param target The target entity to associate the column with.
   */
  private async setColumnProperties(
    c: ExplorerColumnEntity,
    relation: RelationMetadata,
    target: ExplorerTargetEntity,
  ) {
    c.target = target;
    c.id = `${target.tableName}.${relation.propertyPath}`;
    c.property = relation.propertyName;
    c.type = "reference";
    c.referencedTableName = relation.inverseEntityMetadata.tableName;
    c.referencedEntityName = relation.inverseEntityMetadata.targetName;
    c.primary = false;
    c.unique = false;
  }

  /**
   * Checks if the given column is unique within the entity metadata.
   * @param md The entity metadata to check.
   * @param column The column metadata to check for uniqueness.
   * @returns True if the column is unique, false otherwise.
   */
  private isColumnUnique(md: EntityMetadata, column: ColumnMetadata) {
    for (const uniq of md.uniques) {
      if (
        uniq.columns.find((col) => col.propertyName === column.propertyName) !==
        undefined
      ) {
        return true;
      }
    }
    for (const ind of md.indices) {
      if (!ind.isUnique) {
        continue;
      }
      if (ind.columns.find((c) => c.propertyName === column.propertyName)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Checks requested user access to entity.
   * @param targetData The target data of the current row.
   * @param targetParams - Fetch and check entity access params
   */
  private checkEntityAccess(
    targetData: TargetData,
    targetParams: ExplorerTargetParams,
  ) {
    if (targetParams.readRequest) {
      return hasAccessForRoles(
        targetParams?.checkUserAccess.roles,
        targetData.entity.canRead,
      );
    }
    if (targetParams.writeRequest) {
      return hasAccessForRoles(
        targetParams?.checkUserAccess.roles,
        targetData.entity.canWrite,
      );
    }
    return (
      hasAccessForRoles(
        targetParams?.checkUserAccess.roles,
        targetData.entity.canWrite,
      ) &&
      hasAccessForRoles(
        targetParams?.checkUserAccess.roles,
        targetData.entity.canRead,
      )
    );
  }

  /**
   * Converts a string type name to a corresponding ColumnDataType.
   * @param type The string type name.
   * @returns The corresponding ColumnDataType.
   */
  private getColumnType(type: string): ColumnDataType {
    switch (type) {
      case "string":
      case "text":
      case "longtext":
      case "tinytext":
      case "mediumint":
      case "uuid":
      case "varchar":
      case "char":
      case "simple-array":
      case "simple-json":
      case "json":
      case "jsonb":
        return "string";
      case "int":
      case "int2":
      case "int4":
      case "int8":
      case "float4":
      case "float8":
      case "smallint":
      case "integer":
      case "bigint":
      case "numeric":
        return "number";
      case "boolean":
      case "bool":
        return "boolean";
      case "timestamp":
      case "timestamptz":
      case "date":
      case "time":
      case "timetz":
      case "interval":
        return "date";
      default:
        return "unknown";
    }
  }
}

results matching ""

    No results matching ""