File

src/app/core/basic-datatypes/entity/entity.datatype.ts

Description

Datatype for the EntitySchemaService to handle a single reference to another entity. Stored as a simple ID string.

Example:

@DatabaseField({dataType: 'entity', additional: 'Child'}) relatedEntity: string;

Extends

StringDatatype

Index

Properties
Methods

Methods

Async anonymize
anonymize(value, schemaField: EntitySchemaField, parent)
Inherited from DefaultDatatype

Recursively calls anonymize on the referenced entity and saves it.

Parameters :
Name Type Optional
value No
schemaField EntitySchemaField No
parent No
Returns : Promise<string>
Async importMapFunction
importMapFunction(val: any, schemaField: EntitySchemaField, additional: string, importProcessingContext: ImportProcessingContext)
Inherited from DefaultDatatype
Defined in DefaultDatatype:58

Maps a value from an import to an actual entity in the database by comparing the value with the given field of entities. Handles type conversion between numbers and strings to improve matching.

Parameters :
Name Type Optional Description
val any No

The value from an import that should be mapped to an entity reference.

schemaField EntitySchemaField No

The config defining details of the field that will hold the entity reference after mapping.

additional string No

The field of the referenced entity that should be compared with the val. (e.g. if we run importMapFunction for a field that is an entity-reference to a "School" entity, this could be "name" if the "School" entity has a "name" property and the import should use that name to match the correct school)

importProcessingContext ImportProcessingContext No

context to share information across calls for multiple columns and rows.

Returns : Promise<string | undefined>

Promise resolving to the ID of the matched entity or undefined if no match is found.

transformToDatabaseFormat
transformToDatabaseFormat(value)
Inherited from DefaultDatatype
Defined in DefaultDatatype:38
Parameters :
Name Optional
value No
Returns : any
transformToObjectFormat
transformToObjectFormat(value)
Inherited from DefaultDatatype
Defined in DefaultDatatype:42
Parameters :
Name Optional
value No
Returns : any
importIncompleteAdditionalConfigBadge
importIncompleteAdditionalConfigBadge(col: ColumnMapping)
Inherited from DefaultDatatype

Output a label indicating whether the given column mapping needs user configuration for the "additional" config or has a valid, complete "additional" config. returns "undefined" if no user action is required.

Parameters :
Name Type Optional
col ColumnMapping No
Returns : string

Properties

Static dataType
Type : string
Default value : "entity"
Inherited from DefaultDatatype
Defined in DefaultDatatype:41
editComponent
Type : string
Default value : "EditEntity"
Inherited from DefaultDatatype
Defined in DefaultDatatype:43
importAllowsMultiMapping
Default value : true
Inherited from DefaultDatatype
Defined in DefaultDatatype:46
importConfigComponent
Type : string
Default value : "EntityImportConfig"
Inherited from DefaultDatatype
Defined in DefaultDatatype:45
Static label
Type : string
Default value : $localize`:datatype-label:link to another record`
Inherited from DefaultDatatype
Defined in DefaultDatatype:42
viewComponent
Type : string
Default value : "DisplayEntity"
Inherited from DefaultDatatype
Defined in DefaultDatatype:44
import { Injectable, inject } from "@angular/core";
import { StringDatatype } from "../string/string.datatype";
import { EntitySchemaField } from "../../entity/schema/entity-schema-field";
import { EntityMapperService } from "../../entity/entity-mapper/entity-mapper.service";
import { EntityActionsService } from "../../entity/entity-actions/entity-actions.service";
import { Logging } from "app/core/logging/logging.service";
import { ImportProcessingContext } from "../../import/import-processing-context";
import { EntitySchemaService } from "../../entity/schema/entity-schema.service";

/**
 * Datatype for the EntitySchemaService to handle a single reference to another entity.
 * Stored as a simple ID string.
 *
 * Example:
 *
 * `@DatabaseField({dataType: 'entity', additional: 'Child'}) relatedEntity: string;`
 */
@Injectable()
export class EntityDatatype extends StringDatatype {
  private entityMapper = inject(EntityMapperService);
  private removeService = inject(EntityActionsService);
  private schemaService = inject(EntitySchemaService);

