src/app/core/entity/model/entity.ts
/*
* This file is part of ndb-core.
*
* ndb-core is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ndb-core is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ndb-core. If not, see <http://www.gnu.org/licenses/>.
*/
import { v4 as uuid } from "uuid";
import { EntitySchema } from "../schema/entity-schema";
import { DatabaseField } from "../database-field.decorator";
import {
getWarningLevelColor,
WarningLevel,
} from "../../../child-dev-project/warning-level";
import { IconName } from "@fortawesome/fontawesome-svg-core";
import { UpdateMetadata } from "./update-metadata";
import { EntityBlockConfig } from "../../basic-datatypes/entity/entity-block/entity-block-config";
import { Logging } from "../../logging/logging.service";
/**
* This represents a static class of type <T>.
* It can be used for passing a class from which new objects should be created.
* It can also be used to check the ENTITY_TYPE of a class
* For example usage check the {@link EntityMapperService}.
*/
export type EntityConstructor<T extends Entity = Entity> = (new (
id?: string,
) => T) &
typeof Entity;
/**
* "Entity" is a base class for all domain model classes.
* It implements the basic general properties and methods that are required for all Entity types
* e.g. supporting the Entity Schema system or basic database logic.
*
* Entity classes do not deal with database actions, use {@link EntityMapperService} with its find/save/delete functions.
*
* Do not use the Entity class directly. Instead, implement your own Entity types, writing classes that extend "Entity".
* A How-To Guide on how to implement your own types is available:
* - [How to Create a new Entity Type]{@link /additional-documentation/how-to-guides/create-a-new-entity-type.html}
*/
export class Entity {
/**
* The entity's type.
* In classes extending Entity this is usually overridden by the class annotation `@DatabaseEntity('NewEntity')`.
* The type needs to be used as routing path in lower case. The routing path can be defined in the configuration file.
*/
static ENTITY_TYPE = "Entity";
/**
* EntitySchema defining property transformations from/to the database.
* This is auto-generated from the property annotations `@DatabaseField()`.
*
* see {@link /additional-documentation/how-to-guides/create-a-new-entity-type.html}
*/
static schema: EntitySchema;
/**
* True if this type's schema has been customized dynamically from the config.
*/
static _isCustomizedType?: boolean; // todo should be private or renamed to "isCustomizedType"
/**
* Defining which attribute values of an entity should be shown in the `.toString()` method.
*
* The default is the ID of the entity (`entityId`).
* This can be overwritten in subclasses or through the config.
*/
static toStringAttributes = ["entityId"];
/**
* Defining which attributes will be displayed in a tooltip on hover when the record is displayed as an entity-block.
*/
static toBlockDetailsAttributes?: EntityBlockConfig;
/**
* human-readable name/label of the entity in the UI
*/
static get label(): string {
return this._label ?? this.ENTITY_TYPE;
}
static set label(value: string) {
this._label = value;
}
private static _label: string;
/**
* human-readable label for uses of plural of the entity in the UI
*/
static get labelPlural(): string {
return this._labelPlural ?? this.label;
}
static set labelPlural(value: string) {
this._labelPlural = value;
}
private static _labelPlural: string;
/**
* icon id used for this entity
*/
static icon: IconName;
/**
* color used for to highlight this entity type across the app
*/
static color: string;
/**
* Base route of the entity (list/details) view for this entity type.
*/
static get route(): string {
let route = this._route ?? this.ENTITY_TYPE.toLowerCase();
if (!route.startsWith("/")) {
route = "/" + route;
}
return route;
}
static set route(value: string) {
this._route = value;
}
private static _route: string;
/**
* Extract the ENTITY_TYPE from an id.
* @param id An entity's id including prefix.
*/
static extractTypeFromId(id: string): string {
const split = id.indexOf(":");
return id.substring(0, split);
}
/**
* Extract entityId without prefix.
* @param id An entity's id including prefix.
*/
static extractEntityIdFromId(id: string): string {
let type: string = undefined;
try {
const split = id.indexOf(":");
type = id.substring(split + 1);
} catch (e) {
Logging.debug("Error extracting entityId from id", id, e);
}
return type;
}
/**
* Create a prefixed id by adding the type prefix if it isn't already part of the given id.
* @param type The type prefix to be added.
* @param id The id to be extended with a prefix.
*/
static createPrefixedId(type: string, id: string): string {
id = String(id);
const prefix = type + ":";
if (!id.startsWith(prefix)) {
return prefix + id;
} else {
return id;
}
}
/**
* whether this entity type can contain "personally identifiable information" (PII)
* and therefore should follow strict data protection requirements
* and offer a function to anonymize records.
*/
static hasPII: boolean = false;
/**
* Internal database id.
* This is usually combined from the ENTITY_TYPE as a prefix with the entityId field `EntityType:entityId`
* @example "Entity:123"
*/
@DatabaseField({ anonymize: "retain" }) private _id: string;
/** internal database doc revision, used to detect conflicts by PouchDB/CouchDB */
@DatabaseField({ anonymize: "retain" }) _rev: string;
@DatabaseField({
anonymize: "retain",
})
created: UpdateMetadata;
@DatabaseField({
anonymize: "retain",
})
updated: UpdateMetadata;
@DatabaseField({ anonymize: "retain" })
inactive: boolean;
/**
* Whether this entity has been anonymized and therefore cannot be re-activated.
*/
@DatabaseField({ anonymize: "retain" })
anonymized: boolean;
/** whether this entity object is newly created and not yet saved to database */
get isNew(): boolean {
return !this._rev;
}
/** actual id without prefix */
private get entityId(): string {
return Entity.extractEntityIdFromId(this._id);
}
/**
* Set id without prefix.
* @param newEntityId The new id without prefix.
*/
private set entityId(newEntityId: string) {
this._id = Entity.createPrefixedId(this.getType(), newEntityId);
}
/**
* Check, if this entity is considered active or archived.
*
* This is taken from the property "inactive".
* If the property doesn't exist, the default is `true`.
*
* Some subclasses overwrite this functionality, but this logic is considered deprecated (!) now
* and implementations have to make sure that "inactive" property takes precedence!
*/
get isActive(): boolean {
if (this.inactive !== undefined) {
return !this.inactive;
}
if (this["active"] !== undefined) {
return this["active"];
}
return true;
}
/**
* If existing entities with `isActive: false` exist, then these values are assigned to the property "active".
* @param isActive
*/
set isActive(isActive: boolean) {
this["active"] = isActive;
this.inactive = !isActive;
}
/**
* Creates an entity object with the given id. This id is final and won't be changeable after this object has been
* created.
*
* @param id a unique id for this entity; if no id is passed a uuid is generated automatically
*/
constructor(id: string = uuid()) {
this.entityId = id;
}
/**
* Get the class (Entity or the actual subclass of the instance) to call static methods on the correct class considering inheritance
*/
getConstructor(): EntityConstructor<this> {
return this.constructor as EntityConstructor<this>;
}
/**
* Get the entity schema of this class
*/
getSchema(): EntitySchema {
return this.getConstructor().schema;
}
/**
* Returns the id of this entity.
*
* Note that an id is final and can't be changed after the object has been instantiated, hence there is no
* <code>setId()</code> method.
*
* @returns {string} the unique id of this entity
*/
public getId(withoutPrefix = false): string {
return withoutPrefix ? this.entityId : this._id;
}
/**
* Returns the type which is used to categorize this entity in the database.
*
* <b>Important: Do not overwrite this method! Types are handled internally.</b>
*
* @returns {string} the entity's type (which is the class name).
*/
public getType(): string {
return this.getConstructor().ENTITY_TYPE;
}
/**
* Returns a string representation or summary of the instance.
* This can be configured with the static `toStringAttributes` for each subclass.
*
* @returns {string} the instance's string representation.
*/
public toString(): string {
if (
this.anonymized &&
this.getConstructor().toStringAttributes.every(
(attr) => this[attr] === undefined,
)
) {
return $localize`:Entity.toString fallback for anonymized record:[anonymized ${
this.getConstructor().label
}]`;
}
return this.getConstructor()
.toStringAttributes.map((attr) => this[attr])
.join(" ");
}
/**
* Used by some generic UI components to set the color for the entity instance.
* Override this method as needed.
*/
public getColor(): string {
return getWarningLevelColor(this.getWarningLevel());
}
/**
* Override getWarningLevel() to define when the entity is in a critical condition and should be color-coded
* and highlighted in generic components of the UI.
*/
public getWarningLevel(): WarningLevel {
return WarningLevel.NONE;
}
/**
* Shallow copy of the entity.
* The resulting entity will be of the same type as this
* (taking into account subclassing)
*/
public copy(generateNewId: boolean = false): this {
const other = new (this.getConstructor())(this._id);
Object.assign(other, this);
if (generateNewId) {
delete other._rev;
other.entityId = uuid();
}
return other;
}
/**
* Checks if the entity is valid and if the check fails, throws an error explaining the failed check.
*/
assertValid(): void {
return;
}
}