src/app/core/basic-datatypes/entity/entity.datatype.ts
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;
Properties |
Methods |
Async anonymize | ||||||||||||
anonymize(value, schemaField: EntitySchemaField, parent)
|
||||||||||||
Inherited from
DefaultDatatype
|
||||||||||||
Defined in
DefaultDatatype:113
|
||||||||||||
Recursively calls anonymize on the referenced entity and saves it.
Parameters :
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 :
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 :
Returns :
any
|
transformToObjectFormat | ||||
transformToObjectFormat(value)
|
||||
Inherited from
DefaultDatatype
|
||||
Defined in
DefaultDatatype:42
|
||||
Parameters :
Returns :
any
|
importIncompleteAdditionalConfigBadge | ||||||
importIncompleteAdditionalConfigBadge(col: ColumnMapping)
|
||||||
Inherited from
DefaultDatatype
|
||||||
Defined in
DefaultDatatype:140
|
||||||
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 :
Returns :
string
|
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;
}
}