typeorm
Version:
Data-Mapper ORM for TypeScript and ES2023+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.
342 lines • 15.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RelationLoader = void 0;
const DriverUtils_1 = require("../driver/DriverUtils");
const FindOptionsUtils_1 = require("../find-options/FindOptionsUtils");
/**
* Loads relation data for entities and provides lazy-load wrappers
* via getters/setters.
*/
class RelationLoader {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(dataSource) {
this.dataSource = dataSource;
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Loads relation data for the given entity and its relation.
*
* @param relation
* @param entityOrEntities
* @param queryRunner
* @param queryBuilder
* @param loadEagerRelations
*/
load(relation, entityOrEntities, queryRunner, queryBuilder, loadEagerRelations) {
// todo: check all places where it uses non array
if (queryRunner?.isReleased)
queryRunner = undefined; // get new one if already closed
if (relation.isManyToOne || relation.isOneToOneOwner) {
return this.loadManyToOneOrOneToOneOwner(relation, entityOrEntities, queryRunner, queryBuilder, loadEagerRelations);
}
else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
return this.loadOneToManyOrOneToOneNotOwner(relation, entityOrEntities, queryRunner, queryBuilder, loadEagerRelations);
}
else if (relation.isManyToManyOwner) {
return this.loadManyToManyOwner(relation, entityOrEntities, queryRunner, queryBuilder, loadEagerRelations);
}
else {
// many-to-many non owner
return this.loadManyToManyNotOwner(relation, entityOrEntities, queryRunner, queryBuilder, loadEagerRelations);
}
}
/**
* Loads data for many-to-one and one-to-one owner relations.
*
* (ow) post.category<=>category.post
* loaded: category from post
*
* @example
* SELECT category.id AS category_id, category.name AS category_name FROM category category
* INNER JOIN post Post ON Post.category=category.id WHERE Post.id=1
*
* @param relation
* @param entityOrEntities
* @param queryRunner
* @param queryBuilder
* @param loadEagerRelations
*/
loadManyToOneOrOneToOneOwner(relation, entityOrEntities, queryRunner, queryBuilder, loadEagerRelations) {
const entities = Array.isArray(entityOrEntities)
? entityOrEntities
: [entityOrEntities];
const qb = queryBuilder ??
this.dataSource
.createQueryBuilder(queryRunner)
.select(relation.propertyName)
.from(relation.type, relation.propertyName);
const mainAlias = qb.expressionMap.mainAlias.name;
// For self-referencing relations the entity name already exists
// as an alias, so we need to generate a unique join alias name.
const baseName = relation.entityMetadata.name;
let joinAliasName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, { shorten: true }, baseName);
let suffix = 1;
while (qb.expressionMap.aliases.some(({ name }) => name === joinAliasName)) {
joinAliasName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, { shorten: true }, baseName, String(suffix++));
}
const columns = relation.entityMetadata.primaryColumns;
const joinColumns = relation.isOwning
? relation.joinColumns
: relation.inverseRelation.joinColumns;
const conditions = joinColumns
.map((joinColumn) => {
return `${joinAliasName}.${joinColumn.propertyName} = ${mainAlias}.${joinColumn.referencedColumn.propertyName}`;
})
.join(" AND ");
qb.innerJoin(relation.entityMetadata.target, joinAliasName, conditions);
if (columns.length === 1) {
qb.where(`${joinAliasName}.${columns[0].propertyPath} IN (:...${joinAliasName + "_" + columns[0].propertyName})`);
qb.setParameter(joinAliasName + "_" + columns[0].propertyName, entities.map((entity) => columns[0].getEntityValue(entity, true)));
}
else {
const condition = entities
.map((entity, entityIndex) => {
return columns
.map((column, columnIndex) => {
const paramName = joinAliasName +
"_entity_" +
entityIndex +
"_" +
columnIndex;
qb.setParameter(paramName, column.getEntityValue(entity, true));
return (joinAliasName +
"." +
column.propertyPath +
" = :" +
paramName);
})
.join(" AND ");
})
.map((condition) => "(" + condition + ")")
.join(" OR ");
qb.where(condition);
}
this.applyEagerRelations(qb, loadEagerRelations);
return qb.getMany();
}
/**
* Loads data for one-to-many and one-to-one not owner relations.
*
* SELECT post
* FROM post post
* WHERE post.[joinColumn.name] = entity[joinColumn.referencedColumn]
*
* @param relation
* @param entityOrEntities
* @param queryRunner
* @param queryBuilder
* @param loadEagerRelations
*/
loadOneToManyOrOneToOneNotOwner(relation, entityOrEntities, queryRunner, queryBuilder, loadEagerRelations) {
const entities = Array.isArray(entityOrEntities)
? entityOrEntities
: [entityOrEntities];
const columns = relation.inverseRelation.joinColumns;
const qb = queryBuilder ??
this.dataSource
.createQueryBuilder(queryRunner)
.select(relation.propertyName)
.from(relation.inverseRelation.entityMetadata.target, relation.propertyName);
const aliasName = qb.expressionMap.mainAlias.name;
if (columns.length === 1) {
qb.where(`${aliasName}.${columns[0].propertyPath} IN (:...${aliasName + "_" + columns[0].propertyName})`);
qb.setParameter(aliasName + "_" + columns[0].propertyName, entities.map((entity) => columns[0].referencedColumn.getEntityValue(entity, true)));
}
else {
const condition = entities
.map((entity, entityIndex) => {
return columns
.map((column, columnIndex) => {
const paramName = aliasName +
"_entity_" +
entityIndex +
"_" +
columnIndex;
qb.setParameter(paramName, column.referencedColumn.getEntityValue(entity, true));
return (aliasName +
"." +
column.propertyPath +
" = :" +
paramName);
})
.join(" AND ");
})
.map((condition) => "(" + condition + ")")
.join(" OR ");
qb.where(condition);
}
this.applyEagerRelations(qb, loadEagerRelations);
return qb.getMany();
}
/**
* Loads data for many-to-many owner relations.
*
* SELECT category
* FROM category category
* INNER JOIN post_categories post_categories
* ON post_categories.postId = :postId
* AND post_categories.categoryId = category.id
*
* @param relation
* @param entityOrEntities
* @param queryRunner
* @param queryBuilder
* @param loadEagerRelations
*/
loadManyToManyOwner(relation, entityOrEntities, queryRunner, queryBuilder, loadEagerRelations) {
const entities = Array.isArray(entityOrEntities)
? entityOrEntities
: [entityOrEntities];
const parameters = relation.joinColumns.reduce((parameters, joinColumn) => {
parameters[joinColumn.propertyName] = entities.map((entity) => joinColumn.referencedColumn.getEntityValue(entity, true));
return parameters;
}, {});
const qb = queryBuilder ??
this.dataSource
.createQueryBuilder(queryRunner)
.select(relation.propertyName)
.from(relation.type, relation.propertyName);
const mainAlias = qb.expressionMap.mainAlias.name;
const joinAlias = relation.junctionEntityMetadata.tableName;
const joinColumnConditions = relation.joinColumns.map((joinColumn) => {
return `${joinAlias}.${joinColumn.propertyName} IN (:...${joinColumn.propertyName})`;
});
const inverseJoinColumnConditions = relation.inverseJoinColumns.map((inverseJoinColumn) => {
return `${joinAlias}.${inverseJoinColumn.propertyName}=${mainAlias}.${inverseJoinColumn.referencedColumn.propertyName}`;
});
qb.innerJoin(joinAlias, joinAlias, [...joinColumnConditions, ...inverseJoinColumnConditions].join(" AND ")).setParameters(parameters);
this.applyEagerRelations(qb, loadEagerRelations);
return qb.getMany();
}
/**
* Loads data for many-to-many not owner relations.
*
* SELECT post
* FROM post post
* INNER JOIN post_categories post_categories
* ON post_categories.postId = post.id
* AND post_categories.categoryId = post_categories.categoryId
*
* @param relation
* @param entityOrEntities
* @param queryRunner
* @param queryBuilder
* @param loadEagerRelations
*/
loadManyToManyNotOwner(relation, entityOrEntities, queryRunner, queryBuilder, loadEagerRelations) {
const entities = Array.isArray(entityOrEntities)
? entityOrEntities
: [entityOrEntities];
const qb = queryBuilder ??
this.dataSource
.createQueryBuilder(queryRunner)
.select(relation.propertyName)
.from(relation.type, relation.propertyName);
const mainAlias = qb.expressionMap.mainAlias.name;
const joinAlias = relation.junctionEntityMetadata.tableName;
const joinColumnConditions = relation.inverseRelation.joinColumns.map((joinColumn) => {
return `${joinAlias}.${joinColumn.propertyName} = ${mainAlias}.${joinColumn.referencedColumn.propertyName}`;
});
const inverseJoinColumnConditions = relation.inverseRelation.inverseJoinColumns.map((inverseJoinColumn) => {
return `${joinAlias}.${inverseJoinColumn.propertyName} IN (:...${inverseJoinColumn.propertyName})`;
});
const parameters = relation.inverseRelation.inverseJoinColumns.reduce((parameters, joinColumn) => {
parameters[joinColumn.propertyName] = entities.map((entity) => joinColumn.referencedColumn.getEntityValue(entity, true));
return parameters;
}, {});
qb.innerJoin(joinAlias, joinAlias, [...joinColumnConditions, ...inverseJoinColumnConditions].join(" AND ")).setParameters(parameters);
this.applyEagerRelations(qb, loadEagerRelations);
return qb.getMany();
}
/**
* Applies eager relation loading to the given query builder based on the
* configured relation load strategy.
*
* @param qb
* @param loadEagerRelations
*/
applyEagerRelations(qb, loadEagerRelations) {
if (loadEagerRelations === false)
return;
const mainAlias = qb.expressionMap.mainAlias;
if (!mainAlias)
return;
if (qb.expressionMap.relationLoadStrategy === "query") {
qb.concatRelationMetadata(...mainAlias.metadata.eagerRelations);
}
else {
FindOptionsUtils_1.FindOptionsUtils.joinEagerRelations(qb, qb.alias, mainAlias.metadata);
}
}
/**
* Wraps given entity and creates getters/setters for its given relation
* to be able to lazily load data when accessing this relation.
*
* @param relation
* @param entity
* @param queryRunner
*/
enableLazyLoad(relation, entity, queryRunner) {
const relationLoader = this;
const dataIndex = "__" + relation.propertyName + "__"; // in what property of the entity loaded data will be stored
const promiseIndex = "__promise_" + relation.propertyName + "__"; // in what property of the entity loading promise will be stored
const resolveIndex = "__has_" + relation.propertyName + "__"; // indicates if relation data already was loaded or not, we need this flag if loaded data is empty
const setData = (entity, value) => {
entity[dataIndex] = value;
entity[resolveIndex] = true;
delete entity[promiseIndex];
return value;
};
const setPromise = (entity, value) => {
delete entity[resolveIndex];
delete entity[dataIndex];
entity[promiseIndex] = value;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
value.then(
// ensure different value is not assigned yet
(result) => entity[promiseIndex] === value
? setData(entity, result)
: result);
return value;
};
Object.defineProperty(entity, relation.propertyName, {
get: function () {
if (this[resolveIndex] === true ||
this[dataIndex] !== undefined)
// if related data already was loaded then simply return it
return Promise.resolve(this[dataIndex]);
if (this[promiseIndex])
// if related data is loading then return a promise relationLoader loads it
return this[promiseIndex];
// nothing is loaded yet, load relation data and save it in the model once they are loaded
const loader = relationLoader
.load(relation, this, queryRunner)
.then((result) => relation.isOneToOne || relation.isManyToOne
? result.length === 0
? null
: result[0]
: result);
return setPromise(this, loader);
},
set: function (value) {
if (value instanceof Promise) {
// if set data is a promise then wait for its resolve and save in the object
// eslint-disable-next-line @typescript-eslint/no-floating-promises
setPromise(this, value);
}
else {
// if its direct data set (non promise, probably not safe-typed)
setData(this, value);
}
},
configurable: true,
enumerable: false,
});
}
}
exports.RelationLoader = RelationLoader;
//# sourceMappingURL=RelationLoader.js.map