typeorm
Version:
Data-Mapper ORM for TypeScript and ES2023+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.
516 lines • 24.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RelationIdLoader = void 0;
const DriverUtils_1 = require("../driver/DriverUtils");
const TypeORMError_1 = require("../error/TypeORMError");
/**
* Loads relation ids for the given entities.
*/
class RelationIdLoader {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(dataSource, queryRunner, loadEagerRelations) {
this.dataSource = dataSource;
this.queryRunner = queryRunner;
this.loadEagerRelations = loadEagerRelations;
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Loads relation ids of the given entity or entities.
*
* @param relation
* @param entityOrEntities
* @param relatedEntityOrRelatedEntities
*/
load(relation, entityOrEntities, relatedEntityOrRelatedEntities) {
const entities = Array.isArray(entityOrEntities)
? entityOrEntities
: [entityOrEntities];
const relatedEntities = Array.isArray(relatedEntityOrRelatedEntities)
? relatedEntityOrRelatedEntities
: relatedEntityOrRelatedEntities
? [relatedEntityOrRelatedEntities]
: undefined;
// load relation ids depend of relation type
if (relation.isManyToMany) {
return this.loadForManyToMany(relation, entities, relatedEntities);
}
else if (relation.isManyToOne || relation.isOneToOneOwner) {
return this.loadForManyToOneAndOneToOneOwner(relation, entities, relatedEntities);
}
else {
// if (relation.isOneToMany || relation.isOneToOneNotOwner) {
return this.loadForOneToManyAndOneToOneNotOwner(relation, entities, relatedEntities);
}
}
/**
* Loads relation ids of the given entities and groups them into the object with parent and children.
*
* todo: extract this method?
*
* @param relation
* @param entitiesOrEntities
* @param relatedEntityOrEntities
* @param queryBuilder
*/
async loadManyToManyRelationIdsAndGroup(relation, entitiesOrEntities, relatedEntityOrEntities, queryBuilder) {
// console.log("relation:", relation.propertyName);
// console.log("entitiesOrEntities", entitiesOrEntities);
const isMany = relation.isManyToMany || relation.isOneToMany;
const entities = Array.isArray(entitiesOrEntities)
? entitiesOrEntities
: [entitiesOrEntities];
if (!relatedEntityOrEntities) {
relatedEntityOrEntities = await this.dataSource.relationLoader.load(relation, entitiesOrEntities, this.queryRunner, queryBuilder, this.loadEagerRelations);
if (!relatedEntityOrEntities.length)
return entities.map((entity) => ({
entity: entity,
related: isMany ? [] : undefined,
}));
}
// const relationIds = await this.load(relation, relatedEntityOrEntities!, entitiesOrEntities);
const relationIds = await this.load(relation, entitiesOrEntities, relatedEntityOrEntities);
// console.log("entities", entities);
// console.log("relatedEntityOrEntities", relatedEntityOrEntities);
// console.log("relationIds", relationIds);
const relatedEntities = Array.isArray(relatedEntityOrEntities)
? relatedEntityOrEntities
: [relatedEntityOrEntities];
let columns = [], inverseColumns = [];
if (relation.isManyToManyOwner) {
columns = relation.junctionEntityMetadata.inverseColumns.map((column) => column.referencedColumn);
inverseColumns = relation.junctionEntityMetadata.ownerColumns.map((column) => column.referencedColumn);
}
else if (relation.isManyToManyNotOwner) {
columns = relation.junctionEntityMetadata.ownerColumns.map((column) => column.referencedColumn);
inverseColumns =
relation.junctionEntityMetadata.inverseColumns.map((column) => column.referencedColumn);
}
else if (relation.isManyToOne || relation.isOneToOneOwner) {
columns = relation.joinColumns.map((column) => column.referencedColumn);
inverseColumns = relation.entityMetadata.primaryColumns;
}
else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
columns = relation.inverseRelation.entityMetadata.primaryColumns;
inverseColumns = relation.inverseRelation.joinColumns.map((column) => column.referencedColumn);
}
else {
}
return entities.map((entity) => {
const group = {
entity: entity,
related: isMany ? [] : undefined,
};
const entityRelationIds = relationIds.filter((relationId) => {
return inverseColumns.every((column) => {
return column.compareEntityValue(entity, relationId[DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, column.entityMetadata.name +
"_" +
column.propertyAliasName)]);
});
});
if (!entityRelationIds.length)
return group;
relatedEntities.forEach((relatedEntity) => {
entityRelationIds.forEach((relationId) => {
const relatedEntityMatched = columns.every((column) => {
return column.compareEntityValue(relatedEntity, relationId[DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, column.entityMetadata.name +
"_" +
relation.propertyPath.replace(".", "_") +
"_" +
column.propertyPath.replace(".", "_"))]);
});
if (relatedEntityMatched) {
if (isMany) {
;
group.related.push(relatedEntity);
}
else {
group.related = relatedEntity;
}
}
});
});
return group;
});
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Loads relation ids for the many-to-many relation.
*
* @param relation
* @param entities
* @param relatedEntities
*/
loadForManyToMany(relation, entities, relatedEntities) {
const junctionMetadata = relation.junctionEntityMetadata;
const mainAlias = junctionMetadata.name;
const columns = relation.isOwning
? junctionMetadata.ownerColumns
: junctionMetadata.inverseColumns;
const inverseColumns = relation.isOwning
? junctionMetadata.inverseColumns
: junctionMetadata.ownerColumns;
const fieldsToMetadata = new Map();
const qb = this.dataSource.createQueryBuilder(this.queryRunner);
// select all columns from junction table
columns.forEach((column) => {
const referenced = column.referencedColumn;
if (!referenced) {
throw new TypeORMError_1.TypeORMError(`Column "${column.propertyPath}" is missing a referencedColumn in junction table "${junctionMetadata.tableName}".`);
}
const columnName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, referenced.entityMetadata.name +
"_" +
referenced.propertyPath.replace(".", "_"));
fieldsToMetadata.set(columnName, referenced);
qb.addSelect(mainAlias + "." + column.propertyPath, columnName);
});
inverseColumns.forEach((column) => {
const referenced = column.referencedColumn;
if (!referenced) {
throw new TypeORMError_1.TypeORMError(`Column "${column.propertyPath}" is missing a referencedColumn in junction table "${junctionMetadata.tableName}".`);
}
const columnName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, referenced.entityMetadata.name +
"_" +
relation.propertyPath.replace(".", "_") +
"_" +
referenced.propertyPath.replace(".", "_"));
fieldsToMetadata.set(columnName, referenced);
qb.addSelect(mainAlias + "." + column.propertyPath, columnName);
});
// add conditions for the given entities
let condition1;
if (columns.length === 1) {
const values = entities.map((entity) => columns[0].referencedColumn.getEntityValue(entity));
const areAllNumbers = values.every((value) => typeof value === "number");
if (areAllNumbers) {
condition1 = `${mainAlias}.${columns[0].propertyPath} IN (${values.join(", ")})`;
}
else {
qb.setParameter("values1", values);
condition1 =
mainAlias +
"." +
columns[0].propertyPath +
" IN (:...values1)"; // todo: use ANY for postgres
}
}
else {
condition1 =
"(" +
entities
.map((entity, entityIndex) => {
return columns
.map((column) => {
const paramName = "entity1_" +
entityIndex +
"_" +
column.propertyName;
qb.setParameter(paramName, column.referencedColumn.getEntityValue(entity));
return (mainAlias +
"." +
column.propertyPath +
" = :" +
paramName);
})
.join(" AND ");
})
.map((condition) => "(" + condition + ")")
.join(" OR ") +
")";
}
// add conditions for the given inverse entities
let condition2 = "";
if (relatedEntities) {
if (inverseColumns.length === 1) {
const values = relatedEntities.map((entity) => inverseColumns[0].referencedColumn.getEntityValue(entity));
const areAllNumbers = values.every((value) => typeof value === "number");
if (areAllNumbers) {
condition2 = `${mainAlias}.${inverseColumns[0].propertyPath} IN (${values.join(", ")})`;
}
else {
qb.setParameter("values2", values);
condition2 =
mainAlias +
"." +
inverseColumns[0].propertyPath +
" IN (:...values2)"; // todo: use ANY for postgres
}
}
else {
condition2 =
"(" +
relatedEntities
.map((entity, entityIndex) => {
return inverseColumns
.map((column) => {
const paramName = "entity2_" +
entityIndex +
"_" +
column.propertyName;
qb.setParameter(paramName, column.referencedColumn.getEntityValue(entity));
return (mainAlias +
"." +
column.propertyPath +
" = :" +
paramName);
})
.join(" AND ");
})
.map((condition) => "(" + condition + ")")
.join(" OR ") +
")";
}
}
// execute query
const condition = [condition1, condition2]
.filter((v) => v.length > 0)
.join(" AND ");
return this.executeAndHydrateRaw(qb, junctionMetadata.target, mainAlias, condition, fieldsToMetadata);
}
/**
* Loads relation ids for the many-to-one and one-to-one owner relations.
*
* @param relation
* @param entities
* @param relatedEntities
*/
loadForManyToOneAndOneToOneOwner(relation, entities, relatedEntities) {
const mainAlias = relation.entityMetadata.targetName;
const fieldsToMetadata = new Map();
const hasAllJoinColumnsInEntity = relation.joinColumns.every((joinColumn) => {
return !!relation.entityMetadata.nonVirtualColumns.find((column) => column === joinColumn);
});
if (relatedEntities && hasAllJoinColumnsInEntity) {
const relationIdMaps = [];
entities.forEach((entity) => {
const relationIdMap = {};
relation.entityMetadata.primaryColumns.forEach((primaryColumn) => {
const key = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, primaryColumn.entityMetadata.name +
"_" +
primaryColumn.propertyPath.replace(".", "_"));
relationIdMap[key] =
primaryColumn.getEntityValue(entity);
});
relatedEntities.forEach((relatedEntity) => {
relation.joinColumns.forEach((joinColumn) => {
const entityColumnValue = joinColumn.getEntityValue(entity);
const relatedEntityColumnValue = joinColumn.referencedColumn.getEntityValue(relatedEntity);
if (entityColumnValue === undefined ||
relatedEntityColumnValue === undefined)
return;
if (entityColumnValue === relatedEntityColumnValue) {
const key = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, joinColumn.referencedColumn.entityMetadata
.name +
"_" +
relation.propertyPath.replace(".", "_") +
"_" +
joinColumn.referencedColumn.propertyPath.replace(".", "_"));
relationIdMap[key] = relatedEntityColumnValue;
}
});
});
if (Object.keys(relationIdMap).length ===
relation.entityMetadata.primaryColumns.length +
relation.joinColumns.length) {
relationIdMaps.push(relationIdMap);
}
});
// console.log("relationIdMap", relationIdMaps);
// console.log("entities.length", entities.length);
if (relationIdMaps.length === entities.length)
return Promise.resolve(relationIdMaps);
}
// select all columns we need
const qb = this.dataSource.createQueryBuilder(this.queryRunner);
relation.entityMetadata.primaryColumns.forEach((primaryColumn) => {
const columnName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, primaryColumn.entityMetadata.name +
"_" +
primaryColumn.propertyPath.replace(".", "_"));
fieldsToMetadata.set(columnName, primaryColumn);
qb.addSelect(mainAlias + "." + primaryColumn.propertyPath, columnName);
});
relation.joinColumns.forEach((column) => {
const referenced = column.referencedColumn;
if (!referenced) {
throw new TypeORMError_1.TypeORMError(`Join column "${column.propertyPath}" on "${relation.entityMetadata.targetName}" is missing a referencedColumn.`);
}
const columnName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, referenced.entityMetadata.name +
"_" +
relation.propertyPath.replace(".", "_") +
"_" +
referenced.propertyPath.replace(".", "_"));
fieldsToMetadata.set(columnName, referenced);
qb.addSelect(mainAlias + "." + column.propertyPath, columnName);
});
// add condition for entities
let condition;
if (relation.entityMetadata.primaryColumns.length === 1) {
const values = entities.map((entity) => relation.entityMetadata.primaryColumns[0].getEntityValue(entity));
const areAllNumbers = values.every((value) => typeof value === "number");
if (areAllNumbers) {
condition = `${mainAlias}.${relation.entityMetadata.primaryColumns[0].propertyPath} IN (${values.join(", ")})`;
}
else {
qb.setParameter("values", values);
condition =
mainAlias +
"." +
relation.entityMetadata.primaryColumns[0].propertyPath +
" IN (:...values)"; // todo: use ANY for postgres
}
}
else {
condition = entities
.map((entity, entityIndex) => {
return relation.entityMetadata.primaryColumns
.map((column, columnIndex) => {
const paramName = "entity" + entityIndex + "_" + columnIndex;
qb.setParameter(paramName, column.getEntityValue(entity));
return (mainAlias +
"." +
column.propertyPath +
" = :" +
paramName);
})
.join(" AND ");
})
.map((condition) => "(" + condition + ")")
.join(" OR ");
}
// execute query
return this.executeAndHydrateRaw(qb, relation.entityMetadata.target, mainAlias, condition, fieldsToMetadata);
}
/**
* Loads relation ids for the one-to-many and one-to-one not owner relations.
*
* @param relation
* @param entities
* @param relatedEntities
*/
loadForOneToManyAndOneToOneNotOwner(relation, entities, relatedEntities) {
const originalRelation = relation;
relation = relation.inverseRelation;
const fieldsToMetadata = new Map();
if (relation.entityMetadata.primaryColumns.length ===
relation.joinColumns.length) {
const sameReferencedColumns = relation.entityMetadata.primaryColumns.every((column) => {
return relation.joinColumns.indexOf(column) !== -1;
});
if (sameReferencedColumns) {
return Promise.resolve(entities.map((entity) => {
const result = {};
relation.joinColumns.forEach((joinColumn) => {
const value = joinColumn.referencedColumn.getEntityValue(entity);
const joinColumnName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, joinColumn.referencedColumn.entityMetadata
.name +
"_" +
joinColumn.referencedColumn.propertyPath.replace(".", "_"));
const primaryColumnName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, joinColumn.entityMetadata.name +
"_" +
originalRelation.propertyPath.replace(".", "_") +
"_" +
joinColumn.propertyPath.replace(".", "_"));
result[joinColumnName] = value;
result[primaryColumnName] = value;
});
return result;
}));
}
}
const mainAlias = relation.entityMetadata.targetName;
// select all columns we need
const qb = this.dataSource.createQueryBuilder(this.queryRunner);
relation.entityMetadata.primaryColumns.forEach((primaryColumn) => {
const columnName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, primaryColumn.entityMetadata.name +
"_" +
originalRelation.propertyPath.replace(".", "_") +
"_" +
primaryColumn.propertyPath.replace(".", "_"));
fieldsToMetadata.set(columnName, primaryColumn);
qb.addSelect(mainAlias + "." + primaryColumn.propertyPath, columnName);
});
relation.joinColumns.forEach((column) => {
const referenced = column.referencedColumn;
if (!referenced) {
throw new TypeORMError_1.TypeORMError(`Join column "${column.propertyPath}" on "${relation.entityMetadata.targetName}" is missing a referencedColumn.`);
}
const columnName = DriverUtils_1.DriverUtils.buildAlias(this.dataSource.driver, undefined, referenced.entityMetadata.name +
"_" +
referenced.propertyPath.replace(".", "_"));
fieldsToMetadata.set(columnName, referenced);
qb.addSelect(mainAlias + "." + column.propertyPath, columnName);
});
// add condition for entities
let condition;
if (relation.joinColumns.length === 1) {
const values = entities.map((entity) => relation.joinColumns[0].referencedColumn.getEntityValue(entity));
const areAllNumbers = values.every((value) => typeof value === "number");
if (areAllNumbers) {
condition = `${mainAlias}.${relation.joinColumns[0].propertyPath} IN (${values.join(", ")})`;
}
else {
qb.setParameter("values", values);
condition =
mainAlias +
"." +
relation.joinColumns[0].propertyPath +
" IN (:...values)"; // todo: use ANY for postgres
}
}
else {
condition = entities
.map((entity, entityIndex) => {
return relation.joinColumns
.map((joinColumn, joinColumnIndex) => {
const paramName = "entity" + entityIndex + "_" + joinColumnIndex;
qb.setParameter(paramName, joinColumn.referencedColumn.getEntityValue(entity));
return (mainAlias +
"." +
joinColumn.propertyPath +
" = :" +
paramName);
})
.join(" AND ");
})
.map((condition) => "(" + condition + ")")
.join(" OR ");
}
// execute query
return this.executeAndHydrateRaw(qb, relation.entityMetadata.target, mainAlias, condition, fieldsToMetadata);
}
/**
* Executes a raw query and hydrates the results using driver-specific
* value preparation based on the column metadata.
*
* @param qb
* @param target
* @param mainAlias
* @param condition
* @param fieldsToMetadata
*/
executeAndHydrateRaw(qb, target, mainAlias, condition, fieldsToMetadata) {
return qb
.from(target, mainAlias)
.where(condition)
.getRawMany()
.then((result) => {
result.forEach((data) => {
Object.keys(data).forEach((key) => {
const column = fieldsToMetadata.get(key);
if (column) {
data[key] =
this.dataSource.driver.prepareHydratedValue(data[key], column);
}
});
});
return result;
});
}
}
exports.RelationIdLoader = RelationIdLoader;
//# sourceMappingURL=RelationIdLoader.js.map