typeorm
Version:
Data-Mapper ORM for TypeScript and ES2021+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases.
448 lines (445 loc) • 21.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RawSqlResultsToEntityTransformer = void 0;
const OrmUtils_1 = require("../../util/OrmUtils");
const DriverUtils_1 = require("../../driver/DriverUtils");
const ObjectUtils_1 = require("../../util/ObjectUtils");
/**
* Transforms raw sql results returned from the database into entity object.
* Entity is constructed based on its entity metadata.
*/
class RawSqlResultsToEntityTransformer {
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
constructor(expressionMap, driver, rawRelationIdResults, rawRelationCountResults, queryRunner) {
this.expressionMap = expressionMap;
this.driver = driver;
this.rawRelationIdResults = rawRelationIdResults;
this.rawRelationCountResults = rawRelationCountResults;
this.queryRunner = queryRunner;
this.pojo = this.expressionMap.options.includes("create-pojo");
this.selections = new Set(this.expressionMap.selects.map((s) => s.selection));
this.aliasCache = new Map();
this.columnsCache = new Map();
}
// -------------------------------------------------------------------------
// Public Methods
// -------------------------------------------------------------------------
/**
* Since db returns a duplicated rows of the data where accuracies of the same object can be duplicated
* we need to group our result and we must have some unique id (primary key in our case)
*/
transform(rawResults, alias) {
const group = this.group(rawResults, alias);
const entities = [];
for (const results of group.values()) {
const entity = this.transformRawResultsGroup(results, alias);
if (entity !== undefined)
entities.push(entity);
}
return entities;
}
// -------------------------------------------------------------------------
// Protected Methods
// -------------------------------------------------------------------------
/**
* Build an alias from a name and column name.
*/
buildAlias(aliasName, columnName) {
let aliases = this.aliasCache.get(aliasName);
if (!aliases) {
aliases = new Map();
this.aliasCache.set(aliasName, aliases);
}
let columnAlias = aliases.get(columnName);
if (!columnAlias) {
columnAlias = DriverUtils_1.DriverUtils.buildAlias(this.driver, undefined, aliasName, columnName);
aliases.set(columnName, columnAlias);
}
return columnAlias;
}
/**
* Groups given raw results by ids of given alias.
*/
group(rawResults, alias) {
const map = new Map();
const keys = [];
if (alias.metadata.tableType === "view") {
keys.push(...alias.metadata.columns.map((column) => this.buildAlias(alias.name, column.databaseName)));
}
else {
keys.push(...alias.metadata.primaryColumns.map((column) => this.buildAlias(alias.name, column.databaseName)));
}
for (const rawResult of rawResults) {
const id = keys
.map((key) => {
const keyValue = rawResult[key];
if (Buffer.isBuffer(keyValue)) {
return keyValue.toString("hex");
}
if (ObjectUtils_1.ObjectUtils.isObject(keyValue)) {
return JSON.stringify(keyValue);
}
return keyValue;
})
.join("_"); // todo: check partial
const items = map.get(id);
if (!items) {
map.set(id, [rawResult]);
}
else {
items.push(rawResult);
}
}
return map;
}
/**
* Transforms set of data results into single entity.
*/
transformRawResultsGroup(rawResults, alias) {
// let hasColumns = false; // , hasEmbeddedColumns = false, hasParentColumns = false, hasParentEmbeddedColumns = false;
let metadata = alias.metadata;
if (metadata.discriminatorColumn) {
const discriminatorValues = rawResults.map((result) => result[this.buildAlias(alias.name, alias.metadata.discriminatorColumn.databaseName)]);
const discriminatorMetadata = metadata.childEntityMetadatas.find((childEntityMetadata) => {
return (typeof discriminatorValues.find((value) => value ===
childEntityMetadata.discriminatorValue) !== "undefined");
});
if (discriminatorMetadata)
metadata = discriminatorMetadata;
}
const entity = metadata.create(this.queryRunner, {
fromDeserializer: true,
pojo: this.pojo,
});
// get value from columns selections and put them into newly created entity
const hasColumns = this.transformColumns(rawResults, alias, entity, metadata);
const hasRelations = this.transformJoins(rawResults, entity, alias, metadata);
const hasRelationIds = this.transformRelationIds(rawResults, alias, entity, metadata);
const hasRelationCounts = this.transformRelationCounts(rawResults, alias, entity);
// if we have at least one selected column then return this entity
// since entity must have at least primary columns to be really selected and transformed into entity
if (hasColumns)
return entity;
// if we don't have any selected column we should not return entity,
// except for the case when entity only contain a primary column as a relation to another entity
// in this case its absolutely possible our entity to not have any columns except a single relation
const hasOnlyVirtualPrimaryColumns = metadata.primaryColumns.every((column) => column.isVirtual === true); // todo: create metadata.hasOnlyVirtualPrimaryColumns
if (hasOnlyVirtualPrimaryColumns &&
(hasRelations || hasRelationIds || hasRelationCounts))
return entity;
return undefined;
}
// get value from columns selections and put them into object
transformColumns(rawResults, alias, entity, metadata) {
let hasData = false;
const result = rawResults[0];
for (const [key, column] of this.getColumnsToProcess(alias.name, metadata)) {
const value = result[key];
if (value === undefined)
continue;
// we don't mark it as has data because if we will have all nulls in our object - we don't need such object
else if (value !== null && !column.isVirtualProperty)
hasData = true;
column.setEntityValue(entity, this.driver.prepareHydratedValue(value, column));
}
return hasData;
}
/**
* Transforms joined entities in the given raw results by a given alias and stores to the given (parent) entity
*/
transformJoins(rawResults, entity, alias, metadata) {
let hasData = false;
// let discriminatorValue: string = "";
// if (metadata.discriminatorColumn)
// discriminatorValue = rawResults[0][this.buildAlias(alias.name, alias.metadata.discriminatorColumn!.databaseName)];
for (const join of this.expressionMap.joinAttributes) {
// todo: we have problem here - when inner joins are used without selects it still create empty array
// skip joins without metadata
if (!join.metadata)
continue;
// if simple left or inner join was performed without selection then we don't need to do anything
if (!join.isSelected)
continue;
// this check need to avoid setting properties than not belong to entity when single table inheritance used. (todo: check if we still need it)
// const metadata = metadata.childEntityMetadatas.find(childEntityMetadata => discriminatorValue === childEntityMetadata.discriminatorValue);
if (join.relation &&
!metadata.relations.find((relation) => relation === join.relation))
continue;
// some checks to make sure this join is for current alias
if (join.mapToProperty) {
if (join.mapToPropertyParentAlias !== alias.name)
continue;
}
else {
if (!join.relation ||
join.parentAlias !== alias.name ||
join.relationPropertyPath !== join.relation.propertyPath)
continue;
}
// transform joined data into entities
let result = this.transform(rawResults, join.alias);
result = !join.isMany ? result[0] : result;
result = !join.isMany && result === undefined ? null : result; // this is needed to make relations to return null when its joined but nothing was found in the database
// if nothing was joined then simply continue
if (result === undefined)
continue;
// if join was mapped to some property then save result to that property
if (join.mapToPropertyPropertyName) {
entity[join.mapToPropertyPropertyName] = result; // todo: fix embeds
}
else {
// otherwise set to relation
join.relation.setEntityValue(entity, result);
}
hasData = true;
}
return hasData;
}
transformRelationIds(rawSqlResults, alias, entity, metadata) {
let hasData = false;
for (const [index, rawRelationIdResult,] of this.rawRelationIdResults.entries()) {
if (rawRelationIdResult.relationIdAttribute.parentAlias !==
alias.name)
continue;
const relation = rawRelationIdResult.relationIdAttribute.relation;
const valueMap = this.createValueMapFromJoinColumns(relation, rawRelationIdResult.relationIdAttribute.parentAlias, rawSqlResults);
if (valueMap === undefined || valueMap === null) {
continue;
}
// prepare common data for this call
this.prepareDataForTransformRelationIds();
// Extract idMaps from prepared data by hash
const hash = this.hashEntityIds(relation, valueMap);
const idMaps = this.relationIdMaps[index][hash] || [];
// Map data to properties
const properties = rawRelationIdResult.relationIdAttribute.mapToPropertyPropertyPath.split(".");
const mapToProperty = (properties, map, value) => {
const property = properties.shift();
if (property && properties.length === 0) {
map[property] = value;
return map;
}
if (property && properties.length > 0) {
mapToProperty(properties, map[property], value);
}
else {
return map;
}
};
if (relation.isOneToOne || relation.isManyToOne) {
if (idMaps[0] !== undefined) {
mapToProperty(properties, entity, idMaps[0]);
hasData = true;
}
}
else {
mapToProperty(properties, entity, idMaps);
hasData = hasData || idMaps.length > 0;
}
}
return hasData;
}
transformRelationCounts(rawSqlResults, alias, entity) {
let hasData = false;
for (const rawRelationCountResult of this.rawRelationCountResults) {
if (rawRelationCountResult.relationCountAttribute.parentAlias !==
alias.name)
continue;
const relation = rawRelationCountResult.relationCountAttribute.relation;
let referenceColumnName;
if (relation.isOneToMany) {
referenceColumnName =
relation.inverseRelation.joinColumns[0].referencedColumn
.databaseName; // todo: fix joinColumns[0]
}
else {
referenceColumnName = relation.isOwning
? relation.joinColumns[0].referencedColumn.databaseName
: relation.inverseRelation.joinColumns[0].referencedColumn
.databaseName;
}
const referenceColumnValue = rawSqlResults[0][this.buildAlias(alias.name, referenceColumnName)]; // we use zero index since its grouped data // todo: selection with alias for entity columns wont work
if (referenceColumnValue !== undefined &&
referenceColumnValue !== null) {
entity[rawRelationCountResult.relationCountAttribute.mapToPropertyPropertyName] = 0;
for (const result of rawRelationCountResult.results) {
if (result["parentId"] !== referenceColumnValue)
continue;
entity[rawRelationCountResult.relationCountAttribute.mapToPropertyPropertyName] = parseInt(result["cnt"]);
hasData = true;
}
}
}
return hasData;
}
getColumnsToProcess(aliasName, metadata) {
let metadatas = this.columnsCache.get(aliasName);
if (!metadatas) {
metadatas = new Map();
this.columnsCache.set(aliasName, metadatas);
}
let columns = metadatas.get(metadata);
if (!columns) {
columns = metadata.columns
.filter((column) => !column.isVirtual &&
// if user does not selected the whole entity or he used partial selection and does not select this particular column
// then we don't add this column and its value into the entity
(this.selections.has(aliasName) ||
this.selections.has(`${aliasName}.${column.propertyPath}`)) &&
// if table inheritance is used make sure this column is not child's column
!metadata.childEntityMetadatas.some((childMetadata) => childMetadata.target === column.target))
.map((column) => [
this.buildAlias(aliasName, column.databaseName),
column,
]);
metadatas.set(metadata, columns);
}
return columns;
}
createValueMapFromJoinColumns(relation, parentAlias, rawSqlResults) {
let columns;
if (relation.isManyToOne || relation.isOneToOneOwner) {
columns = relation.entityMetadata.primaryColumns.map((joinColumn) => joinColumn);
}
else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
columns = relation.inverseRelation.joinColumns.map((joinColumn) => joinColumn);
}
else {
if (relation.isOwning) {
columns = relation.joinColumns.map((joinColumn) => joinColumn);
}
else {
columns = relation.inverseRelation.inverseJoinColumns.map((joinColumn) => joinColumn);
}
}
return columns.reduce((valueMap, column) => {
for (const rawSqlResult of rawSqlResults) {
if (relation.isManyToOne || relation.isOneToOneOwner) {
valueMap[column.databaseName] =
this.driver.prepareHydratedValue(rawSqlResult[this.buildAlias(parentAlias, column.databaseName)], column);
}
else {
valueMap[column.databaseName] =
this.driver.prepareHydratedValue(rawSqlResult[this.buildAlias(parentAlias, column.referencedColumn.databaseName)], column.referencedColumn);
}
}
return valueMap;
}, {});
}
extractEntityPrimaryIds(relation, relationIdRawResult) {
let columns;
if (relation.isManyToOne || relation.isOneToOneOwner) {
columns = relation.entityMetadata.primaryColumns.map((joinColumn) => joinColumn);
}
else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
columns = relation.inverseRelation.joinColumns.map((joinColumn) => joinColumn);
}
else {
if (relation.isOwning) {
columns = relation.joinColumns.map((joinColumn) => joinColumn);
}
else {
columns = relation.inverseRelation.inverseJoinColumns.map((joinColumn) => joinColumn);
}
}
return columns.reduce((data, column) => {
data[column.databaseName] = relationIdRawResult[column.databaseName];
return data;
}, {});
}
/*private removeVirtualColumns(entity: ObjectLiteral, alias: Alias) {
const virtualColumns = this.expressionMap.selects
.filter(select => select.virtual)
.map(select => select.selection.replace(alias.name + ".", ""));
virtualColumns.forEach(virtualColumn => delete entity[virtualColumn]);
}*/
/** Prepare data to run #transformRelationIds, as a lot of result independent data is needed in every call */
prepareDataForTransformRelationIds() {
// Return early if the relationIdMaps were already calculated
if (this.relationIdMaps) {
return;
}
// Ensure this prepare function is only called once
this.relationIdMaps = this.rawRelationIdResults.map((rawRelationIdResult) => {
const relation = rawRelationIdResult.relationIdAttribute.relation;
// Calculate column metadata
let columns;
if (relation.isManyToOne || relation.isOneToOneOwner) {
columns = relation.joinColumns;
}
else if (relation.isOneToMany ||
relation.isOneToOneNotOwner) {
columns = relation.inverseEntityMetadata.primaryColumns;
}
else {
// ManyToMany
if (relation.isOwning) {
columns = relation.inverseJoinColumns;
}
else {
columns = relation.inverseRelation.joinColumns;
}
}
// Calculate the idMaps for the rawRelationIdResult
return rawRelationIdResult.results.reduce((agg, result) => {
let idMap = columns.reduce((idMap, column) => {
let value = result[column.databaseName];
if (relation.isOneToMany ||
relation.isOneToOneNotOwner) {
if (column.isVirtual &&
column.referencedColumn &&
column.referencedColumn.propertyName !==
column.propertyName) {
// if column is a relation
value =
column.referencedColumn.createValueMap(value);
}
return OrmUtils_1.OrmUtils.mergeDeep(idMap, column.createValueMap(value));
}
if (!column.isPrimary &&
column.referencedColumn.referencedColumn) {
// if column is a relation
value =
column.referencedColumn.referencedColumn.createValueMap(value);
}
return OrmUtils_1.OrmUtils.mergeDeep(idMap, column.referencedColumn.createValueMap(value));
}, {});
if (columns.length === 1 &&
!rawRelationIdResult.relationIdAttribute.disableMixedMap) {
if (relation.isOneToMany ||
relation.isOneToOneNotOwner) {
idMap = columns[0].getEntityValue(idMap);
}
else {
idMap =
columns[0].referencedColumn.getEntityValue(idMap);
}
}
// If an idMap is found, set it in the aggregator under the correct hash
if (idMap !== undefined) {
const hash = this.hashEntityIds(relation, result);
if (agg[hash]) {
agg[hash].push(idMap);
}
else {
agg[hash] = [idMap];
}
}
return agg;
}, {});
});
}
/**
* Use a simple JSON.stringify to create a simple hash of the primary ids of an entity.
* As this.extractEntityPrimaryIds always creates the primary id object in the same order, if the same relation is
* given, a simple JSON.stringify should be enough to get a unique hash per entity!
*/
hashEntityIds(relation, data) {
const entityPrimaryIds = this.extractEntityPrimaryIds(relation, data);
return JSON.stringify(entityPrimaryIds);
}
}
exports.RawSqlResultsToEntityTransformer = RawSqlResultsToEntityTransformer;
//# sourceMappingURL=RawSqlResultsToEntityTransformer.js.map