ionic-orm-3
Version:
Data-mapper ORM for Ionic WebSQL and SQLite
452 lines • 28.2 kB
JavaScript
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