import { ObjectLiteral } from "../common/ObjectLiteral" import { EntityMetadata } from "../metadata/EntityMetadata" import { SubjectChangeMap } from "./SubjectChangeMap" import { OrmUtils } from "../util/OrmUtils" import { RelationMetadata } from "../metadata/RelationMetadata" import { ColumnMetadata } from "../metadata/ColumnMetadata" import { ObjectUtils } from "../util/ObjectUtils" import { InstanceChecker } from "../util/InstanceChecker" /** * Subject is a subject of persistence. * It holds information about each entity that needs to be persisted: * - what entity should be persisted * - what is database representation of the persisted entity * - what entity metadata of the persisted entity * - what is allowed to with persisted entity (insert/update/remove) * * Having this collection of subjects we can perform database queries. */ export class Subject { readonly "@instanceof" = Symbol.for("Subject") // ------------------------------------------------------------------------- // Properties // ------------------------------------------------------------------------- /** * Entity metadata of the subject entity. */ metadata: EntityMetadata /** * Subject identifier. * This identifier is not limited to table entity primary columns. * This can be entity id or ids as well as some unique entity properties, like name or title. * Insert / Update / Remove operation will be executed by a given identifier. */ identifier: ObjectLiteral | undefined = undefined /** * Copy of entity but with relational ids fulfilled. */ entityWithFulfilledIds: ObjectLiteral | undefined = undefined /** * If subject was created by cascades this property will contain subject * from where this subject was created. */ parentSubject?: Subject /** * Gets entity sent to the persistence (e.g. changed entity). * If entity is not set then this subject is created only for the entity loaded from the database, * or this subject is used for the junction operation (junction operations are relying only on identifier). */ entity?: ObjectLiteral /** * Database entity. * THIS IS NOT RAW ENTITY DATA, its a real entity. */ databaseEntity?: ObjectLiteral /** * Indicates if database entity was loaded. * No matter if it was found or not, it indicates the fact of loading. */ databaseEntityLoaded: boolean = false /** * Changes needs to be applied in the database for the given subject. */ changeMaps: SubjectChangeMap[] = [] /** * Generated values returned by a database (for example generated id or default values). * Used in insert and update operations. * Has entity-like structure (not just column database name and values). */ generatedMap?: ObjectLiteral /** * Inserted values with updated values of special and default columns. * Has entity-like structure (not just column database name and values). */ insertedValueSet?: ObjectLiteral /** * Indicates if this subject can be inserted into the database. * This means that this subject either is newly persisted, either can be inserted by cascades. */ canBeInserted: boolean = false /** * Indicates if this subject can be updated in the database. * This means that this subject either was persisted, either can be updated by cascades. */ canBeUpdated: boolean = false /** * Indicates if this subject MUST be removed from the database. * This means that this subject either was removed, either was removed by cascades. */ mustBeRemoved: boolean = false /** * Indicates if this subject can be soft-removed from the database. * This means that this subject either was soft-removed, either was soft-removed by cascades. */ canBeSoftRemoved: boolean = false /** * Indicates if this subject can be recovered from the database. * This means that this subject either was recovered, either was recovered by cascades. */ canBeRecovered: boolean = false /** * Relations updated by the change maps. */ updatedRelationMaps: { relation: RelationMetadata value: ObjectLiteral }[] = [] /** * List of updated columns */ diffColumns: ColumnMetadata[] = [] /** * List of updated relations */ diffRelations: RelationMetadata[] = [] // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- constructor(options: { metadata: EntityMetadata parentSubject?: Subject entity?: ObjectLiteral canBeInserted?: boolean canBeUpdated?: boolean mustBeRemoved?: boolean canBeSoftRemoved?: boolean canBeRecovered?: boolean identifier?: ObjectLiteral changeMaps?: SubjectChangeMap[] }) { this.metadata = options.metadata this.entity = options.entity this.parentSubject = options.parentSubject if (options.canBeInserted !== undefined) this.canBeInserted = options.canBeInserted if (options.canBeUpdated !== undefined) this.canBeUpdated = options.canBeUpdated if (options.mustBeRemoved !== undefined) this.mustBeRemoved = options.mustBeRemoved if (options.canBeSoftRemoved !== undefined) this.canBeSoftRemoved = options.canBeSoftRemoved if (options.canBeRecovered !== undefined) this.canBeRecovered = options.canBeRecovered if (options.identifier !== undefined) this.identifier = options.identifier if (options.changeMaps !== undefined) this.changeMaps.push(...options.changeMaps) this.recompute() } // ------------------------------------------------------------------------- // Accessors // ------------------------------------------------------------------------- /** * Checks if this subject must be inserted into the database. * Subject can be inserted into the database if it is allowed to be inserted (explicitly persisted or by cascades) * and if it does not have database entity set. */ get mustBeInserted() { return this.canBeInserted && !this.databaseEntity } /** * Checks if this subject must be updated into the database. * Subject can be updated in the database if it is allowed to be updated (explicitly persisted or by cascades) * and if it does have differentiated columns or relations. */ get mustBeUpdated() { return ( this.canBeUpdated && this.identifier && (this.databaseEntityLoaded === false || (this.databaseEntityLoaded && this.databaseEntity)) && // ((this.entity && this.databaseEntity) || (!this.entity && !this.databaseEntity)) && this.changeMaps.length > 0 ) } /** * Checks if this subject must be soft-removed into the database. * Subject can be updated in the database if it is allowed to be soft-removed (explicitly persisted or by cascades) * and if it does have differentiated columns or relations. */ get mustBeSoftRemoved() { return ( this.canBeSoftRemoved && this.identifier && (this.databaseEntityLoaded === false || (this.databaseEntityLoaded && this.databaseEntity)) ) } /** * Checks if this subject must be recovered into the database. * Subject can be updated in the database if it is allowed to be recovered (explicitly persisted or by cascades) * and if it does have differentiated columns or relations. */ get mustBeRecovered() { return ( this.canBeRecovered && this.identifier && (this.databaseEntityLoaded === false || (this.databaseEntityLoaded && this.databaseEntity)) ) } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Creates a value set needs to be inserted / updated in the database. * Value set is based on the entity and change maps of the subject. * Important note: this method pops data from this subject's change maps. */ createValueSetAndPopChangeMap(): ObjectLiteral { const changeMapsWithoutValues: SubjectChangeMap[] = [] const changeSet = this.changeMaps.reduce((updateMap, changeMap) => { let value = changeMap.value if (InstanceChecker.isSubject(value)) { // referenced columns can refer on values both which were just inserted and which were present in the model // if entity was just inserted valueSets must contain all values from the entity and values just inserted in the database // so, here we check if we have a value set then we simply use it as value to get our reference column values // otherwise simply use an entity which cannot be just inserted at the moment and have all necessary data value = value.insertedValueSet ? value.insertedValueSet : value.entity } // value = changeMap.valueFactory ? changeMap.valueFactory(value) : changeMap.column.createValueMap(value); let valueMap: ObjectLiteral | undefined if (this.metadata.isJunction && changeMap.column) { valueMap = changeMap.column.createValueMap( changeMap.column.referencedColumn!.getEntityValue(value), ) } else if (changeMap.column) { valueMap = changeMap.column.createValueMap(value) } else if (changeMap.relation) { // value can be a related object, for example: post.question = { id: 1 } // or value can be a null or direct relation id, e.g. post.question = 1 // if its a direction relation id then we just set it to the valueMap, // however if its an object then we need to extract its relation id map and set it to the valueMap if (ObjectUtils.isObject(value) && !Buffer.isBuffer(value)) { // get relation id, e.g. referenced column name and its value, // for example: { id: 1 } which then will be set to relation, e.g. post.category = { id: 1 } const relationId = changeMap.relation!.getRelationIdMap(value) // but relation id can be empty, for example in the case when you insert a new post with category // and both post and category are newly inserted objects (by cascades) and in this case category will not have id // this means we need to insert post without question id and update post's questionId once question be inserted // that's why we create a new changeMap operation for future updation of the post entity if (relationId === undefined) { changeMapsWithoutValues.push(changeMap) this.canBeUpdated = true return updateMap } valueMap = changeMap.relation!.createValueMap(relationId) this.updatedRelationMaps.push({ relation: changeMap.relation, value: relationId, }) } else { // value can be "null" or direct relation id here valueMap = changeMap.relation!.createValueMap(value) this.updatedRelationMaps.push({ relation: changeMap.relation, value: value, }) } } OrmUtils.mergeDeep(updateMap, valueMap) return updateMap }, {} as ObjectLiteral) this.changeMaps = changeMapsWithoutValues return changeSet } /** * Recomputes entityWithFulfilledIds and identifier when entity changes. */ recompute(): void { if (this.entity) { this.entityWithFulfilledIds = Object.assign({}, this.entity) if (this.parentSubject) { this.metadata.primaryColumns.forEach((primaryColumn) => { if ( primaryColumn.relationMetadata && primaryColumn.relationMetadata.inverseEntityMetadata === this.parentSubject!.metadata ) { const value = primaryColumn.referencedColumn!.getEntityValue( this.parentSubject!.entity!, ) primaryColumn.setEntityValue( this.entityWithFulfilledIds!, value, ) } }) } this.identifier = this.metadata.getEntityIdMap( this.entityWithFulfilledIds, ) } else if (this.databaseEntity) { this.identifier = this.metadata.getEntityIdMap(this.databaseEntity) } } }