typeorm
Version:
Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.
219 lines (217 loc) • 13.2 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.ManyToManySubjectBuilder = void 0;
const Subject_1 = require("../Subject");
const OrmUtils_1 = require("../../util/OrmUtils");
/**
* Builds operations needs to be executed for many-to-many relations of the given subjects.
*
* by example: post contains owner many-to-many relation with categories in the property called "categories", e.g.
* @ManyToMany(type => Category, category => category.posts) categories: Category[]
* If user adds categories into the post and saves post we need to bind them.
* This operation requires updation of junction table.
*/
class ManyToManySubjectBuilder {
// ---------------------------------------------------------------------
// Constructor
// ---------------------------------------------------------------------
constructor(subjects) {
this.subjects = subjects;
}
// ---------------------------------------------------------------------
// Public Methods
// ---------------------------------------------------------------------
/**
* Builds operations for any changes in the many-to-many relations of the subjects.
*/
build() {
this.subjects.forEach((subject) => {
// if subject doesn't have entity then no need to find something that should be inserted or removed
if (!subject.entity)
return;
// go through all persistence enabled many-to-many relations and build subject operations for them
subject.metadata.manyToManyRelations.forEach((relation) => {
// skip relations for which persistence is disabled
if (relation.persistenceEnabled === false)
return;
this.buildForSubjectRelation(subject, relation);
});
});
}
/**
* Builds operations for removal of all many-to-many records of all many-to-many relations of the given subject.
*/
buildForAllRemoval(subject) {
// if subject does not have a database entity then it means it does not exist in the database
// if it does not exist in the database then we don't have anything for deletion
if (!subject.databaseEntity)
return;
// go through all persistence enabled many-to-many relations and build subject operations for them
subject.metadata.manyToManyRelations.forEach((relation) => {
// skip relations for which persistence is disabled
if (relation.persistenceEnabled === false)
return;
// get all related entities (actually related entity relation ids) bind to this subject entity
// by example: returns category ids of the post we are currently working with (subject.entity is post)
const relatedEntityRelationIdsInDatabase = relation.getEntityValue(subject.databaseEntity);
// go through all related entities and create a new junction subject for each row in junction table
relatedEntityRelationIdsInDatabase.forEach((relationId) => {
const junctionSubject = new Subject_1.Subject({
metadata: relation.junctionEntityMetadata,
parentSubject: subject,
mustBeRemoved: true,
identifier: this.buildJunctionIdentifier(subject, relation, relationId),
});
// we use unshift because we need to perform those operations before post deletion is performed
// but post deletion was already added as an subject
// this is temporary solution, later we need to implement proper sorting of subjects before their removal
this.subjects.push(junctionSubject);
});
});
}
// ---------------------------------------------------------------------
// Protected Methods
// ---------------------------------------------------------------------
/**
* Builds operations for a given subject and relation.
*
* by example: subject is "post" entity we are saving here and relation is "categories" inside it here.
*/
buildForSubjectRelation(subject, relation) {
// load from db all relation ids of inverse entities that are "bind" to the subject's entity
// this way we gonna check which relation ids are missing and which are new (e.g. inserted or removed)
let databaseRelatedEntityIds = [];
// if subject don't have database entity it means all related entities in persisted subject are new and must be bind
// and we don't need to remove something that is not exist
if (subject.databaseEntity) {
const databaseRelatedEntityValue = relation.getEntityValue(subject.databaseEntity);
if (databaseRelatedEntityValue) {
databaseRelatedEntityIds = databaseRelatedEntityValue.map((e) => relation.inverseEntityMetadata.getEntityIdMap(e));
}
}
// extract entity's relation value
// by example: categories inside our post (subject.entity is post)
let relatedEntities = relation.getEntityValue(subject.entity);
if (relatedEntities === null)
// if value set to null its equal if we set it to empty array - all items must be removed from the database
relatedEntities = [];
if (!Array.isArray(relatedEntities))
return;
// from all related entities find only those which aren't found in the db - for them we will create operation subjects
relatedEntities.forEach((relatedEntity) => {
// by example: relatedEntity is category from categories saved with post
// todo: check how it will work for entities which are saved by cascades, but aren't saved in the database yet
// extract only relation id from the related entities, since we only need it for comparison
// by example: extract from category only relation id (category id, or let's say category title, depend on join column options)
let relatedEntityRelationIdMap = relation.inverseEntityMetadata.getEntityIdMap(relatedEntity);
// try to find a subject of this related entity, maybe it was loaded or was marked for persistence
const relatedEntitySubject = this.subjects.find((subject) => {
return subject.entity === relatedEntity;
});
// if subject with entity was found take subject identifier as relation id map since it may contain extra properties resolved
if (relatedEntitySubject)
relatedEntityRelationIdMap = relatedEntitySubject.identifier;
// if related entity relation id map is empty it means related entity is newly persisted
if (!relatedEntityRelationIdMap) {
// we decided to remove this error because it brings complications when saving object with non-saved entities
// if related entity does not have a subject then it means user tries to bind entity which wasn't saved
// in this persistence because he didn't pass this entity for save or he did not set cascades
// but without entity being inserted we cannot bind it in the relation operation, so we throw an exception here
// we decided to remove this error because it brings complications when saving object with non-saved entities
// if (!relatedEntitySubject)
// throw new TypeORMError(`Many-to-many relation "${relation.entityMetadata.name}.${relation.propertyPath}" contains ` +
// `entities which do not exist in the database yet, thus they cannot be bind in the database. ` +
// `Please setup cascade insertion or save entities before binding it.`);
if (!relatedEntitySubject)
return;
}
// try to find related entity in the database
// by example: find post's category in the database post's categories
const relatedEntityExistInDatabase = databaseRelatedEntityIds.find((databaseRelatedEntityRelationId) => {
return OrmUtils_1.OrmUtils.compareIds(databaseRelatedEntityRelationId, relatedEntityRelationIdMap);
});
// if entity is found then don't do anything - it means binding in junction table already exist, we don't need to add anything
if (relatedEntityExistInDatabase)
return;
const ownerValue = relation.isOwning
? subject
: relatedEntitySubject || relatedEntity; // by example: ownerEntityMap is post from subject here
const inverseValue = relation.isOwning
? relatedEntitySubject || relatedEntity
: subject; // by example: inverseEntityMap is category from categories array here
// create a new subject for insert operation of junction rows
const junctionSubject = new Subject_1.Subject({
metadata: relation.junctionEntityMetadata,
parentSubject: subject,
canBeInserted: true,
});
this.subjects.push(junctionSubject);
relation.junctionEntityMetadata.ownerColumns.forEach((column) => {
junctionSubject.changeMaps.push({
column: column,
value: ownerValue,
// valueFactory: (value) => column.referencedColumn!.getEntityValue(value) // column.referencedColumn!.getEntityValue(ownerEntityMap),
});
});
relation.junctionEntityMetadata.inverseColumns.forEach((column) => {
junctionSubject.changeMaps.push({
column: column,
value: inverseValue,
// valueFactory: (value) => column.referencedColumn!.getEntityValue(value) // column.referencedColumn!.getEntityValue(inverseEntityMap),
});
});
});
// get all inverse entities relation ids that are "bind" to the currently persisted entity
const changedInverseEntityRelationIds = [];
relatedEntities.forEach((relatedEntity) => {
// relation.inverseEntityMetadata!.getEntityIdMap(relatedEntity)
let relatedEntityRelationIdMap = relation.inverseEntityMetadata.getEntityIdMap(relatedEntity);
// try to find a subject of this related entity, maybe it was loaded or was marked for persistence
const relatedEntitySubject = this.subjects.find((subject) => {
return subject.entity === relatedEntity;
});
// if subject with entity was found take subject identifier as relation id map since it may contain extra properties resolved
if (relatedEntitySubject)
relatedEntityRelationIdMap = relatedEntitySubject.identifier;
if (relatedEntityRelationIdMap !== undefined &&
relatedEntityRelationIdMap !== null)
changedInverseEntityRelationIds.push(relatedEntityRelationIdMap);
});
// now from all entities in the persisted entity find only those which aren't found in the db
const removedJunctionEntityIds = databaseRelatedEntityIds.filter((existRelationId) => {
return !changedInverseEntityRelationIds.find((changedRelationId) => {
return OrmUtils_1.OrmUtils.compareIds(changedRelationId, existRelationId);
});
});
// finally create a new junction remove operations for missing related entities
removedJunctionEntityIds.forEach((removedEntityRelationId) => {
const junctionSubject = new Subject_1.Subject({
metadata: relation.junctionEntityMetadata,
parentSubject: subject,
mustBeRemoved: true,
identifier: this.buildJunctionIdentifier(subject, relation, removedEntityRelationId),
});
this.subjects.push(junctionSubject);
});
}
/**
* Creates identifiers for junction table.
* Example: { postId: 1, categoryId: 2 }
*/
buildJunctionIdentifier(subject, relation, relationId) {
const ownerEntityMap = relation.isOwning ? subject.entity : relationId;
const inverseEntityMap = relation.isOwning
? relationId
: subject.entity;
const identifier = {};
relation.junctionEntityMetadata.ownerColumns.forEach((column) => {
OrmUtils_1.OrmUtils.mergeDeep(identifier, column.createValueMap(column.referencedColumn.getEntityValue(ownerEntityMap)));
});
relation.junctionEntityMetadata.inverseColumns.forEach((column) => {
OrmUtils_1.OrmUtils.mergeDeep(identifier, column.createValueMap(column.referencedColumn.getEntityValue(inverseEntityMap)));
});
return identifier;
}
}
exports.ManyToManySubjectBuilder = ManyToManySubjectBuilder;
//# sourceMappingURL=ManyToManySubjectBuilder.js.map
;