  static override dataType = "entity";
  static override label: string = $localize`:datatype-label:link to another record`;
  override editComponent = "EditEntity";
  override viewComponent = "DisplayEntity";
  override importConfigComponent = "EntityImportConfig";
  override importAllowsMultiMapping = true;

  /**
   * Maps a value from an import to an actual entity in the database by comparing the value with the given field of entities.
   * Handles type conversion between numbers and strings to improve matching.
   *
   * @param val The value from an import that should be mapped to an entity reference.
   * @param schemaField The config defining details of the field that will hold the entity reference after mapping.
   * @param additional The field of the referenced entity that should be compared with the val. (e.g. if we run importMapFunction for a field that is an entity-reference to a "School" entity, this could be "name" if the "School" entity has a "name" property and the import should use that name to match the correct school)
   * @param importProcessingContext context to share information across calls for multiple columns and rows.
   * @returns Promise resolving to the ID of the matched entity or undefined if no match is found.
   */
  override async importMapFunction(
    val: any,
    schemaField: EntitySchemaField,
    additional: string,
    importProcessingContext: ImportProcessingContext,
  ): Promise<string | undefined> {
    if (!additional || val == null) {
      return undefined;
    }

    const context = new EntityFieldImportContext(
      importProcessingContext,
      schemaField,
    );

    await this.loadImportMapEntities(schemaField.additional, context);

    context.filteredEntities = context.filteredEntities.filter(
      (entity) => normalizeValue(entity[additional]) === normalizeValue(val),
    );

    // return first match
    return context.filteredEntities.length > 0
      ? context.filteredEntities[0]._id
      : undefined;
  }

  /**
   * Load the required entity type's entities into cache if not available yet.
   * @private
   */
  private async loadImportMapEntities(
    entityType: string,
    context: EntityFieldImportContext,
  ) {
    if (context.entities) {
      return;
    }

    try {
      context.entities = (await this.entityMapper.loadType(entityType)).map(
        (e) => this.schemaService.transformEntityToDatabaseFormat(e),
      );
    } catch (error) {
      Logging.error("Error in EntityDatatype importMapFunction:", error);
      return undefined;
    }
  }

  /**
   * Recursively calls anonymize on the referenced entity and saves it.
   * @param value
   * @param schemaField
   * @param parent
   */
  override async anonymize(
    value,
    schemaField: EntitySchemaField,
    parent,
  ): Promise<string> {
    const referencedEntity = await this.entityMapper.load(
      schemaField.additional,
      value,
    );

    if (!referencedEntity) {
      // TODO: remove broken references?
      return value;
    }

    await this.removeService.anonymize(referencedEntity);
    return value;
  }
}

/**
 * Normalizes a value for comparison, converting it to a standardized string format.
 * Ensures both numbers and strings are treated consistently.
 *
 * @param val The value to normalize.
 * @returns The normalized value as a string.
 */
function normalizeValue(val: any): string {
  if (val == null) {
    return "";
  }
  return String(val).trim().toLowerCase(); // Convert everything to string and trim spaces
}

/**
 * Manage cache access to the current import processing context.
 */
class EntityFieldImportContext {
  private contextKey: string;

  constructor(
    private globalContext: ImportProcessingContext,
    private schemaField: EntitySchemaField,
  ) {
    this.contextKey = `${schemaField.id}_${globalContext.rowIndex}`;

    if (!globalContext[this.contextKey]) {
      globalContext[this.contextKey] = {};
    }
  }

  /**
   * Entities (in database format for easier comparison!)
   */
  get entities(): any[] | undefined {
    return this.globalContext[`entities_${this.schemaField.additional}`];
  }

  set entities(value: any[]) {
    this.globalContext[`entities_${this.schemaField.additional}`] = value;
  }

  /**
   * Entities already filter by any other column conditions
   * (in database format for easier comparison!)
   */
  get filteredEntities(): any[] {
    return (
      this.globalContext[this.contextKey].filteredEntities ??
      this.entities ??
      []
    );
  }

  set filteredEntities(value: any[]) {
    this.globalContext[this.contextKey].filteredEntities = value;
  }
}

results matching ""

    No results matching ""