import {RelationType} from "./types/RelationTypes"; import {EntityMetadata} from "./EntityMetadata"; import {ForeignKeyMetadata} from "./ForeignKeyMetadata"; import {ObjectLiteral} from "../common/ObjectLiteral"; import {ColumnMetadata} from "./ColumnMetadata"; import {EmbeddedMetadata} from "./EmbeddedMetadata"; import {RelationMetadataArgs} from "../metadata-args/RelationMetadataArgs"; import {OnDeleteType} from "./types/OnDeleteType"; import {PropertyTypeFactory} from "./types/PropertyTypeInFunction"; /** * Contains all information about some entity's relation. */ export class RelationMetadata { // --------------------------------------------------------------------- // Public Properties // --------------------------------------------------------------------- /** * Entity metadata of the entity where this relation is placed. * * For example for @ManyToMany(type => Category) in Post, entityMetadata will be metadata of Post entity. */ entityMetadata: EntityMetadata; /** * Entity metadata of the entity that is targeted by this relation. * * For example for @ManyToMany(type => Category) in Post, inverseEntityMetadata will be metadata of Category entity. */ inverseEntityMetadata: EntityMetadata; /** * Entity metadata of the junction table. * Junction tables have their own entity metadata objects. * Defined only for many-to-many relations. */ junctionEntityMetadata?: EntityMetadata; /** * Embedded metadata where this relation is. * If this relation is not in embed then this property value is undefined. */ embeddedMetadata?: EmbeddedMetadata; /** * Relation type, e.g. is it one-to-one, one-to-many, many-to-one or many-to-many. */ relationType: RelationType; /** * Target entity to which this relation is applied. * Target IS NOT equal to entityMetadata.target, because relation * * For example for @ManyToMany(type => Category) in Post, target will be Post. * If @ManyToMany(type => Category) is in Counters which is embedded into Post, target will be Counters. * If @ManyToMany(type => Category) is in abstract class BaseUser which Post extends, target will be BaseUser. * Target can be string if its defined in entity schema instead of class. */ target: Function|string; /** * Target's property name to which relation decorator is applied. */ propertyName: string; /** * Gets full path to this column property (including relation name). * Full path is relevant when column is used in embeds (one or multiple nested). * For example it will return "counters.subcounters.likes". * If property is not in embeds then it returns just property name of the column. */ propertyPath: string; /** * Indicates if this is a parent (can be only many-to-one relation) relation in the tree tables. */ isTreeParent: boolean = false; /** * Indicates if this is a children (can be only one-to-many relation) relation in the tree tables. */ isTreeChildren: boolean = false; /** * Indicates if this relation's column is a primary key. * Can be used only for many-to-one and owner one-to-one relations. */ isPrimary: boolean = false; /** * Indicates if this relation is lazily loaded. */ isLazy: boolean = false; /** * Indicates if this relation is eagerly loaded. */ isEager: boolean = false; /** * Indicates if persistence is enabled for the relation. * By default its enabled, but if you want to avoid any changes in the relation to be reflected in the database you can disable it. * If its disabled you can only change a relation from inverse side of a relation or using relation query builder functionality. * This is useful for performance optimization since its disabling avoid multiple extra queries during entity save. */ persistenceEnabled: boolean = true; /** * If set to true then related objects are allowed to be inserted to the database. */ isCascadeInsert: boolean = false; /** * If set to true then related objects are allowed to be updated in the database. */ isCascadeUpdate: boolean = false; /** * If set to true then related objects are allowed to be remove from the database. */ isCascadeRemove: boolean = false; /** * Indicates if relation column value can be nullable or not. */ isNullable: boolean = true; /** * What to do with a relation on deletion of the row containing a foreign key. */ onDelete?: OnDeleteType; /** * Gets the property's type to which this relation is applied. * * For example for @ManyToMany(type => Category) in Post, target will be Category. */ type: Function|string; /** * Indicates if this side is an owner of this relation. */ isOwning: boolean = false; /** * Checks if this relation's type is "one-to-one". */ isOneToOne: boolean = false; /** * Checks if this relation is owner side of the "one-to-one" relation. * Owner side means this side of relation has a join column in the table. */ isOneToOneOwner: boolean = false; /** * Checks if this relation has a join column (e.g. is it many-to-one or one-to-one owner side). */ isWithJoinColumn: boolean = false; /** * Checks if this relation is NOT owner side of the "one-to-one" relation. * NOT owner side means this side of relation does not have a join column in the table. */ isOneToOneNotOwner: boolean = false; /** * Checks if this relation's type is "one-to-many". */ isOneToMany: boolean = false; /** * Checks if this relation's type is "many-to-one". */ isManyToOne: boolean = false; /** * Checks if this relation's type is "many-to-many". */ isManyToMany: boolean = false; /** * Checks if this relation's type is "many-to-many", and is owner side of the relationship. * Owner side means this side of relation has a join table. */ isManyToManyOwner: boolean = false; /** * Checks if this relation's type is "many-to-many", and is NOT owner side of the relationship. * Not owner side means this side of relation does not have a join table. */ isManyToManyNotOwner: boolean = false; /** * Gets the property path of the inverse side of the relation. */ inverseSidePropertyPath: string; /** * Inverse side of the relation set by user. * * Inverse side set in the relation can be either string - property name of the column on inverse side, * either can be a function that accepts a map of properties with the object and returns one of them. * Second approach is used to achieve type-safety. */ givenInverseSidePropertyFactory: PropertyTypeFactory; /** * Gets the relation metadata of the inverse side of this relation. */ inverseRelation?: RelationMetadata; /** * Join table name. */ joinTableName: string; /** * Foreign keys created for this relation. */ foreignKeys: ForeignKeyMetadata[] = []; /** * Join table columns. * Join columns can be obtained only from owner side of the relation. * From non-owner side of the relation join columns will be empty. * If this relation is a many-to-one/one-to-one then it takes join columns from the current entity. * If this relation is many-to-many then it takes all owner join columns from the junction entity. */ joinColumns: ColumnMetadata[] = []; /** * Inverse join table columns. * Inverse join columns are supported only for many-to-many relations * and can be obtained only from owner side of the relation. * From non-owner side of the relation join columns will be undefined. */ inverseJoinColumns: ColumnMetadata[] = []; // --------------------------------------------------------------------- // Constructor // --------------------------------------------------------------------- constructor(options: { entityMetadata: EntityMetadata, embeddedMetadata?: EmbeddedMetadata, args: RelationMetadataArgs }) { this.entityMetadata = options.entityMetadata; this.embeddedMetadata = options.embeddedMetadata!; const args = options.args; this.target = args.target; this.propertyName = args.propertyName; this.relationType = args.relationType; if (args.inverseSideProperty) this.givenInverseSidePropertyFactory = args.inverseSideProperty; this.isLazy = args.isLazy || false; this.isCascadeInsert = args.options.cascade === true || (args.options.cascade instanceof Array && args.options.cascade.indexOf("insert") !== -1); this.isCascadeUpdate = args.options.cascade === true || (args.options.cascade instanceof Array && args.options.cascade.indexOf("update") !== -1); this.isCascadeRemove = args.options.cascade === true || (args.options.cascade instanceof Array && args.options.cascade.indexOf("remove") !== -1); this.isPrimary = args.options.primary || false; this.isNullable = args.options.nullable === false || this.isPrimary ? false : true; this.onDelete = args.options.onDelete; this.isEager = args.options.eager || false; this.persistenceEnabled = args.options.persistence === false ? false : true; this.isTreeParent = args.isTreeParent || false; this.isTreeChildren = args.isTreeChildren || false; this.type = args.type instanceof Function ? (args.type as () => any)() : args.type; this.isOneToOne = this.relationType === "one-to-one"; this.isOneToMany = this.relationType === "one-to-many"; this.isManyToOne = this.relationType === "many-to-one"; this.isManyToMany = this.relationType === "many-to-many"; this.isOneToOneNotOwner = this.isOneToOne ? true : false; this.isManyToManyNotOwner = this.isManyToMany ? true : false; } // --------------------------------------------------------------------- // Public Methods // --------------------------------------------------------------------- /** * Creates join column ids map from the given related entity ids array. */ getRelationIdMap(entity: ObjectLiteral): ObjectLiteral|undefined { const joinColumns = this.isOwning ? this.joinColumns : this.inverseRelation!.joinColumns; const referencedColumns = joinColumns.map(joinColumn => joinColumn.referencedColumn!); // console.log("entity", entity); // console.log("referencedColumns", referencedColumns); return EntityMetadata.getValueMap(entity, referencedColumns); } /** * Ensures that given object is an entity id map. * If given id is an object then it means its already id map. * If given id isn't an object then it means its a value of the id column * and it creates a new id map with this value and name of the primary column. */ ensureRelationIdMap(id: any): ObjectLiteral { if (id instanceof Object) return id; const joinColumns = this.isOwning ? this.joinColumns : this.inverseRelation!.joinColumns; const referencedColumns = joinColumns.map(joinColumn => joinColumn.referencedColumn!); if (referencedColumns.length > 1) throw new Error(`Cannot create relation id map for a single value because relation contains multiple referenced columns.`); return referencedColumns[0].createValueMap(id); } /** * Extracts column value from the given entity. * If column is in embedded (or recursive embedded) it extracts its value from there. */ getEntityValue(entity: ObjectLiteral, getLazyRelationsPromiseValue: boolean = false): any|undefined { // extract column value from embeddeds of entity if column is in embedded if (this.embeddedMetadata) { // example: post[data][information][counters].id where "data", "information" and "counters" are embeddeds // we need to get value of "id" column from the post real entity object // first step - we extract all parent properties of the entity relative to this column, e.g. [data, information, counters] const propertyNames = [...this.embeddedMetadata.parentPropertyNames]; // next we need to access post[data][information][counters][this.propertyName] to get column value from the counters // this recursive function takes array of generated property names and gets the post[data][information][counters] embed const extractEmbeddedColumnValue = (propertyNames: string[], value: ObjectLiteral): any => { const propertyName = propertyNames.shift(); if (propertyName) { if (value[propertyName]) { return extractEmbeddedColumnValue(propertyNames, value[propertyName]); } return undefined; } return value; }; // once we get nested embed object we get its column, e.g. post[data][information][counters][this.propertyName] const embeddedObject = extractEmbeddedColumnValue(propertyNames, entity); if (this.isLazy) { if (embeddedObject["__" + this.propertyName + "__"] !== undefined) return embeddedObject["__" + this.propertyName + "__"]; if (getLazyRelationsPromiseValue === true) return embeddedObject[this.propertyName]; return undefined; } return embeddedObject ? embeddedObject[this.isLazy ? "__" + this.propertyName + "__" : this.propertyName] : undefined; } else { // no embeds - no problems. Simply return column name by property name of the entity if (this.isLazy) { if (entity["__" + this.propertyName + "__"] !== undefined) return entity["__" + this.propertyName + "__"]; if (getLazyRelationsPromiseValue === true) return entity[this.propertyName]; return undefined; } return entity[this.propertyName]; } } /** * Sets given entity's relation's value. * Using of this method helps to set entity relation's value of the lazy and non-lazy relations. * * If merge is set to true, it merges given value into currently */ setEntityValue(entity: ObjectLiteral, value: any): void { const propertyName = this.isLazy ? "__" + this.propertyName + "__" : this.propertyName; if (this.embeddedMetadata) { // first step - we extract all parent properties of the entity relative to this column, e.g. [data, information, counters] const extractEmbeddedColumnValue = (embeddedMetadatas: EmbeddedMetadata[], map: ObjectLiteral): any => { // if (!object[embeddedMetadata.propertyName]) // object[embeddedMetadata.propertyName] = embeddedMetadata.create(); const embeddedMetadata = embeddedMetadatas.shift(); if (embeddedMetadata) { if (!map[embeddedMetadata.propertyName]) map[embeddedMetadata.propertyName] = embeddedMetadata.create(); extractEmbeddedColumnValue(embeddedMetadatas, map[embeddedMetadata.propertyName]); return map; } map[propertyName] = value; return map; }; return extractEmbeddedColumnValue([...this.embeddedMetadata.embeddedMetadataTree], entity); } else { entity[propertyName] = value; } } /** * Creates entity id map from the given entity ids array. */ createValueMap(value: any) { // extract column value from embeds of entity if column is in embedded if (this.embeddedMetadata) { // example: post[data][information][counters].id where "data", "information" and "counters" are embeddeds // we need to get value of "id" column from the post real entity object and return it in a // { data: { information: { counters: { id: ... } } } } format // first step - we extract all parent properties of the entity relative to this column, e.g. [data, information, counters] const propertyNames = [...this.embeddedMetadata.parentPropertyNames]; // now need to access post[data][information][counters] to get column value from the counters // and on each step we need to create complex literal object, e.g. first { data }, // then { data: { information } }, then { data: { information: { counters } } }, // then { data: { information: { counters: [this.propertyName]: entity[data][information][counters][this.propertyName] } } } // this recursive function helps doing that const extractEmbeddedColumnValue = (propertyNames: string[], map: ObjectLiteral): any => { const propertyName = propertyNames.shift(); if (propertyName) { map[propertyName] = {}; extractEmbeddedColumnValue(propertyNames, map[propertyName]); return map; } map[this.propertyName] = value; return map; }; return extractEmbeddedColumnValue(propertyNames, {}); } else { // no embeds - no problems. Simply return column property name and its value of the entity return { [this.propertyName]: value }; } } // --------------------------------------------------------------------- // Builder Methods // --------------------------------------------------------------------- /** * Builds some depend relation metadata properties. * This builder method should be used only after embedded metadata tree was build. */ build() { this.propertyPath = this.buildPropertyPath(); } /** * Registers given foreign keys in the relation. * This builder method should be used to register foreign key in the relation. */ registerForeignKeys(...foreignKeys: ForeignKeyMetadata[]) { this.foreignKeys.push(...foreignKeys); this.joinColumns = this.foreignKeys[0] ? this.foreignKeys[0].columns : []; this.inverseJoinColumns = this.foreignKeys[1] ? this.foreignKeys[1].columns : []; this.isOwning = this.isManyToOne || ((this.isManyToMany || this.isOneToOne) && this.joinColumns.length > 0); this.isOneToOneOwner = this.isOneToOne && this.isOwning; this.isOneToOneNotOwner = this.isOneToOne && !this.isOwning; this.isManyToManyOwner = this.isManyToMany && this.isOwning; this.isManyToManyNotOwner = this.isManyToMany && !this.isOwning; this.isWithJoinColumn = this.isManyToOne || this.isOneToOneOwner; } /** * Registers a given junction entity metadata. * This builder method can be called after junction entity metadata for the many-to-many relation was created. */ registerJunctionEntityMetadata(junctionEntityMetadata: EntityMetadata) { this.junctionEntityMetadata = junctionEntityMetadata; this.joinTableName = junctionEntityMetadata.tableName; if (this.inverseRelation) { this.inverseRelation.junctionEntityMetadata = junctionEntityMetadata; this.joinTableName = junctionEntityMetadata.tableName; } } /** * Builds inverse side property path based on given inverse side property factory. * This builder method should be used only after properties map of the inverse entity metadata was build. */ buildInverseSidePropertyPath(): string { if (this.givenInverseSidePropertyFactory) { const ownerEntityPropertiesMap = this.inverseEntityMetadata.propertiesMap; if (typeof this.givenInverseSidePropertyFactory === "function") return this.givenInverseSidePropertyFactory(ownerEntityPropertiesMap); if (typeof this.givenInverseSidePropertyFactory === "string") return this.givenInverseSidePropertyFactory; } else if (this.isTreeParent && this.entityMetadata.treeChildrenRelation) { return this.entityMetadata.treeChildrenRelation.propertyName; } else if (this.isTreeChildren && this.entityMetadata.treeParentRelation) { return this.entityMetadata.treeParentRelation.propertyName; } return ""; } /** * Builds relation's property path based on its embedded tree. */ buildPropertyPath(): string { if (!this.embeddedMetadata || !this.embeddedMetadata.parentPropertyNames.length) return this.propertyName; return this.embeddedMetadata.parentPropertyNames.join(".") + "." + this.propertyName; } }