typeorm
Version:
Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.
284 lines (282 loc) • 13.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.RelationLoader = void 0;
const FindOptionsUtils_1 = require("../find-options/FindOptionsUtils");
/**
* Wraps entities and creates getters/setters for their relations
* to be able to lazily load relations when accessing these relations.
*/
class RelationLoader {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(connection) {
this.connection = connection;
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Loads relation data for the given entity and its relation.
*/
load(relation, entityOrEntities, queryRunner, queryBuilder) {
// todo: check all places where it uses non array
if (queryRunner && queryRunner.isReleased)
queryRunner = undefined; // get new one if already closed
if (relation.isManyToOne || relation.isOneToOneOwner) {
return this.loadManyToOneOrOneToOneOwner(relation, entityOrEntities, queryRunner, queryBuilder);
}
else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
return this.loadOneToManyOrOneToOneNotOwner(relation, entityOrEntities, queryRunner, queryBuilder);
}
else if (relation.isManyToManyOwner) {
return this.loadManyToManyOwner(relation, entityOrEntities, queryRunner, queryBuilder);
}
else {
// many-to-many non owner
return this.loadManyToManyNotOwner(relation, entityOrEntities, queryRunner, queryBuilder);
}
}
/**
* 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
*/
loadManyToOneOrOneToOneOwner(relation, entityOrEntities, queryRunner, queryBuilder) {
const entities = Array.isArray(entityOrEntities)
? entityOrEntities
: [entityOrEntities];
const joinAliasName = relation.entityMetadata.name;
const qb = queryBuilder
? queryBuilder
: this.connection
.createQueryBuilder(queryRunner)
.select(relation.propertyName) // category
.from(relation.type, relation.propertyName);
const mainAlias = qb.expressionMap.mainAlias.name;
const columns = relation.entityMetadata.primaryColumns;
const joinColumns = relation.isOwning
? relation.joinColumns
: relation.inverseRelation.joinColumns;
const conditions = joinColumns
.map((joinColumn) => {
return `${relation.entityMetadata.name}.${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);
}
FindOptionsUtils_1.FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias.metadata);
return qb.getMany();
// return qb.getOne(); todo: fix all usages
}
/**
* 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]
*/
loadOneToManyOrOneToOneNotOwner(relation, entityOrEntities, queryRunner, queryBuilder) {
const entities = Array.isArray(entityOrEntities)
? entityOrEntities
: [entityOrEntities];
const columns = relation.inverseRelation.joinColumns;
const qb = queryBuilder
? queryBuilder
: this.connection
.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);
}
FindOptionsUtils_1.FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias.metadata);
return qb.getMany();
// return relation.isOneToMany ? qb.getMany() : qb.getOne(); todo: fix all usages
}
/**
* 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
*/
loadManyToManyOwner(relation, entityOrEntities, queryRunner, queryBuilder) {
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
? queryBuilder
: this.connection
.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);
FindOptionsUtils_1.FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias.metadata);
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
*/
loadManyToManyNotOwner(relation, entityOrEntities, queryRunner, queryBuilder) {
const entities = Array.isArray(entityOrEntities)
? entityOrEntities
: [entityOrEntities];
const qb = queryBuilder
? queryBuilder
: this.connection
.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);
FindOptionsUtils_1.FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias.metadata);
return qb.getMany();
}
/**
* Wraps given entity and creates getters/setters for its given relation
* to be able to lazily load data when accessing this relation.
*/
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;
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
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
;