UNPKG

ionic-orm-3

Version:

Data-mapper ORM for Ionic WebSQL and SQLite

452 lines 28.2 kB
import { PersistOperation, OperateEntity } from "./operation/PersistOperation"; import { InsertOperation } from "./operation/InsertOperation"; import { UpdateByRelationOperation } from "./operation/UpdateByRelationOperation"; import { UpdateOperation } from "./operation/UpdateOperation"; import { CascadesNotAllowedError } from "./error/CascadesNotAllowedError"; import { RemoveOperation } from "./operation/RemoveOperation"; import { UpdateByInverseSideOperation } from "./operation/UpdateByInverseSideOperation"; /** * 1. collect all exist objects from the db entity * 2. collect all objects from the new entity * 3. first need to go throw all relations of the new entity and: * 3.1. find all objects that are new (e.g. cascade="insert") by comparing ids from the exist objects * 3.2. check if relation has rights to do cascade operation and throw exception if it cannot * 3.3. save new objects for insert operation * 4. second need to go throw all relations of the db entity and: * 4.1. find all objects that are removed (e.g. cascade="remove") by comparing data with collected objects of the new entity * 4.2. check if relation has rights to do cascade operation and throw exception if it cannot * 4.3. save new objects for remove operation * 5. third need to go throw collection of all new entities * 5.1. compare with entities from the collection of db entities, find difference and generate a change set * 5.2. check if relation has rights to do cascade operation and throw exception if it cannot * 5.3. * 6. go throw all relations and find junction * 6.1. * * if relation has "all" then all of above: * if relation has "insert" it can insert a new entity * if relation has "update" it can only update related entity * if relation has "remove" it can only remove related entity */ var EntityPersistOperationBuilder = (function () { // ------------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------------- function EntityPersistOperationBuilder(entityMetadatas) { this.entityMetadatas = entityMetadatas; // ------------------------------------------------------------------------- // Properties // ------------------------------------------------------------------------- this.strictCascadesMode = false; } // ------------------------------------------------------------------------- // Public Methods // ------------------------------------------------------------------------- /** * Finds columns and relations from entity2 which does not exist or does not match in entity1. */ EntityPersistOperationBuilder.prototype.buildFullPersistment = function (dbEntity, persistedEntity, dbEntities, allPersistedEntities) { // const dbEntities = this.extractObjectsById(dbEntity, metadata); // const allPersistedEntities = this.extractObjectsById(persistedEntity, metadata); var metadata = persistedEntity.metadata; var persistOperation = new PersistOperation(); persistOperation.dbEntity = dbEntity; persistOperation.persistedEntity = persistedEntity; persistOperation.allDbEntities = dbEntities; persistOperation.allPersistedEntities = allPersistedEntities; persistOperation.inserts = this.findCascadeInsertedEntities(persistedEntity, dbEntities); persistOperation.updatesByRelations = this.updateRelations(persistOperation.inserts, persistedEntity); persistOperation.updatesByInverseRelations = this.updateInverseRelations(metadata, dbEntity, persistedEntity); if (dbEntity) persistOperation.updates = this.findCascadeUpdateEntities(persistOperation.updatesByRelations, metadata, dbEntity, persistedEntity, dbEntities); persistOperation.junctionInserts = this.findJunctionInsertOperations(metadata, persistedEntity, dbEntities); if (dbEntity) persistOperation.removes = this.findCascadeRemovedEntities(metadata, dbEntity, allPersistedEntities, undefined, undefined, undefined); if (dbEntity) persistOperation.junctionRemoves = this.findJunctionRemoveOperations(metadata, dbEntity, allPersistedEntities); // persistOperation.log(); return persistOperation; }; /** * Finds columns and relations from entity2 which does not exist or does not match in entity1. */ EntityPersistOperationBuilder.prototype.buildOnlyRemovement = function (metadata, dbEntity, persistedEntity, dbEntities, allPersistedEntities) { // const dbEntities = this.extractObjectsById(dbEntity, metadata); // const allEntities = this.extractObjectsById(newEntity, metadata); var persistOperation = new PersistOperation(); persistOperation.dbEntity = dbEntity; persistOperation.persistedEntity = persistedEntity; persistOperation.allDbEntities = dbEntities; persistOperation.allPersistedEntities = allPersistedEntities; if (dbEntity) { persistOperation.removes = this.findCascadeRemovedEntities(metadata, dbEntity, allPersistedEntities, undefined, undefined, undefined); } persistOperation.junctionRemoves = this.findJunctionRemoveOperations(metadata, dbEntity, allPersistedEntities); return persistOperation; }; // ------------------------------------------------------------------------- // Private Methods // ------------------------------------------------------------------------- EntityPersistOperationBuilder.prototype.findCascadeInsertedEntities = function (newEntityWithId, dbEntities, fromRelation, operations) { var _this = this; if (operations === void 0) { operations = []; } var newEntity = newEntityWithId.entity; var metadata = this.entityMetadatas.findByTarget(newEntityWithId.entityTarget); var isObjectNew = !this.findEntityWithId(dbEntities, metadata.target, metadata.getEntityIdMap(newEntity)); // if object is new and should be inserted, we check if cascades are allowed before add it to operations list if (isObjectNew && fromRelation && !this.checkCascadesAllowed("insert", metadata, fromRelation)) { return operations; // looks like object is new here, but cascades are not allowed - then we should stop iteration } else if (isObjectNew && !operations.find(function (o) { return o.entity === newEntity; })) { operations.push(new InsertOperation(newEntityWithId.entityTarget, newEntity)); } metadata.relations.forEach(function (relation) { var value = _this.getEntityRelationValue(relation, newEntity); var inverseMetadata = relation.inverseEntityMetadata; if (!value) return; if (value instanceof Array) { value.forEach(function (subValue) { var subValueWithId = new OperateEntity(inverseMetadata, subValue); _this.findCascadeInsertedEntities(subValueWithId, dbEntities, relation, operations); }); } else { var valueWithId = new OperateEntity(inverseMetadata, value); _this.findCascadeInsertedEntities(valueWithId, dbEntities, relation, operations); } }); return operations; }; EntityPersistOperationBuilder.prototype.findCascadeUpdateEntities = function (updatesByRelations, metadata, dbEntityWithId, newEntityWithId, dbEntities, fromRelation, operations) { var _this = this; if (operations === void 0) { operations = []; } var dbEntity = dbEntityWithId.entity; var newEntity = newEntityWithId.entity; if (!dbEntity) return operations; var diffColumns = this.diffColumns(metadata, newEntity, dbEntity); var diffRelations = this.diffRelations(updatesByRelations, metadata, newEntity, dbEntity); if (diffColumns.length && fromRelation && !this.checkCascadesAllowed("update", metadata, fromRelation)) { return operations; } else if (diffColumns.length || diffRelations.length) { var entityId = metadata.getEntityIdMap(newEntity); if (entityId) operations.push(new UpdateOperation(newEntityWithId.entityTarget, newEntity, entityId, diffColumns, diffRelations)); } metadata.relations.forEach(function (relation) { var relMetadata = relation.inverseEntityMetadata; var relationIdColumnName = relMetadata.firstPrimaryColumn.propertyName; // todo: join column metadata should be used here instead of primary column var value = _this.getEntityRelationValue(relation, newEntity); var referencedColumnName = relation.isOwning ? relation.referencedColumnName : relation.inverseRelation.referencedColumnName; // const dbValue = this.getEntityRelationValue(relation, dbEntity); if (!value /* || !dbValue*/) return; if (value instanceof Array) { value.forEach(function (subEntity) { /*const subDbEntity = dbValue.find((subDbEntity: any) => { return subDbEntity[relationIdColumnName] === subEntity[relationIdColumnName]; });*/ var dbValue = dbEntities.find(function (dbValue) { return dbValue.entityTarget === relation.entityMetadata.target && dbValue.entity[referencedColumnName] === subEntity[relationIdColumnName]; }); if (dbValue) { var dbValueWithId = new OperateEntity(relMetadata, dbValue.entity); var subEntityWithId = new OperateEntity(relMetadata, subEntity); _this.findCascadeUpdateEntities(updatesByRelations, relMetadata, dbValueWithId, subEntityWithId, dbEntities, relation, operations); } }); } else { var dbValue = dbEntities.find(function (dbValue) { return dbValue.entityTarget === relation.entityMetadata.target && dbValue.entity[referencedColumnName] === value[relationIdColumnName]; }); if (dbValue) { var dbValueWithId = new OperateEntity(relMetadata, dbValue.entity); var valueWithId = new OperateEntity(relMetadata, value); _this.findCascadeUpdateEntities(updatesByRelations, relMetadata, dbValueWithId, valueWithId, dbEntities, relation, operations); } } }); return operations; }; EntityPersistOperationBuilder.prototype.findCascadeRemovedEntities = function (metadata, dbEntityWithId, allPersistedEntities, fromRelation, fromMetadata, fromEntityId, parentAlreadyRemoved) { var _this = this; if (parentAlreadyRemoved === void 0) { parentAlreadyRemoved = false; } var dbEntity = dbEntityWithId.entity; var operations = []; var entityId = metadata.getEntityIdMap(dbEntity); var isObjectRemoved = parentAlreadyRemoved || !this.findEntityWithId(allPersistedEntities, metadata.target, entityId); // if object is removed and should be removed, we check if cascades are allowed before add it to operations list if (isObjectRemoved && fromRelation && !this.checkCascadesAllowed("remove", metadata, fromRelation)) { return operations; // looks like object is removed here, but cascades are not allowed - then we should stop iteration } else if (isObjectRemoved) { operations.push(new RemoveOperation(dbEntityWithId.entityTarget, dbEntity, entityId, fromMetadata, fromRelation, fromEntityId)); } metadata.relations.forEach(function (relation) { var dbValue = _this.getEntityRelationValue(relation, dbEntity); var relMetadata = relation.inverseEntityMetadata; if (!dbValue) return; if (dbValue instanceof Array) { dbValue.forEach(function (subDbEntity) { if (subDbEntity) { var subDbEntityWithId = new OperateEntity(relMetadata, subDbEntity); var relationOperations = _this.findCascadeRemovedEntities(relMetadata, subDbEntityWithId, allPersistedEntities, relation, metadata, metadata.getEntityIdMap(dbEntity), isObjectRemoved); relationOperations.forEach(function (o) { return operations.push(o); }); } }); } else if (dbValue) { var dbValueWithId = new OperateEntity(relMetadata, dbValue); var relationOperations = _this.findCascadeRemovedEntities(relMetadata, dbValueWithId, allPersistedEntities, relation, metadata, metadata.getEntityIdMap(dbEntity), isObjectRemoved); relationOperations.forEach(function (o) { return operations.push(o); }); } }, []); return operations; }; EntityPersistOperationBuilder.prototype.updateInverseRelations = function (metadata, dbOperateEntity, newOperateEntity, operations) { if (operations === void 0) { operations = []; } // const dbEntity = dbOperateEntity.entity; var newEntity = newOperateEntity.entity; metadata.relations .filter(function (relation) { return relation.isOneToMany; }) // todo: maybe need to check isOneToOne and not owner .forEach(function (relation) { var relationMetadata = relation.inverseEntityMetadata; // to find new objects in relation go throw all objects in newEntity and check if they don't exist in dbEntity if (newEntity && newEntity[relation.propertyName] instanceof Array) { if (!dbOperateEntity) { newEntity[relation.propertyName].forEach(function (subEntity) { operations.push(new UpdateByInverseSideOperation(relationMetadata.target, newOperateEntity.entityTarget, "update", subEntity, newEntity, relation)); }); } else { newEntity[relation.propertyName].filter(function (newSubEntity) { if (!dbOperateEntity.entity[relation.propertyName]) return true; return !dbOperateEntity.entity[relation.propertyName].find(function (dbSubEntity) { return relation.inverseEntityMetadata.compareEntities(newSubEntity, dbSubEntity); }); }).forEach(function (subEntity) { operations.push(new UpdateByInverseSideOperation(relationMetadata.target, newOperateEntity.entityTarget, "update", subEntity, newEntity, relation)); }); } } // we also need to find removed elements. to find them need to traverse dbEntity and find its elements missing in newEntity if (dbOperateEntity && dbOperateEntity.entity[relation.propertyName] instanceof Array) { dbOperateEntity.entity[relation.propertyName].filter(function (dbSubEntity) { if (!newEntity /* are you sure about this? */ || !newEntity[relation.propertyName]) return true; return !newEntity[relation.propertyName].find(function (newSubEntity) { return relation.inverseEntityMetadata.compareEntities(dbSubEntity, newSubEntity); }); }).forEach(function (subEntity) { operations.push(new UpdateByInverseSideOperation(relationMetadata.target, newOperateEntity.entityTarget, "remove", subEntity, newEntity, relation)); }); } }); return operations; }; /** * To update relation, you need: * update table where this relation (owner side) * set its relation property to inserted id * where * */ EntityPersistOperationBuilder.prototype.updateRelations = function (insertOperations, newEntity) { var _this = this; return insertOperations.reduce(function (operations, insertOperation) { return operations.concat(_this.findRelationsWithEntityInside(insertOperation, newEntity)); }, []); }; EntityPersistOperationBuilder.prototype.findRelationsWithEntityInside = function (insertOperation, entityToSearchInWithId) { var _this = this; var entityToSearchIn = entityToSearchInWithId.entity; var metadata = this.entityMetadatas.findByTarget(entityToSearchInWithId.entityTarget); var operations = []; metadata.relations.forEach(function (relation) { var value = _this.getEntityRelationValue(relation, entityToSearchIn); var inverseMetadata = relation.inverseEntityMetadata; if (!value) return; if (value instanceof Array) { value.forEach(function (sub) { if (!relation.isManyToMany && sub === insertOperation.entity) operations.push(new UpdateByRelationOperation(entityToSearchInWithId.entityTarget, entityToSearchIn, insertOperation, relation)); var subWithId = new OperateEntity(inverseMetadata, sub); var subOperations = _this.findRelationsWithEntityInside(insertOperation, subWithId); subOperations.forEach(function (o) { return operations.push(o); }); }); } else if (value) { if (value === insertOperation.entity) { operations.push(new UpdateByRelationOperation(entityToSearchInWithId.entityTarget, entityToSearchIn, insertOperation, relation)); } var valueWithId = new OperateEntity(inverseMetadata, value); var subOperations = _this.findRelationsWithEntityInside(insertOperation, valueWithId); subOperations.forEach(function (o) { return operations.push(o); }); } }); return operations; }; EntityPersistOperationBuilder.prototype.findJunctionInsertOperations = function (metadata, newEntityWithId, dbEntities, isRoot) { var _this = this; if (isRoot === void 0) { isRoot = true; } var newEntity = newEntityWithId.entity; var dbEntity = dbEntities.find(function (dbEntity) { return dbEntity.compareId(metadata.getEntityIdMap(newEntity)) && dbEntity.entityTarget === metadata.target; }); return metadata.relations.reduce(function (operations, relation) { var relationMetadata = relation.inverseEntityMetadata; var value = _this.getEntityRelationValue(relation, newEntity); if (value === null || value === undefined) return operations; var dbValue = dbEntity ? _this.getEntityRelationValue(relation, dbEntity.entity) : null; if (value instanceof Array) { value.forEach(function (subEntity) { if (relation.isManyToMany) { var relationIdProperty_1 = relationMetadata.firstPrimaryColumn.propertyName; // todo: join column metadata should be used instead of primaryColumn var has = !dbValue || !dbValue.find(function (e) { return e[relationIdProperty_1] === subEntity[relationIdProperty_1]; }); if (has) { operations.push({ metadata: relation.junctionEntityMetadata, entity1: newEntity, entity2: subEntity, entity1Target: newEntityWithId.entityTarget, entity2Target: relationMetadata.target }); } } if (isRoot || _this.checkCascadesAllowed("update", metadata, relation)) { var subEntityWithId = new OperateEntity(relationMetadata, subEntity); var subOperations = _this.findJunctionInsertOperations(relationMetadata, subEntityWithId, dbEntities, false); subOperations.forEach(function (o) { return operations.push(o); }); } }); } else { if (isRoot || _this.checkCascadesAllowed("update", metadata, relation)) { var valueWithId = new OperateEntity(relationMetadata, value); var subOperations = _this.findJunctionInsertOperations(relationMetadata, valueWithId, dbEntities, false); subOperations.forEach(function (o) { return operations.push(o); }); } } return operations; }, []); }; EntityPersistOperationBuilder.prototype.findJunctionRemoveOperations = function (metadata, dbEntityWithId, newEntities, isRoot) { var _this = this; if (isRoot === void 0) { isRoot = true; } var dbEntity = dbEntityWithId.entity; // if (!dbEntity) // if new entity is persisted then it does not have anything to be deleted // return []; var newEntity = newEntities.find(function (newEntity) { return newEntity.compareId(dbEntity) && newEntity.entityTarget === metadata.target; }); return metadata.relations .filter(function (relation) { return dbEntity[relation.propertyName] !== null && dbEntity[relation.propertyName] !== undefined; }) .reduce(function (operations, relation) { var relationMetadata = relation.inverseEntityMetadata; var relationIdProperty = relationMetadata.firstPrimaryColumn.propertyName; // todo: this should be got from join table metadata, not primaryColumn var value = newEntity ? _this.getEntityRelationValue(relation, newEntity.entity) : null; var dbValue = _this.getEntityRelationValue(relation, dbEntity); if (dbValue instanceof Array) { dbValue.forEach(function (subEntity) { if (!subEntity) return; if (relation.isManyToMany) { var has = !value || !value.find(function (e) { return e[relationIdProperty] === subEntity[relationIdProperty]; }); if (has) { operations.push({ metadata: relation.junctionEntityMetadata, entity1: dbEntity, entity2: subEntity, entity1Target: dbEntityWithId.entityTarget, entity2Target: relationMetadata.target }); } } if (isRoot || _this.checkCascadesAllowed("update", metadata, relation)) { var subEntityWithId = new OperateEntity(relationMetadata, subEntity); var subOperations = _this.findJunctionRemoveOperations(relationMetadata, subEntityWithId, newEntities, false); subOperations.forEach(function (o) { return operations.push(o); }); } }); } else if (dbValue) { if (isRoot || _this.checkCascadesAllowed("update", metadata, relation)) { var dbValueWithId = new OperateEntity(relationMetadata, dbValue); var subOperations = _this.findJunctionRemoveOperations(relationMetadata, dbValueWithId, newEntities, false); subOperations.forEach(function (o) { return operations.push(o); }); } } return operations; }, []); }; EntityPersistOperationBuilder.prototype.diffColumns = function (metadata, newEntity, dbEntity) { // console.log("differenting columns: newEntity: ", newEntity); // console.log("differenting columns: dbEntity: ", dbEntity); return metadata.allColumns.filter(function (column) { if (column.isVirtual || column.isParentId || column.isDiscriminator || column.isUpdateDate || column.isVersion || column.isCreateDate || column.getEntityValue(newEntity) === column.getEntityValue(dbEntity)) return false; // filter out "relational columns" only in the case if there is a relation object in entity if (!column.isInEmbedded && metadata.hasRelationWithDbName(column.propertyName)) { var relation = metadata.findRelationWithDbName(column.propertyName); if (newEntity[relation.propertyName] !== null && newEntity[relation.propertyName] !== undefined) return false; } return true; }); }; EntityPersistOperationBuilder.prototype.diffRelations = function (updatesByRelations, metadata, newEntity, dbEntity) { var _this = this; return metadata.allRelations.filter(function (relation) { if (!relation.isManyToOne && !(relation.isOneToOne && relation.isOwning)) return false; // try to find if there is update by relation operation - we dont need to generate update relation operation for this if (updatesByRelations.find(function (operation) { return operation.targetEntity === newEntity && operation.updatedRelation === relation; })) return false; if (!newEntity[relation.propertyName] && !dbEntity[relation.propertyName]) return false; if (!newEntity[relation.propertyName] || !dbEntity[relation.propertyName]) return true; var relatedMetadata = _this.entityMetadatas.findByTarget(relation.entityMetadata.target); return !relatedMetadata.compareEntities(newEntity[relation.propertyName], dbEntity[relation.propertyName]); }); }; EntityPersistOperationBuilder.prototype.findEntityWithId = function (entityWithIds, entityTarget, id) { return entityWithIds.find(function (entityWithId) { return entityWithId.compareId(id) && entityWithId.entityTarget === entityTarget; }); }; EntityPersistOperationBuilder.prototype.checkCascadesAllowed = function (type, metadata, relation) { var isAllowed = false; switch (type) { case "insert": isAllowed = relation.isCascadeInsert; break; case "update": isAllowed = relation.isCascadeUpdate; break; case "remove": isAllowed = relation.isCascadeRemove; break; } if (isAllowed === false && this.strictCascadesMode) throw new CascadesNotAllowedError(type, metadata, relation); return isAllowed; }; EntityPersistOperationBuilder.prototype.getEntityRelationValue = function (relation, entity) { return relation.isLazy ? entity["__" + relation.propertyName + "__"] : entity[relation.propertyName]; }; return EntityPersistOperationBuilder; }()); export { EntityPersistOperationBuilder }; //# sourceMappingURL=EntityPersistOperationsBuilder.js.map