import { Subject } from "./Subject" import { DateUtils } from "../util/DateUtils" import { ObjectLiteral } from "../common/ObjectLiteral" import { OrmUtils } from "../util/OrmUtils" import { ApplyValueTransformers } from "../util/ApplyValueTransformers" import { ObjectUtils } from "../util/ObjectUtils" /** * Finds what columns are changed in the subject entities. */ export class SubjectChangedColumnsComputer { // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Finds what columns are changed in the subject entities. */ compute(subjects: Subject[]) { subjects.forEach((subject) => { this.computeDiffColumns(subject) this.computeDiffRelationalColumns(subjects, subject) }) } // ------------------------------------------------------------------------- // Protected Methods // ------------------------------------------------------------------------- /** * Differentiate columns from the updated entity and entity stored in the database. */ protected computeDiffColumns(subject: Subject): void { // if there is no persisted entity then nothing to compute changed in it if (!subject.entity) return subject.metadata.columns.forEach((column) => { // ignore special columns if ( column.isVirtual || column.isDiscriminator // || // column.isUpdateDate || // column.isVersion || // column.isCreateDate ) return const changeMap = subject.changeMaps.find( (changeMap) => changeMap.column === column, ) if (changeMap) { subject.changeMaps.splice( subject.changeMaps.indexOf(changeMap), 1, ) } // get user provided value - column value from the user provided persisted entity const entityValue = column.getEntityValue(subject.entity!) // we don't perform operation over undefined properties (but we DO need null properties!) if (entityValue === undefined) return // if there is no database entity then all columns are treated as new, e.g. changed if (subject.databaseEntity) { // skip transform database value for json / jsonb for comparison later on const shouldTransformDatabaseEntity = column.type !== "json" && column.type !== "jsonb" // get database value of the column let databaseValue = column.getEntityValue( subject.databaseEntity, shouldTransformDatabaseEntity, ) // filter out "relational columns" only in the case if there is a relation object in entity if (column.relationMetadata) { const value = column.relationMetadata.getEntityValue( subject.entity!, ) if (value !== null && value !== undefined) return } let normalizedValue = entityValue // normalize special values to make proper comparision if (entityValue !== null) { switch (column.type) { case "date": normalizedValue = DateUtils.mixedDateToDateString(entityValue) break case "time": case "time with time zone": case "time without time zone": case "timetz": normalizedValue = DateUtils.mixedDateToTimeString(entityValue) break case "datetime": case "datetime2": case Date: case "timestamp": case "timestamp without time zone": case "timestamp with time zone": case "timestamp with local time zone": case "timestamptz": normalizedValue = DateUtils.mixedDateToUtcDatetimeString( entityValue, ) databaseValue = DateUtils.mixedDateToUtcDatetimeString( databaseValue, ) break case "json": case "jsonb": // JSON.stringify doesn't work because postgresql sorts jsonb before save. // If you try to save json '[{"messages": "", "attribute Key": "", "level":""}] ' as jsonb, // then postgresql will save it as '[{"level": "", "message":"", "attributeKey": ""}]' if ( OrmUtils.deepCompare(entityValue, databaseValue) ) return break case "simple-array": normalizedValue = DateUtils.simpleArrayToString(entityValue) databaseValue = DateUtils.simpleArrayToString(databaseValue) break case "simple-enum": normalizedValue = DateUtils.simpleEnumToString(entityValue) databaseValue = DateUtils.simpleEnumToString(databaseValue) break case "simple-json": normalizedValue = DateUtils.simpleJsonToString(entityValue) databaseValue = DateUtils.simpleJsonToString(databaseValue) break } if (column.transformer) { normalizedValue = ApplyValueTransformers.transformTo( column.transformer, entityValue, ) } } // if value is not changed - then do nothing if ( Buffer.isBuffer(normalizedValue) && Buffer.isBuffer(databaseValue) ) { if (normalizedValue.equals(databaseValue)) { return } } else { if (normalizedValue === databaseValue) return } } if (!subject.diffColumns.includes(column)) subject.diffColumns.push(column) subject.changeMaps.push({ column: column, value: entityValue, }) }) } /** * Difference columns of the owning one-to-one and many-to-one columns. */ protected computeDiffRelationalColumns( allSubjects: Subject[], subject: Subject, ): void { // if there is no persisted entity then nothing to compute changed in it if (!subject.entity) return subject.metadata.relationsWithJoinColumns.forEach((relation) => { // get the related entity from the persisted entity let relatedEntity = relation.getEntityValue(subject.entity!) // we don't perform operation over undefined properties (but we DO need null properties!) if (relatedEntity === undefined) return // if there is no database entity then all relational columns are treated as new, e.g. changed if (subject.databaseEntity) { // here we cover two scenarios: // 1. related entity can be another entity which is natural way // 2. related entity can be just an entity id // if relation entity is just a relation id set (for example post.tag = 1) // then we create an id map from it to make a proper comparision let relatedEntityRelationIdMap: ObjectLiteral = relatedEntity if ( relatedEntityRelationIdMap !== null && ObjectUtils.isObject(relatedEntityRelationIdMap) ) relatedEntityRelationIdMap = relation.getRelationIdMap( relatedEntityRelationIdMap, )! // get database related entity. Since loadRelationIds are used on databaseEntity // related entity will contain only its relation ids const databaseRelatedEntityRelationIdMap = relation.getEntityValue(subject.databaseEntity) // if relation ids are equal then we don't need to update anything const areRelatedIdsEqual = OrmUtils.compareIds( relatedEntityRelationIdMap, databaseRelatedEntityRelationIdMap, ) if (areRelatedIdsEqual) { return } else { subject.diffRelations.push(relation) } } // if there is an inserted subject for the related entity of the persisted entity then use it as related entity // this code is used for related entities without ids to be properly inserted (and then updated if needed) const valueSubject = allSubjects.find( (subject) => subject.mustBeInserted && subject.entity === relatedEntity, ) if (valueSubject) relatedEntity = valueSubject // find if there is already a relation to be changed const changeMap = subject.changeMaps.find( (changeMap) => changeMap.relation === relation, ) if (changeMap) { // and update its value if it was found changeMap.value = relatedEntity } else { // if it wasn't found add a new relation for change subject.changeMaps.push({ relation: relation, value: relatedEntity, }) } }) } }