typeorm
Version:
Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.
276 lines (274 loc) • 14.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");
const OrmUtils_1 = require("../../util/OrmUtils");
class RelationIdLoader {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(connection, queryRunner, relationIdAttributes) {
this.connection = connection;
this.queryRunner = queryRunner;
this.relationIdAttributes = relationIdAttributes;
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
async load(rawEntities) {
const promises = this.relationIdAttributes.map(async (relationIdAttr) => {
if (relationIdAttr.relation.isManyToOne ||
relationIdAttr.relation.isOneToOneOwner) {
// example: Post and Tag
// loadRelationIdAndMap("post.tagId", "post.tag")
// we expect it to load id of tag
if (relationIdAttr.queryBuilderFactory)
throw new TypeORMError_1.TypeORMError("Additional condition can not be used with ManyToOne or OneToOne owner relations.");
const duplicates = {};
const results = rawEntities
.map((rawEntity) => {
const result = {};
const duplicateParts = [];
relationIdAttr.relation.joinColumns.forEach((joinColumn) => {
result[joinColumn.databaseName] =
this.connection.driver.prepareHydratedValue(rawEntity[DriverUtils_1.DriverUtils.buildAlias(this.connection.driver, undefined, relationIdAttr.parentAlias, joinColumn.databaseName)], joinColumn.referencedColumn);
const duplicatePart = `${joinColumn.databaseName}:${result[joinColumn.databaseName]}`;
if (duplicateParts.indexOf(duplicatePart) === -1) {
duplicateParts.push(duplicatePart);
}
});
relationIdAttr.relation.entityMetadata.primaryColumns.forEach((primaryColumn) => {
result[primaryColumn.databaseName] =
this.connection.driver.prepareHydratedValue(rawEntity[DriverUtils_1.DriverUtils.buildAlias(this.connection.driver, undefined, relationIdAttr.parentAlias, primaryColumn.databaseName)], primaryColumn);
const duplicatePart = `${primaryColumn.databaseName}:${result[primaryColumn.databaseName]}`;
if (duplicateParts.indexOf(duplicatePart) === -1) {
duplicateParts.push(duplicatePart);
}
});
duplicateParts.sort();
const duplicate = duplicateParts.join("::");
if (duplicates[duplicate]) {
return null;
}
duplicates[duplicate] = true;
return result;
})
.filter((v) => v);
return {
relationIdAttribute: relationIdAttr,
results: results,
};
}
else if (relationIdAttr.relation.isOneToMany ||
relationIdAttr.relation.isOneToOneNotOwner) {
// example: Post and Category
// loadRelationIdAndMap("post.categoryIds", "post.categories")
// we expect it to load array of category ids
const relation = relationIdAttr.relation; // "post.categories"
const joinColumns = relation.isOwning
? relation.joinColumns
: relation.inverseRelation.joinColumns;
const table = relation.inverseEntityMetadata.target; // category
const tableName = relation.inverseEntityMetadata.tableName; // category
const tableAlias = relationIdAttr.alias || tableName; // if condition (custom query builder factory) is set then relationIdAttr.alias defined
const duplicates = {};
const parameters = {};
const condition = rawEntities
.map((rawEntity, index) => {
const duplicateParts = [];
const parameterParts = {};
const queryPart = joinColumns
.map((joinColumn) => {
const parameterName = joinColumn.databaseName + index;
const parameterValue = rawEntity[DriverUtils_1.DriverUtils.buildAlias(this.connection.driver, undefined, relationIdAttr.parentAlias, joinColumn.referencedColumn
.databaseName)];
const duplicatePart = `${tableAlias}:${joinColumn.propertyPath}:${parameterValue}`;
if (duplicateParts.indexOf(duplicatePart) !== -1) {
return "";
}
duplicateParts.push(duplicatePart);
parameterParts[parameterName] =
parameterValue;
return (tableAlias +
"." +
joinColumn.propertyPath +
" = :" +
parameterName);
})
.filter((v) => v)
.join(" AND ");
duplicateParts.sort();
const duplicate = duplicateParts.join("::");
if (duplicates[duplicate]) {
return "";
}
duplicates[duplicate] = true;
Object.assign(parameters, parameterParts);
return queryPart;
})
.filter((v) => v)
.map((condition) => "(" + condition + ")")
.join(" OR ");
// ensure we won't perform redundant queries for joined data which was not found in selection
// example: if post.category was not found in db then no need to execute query for category.imageIds
if (!condition)
return {
relationIdAttribute: relationIdAttr,
results: [],
};
// generate query:
// SELECT category.id, category.postId FROM category category ON category.postId = :postId
const qb = this.connection.createQueryBuilder(this.queryRunner);
const columns = OrmUtils_1.OrmUtils.uniq([
...joinColumns,
...relation.inverseRelation.entityMetadata
.primaryColumns,
], (column) => column.propertyPath);
columns.forEach((joinColumn) => {
qb.addSelect(tableAlias + "." + joinColumn.propertyPath, joinColumn.databaseName);
});
qb.from(table, tableAlias)
.where("(" + condition + ")") // need brackets because if we have additional condition and no brackets, it looks like (a = 1) OR (a = 2) AND b = 1, that is incorrect
.setParameters(parameters);
// apply condition (custom query builder factory)
if (relationIdAttr.queryBuilderFactory)
relationIdAttr.queryBuilderFactory(qb);
const results = await qb.getRawMany();
results.forEach((result) => {
joinColumns.forEach((column) => {
result[column.databaseName] =
this.connection.driver.prepareHydratedValue(result[column.databaseName], column.referencedColumn);
});
relation.inverseRelation.entityMetadata.primaryColumns.forEach((column) => {
result[column.databaseName] =
this.connection.driver.prepareHydratedValue(result[column.databaseName], column);
});
});
return {
relationIdAttribute: relationIdAttr,
results,
};
}
else {
// many-to-many
// example: Post and Category
// owner side: loadRelationIdAndMap("post.categoryIds", "post.categories")
// inverse side: loadRelationIdAndMap("category.postIds", "category.posts")
// we expect it to load array of post ids
const relation = relationIdAttr.relation;
const joinColumns = relation.isOwning
? relation.joinColumns
: relation.inverseRelation.inverseJoinColumns;
const inverseJoinColumns = relation.isOwning
? relation.inverseJoinColumns
: relation.inverseRelation.joinColumns;
const junctionAlias = relationIdAttr.junctionAlias;
const inverseSideTableName = relationIdAttr.joinInverseSideMetadata.tableName;
const inverseSideTableAlias = relationIdAttr.alias || inverseSideTableName;
const junctionTableName = relation.isOwning
? relation.junctionEntityMetadata.tableName
: relation.inverseRelation.junctionEntityMetadata
.tableName;
const mappedColumns = rawEntities.map((rawEntity) => {
return joinColumns.reduce((map, joinColumn) => {
map[joinColumn.propertyPath] =
rawEntity[DriverUtils_1.DriverUtils.buildAlias(this.connection.driver, undefined, relationIdAttr.parentAlias, joinColumn.referencedColumn
.databaseName)];
return map;
}, {});
});
// ensure we won't perform redundant queries for joined data which was not found in selection
// example: if post.category was not found in db then no need to execute query for category.imageIds
if (mappedColumns.length === 0)
return {
relationIdAttribute: relationIdAttr,
results: [],
};
const parameters = {};
const duplicates = {};
const joinColumnConditions = mappedColumns
.map((mappedColumn, index) => {
const duplicateParts = [];
const parameterParts = {};
const queryPart = Object.keys(mappedColumn)
.map((key) => {
const parameterName = key + index;
const parameterValue = mappedColumn[key];
const duplicatePart = `${junctionAlias}:${key}:${parameterValue}`;
if (duplicateParts.indexOf(duplicatePart) !== -1) {
return "";
}
duplicateParts.push(duplicatePart);
parameterParts[parameterName] =
parameterValue;
return (junctionAlias +
"." +
key +
" = :" +
parameterName);
})
.filter((s) => s)
.join(" AND ");
duplicateParts.sort();
const duplicate = duplicateParts.join("::");
if (duplicates[duplicate]) {
return "";
}
duplicates[duplicate] = true;
Object.assign(parameters, parameterParts);
return queryPart;
})
.filter((s) => s);
const inverseJoinColumnCondition = inverseJoinColumns
.map((joinColumn) => {
return (junctionAlias +
"." +
joinColumn.propertyPath +
" = " +
inverseSideTableAlias +
"." +
joinColumn.referencedColumn.propertyPath);
})
.join(" AND ");
const condition = joinColumnConditions
.map((condition) => {
return ("(" +
condition +
" AND " +
inverseJoinColumnCondition +
")");
})
.join(" OR ");
const qb = this.connection.createQueryBuilder(this.queryRunner);
inverseJoinColumns.forEach((joinColumn) => {
qb.addSelect(junctionAlias + "." + joinColumn.propertyPath, joinColumn.databaseName).addOrderBy(junctionAlias + "." + joinColumn.propertyPath);
});
joinColumns.forEach((joinColumn) => {
qb.addSelect(junctionAlias + "." + joinColumn.propertyPath, joinColumn.databaseName).addOrderBy(junctionAlias + "." + joinColumn.propertyPath);
});
qb.from(inverseSideTableName, inverseSideTableAlias)
.innerJoin(junctionTableName, junctionAlias, condition)
.setParameters(parameters);
// apply condition (custom query builder factory)
if (relationIdAttr.queryBuilderFactory)
relationIdAttr.queryBuilderFactory(qb);
const results = await qb.getRawMany();
results.forEach((result) => {
;
[...joinColumns, ...inverseJoinColumns].forEach((column) => {
result[column.databaseName] =
this.connection.driver.prepareHydratedValue(result[column.databaseName], column.referencedColumn);
});
});
return {
relationIdAttribute: relationIdAttr,
results,
};
}
});
return Promise.all(promises);
}
}
exports.RelationIdLoader = RelationIdLoader;
//# sourceMappingURL=RelationIdLoader.js.map
;