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: unknown, schemaField: EntitySchemaField, parent: unknown)
|
||||||||||||
|
Inherited from
DefaultDatatype
|
||||||||||||
|
Defined in
DefaultDatatype:225
|
||||||||||||
|
Recursively calls anonymize on the referenced entity and saves it.
Parameters :
Returns :
Promise<string>
|
| getExportColumns | ||||||
getExportColumns(schemaField: EntitySchemaField)
|
||||||
|
Inherited from
DefaultDatatype
|
||||||
|
Defined in
DefaultDatatype:52
|
||||||
|
Parameters :
Returns :
ExportColumnMapping[]
|
| Async importMapFunction | ||||||||||||||||||||
importMapFunction(val: any, schemaField: EntitySchemaField, additional: string | EntityAdditional, importProcessingContext: ImportProcessingContext)
|
||||||||||||||||||||
|
Inherited from
DefaultDatatype
|
||||||||||||||||||||
|
Defined in
DefaultDatatype:88
|
||||||||||||||||||||
|
Maps a value from an import to an actual entity in the database. Finds all column mappings targeting this field, resolves each column's comparison value (applying any configured value mapping), and progressively filters candidate entities until a unique match is found. Can be a plain string (legacy) or an object
Parameters :
Returns :
Promise<string | undefined>
Promise resolving to the ID of the matched entity or undefined if no match is found. |
| transformToDatabaseFormat | ||||||
transformToDatabaseFormat(value: unknown)
|
||||||
|
Inherited from
DefaultDatatype
|
||||||
|
Defined in
DefaultDatatype:39
|
||||||
|
Parameters :
Returns :
any
|
| transformToObjectFormat | ||||||
transformToObjectFormat(value: unknown)
|
||||||
|
Inherited from
DefaultDatatype
|
||||||
|
Defined in
DefaultDatatype:46
|
||||||
|
Parameters :
Returns :
any
|
| Static detectAllFieldsInEntity | ||||||||||||
detectAllFieldsInEntity(entityOrType: Entity | EntityConstructor, dataTypes: string | string[])
|
||||||||||||
|
Inherited from
DefaultDatatype
|
||||||||||||
|
Defined in
DefaultDatatype:98
|
||||||||||||
|
Detect all fields of the given datatype(s) in an entity's schema.
Parameters :
Returns :
literal type[]
Array of matching fields with their id and schema definition. |
| Static detectFieldInEntity | ||||||||||||
detectFieldInEntity(entityOrType: Entity | EntityConstructor, dataTypes: string | string[])
|
||||||||||||
|
Inherited from
DefaultDatatype
|
||||||||||||
|
Defined in
DefaultDatatype:83
|
||||||||||||
|
Detect the first field of the given datatype(s) in an entity's schema. Scans the schema for a field whose Subclasses typically override this without the extra
Parameters :
Returns :
string | undefined
The field name of the first matching field, or |
| normalizeSchemaField | ||||||||
normalizeSchemaField(schemaField: EntitySchemaField)
|
||||||||
|
Inherited from
DefaultDatatype
|
||||||||
|
Defined in
DefaultDatatype:217
|
||||||||
|
Return the (potentially adjusted) schema field for this datatype. Called when schema fields are set up (e.g. from config), allowing the datatype to normalize or fill in required settings. Override this in a subclass to enforce constraints
(e.g. always setting
Parameters :
Returns :
EntitySchemaField
The schema field to use (default: unchanged) |
| sortValue | ||||||
sortValue(_fieldValue: EntityType)
|
||||||
|
Inherited from
DefaultDatatype
|
||||||
|
Defined in
DefaultDatatype:253
|
||||||
|
Return a comparable primitive for sorting this field's value in a list column.
Return
Parameters :
Returns :
number | string | undefined
|
| Static dataType |
Type : string
|
Default value : "entity"
|
|
Inherited from
DefaultDatatype
|
|
Defined in
DefaultDatatype:45
|
| editComponent |
Type : string
|
Default value : "EditEntity"
|
|
Inherited from
DefaultDatatype
|
|
Defined in
DefaultDatatype:47
|
| importAllowsMultiMapping |
Type : unknown
|
Default value : true
|
|
Inherited from
DefaultDatatype
|
|
Defined in
DefaultDatatype:50
|
| importConfigComponent |
Type : string
|
Default value : "EntityImportConfig"
|
|
Inherited from
DefaultDatatype
|
|
Defined in
DefaultDatatype:49
|
| Static label |
Type : string
|
Default value : $localize`:datatype-label:link to another record`
|
|
Inherited from
DefaultDatatype
|
|
Defined in
DefaultDatatype:46
|
| viewComponent |
Type : string
|
Default value : "DisplayEntity"
|
|
Inherited from
DefaultDatatype
|
|
Defined in
DefaultDatatype:48
|
import { inject, Injectable } 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";
import { Entity, EntityConstructor } from "../../entity/model/entity";
import { ExportColumnMapping } from "../../entity/default-datatype/default.datatype";
import { EntityRegistry } from "../../entity/database-entity.decorator";
/**
* 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);
private readonly entityRegistry = inject(EntityRegistry);
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;
override getExportColumns(
schemaField: EntitySchemaField,
): ExportColumnMapping[] {
if (!schemaField.label) {
return [];
}
return [
{
keySuffix: "",
label: schemaField.label,
resolveValue: (value) => value,
},
{
keySuffix: "_readable",
label: schemaField.label + " (readable)",
resolveValue: async (value: string | string[]) =>
this.loadRelatedEntitiesToString(value, schemaField),
},
];
}
/**
* Maps a value from an import to an actual entity in the database.
*
* Finds all column mappings targeting this field, resolves each column's comparison value
* (applying any configured value mapping), and progressively filters candidate entities
* until a unique match is found.
*
* @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.
* Can be a plain string (legacy) or an object `{ refField: string, valueMapping?: any }`.
* @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 | EntityAdditional,
importProcessingContext: ImportProcessingContext,
): Promise<string | undefined> {
const fieldConfig = normalizeEntityAdditional(additional);
if (!fieldConfig?.refField || val == null) {
return undefined;
}
const context = new EntityFieldImportContext(
importProcessingContext,
schemaField,
);
await this.loadImportMapEntities(schemaField.additional, context);
const columnMappings = this.getColumnMappingsForField(
schemaField.id,
importProcessingContext,
);
for (const mapping of columnMappings) {
const mappingConfig = normalizeEntityAdditional(mapping.additional);
const rawValue = importProcessingContext.row[mapping.column];
const expectedValue = await this.resolveColumnValue(
rawValue,
mappingConfig?.refField,
mappingConfig?.valueMapping,
context,
importProcessingContext,
);
context.filteredEntities = context.filteredEntities.filter(
(entity) =>
normalizeValue(entity[mappingConfig?.refField]) === expectedValue,
);
}
return this.pickSingleMatch(context.filteredEntities);
}
/**
* Returns all column mappings that target the given field.
*/
private getColumnMappingsForField(
fieldId: string,
importProcessingContext: ImportProcessingContext,
) {
return importProcessingContext.importSettings.columnMapping.filter(
(m) => m.propertyName === fieldId,
);
}
/**
* Resolves the effective comparison value for a column,
* applying any configured value mapping through the referenced field's datatype.
*/
private async resolveColumnValue(
rawValue: any,
refField: string,
valueMapping: any | undefined,
context: EntityFieldImportContext,
importProcessingContext: ImportProcessingContext,
): Promise<string> {
if (valueMapping === undefined) {
return normalizeValue(rawValue);
}
const refFieldSchema = context.refEntityCtor?.schema?.get(refField);
const refDatatype = refFieldSchema
? this.schemaService.getDatatypeOrDefault(refFieldSchema.dataType)
: null;
if (!refDatatype) {
return normalizeValue(rawValue);
}
const mappedValue = await refDatatype.importMapFunction(
rawValue,
refFieldSchema,
valueMapping,
importProcessingContext,
);
const dbFormat = refDatatype.transformToDatabaseFormat(
mappedValue,
refFieldSchema,
);
return normalizeValue(dbFormat);
}
/**
* Returns the entity ID if exactly one candidate remains, otherwise undefined.
*/
private pickSingleMatch(candidates: any[]): string | undefined {
if (candidates.length === 1) {
return candidates[0]._id;
}
if (candidates.length > 1) {
Logging.debug(
"No unique match found in EntityDatatype importMapFunction",
candidates.length,
);
}
return undefined;
}
/**
* Load the required entity type's entities into context's cache if not available yet.
*/
private async loadImportMapEntities(
entityType: string,
context: EntityFieldImportContext,
): Promise<void> {
if (context.entities) {
return;
}
try {
context.entities = (await this.entityMapper.loadType(entityType)).map(
(e) => this.schemaService.transformEntityToDatabaseFormat(e),
);
context.refEntityCtor = this.entityRegistry.get(entityType);
} catch (error) {
Logging.error("Error loading entities for import mapping:", error);
context.entities = [];
}
}
/**
* 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;
}
private async loadRelatedEntitiesToString(
value: string | string[],
schemaField: EntitySchemaField,
): Promise<string[]> {
if (!value) return [];
const relatedEntitiesToStrings: string[] = [];
const relatedEntitiesIds: string[] = Array.isArray(value) ? value : [value];
for (const relatedEntityId of relatedEntitiesIds) {
const entityType =
Entity.extractTypeFromId(relatedEntityId) || schemaField.additional;
const relatedEntity = await this.entityMapper
.load(entityType, relatedEntityId)
.catch(() => undefined);
relatedEntitiesToStrings.push(relatedEntity?.toString() ?? "<not_found>");
}
return relatedEntitiesToStrings;
}
}
/**
* Structure for the `additional` field of an entity reference ColumnMapping.
* Can be a plain string (legacy) or an object with optional valueMapping config.
*/
export interface EntityAdditional {
/** The property of the referenced entity to match against the import value. */
refField: string;
/** Optional: additional config for transforming the import value (passed to the sub-field's importMapFunction). */
valueMapping?: any;
}
/**
* Normalizes the `additional` config of an entity reference column mapping.
* Accepts legacy string format or new object format.
*/
export function normalizeEntityAdditional(
additional: string | EntityAdditional | any,
): EntityAdditional | undefined {
if (!additional) {
return undefined;
}
if (typeof additional === "string") {
return { refField: additional };
}
return additional as EntityAdditional;
}
/**
* 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;
}
/**
* Constructor of the referenced entity type (to access schema for value mapping)
*/
get refEntityCtor(): EntityConstructor | undefined {
return this.globalContext[`ctor_${this.schemaField.additional}`];
}
set refEntityCtor(value: EntityConstructor) {
this.globalContext[`ctor_${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;
}
}