working on relations in embeds

This commit is contained in:
Zotov Dmitry 2017-05-04 17:38:38 +05:00
parent 90addb83e1
commit 3dd69a3887
28 changed files with 1505 additions and 259 deletions

View File

@ -309,10 +309,15 @@ export class MetadataArgsStorage {
return Object.getPrototypeOf(target1.prototype).constructor === target2;
}
findJoinTable(target: Function|string, propertyName: string) {
findJoinTable(target: Function|string, propertyName: string): JoinTableMetadataArgs|undefined {
return this.joinTables.toArray().find(joinTable => {
return joinTable.target === target && joinTable.propertyName === propertyName;
});
}
findJoinColumns(target: Function|string, propertyName: string): JoinColumnMetadataArgs[] {
return this.joinColumns.toArray().filter(joinColumn => {
return joinColumn.target === target && joinColumn.propertyName === propertyName;
});
}
}

View File

@ -124,7 +124,7 @@ export class EntityMetadataBuilder {
entityMetadata.relations.forEach(relation => {
const inverseEntityMetadata = entityMetadatas.find(m => m.target === relation.type || (typeof relation.type === "string" && m.targetName === relation.type));
if (!inverseEntityMetadata)
throw new Error("Entity metadata for " + entityMetadata.name + "#" + relation.propertyName + " was not found.");
throw new Error("Entity metadata for " + entityMetadata.name + "#" + relation.propertyPath + " was not found.");
relation.inverseEntityMetadata = inverseEntityMetadata;
});
@ -178,10 +178,6 @@ export class EntityMetadataBuilder {
});
entityMetadatas.forEach(entityMetadata => {
const mergedArgs = allMergedArgs.find(mergedArgs => {
return mergedArgs.table.target === entityMetadata.target;
});
if (!mergedArgs) return;
// create entity's relations join columns
entityMetadata.oneToOneRelations
@ -211,7 +207,7 @@ export class EntityMetadataBuilder {
// we need to go thought each many-to-one relation without join column decorator set
// and create join column metadata args for them
const joinColumnArgsArray = mergedArgs.joinColumns.filterByProperty(relation.propertyName);
const joinColumnArgsArray = metadataArgsStorage.findJoinColumns(relation.target, relation.propertyName);
const hasAnyReferencedColumnName = joinColumnArgsArray.find(joinColumnArgs => !!joinColumnArgs.referencedColumnName);
const manyToOneWithoutJoinColumn = joinColumnArgsArray.length === 0 && relation.isManyToOne;

View File

@ -631,18 +631,22 @@ export class EntityMetadata {
/**
* Creates an object - map of columns and relations of the entity.
*
* example: Post{ id: number, name: string, counterEmbed: { count: number }, category: Category }.
* This method will create following object:
* { id: "id", counterEmbed: { count: "counterEmbed.count" }, category: "category" }
*/
createPropertiesMap(): { [name: string]: string|any } {
const entity: { [name: string]: string|any } = {};
this._columns.forEach(column => entity[column.propertyName] = column.propertyName);
this.relations.forEach(relation => entity[relation.propertyName] = relation.propertyName);
return entity;
const map: { [name: string]: string|any } = {};
this.columns.forEach(column => OrmUtils.mergeDeep(map, column.createValueMap(column.propertyPath)));
this.relations.forEach(relation => OrmUtils.mergeDeep(map, relation.createValueMap(relation.propertyPath)));
return map;
}
/**
* Computes property name of the entity using given PropertyTypeInFunction.
*/
computePropertyName(nameOrFn: PropertyTypeInFunction<any>) {
computePropertyPath(nameOrFn: PropertyTypeInFunction<any>) {
return typeof nameOrFn === "string" ? nameOrFn : nameOrFn(this.createPropertiesMap());
}
@ -1007,27 +1011,13 @@ export class EntityMetadata {
* Checks if given entity has an id.
*/
hasId(entity: ObjectLiteral): boolean {
if (!entity)
return false;
// if (this.metadata.parentEntityMetadata) {
// return this.metadata.parentEntityMetadata.parentIdColumns.every(parentIdColumn => {
// const columnName = parentIdColumn.propertyName;
// return !!entity &&
// entity.hasOwnProperty(columnName) &&
// entity[columnName] !== null &&
// entity[columnName] !== undefined &&
// entity[columnName] !== "";
// });
// } else {
return this.primaryColumns.every(primaryColumn => {
const columnName = primaryColumn.propertyName;
return !!entity &&
entity.hasOwnProperty(columnName) &&
entity[columnName] !== null &&
entity[columnName] !== undefined &&
entity[columnName] !== "";
return this.primaryColumns.every(primaryColumn => { /// todo: this.metadata.parentEntityMetadata ?
const value = primaryColumn.getValue(entity);
return value !== null && value !== undefined && value!== "";
});
// }
}
/**

View File

@ -401,13 +401,13 @@ export class RelationMetadata {
* Checks if inverse side is specified by a relation.
*/
get hasInverseSide(): boolean {
return this.inverseEntityMetadata && this.inverseEntityMetadata.hasRelationWithPropertyName(this.inverseSideProperty);
return this.inverseEntityMetadata && this.inverseEntityMetadata.hasRelationWithPropertyName(this.inverseSidePropertyPath);
}
/**
* Gets the property name of the inverse side of the relation.
*/
get inverseSideProperty(): string { // todo: should be called inverseSidePropertyName ?
get inverseSidePropertyPath(): string { // todo: should be called inverseSidePropertyName ?
if (this._inverseSideProperty) {
return this.computeInverseSide(this._inverseSideProperty);
@ -427,9 +427,9 @@ export class RelationMetadata {
* Gets the relation metadata of the inverse side of this relation.
*/
get inverseRelation(): RelationMetadata {
const relation = this.inverseEntityMetadata.findRelationWithPropertyName(this.inverseSideProperty);
const relation = this.inverseEntityMetadata.findRelationWithPropertyPath(this.inverseSidePropertyPath);
if (!relation)
throw new Error(`Inverse side was not found in the relation ${this.entityMetadata.name}#${this.inverseSideProperty}`);
throw new Error(`Inverse side was not found in the relation ${this.entityMetadata.name}#${this.inverseSidePropertyPath}`);
return relation;
}
@ -511,6 +511,45 @@ export class RelationMetadata {
}
}
/**
* Creates entity id map from the given entity ids array.
*
* @stable
*/
createValueMap(value: any) {
// extract column value from embeds of entity if column is in embedded
if (this.embeddedMetadata) {
// example: post[data][information][counters].id where "data", "information" and "counters" are embeddeds
// we need to get value of "id" column from the post real entity object and return it in a
// { data: { information: { counters: { id: ... } } } } format
// first step - we extract all parent properties of the entity relative to this column, e.g. [data, information, counters]
const propertyNames = this.embeddedMetadata.parentPropertyNames;
// now need to access post[data][information][counters] to get column value from the counters
// and on each step we need to create complex literal object, e.g. first { data },
// then { data: { information } }, then { data: { information: { counters } } },
// then { data: { information: { counters: [this.propertyName]: entity[data][information][counters][this.propertyName] } } }
// this recursive function helps doing that
const extractEmbeddedColumnValue = (propertyNames: string[], map: ObjectLiteral): any => {
const propertyName = propertyNames.shift();
if (propertyName) {
map[propertyName] = {};
extractEmbeddedColumnValue(propertyNames, map[propertyName]);
return map;
}
map[this.propertyName] = value;
return map;
};
return extractEmbeddedColumnValue(propertyNames, {});
} else { // no embeds - no problems. Simply return column property name and its value of the entity
return { [this.propertyName]: value };
}
}
/**
* todo: lazy relations are not supported here? implement logic?
*

View File

@ -394,12 +394,12 @@ export class Subject {
// 1. related entity can be another entity which is natural way
// 2. related entity can be entity id which is hacked way of updating entity
// todo: what to do if there is a column with relationId? (cover this too?)
const updatedEntityRelationId: any =
this.entity[relation.propertyName] instanceof Object ?
relation.inverseEntityMetadata.getEntityIdMixedMap(this.entity[relation.propertyName])
: this.entity[relation.propertyName];
const entityValue = relation.getEntityValue(this.entity);
const updatedEntityRelationId: any = entityValue instanceof Object
? relation.inverseEntityMetadata.getEntityIdMixedMap(entityValue)
: entityValue;
const dbEntityRelationId = this.databaseEntity[relation.propertyName];
const dbEntityRelationId = relation.getEntityValue(this.databaseEntity);
// todo: try to find if there is update by relation operation - we dont need to generate update relation operation for this
// todo: if (updatesByRelations.find(operation => operation.targetEntity === this && operation.updatedRelation === relation))

View File

@ -345,7 +345,7 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
// note that if databaseEntity has relation, it can only be a relation id,
// because of query builder option "RELATION_ID_VALUES" we used
const relationIdInDatabaseEntity = subject.databaseEntity[relation.propertyName]; // (example) returns post.detailsId
const relationIdInDatabaseEntity = relation.getEntityValue(subject.databaseEntity); // (example) returns post.detailsId
// if database relation id does not exist in the database object then nothing to remove
if (relationIdInDatabaseEntity === null || relationIdInDatabaseEntity === undefined)
@ -356,7 +356,7 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
if (subject.hasEntity) {
const persistValue = relation.getEntityValue(subject.entity);
if (persistValue === null) persistValueRelationId = null;
if (persistValue) persistValueRelationId = persistValue[relation.joinColumns[0].referencedColumn.propertyName];
if (persistValue) persistValueRelationId = relation.joinColumns[0].referencedColumn.getValue(persistValue);
if (persistValueRelationId === undefined) return; // skip undefined properties
}
@ -374,7 +374,7 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
// (example) here we seek a Details loaded from the database in the subjects
// (example) here relatedSubject.databaseEntity is a Details
// (example) and we need to compare details.id === post.detailsId
return relatedSubject.databaseEntity[relation.joinColumns[0].referencedColumn.propertyName] === relationIdInDatabaseEntity;
return relation.joinColumns[0].referencedColumn.getValue(relatedSubject.databaseEntity) === relationIdInDatabaseEntity;
});
// if not loaded yet then load it from the database
@ -384,7 +384,7 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
const databaseEntity = await this.connection
.getRepository<ObjectLiteral>(valueMetadata.target)
.createQueryBuilder(qbAlias, this.queryRunnerProvider) // todo: this wont work for mongodb. implement this in some method and call it here instead?
.where(qbAlias + "." + relation.joinColumns[0].referencedColumn.propertyName + "=:id")
.where(qbAlias + "." + relation.joinColumns[0].referencedColumn.propertyPath + "=:id")
.setParameter("id", relationIdInDatabaseEntity) // (example) subject.entity is a post here
.enableAutoRelationIdsLoad()
.getOne();
@ -461,7 +461,7 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
const databaseEntity = await this.connection
.getRepository<ObjectLiteral>(valueMetadata.target)
.createQueryBuilder(qbAlias, this.queryRunnerProvider) // todo: this wont work for mongodb. implement this in some method and call it here instead?
.where(qbAlias + "." + relation.inverseSideProperty + "=:id")
.where(qbAlias + "." + relation.inverseSidePropertyPath + "=:id")
.setParameter("id", relationIdInDatabaseEntity) // (example) subject.entity is a details here, and the value is details.id
.enableAutoRelationIdsLoad()
.getOne();
@ -610,7 +610,7 @@ export class SubjectBuilder<Entity extends ObjectLiteral> {
databaseEntities = await this.connection
.getRepository<ObjectLiteral>(valueMetadata.target)
.createQueryBuilder(qbAlias, this.queryRunnerProvider) // todo: this wont work for mongodb. implement this in some method and call it here instead?
.where(qbAlias + "." + relation.inverseSideProperty + "=:id")
.where(qbAlias + "." + relation.inverseSidePropertyPath + "=:id")
.setParameter("id", relationIdInDatabaseEntity)
.enableAutoRelationIdsLoad()
.getMany();

View File

@ -325,6 +325,7 @@ export class SubjectOperationExecutor {
// we need to update relation ids if newly inserted objects are used from inverse side in one-to-many inverse relation
// we also need to update relation ids if newly inserted objects are used from inverse side in one-to-one inverse relation
const oneToManyAndOneToOneNonOwnerRelations = subject.metadata.oneToManyRelations.concat(subject.metadata.oneToOneRelations.filter(relation => !relation.isOwning));
// console.log(oneToManyAndOneToOneNonOwnerRelations);
subject.metadata.extractRelationValuesFromEntity(subject.entity, oneToManyAndOneToOneNonOwnerRelations)
.forEach(([relation, subRelatedEntity, inverseEntityMetadata]) => {
relation.inverseRelation.joinColumns.forEach(joinColumn => {
@ -723,7 +724,7 @@ export class SubjectOperationExecutor {
throw new Error(`Internal error. Cannot get id of the updating entity.`);
const addEmbeddedValuesRecursively = (entity: any, value: any, embeddeds: EmbeddedMetadata[]) => {
/*const addEmbeddedValuesRecursively = (entity: any, value: any, embeddeds: EmbeddedMetadata[]) => {
embeddeds.forEach(embedded => {
if (!entity[embedded.propertyName])
return;
@ -746,14 +747,14 @@ export class SubjectOperationExecutor {
}
addEmbeddedValuesRecursively(entity[embedded.propertyName], value[embedded.prefix], embedded.embeddeds);
});
};
};*/
const value: ObjectLiteral = {};
subject.metadata.columnsWithoutEmbeddeds.forEach(column => {
subject.metadata.columns.forEach(column => {
if (entity[column.propertyName] !== undefined)
value[column.fullName] = entity[column.propertyName];
value[column.fullName] = column.getValue(entity);
});
addEmbeddedValuesRecursively(entity, value, subject.metadata.embeddeds);
// addEmbeddedValuesRecursively(entity, value, subject.metadata.embeddeds);
// if number of updated columns = 0 no need to update updated date and version columns
if (Object.keys(value).length === 0)
@ -787,10 +788,9 @@ export class SubjectOperationExecutor {
});
subject.diffRelations.forEach(relation => {
const metadata = this.connection.getMetadata(relation.entityTarget);
let valueMap = valueMaps.find(valueMap => valueMap.tableName === metadata.tableName);
let valueMap = valueMaps.find(valueMap => valueMap.tableName === relation.entityMetadata.tableName);
if (!valueMap) {
valueMap = { tableName: metadata.tableName, metadata: metadata, values: {} };
valueMap = { tableName: relation.entityMetadata.tableName, metadata: relation.entityMetadata, values: {} };
valueMaps.push(valueMap);
}
@ -1019,8 +1019,9 @@ export class SubjectOperationExecutor {
const junctionMetadata = junctionRemove.relation.junctionEntityMetadata;
const entity = subject.hasEntity ? subject.entity : subject.databaseEntity;
const firstJoinColumns = junctionRemove.relation.isOwning ? junctionRemove.relation.joinColumns : junctionRemove.relation.inverseJoinColumns;
const secondJoinColumns = junctionRemove.relation.isOwning ? junctionRemove.relation.inverseJoinColumns : junctionRemove.relation.joinColumns;
console.log(junctionRemove);
const firstJoinColumns = junctionRemove.relation.isOwning ? junctionRemove.relation.joinColumns : junctionRemove.relation.inverseRelation.inverseJoinColumns;
const secondJoinColumns = junctionRemove.relation.isOwning ? junctionRemove.relation.inverseJoinColumns : junctionRemove.relation.inverseRelation.joinColumns;
let conditions: ObjectLiteral = {};
firstJoinColumns.forEach(joinColumn => {
conditions[joinColumn.fullName] = joinColumn.referencedColumn.getValue(entity);

View File

@ -1402,20 +1402,20 @@ export class QueryBuilder<Entity> {
protected replacePropertyNames(statement: string) {
this.expressionMap.aliases.forEach(alias => {
if (!alias.hasMetadata) return;
alias.metadata.embeddeds.forEach(embedded => {
embedded.columns.forEach(column => {
const expression = "([ =]|^.{0})" + alias.name + "\\." + embedded.propertyName + "\\." + column.propertyName + "([ =]|.{0}$)";
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escapeAlias(alias.name) + "." + this.escapeColumn(column.fullName) + "$2");
});
// todo: what about embedded relations here?
});
alias.metadata.columns.filter(column => !column.isInEmbedded).forEach(column => {
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + column.propertyName + "([ =]|.{0}$)";
// alias.metadata.embeddeds.forEach(embedded => {
// embedded.columns.forEach(column => {
// const expression = "([ =]|^.{0})" + alias.name + "\\." + embedded.propertyName + "\\." + column.propertyName + "([ =]|.{0}$)";
// statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escapeAlias(alias.name) + "." + this.escapeColumn(column.fullName) + "$2");
// });
// todo: what about embedded relations here?
// });
alias.metadata.columns.forEach(column => {
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + column.propertyPath + "([ =]|.{0}$)";
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escapeAlias(alias.name) + "." + this.escapeColumn(column.fullName) + "$2");
});
alias.metadata.relationsWithJoinColumns/*.filter(relation => !relation.isInEmbedded)*/.forEach(relation => {
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + relation.propertyName + "([ =]|.{0}$)";
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escapeAlias(alias.name) + "." + this.escapeColumn(relation.name) + "$2");
const expression = "([ =\(]|^.{0})" + alias.name + "\\." + relation.propertyPath + "([ =]|.{0}$)";
statement = statement.replace(new RegExp(expression, "gm"), "$1" + this.escapeAlias(alias.name) + "." + this.escapeColumn(relation.joinColumns[0].fullName) + "$2"); // todo: fix relation.joinColumns[0], what if multiple columns
});
});
return statement;

View File

@ -59,7 +59,7 @@ export class RelationIdLoader {
const inverseSideTable = relation.inverseEntityMetadata.target; // Post
const inverseSideTableName = relation.inverseEntityMetadata.tableName; // post
const inverseSideTableAlias = relationIdAttr.alias || inverseSideTableName; // if condition (custom query builder factory) is set then relationIdAttr.alias defined
const inverseSidePropertyName = inverseRelation.propertyName; // "category" from "post.category"
const inverseSidePropertyName = inverseRelation.propertyPath; // "category" from "post.category"
const referenceColumnValues = rawEntities
.map(rawEntity => rawEntity[relationIdAttr.parentAlias + "_" + referenceColumnName])
@ -165,12 +165,12 @@ export class RelationIdLoader {
const inverseSideTableName = relationIdAttr.joinInverseSideMetadata.tableName;
const inverseSideTableAlias = relationIdAttr.alias || inverseSideTableName;
const junctionTableName = relationIdAttr.relation.junctionEntityMetadata.tableName;
const condition = junctionAlias + "." + firstJunctionColumn.propertyName + " IN (" + referenceColumnValues + ")" +
" AND " + junctionAlias + "." + secondJunctionColumn.propertyName + " = " + inverseSideTableAlias + "." + inverseJoinColumnName;
const condition = junctionAlias + "." + firstJunctionColumn.propertyPath + " IN (" + referenceColumnValues + ")" +
" AND " + junctionAlias + "." + secondJunctionColumn.propertyPath + " = " + inverseSideTableAlias + "." + inverseJoinColumnName;
const qb = new QueryBuilder(this.connection, this.queryRunnerProvider)
.select(inverseSideTableAlias + "." + inverseJoinColumnName, "id")
.addSelect(junctionAlias + "." + firstJunctionColumn.propertyName, "manyToManyId")
.addSelect(junctionAlias + "." + firstJunctionColumn.propertyPath, "manyToManyId")
.fromTable(inverseSideTableName, inverseSideTableAlias)
.innerJoin(junctionTableName, junctionAlias, condition);

View File

@ -46,12 +46,9 @@ export class SpecificRepository<Entity extends ObjectLiteral> {
* Should be used when you want quickly and efficiently set a relation (for many-to-one and one-to-many) to some entity.
* Note that event listeners and event subscribers won't work (and will not send any events) when using this operation.
*/
async setRelation(relationName: string|((t: Entity) => string|any), entityId: any, relatedEntityId: any): Promise<void> {
const propertyName = this.metadata.computePropertyName(relationName);
if (!this.metadata.hasRelationWithPropertyName(propertyName))
throw new Error(`Relation ${propertyName} was not found in the ${this.metadata.name} entity.`);
const relation = this.metadata.findRelationWithPropertyName(propertyName);
async setRelation(relationProperty: string|((t: Entity) => string|any), entityId: any, relatedEntityId: any): Promise<void> {
const propertyPath = this.metadata.computePropertyPath(relationProperty);
const relation = this.metadata.findRelationWithPropertyPath(propertyPath);
// if (relation.isManyToMany || relation.isOneToMany || relation.isOneToOneNotOwner)
// throw new Error(`Only many-to-one and one-to-one with join column are supported for this operation. ${this.metadata.name}#${propertyName} relation type is ${relation.relationType}`);
if (relation.isManyToMany)
@ -97,14 +94,10 @@ export class SpecificRepository<Entity extends ObjectLiteral> {
* Should be used when you want quickly and efficiently set a relation (for many-to-one and one-to-many) to some entity.
* Note that event listeners and event subscribers won't work (and will not send any events) when using this operation.
*/
async setInverseRelation(relationName: string|((t: Entity) => string|any), relatedEntityId: any, entityId: any): Promise<void> {
const propertyName = this.metadata.computePropertyName(relationName);
if (!this.metadata.hasRelationWithPropertyName(propertyName))
throw new Error(`Relation ${propertyName} was not found in the ${this.metadata.name} entity.`);
async setInverseRelation(relationProperty: string|((t: Entity) => string|any), relatedEntityId: any, entityId: any): Promise<void> {
const propertyPath = this.metadata.computePropertyPath(relationProperty);
// todo: fix issues with joinColumns[0]
const relation = this.metadata.findRelationWithPropertyName(propertyName);
const relation = this.metadata.findRelationWithPropertyPath(propertyPath);
// if (relation.isManyToMany || relation.isOneToMany || relation.isOneToOneNotOwner)
// throw new Error(`Only many-to-one and one-to-one with join column are supported for this operation. ${this.metadata.name}#${propertyName} relation type is ${relation.relationType}`);
if (relation.isManyToMany)
@ -147,14 +140,11 @@ export class SpecificRepository<Entity extends ObjectLiteral> {
* Should be used when you want quickly and efficiently add a relation between two entities.
* Note that event listeners and event subscribers won't work (and will not send any events) when using this operation.
*/
async addToRelation(relationName: string|((t: Entity) => string|any), entityId: any, relatedEntityIds: any[]): Promise<void> {
const propertyName = this.metadata.computePropertyName(relationName);
if (!this.metadata.hasRelationWithPropertyName(propertyName))
throw new Error(`Relation ${propertyName} was not found in the ${this.metadata.name} entity.`);
const relation = this.metadata.findRelationWithPropertyName(propertyName);
async addToRelation(relationProperty: string|((t: Entity) => string|any), entityId: any, relatedEntityIds: any[]): Promise<void> {
const propertyPath = this.metadata.computePropertyPath(relationProperty);
const relation = this.metadata.findRelationWithPropertyPath(propertyPath);
if (!relation.isManyToMany)
throw new Error(`Only many-to-many relation supported for this operation. However ${this.metadata.name}#${propertyName} relation type is ${relation.relationType}`);
throw new Error(`Only many-to-many relation supported for this operation. However ${this.metadata.name}#${propertyPath} relation type is ${relation.relationType}`);
const queryRunnerProvider = this.queryRunnerProvider ? this.queryRunnerProvider : new QueryRunnerProvider(this.connection.driver);
const queryRunner = await queryRunnerProvider.provide();
@ -195,15 +185,11 @@ export class SpecificRepository<Entity extends ObjectLiteral> {
* Should be used when you want quickly and efficiently add a relation between two entities.
* Note that event listeners and event subscribers won't work (and will not send any events) when using this operation.
*/
async addToInverseRelation(relationName: string|((t: Entity) => string|any), relatedEntityId: any, entityIds: any[]): Promise<void> {
const propertyName = this.metadata.computePropertyName(relationName);
if (!this.metadata.hasRelationWithPropertyName(propertyName))
throw new Error(`Relation ${propertyName} was not found in the ${this.metadata.name} entity.`);
const relation = this.metadata.findRelationWithPropertyName(propertyName);
async addToInverseRelation(relationProperty: string|((t: Entity) => string|any), relatedEntityId: any, entityIds: any[]): Promise<void> {
const propertyPath = this.metadata.computePropertyPath(relationProperty);
const relation = this.metadata.findRelationWithPropertyName(propertyPath);
if (!relation.isManyToMany)
throw new Error(`Only many-to-many relation supported for this operation. However ${this.metadata.name}#${propertyName} relation type is ${relation.relationType}`);
throw new Error(`Only many-to-many relation supported for this operation. However ${this.metadata.name}#${propertyPath} relation type is ${relation.relationType}`);
const queryRunnerProvider = this.queryRunnerProvider ? this.queryRunnerProvider : new QueryRunnerProvider(this.connection.driver);
const queryRunner = await queryRunnerProvider.provide();
@ -247,14 +233,11 @@ export class SpecificRepository<Entity extends ObjectLiteral> {
* Should be used when you want quickly and efficiently remove a many-to-many relation between two entities.
* Note that event listeners and event subscribers won't work (and will not send any events) when using this operation.
*/
async removeFromRelation(relationName: string|((t: Entity) => string|any), entityId: any, relatedEntityIds: any[]): Promise<void> {
const propertyName = this.metadata.computePropertyName(relationName);
if (!this.metadata.hasRelationWithPropertyName(propertyName))
throw new Error(`Relation ${propertyName} was not found in the ${this.metadata.name} entity.`);
const relation = this.metadata.findRelationWithPropertyName(propertyName);
async removeFromRelation(relationProperty: string|((t: Entity) => string|any), entityId: any, relatedEntityIds: any[]): Promise<void> {
const propertyPath = this.metadata.computePropertyPath(relationProperty);
const relation = this.metadata.findRelationWithPropertyPath(propertyPath);
if (!relation.isManyToMany)
throw new Error(`Only many-to-many relation supported for this operation. However ${this.metadata.name}#${propertyName} relation type is ${relation.relationType}`);
throw new Error(`Only many-to-many relation supported for this operation. However ${this.metadata.name}#${propertyPath} relation type is ${relation.relationType}`);
// check if given relation entity ids is empty - then nothing to do here (otherwise next code will remove all ids)
if (!relatedEntityIds || !relatedEntityIds.length)
@ -296,14 +279,11 @@ export class SpecificRepository<Entity extends ObjectLiteral> {
* Should be used when you want quickly and efficiently remove a many-to-many relation between two entities.
* Note that event listeners and event subscribers won't work (and will not send any events) when using this operation.
*/
async removeFromInverseRelation(relationName: string|((t: Entity) => string|any), relatedEntityId: any, entityIds: any[]): Promise<void> {
const propertyName = this.metadata.computePropertyName(relationName);
if (!this.metadata.hasRelationWithPropertyName(propertyName))
throw new Error(`Relation ${propertyName} was not found in the ${this.metadata.name} entity.`);
const relation = this.metadata.findRelationWithPropertyName(propertyName);
async removeFromInverseRelation(relationProperty: string|((t: Entity) => string|any), relatedEntityId: any, entityIds: any[]): Promise<void> {
const propertyPath = this.metadata.computePropertyPath(relationProperty);
const relation = this.metadata.findRelationWithPropertyName(propertyPath);
if (!relation.isManyToMany)
throw new Error(`Only many-to-many relation supported for this operation. However ${this.metadata.name}#${propertyName} relation type is ${relation.relationType}`);
throw new Error(`Only many-to-many relation supported for this operation. However ${this.metadata.name}#${propertyPath} relation type is ${relation.relationType}`);
// check if given entity ids is empty - then nothing to do here (otherwise next code will remove all ids)
if (!entityIds || !entityIds.length)
@ -526,8 +506,8 @@ export class SpecificRepository<Entity extends ObjectLiteral> {
if (relationOrName instanceof RelationMetadata)
return relationOrName;
const relationName = relationOrName instanceof Function ? relationOrName(this.metadata.createPropertiesMap()) : relationOrName;
return this.metadata.findRelationWithPropertyName(relationName);
const relationPropertyPath = relationOrName instanceof Function ? relationOrName(this.metadata.createPropertiesMap()) : relationOrName;
return this.metadata.findRelationWithPropertyPath(relationPropertyPath);
}
/**

View File

@ -18,177 +18,399 @@ describe("embedded > embedded-many-to-many", () => {
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
it("should insert, load, update and remove entities with embeddeds when primary column defined only in embedded entity", () => Promise.all(connections.map(async connection => {
describe("owner side", () => {
const user1 = new User();
user1.name = "Alice";
await connection.getRepository(User).persist(user1);
it("should insert, load, update and remove entities with embeddeds when embedded entity having ManyToMany relation", () => Promise.all(connections.map(async connection => {
const user2 = new User();
user2.name = "Bob";
await connection.getRepository(User).persist(user2);
const user1 = new User();
user1.name = "Alice";
await connection.getRepository(User).persist(user1);
const user3 = new User();
user3.name = "Clara";
await connection.getRepository(User).persist(user3);
const user2 = new User();
user2.name = "Bob";
await connection.getRepository(User).persist(user2);
const postRepository = connection.getRepository(Post);
const user3 = new User();
user3.name = "Clara";
await connection.getRepository(User).persist(user3);
const post1 = new Post();
post1.title = "About cars";
post1.counters = new Counters();
post1.counters.code = 1;
post1.counters.comments = 1;
post1.counters.favorites = 2;
post1.counters.likes = 3;
post1.counters.likedUsers = [user1, user2];
post1.counters.subcounters = new Subcounters();
post1.counters.subcounters.version = 1;
post1.counters.subcounters.watches = 5;
await postRepository.persist(post1);
const postRepository = connection.getRepository(Post);
const post2 = new Post();
post2.title = "About airplanes";
post2.counters = new Counters();
post2.counters.code = 2;
post2.counters.comments = 2;
post2.counters.favorites = 3;
post2.counters.likes = 4;
post2.counters.likedUsers = [user3];
post2.counters.subcounters = new Subcounters();
post2.counters.subcounters.version = 1;
post2.counters.subcounters.watches = 10;
await postRepository.persist(post2);
const post1 = new Post();
post1.title = "About cars";
post1.counters = new Counters();
post1.counters.code = 1;
post1.counters.comments = 1;
post1.counters.favorites = 2;
post1.counters.likes = 3;
post1.counters.likedUsers = [user1, user2];
post1.counters.subcounters = new Subcounters();
post1.counters.subcounters.version = 1;
post1.counters.subcounters.watches = 5;
await postRepository.persist(post1);
const loadedPosts = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUsers", "likedUser")
.orderBy("post.id, likedUser.id")
.getMany();
const post2 = new Post();
post2.title = "About airplanes";
post2.counters = new Counters();
post2.counters.code = 2;
post2.counters.comments = 2;
post2.counters.favorites = 3;
post2.counters.likes = 4;
post2.counters.likedUsers = [user3];
post2.counters.subcounters = new Subcounters();
post2.counters.subcounters.version = 1;
post2.counters.subcounters.watches = 10;
await postRepository.persist(post2);
expect(loadedPosts[0].should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
likedUsers: [
const loadedPosts = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUsers", "likedUser")
.orderBy("post.id")
.addOrderBy("likedUser.id")
.getMany();
expect(loadedPosts[0].should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
likedUsers: [
{
id: 1,
name: "Alice"
},
{
id: 2,
name: "Bob"
}
],
subcounters: {
version: 1,
watches: 5
}
}
}
));
expect(loadedPosts[1].should.be.eql(
{
id: 2,
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
likedUsers: [
{
id: 3,
name: "Clara"
}
],
subcounters: {
version: 1,
watches: 10
}
}
}
));
const loadedPost = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUsers", "likedUser")
.orderBy("likedUser.id")
.where("post.id = :id", {id: 1})
.getOne();
expect(loadedPost!.should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
likedUsers: [
{
id: 1,
name: "Alice"
},
{
id: 2,
name: "Bob"
}
],
subcounters: {
version: 1,
watches: 5
}
}
}
));
loadedPost!.counters.favorites += 1;
loadedPost!.counters.subcounters.watches += 1;
loadedPost!.counters.likedUsers = [user1];
await postRepository.persist(loadedPost!);
const loadedPost2 = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUsers", "likedUser")
.orderBy("likedUser.id")
.where("post.id = :id", {id: 1})
.getOne();
expect(loadedPost2!.should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 3,
likes: 3,
likedUsers: [
{
id: 1,
name: "Alice"
}
],
subcounters: {
version: 1,
watches: 6
}
}
}
));
await postRepository.remove(loadedPost2!);
const loadedPosts2 = (await postRepository.find())!;
expect(loadedPosts2.length).to.be.equal(1);
expect(loadedPosts2[0].title).to.be.equal("About airplanes");
})));
});
describe("inverse side", () => {
it("should insert, load, update and remove entities with embeddeds when embedded entity having ManyToMany relation", () => Promise.all(connections.map(async connection => {
const post1 = new Post();
post1.title = "About cars";
post1.counters = new Counters();
post1.counters.code = 1;
post1.counters.comments = 1;
post1.counters.favorites = 2;
post1.counters.likes = 3;
post1.counters.subcounters = new Subcounters();
post1.counters.subcounters.version = 1;
post1.counters.subcounters.watches = 5;
await connection.getRepository(Post).persist(post1);
const post2 = new Post();
post2.title = "About airplanes";
post2.counters = new Counters();
post2.counters.code = 2;
post2.counters.comments = 2;
post2.counters.favorites = 3;
post2.counters.likes = 4;
post2.counters.subcounters = new Subcounters();
post2.counters.subcounters.version = 1;
post2.counters.subcounters.watches = 10;
await connection.getRepository(Post).persist(post2);
const user1 = new User();
user1.name = "Alice";
user1.likedPosts = [post1, post2];
await connection.getRepository(User).persist(user1);
const user2 = new User();
user2.name = "Bob";
user2.likedPosts = [post1];
await connection.getRepository(User).persist(user2);
const user3 = new User();
user3.name = "Clara";
user3.likedPosts = [post2];
await connection.getRepository(User).persist(user3);
const loadedUsers = await connection.entityManager
.createQueryBuilder(User, "user")
.leftJoinAndSelect("user.likedPosts", "likedPost")
.orderBy("user.id")
.addOrderBy("likedPost.id")
.getMany();
expect(loadedUsers[0].should.be.eql(
{
id: 1,
name: "Alice",
likedPosts: [
{
id: 1,
name: "Alice"
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
subcounters: {
version: 1,
watches: 5
}
}
},
{
id: 2,
name: "Bob"
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
subcounters: {
version: 1,
watches: 10
}
}
}
],
subcounters: {
version: 1,
watches: 5
}
]
}
}
));
expect(loadedPosts[1].should.be.eql(
{
id: 2,
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
likedUsers: [
{
id: 3,
name: "Clara"
}
],
subcounters: {
version: 1,
watches: 10
}
}
}
));
const loadedPost = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUsers", "likedUser")
.orderBy("likedUser.id")
.where("post.id = :id", { id: 1 })
.getOne();
expect(loadedPost!.should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
likedUsers: [
));
expect(loadedUsers[1].should.be.eql(
{
id: 2,
name: "Bob",
likedPosts: [
{
id: 1,
name: "Alice"
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
subcounters: {
version: 1,
watches: 5
}
}
}
]
}
));
expect(loadedUsers[2].should.be.eql(
{
id: 3,
name: "Clara",
likedPosts: [
{
id: 2,
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
subcounters: {
version: 1,
watches: 10
}
}
}
]
}
));
const loadedUser = await connection.entityManager
.createQueryBuilder(User, "user")
.leftJoinAndSelect("user.likedPosts", "likedPost")
.orderBy("likedPost.id")
.where("user.id = :id", {id: 1})
.getOne();
expect(loadedUser!.should.be.eql(
{
id: 1,
name: "Alice",
likedPosts: [
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
subcounters: {
version: 1,
watches: 5
}
}
},
{
id: 2,
name: "Bob"
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
subcounters: {
version: 1,
watches: 10
}
}
}
],
subcounters: {
version: 1,
watches: 5
}
]
}
}
));
));
loadedPost!.counters.favorites += 1;
loadedPost!.counters.subcounters.watches += 1;
loadedPost!.counters.likedUsers = [user1];
await postRepository.persist(loadedPost!);
loadedUser!.name = "Anna";
loadedUser!.likedPosts = [post1];
await connection.getRepository(User).persist(loadedUser!);
const loadedPost2 = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUsers", "likedUser")
.orderBy("likedUser.id")
.where("post.id = :id", { id: 1 })
.getOne();
const loadedUser2 = await connection.entityManager
.createQueryBuilder(User, "user")
.leftJoinAndSelect("user.likedPosts", "likedPost")
.orderBy("likedPost.id")
.where("user.id = :id", {id: 1})
.getOne();
expect(loadedPost2!.should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 3,
likes: 3,
likedUsers: [
expect(loadedUser2!.should.be.eql(
{
id: 1,
name: "Anna",
likedPosts: [
{
id: 1,
name: "Alice"
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
subcounters: {
version: 1,
watches: 5
}
}
}
],
subcounters: {
version: 1,
watches: 6
}
]
}
}
));
));
await postRepository.remove(loadedPost2!);
await connection.getRepository(User).remove(loadedUser2!);
const loadedPosts2 = (await postRepository.find())!;
expect(loadedPosts2.length).to.be.equal(1);
expect(loadedPosts2[0].title).to.be.equal("About airplanes");
})));
const loadedUsers2 = (await connection.getRepository(User).find())!;
expect(loadedUsers2.length).to.be.equal(2);
expect(loadedUsers2[0].name).to.be.equal("Bob");
expect(loadedUsers2[1].name).to.be.equal("Clara");
})));
});
});

View File

@ -1,10 +1,10 @@
import {EmbeddableEntity} from "../../../../../src/decorator/entity/EmbeddableEntity";
import {Column} from "../../../../../src/decorator/columns/Column";
import {Embedded} from "../../../../../src/decorator/Embedded";
import {Subcounters} from "./Subcounters";
import {User} from "./User";
import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany";
import {JoinTable} from "../../../../../src/decorator/relations/JoinTable";
import {Subcounters} from "./Subcounters";
import {User} from "./User";
@EmbeddableEntity()
export class Counters {
@ -24,7 +24,7 @@ export class Counters {
@Embedded(() => Subcounters)
subcounters: Subcounters;
@ManyToMany(type => User)
@ManyToMany(type => User, user => user.likedPosts)
@JoinTable()
likedUsers: User[];

View File

@ -1,6 +1,8 @@
import {Column} from "../../../../../src/decorator/columns/Column";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {ManyToMany} from "../../../../../src/decorator/relations/ManyToMany";
import {Post} from "./Post";
@Entity()
export class User {
@ -11,4 +13,7 @@ export class User {
@Column()
name: string;
@ManyToMany(type => Post, post => post.counters.likedUsers)
likedPosts: Post[];
}

View File

@ -0,0 +1,362 @@
import "reflect-metadata";
import {Post} from "./entity/Post";
import {Counters} from "./entity/Counters";
import {Connection} from "../../../../src/connection/Connection";
import {expect} from "chai";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Subcounters} from "./entity/Subcounters";
import {User} from "./entity/User";
describe("embedded > embedded-many-to-one", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchemaOnConnection: true,
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
describe("owner side", () => {
it("should insert, load, update and remove entities with embeddeds when embedded entity having ManyToOne relation", () => Promise.all(connections.map(async connection => {
const user1 = new User();
user1.name = "Alice";
await connection.getRepository(User).persist(user1);
const user2 = new User();
user2.name = "Bob";
await connection.getRepository(User).persist(user2);
const user3 = new User();
user3.name = "Clara";
await connection.getRepository(User).persist(user3);
const postRepository = connection.getRepository(Post);
const post1 = new Post();
post1.title = "About cars";
post1.counters = new Counters();
post1.counters.code = 1;
post1.counters.comments = 1;
post1.counters.favorites = 2;
post1.counters.likes = 3;
post1.counters.likedUser = user1;
post1.counters.subcounters = new Subcounters();
post1.counters.subcounters.version = 1;
post1.counters.subcounters.watches = 5;
await postRepository.persist(post1);
const post2 = new Post();
post2.title = "About airplanes";
post2.counters = new Counters();
post2.counters.code = 2;
post2.counters.comments = 2;
post2.counters.favorites = 3;
post2.counters.likes = 4;
post2.counters.likedUser = user2;
post2.counters.subcounters = new Subcounters();
post2.counters.subcounters.version = 1;
post2.counters.subcounters.watches = 10;
await postRepository.persist(post2);
const loadedPosts = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUser", "likedUser")
.orderBy("post.id")
.getMany();
expect(loadedPosts[0].should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
likedUser: { id: 1, name: "Alice" },
subcounters: {
version: 1,
watches: 5
}
}
}
));
expect(loadedPosts[1].should.be.eql(
{
id: 2,
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
likedUser: { id: 2, name: "Bob" },
subcounters: {
version: 1,
watches: 10
}
}
}
));
const loadedPost = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUser", "likedUser")
.where("post.id = :id", { id: 1 })
.getOne();
expect(loadedPost!.should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
likedUser: { id: 1, name: "Alice" },
subcounters: {
version: 1,
watches: 5
}
}
}
));
loadedPost!.counters.favorites += 1;
loadedPost!.counters.subcounters.watches += 1;
loadedPost!.counters.likedUser = user3;
await postRepository.persist(loadedPost!);
const loadedPost2 = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUser", "likedUser")
.where("post.id = :id", { id: 1 })
.getOne();
expect(loadedPost2!.should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 3,
likes: 3,
likedUser: { id: 3, name: "Clara" },
subcounters: {
version: 1,
watches: 6
}
}
}
));
await postRepository.remove(loadedPost2!);
const loadedPosts2 = (await postRepository.find())!;
expect(loadedPosts2.length).to.be.equal(1);
expect(loadedPosts2[0].title).to.be.equal("About airplanes");
})));
});
describe("inverse side", () => {
it("should insert, load, update and remove entities with embeddeds when embedded entity having ManyToOne relation", () => Promise.all(connections.map(async connection => {
const post1 = new Post();
post1.title = "About cars";
post1.counters = new Counters();
post1.counters.code = 1;
post1.counters.comments = 1;
post1.counters.favorites = 2;
post1.counters.likes = 3;
post1.counters.subcounters = new Subcounters();
post1.counters.subcounters.version = 1;
post1.counters.subcounters.watches = 5;
await connection.getRepository(Post).persist(post1);
const post2 = new Post();
post2.title = "About airplanes";
post2.counters = new Counters();
post2.counters.code = 2;
post2.counters.comments = 2;
post2.counters.favorites = 3;
post2.counters.likes = 4;
post2.counters.subcounters = new Subcounters();
post2.counters.subcounters.version = 1;
post2.counters.subcounters.watches = 10;
await connection.getRepository(Post).persist(post2);
const post3 = new Post();
post3.title = "About horses";
post3.counters = new Counters();
post3.counters.code = 3;
post3.counters.comments = 5;
post3.counters.favorites = 10;
post3.counters.likes = 15;
post3.counters.subcounters = new Subcounters();
post3.counters.subcounters.version = 1;
post3.counters.subcounters.watches = 30;
await connection.getRepository(Post).persist(post3);
const user1 = new User();
user1.name = "Alice";
user1.likedPosts = [post1, post2];
await connection.getRepository(User).persist(user1);
const user2 = new User();
user2.name = "Bob";
user2.likedPosts = [post3];
await connection.getRepository(User).persist(user2);
const loadedUsers = await connection.entityManager
.createQueryBuilder(User, "user")
.leftJoinAndSelect("user.likedPosts", "likedPost")
.orderBy("user.id, likedPost.id")
.getMany();
expect(loadedUsers[0].should.be.eql(
{
id: 1,
name: "Alice",
likedPosts: [
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
subcounters: {
version: 1,
watches: 5
}
}
},
{
id: 2,
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
subcounters: {
version: 1,
watches: 10
}
}
}
]
}
));
expect(loadedUsers[1].should.be.eql(
{
id: 2,
name: "Bob",
likedPosts: [
{
id: 3,
title: "About horses",
counters: {
code: 3,
comments: 5,
favorites: 10,
likes: 15,
subcounters: {
version: 1,
watches: 30
}
}
}
]
}
));
const loadedUser = await connection.entityManager
.createQueryBuilder(User, "user")
.leftJoinAndSelect("user.likedPosts", "likedPost")
.orderBy("likedPost.id")
.where("user.id = :id", { id: 1 })
.getOne();
expect(loadedUser!.should.be.eql(
{
id: 1,
name: "Alice",
likedPosts: [
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
subcounters: {
version: 1,
watches: 5
}
}
},
{
id: 2,
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
subcounters: {
version: 1,
watches: 10
}
}
}
]
}
));
loadedUser!.name = "Anna";
loadedUser!.likedPosts = [post1];
await connection.getRepository(User).persist(loadedUser!);
const loadedUser2 = await connection.entityManager
.createQueryBuilder(User, "user")
.leftJoinAndSelect("user.likedPosts", "likedPost")
.orderBy("likedPost.id")
.where("user.id = :id", { id: 1 })
.getOne();
expect(loadedUser2!.should.be.eql(
{
id: 1,
name: "Anna",
likedPosts: [
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
subcounters: {
version: 1,
watches: 5
}
}
}
]
}
));
})));
});
});

View File

@ -0,0 +1,31 @@
import {EmbeddableEntity} from "../../../../../src/decorator/entity/EmbeddableEntity";
import {Column} from "../../../../../src/decorator/columns/Column";
import {JoinColumn} from "../../../../../src/decorator/relations/JoinColumn";
import {ManyToOne} from "../../../../../src/decorator/relations/ManyToOne";
import {Embedded} from "../../../../../src/decorator/Embedded";
import {User} from "./User";
import {Subcounters} from "./Subcounters";
@EmbeddableEntity()
export class Counters {
@Column()
code: number;
@Column()
likes: number;
@Column()
comments: number;
@Column()
favorites: number;
@Embedded(() => Subcounters)
subcounters: Subcounters;
@ManyToOne(type => User)
@JoinColumn()
likedUser: User;
}

View File

@ -0,0 +1,19 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {Column} from "../../../../../src/decorator/columns/Column";
import {Embedded} from "../../../../../src/decorator/Embedded";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Counters} from "./Counters";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Embedded(() => Counters)
counters: Counters;
}

View File

@ -0,0 +1,13 @@
import {EmbeddableEntity} from "../../../../../src/decorator/entity/EmbeddableEntity";
import {Column} from "../../../../../src/decorator/columns/Column";
@EmbeddableEntity()
export class Subcounters {
@Column()
version: number;
@Column()
watches: number;
}

View File

@ -0,0 +1,19 @@
import {Column} from "../../../../../src/decorator/columns/Column";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {OneToMany} from "../../../../../src/decorator/relations/OneToMany";
import {Post} from "./Post";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToMany(type => Post, post => post.counters.likedUser)
likedPosts: Post[];
}

View File

@ -0,0 +1,331 @@
import "reflect-metadata";
import {Post} from "./entity/Post";
import {Counters} from "./entity/Counters";
import {Connection} from "../../../../src/connection/Connection";
import {expect} from "chai";
import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../../utils/test-utils";
import {Subcounters} from "./entity/Subcounters";
import {User} from "./entity/User";
describe("embedded > embedded-one-to-one", () => {
let connections: Connection[];
before(async () => connections = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchemaOnConnection: true,
}));
beforeEach(() => reloadTestingDatabases(connections));
after(() => closeTestingConnections(connections));
describe("owner side", () => {
it("should insert, load, update and remove entities with embeddeds when embedded entity having OneToOne relation", () => Promise.all(connections.map(async connection => {
const user1 = new User();
user1.name = "Alice";
await connection.getRepository(User).persist(user1);
const user2 = new User();
user2.name = "Bob";
await connection.getRepository(User).persist(user2);
const user3 = new User();
user3.name = "Clara";
await connection.getRepository(User).persist(user3);
const postRepository = connection.getRepository(Post);
const post1 = new Post();
post1.title = "About cars";
post1.counters = new Counters();
post1.counters.code = 1;
post1.counters.comments = 1;
post1.counters.favorites = 2;
post1.counters.likes = 3;
post1.counters.likedUser = user1;
post1.counters.subcounters = new Subcounters();
post1.counters.subcounters.version = 1;
post1.counters.subcounters.watches = 5;
await postRepository.persist(post1);
const post2 = new Post();
post2.title = "About airplanes";
post2.counters = new Counters();
post2.counters.code = 2;
post2.counters.comments = 2;
post2.counters.favorites = 3;
post2.counters.likes = 4;
post2.counters.likedUser = user2;
post2.counters.subcounters = new Subcounters();
post2.counters.subcounters.version = 1;
post2.counters.subcounters.watches = 10;
await postRepository.persist(post2);
const loadedPosts = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUser", "likedUser")
.orderBy("post.id")
.getMany();
expect(loadedPosts[0].should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
likedUser: { id: 1, name: "Alice" },
subcounters: {
version: 1,
watches: 5
}
}
}
));
expect(loadedPosts[1].should.be.eql(
{
id: 2,
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
likedUser: { id: 2, name: "Bob" },
subcounters: {
version: 1,
watches: 10
}
}
}
));
const loadedPost = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUser", "likedUser")
.where("post.id = :id", { id: 1 })
.getOne();
expect(loadedPost!.should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
likedUser: { id: 1, name: "Alice" },
subcounters: {
version: 1,
watches: 5
}
}
}
));
loadedPost!.counters.favorites += 1;
loadedPost!.counters.subcounters.watches += 1;
loadedPost!.counters.likedUser = user3;
await postRepository.persist(loadedPost!);
const loadedPost2 = await connection.entityManager
.createQueryBuilder(Post, "post")
.leftJoinAndSelect("post.counters.likedUser", "likedUser")
.where("post.id = :id", { id: 1 })
.getOne();
expect(loadedPost2!.should.be.eql(
{
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 3,
likes: 3,
likedUser: { id: 3, name: "Clara" },
subcounters: {
version: 1,
watches: 6
}
}
}
));
await postRepository.remove(loadedPost2!);
const loadedPosts2 = (await postRepository.find())!;
expect(loadedPosts2.length).to.be.equal(1);
expect(loadedPosts2[0].title).to.be.equal("About airplanes");
})));
});
// uncomment this section once inverse side persistment of one-to-one relation will be finished
describe.skip("inverse side", () => {
it("should insert, load, update and remove entities with embeddeds when embedded entity having OneToOne relation", () => Promise.all(connections.map(async connection => {
const post1 = new Post();
post1.title = "About cars";
post1.counters = new Counters();
post1.counters.code = 1;
post1.counters.comments = 1;
post1.counters.favorites = 2;
post1.counters.likes = 3;
post1.counters.subcounters = new Subcounters();
post1.counters.subcounters.version = 1;
post1.counters.subcounters.watches = 5;
await connection.getRepository(Post).persist(post1);
const post2 = new Post();
post2.title = "About airplanes";
post2.counters = new Counters();
post2.counters.code = 2;
post2.counters.comments = 2;
post2.counters.favorites = 3;
post2.counters.likes = 4;
post2.counters.subcounters = new Subcounters();
post2.counters.subcounters.version = 1;
post2.counters.subcounters.watches = 10;
await connection.getRepository(Post).persist(post2);
const post3 = new Post();
post3.title = "About horses";
post3.counters = new Counters();
post3.counters.code = 3;
post3.counters.comments = 4;
post3.counters.favorites = 5;
post3.counters.likes = 6;
post3.counters.subcounters = new Subcounters();
post3.counters.subcounters.version = 1;
post3.counters.subcounters.watches = 12;
await connection.getRepository(Post).persist(post3);
const user1 = new User();
user1.name = "Alice";
user1.likedPost = post1;
await connection.getRepository(User).persist(user1);
const user2 = new User();
user2.name = "Bob";
user2.likedPost = post2;
await connection.getRepository(User).persist(user2);
const loadedUsers = await connection.entityManager
.createQueryBuilder(User, "user")
.leftJoinAndSelect("user.likedPost", "likedPost")
.orderBy("user.id")
.getMany();
expect(loadedUsers[0].should.be.eql(
{
id: 1,
name: "Alice",
likedPost: {
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
subcounters: {
version: 1,
watches: 5
}
}
}
}
));
expect(loadedUsers[1].should.be.eql(
{
id: 2,
name: "Bob",
likedPost: {
id: 2,
title: "About airplanes",
counters: {
code: 2,
comments: 2,
favorites: 3,
likes: 4,
subcounters: {
version: 1,
watches: 10
}
}
}
}
));
const loadedUser = await connection.entityManager
.createQueryBuilder(User, "user")
.leftJoinAndSelect("user.likedPost", "likedPost")
.where("user.id = :id", { id: 1 })
.getOne();
expect(loadedUser!.should.be.eql(
{
id: 1,
name: "Alice",
likedPost: {
id: 1,
title: "About cars",
counters: {
code: 1,
comments: 1,
favorites: 2,
likes: 3,
subcounters: {
version: 1,
watches: 5
}
}
}
}
));
loadedUser!.name = "Anna";
loadedUser!.likedPost = post3;
await connection.getRepository(User).persist(loadedUser!);
const loadedUser2 = await connection.entityManager
.createQueryBuilder(User, "user")
.leftJoinAndSelect("user.likedPost", "likedPost")
.where("user.id = :id", { id: 1 })
.getOne();
expect(loadedUser2!.should.be.eql(
{
id: 1,
name: "Anna",
likedPost: {
id: 3,
title: "About horses",
counters: {
code: 3,
comments: 4,
favorites: 5,
likes: 6,
subcounters: {
version: 1,
watches: 12
}
}
}
}
));
await connection.getRepository(User).remove(loadedUser2!);
const loadedUsers2 = (await connection.getRepository(User).find())!;
expect(loadedUsers2.length).to.be.equal(1);
expect(loadedUsers2[0].name).to.be.equal("Bob");
})));
});
});

View File

@ -0,0 +1,31 @@
import {EmbeddableEntity} from "../../../../../src/decorator/entity/EmbeddableEntity";
import {Column} from "../../../../../src/decorator/columns/Column";
import {JoinColumn} from "../../../../../src/decorator/relations/JoinColumn";
import {Embedded} from "../../../../../src/decorator/Embedded";
import {OneToOne} from "../../../../../src/decorator/relations/OneToOne";
import {User} from "./User";
import {Subcounters} from "./Subcounters";
@EmbeddableEntity()
export class Counters {
@Column()
code: number;
@Column()
likes: number;
@Column()
comments: number;
@Column()
favorites: number;
@Embedded(() => Subcounters)
subcounters: Subcounters;
@OneToOne(type => User)
@JoinColumn()
likedUser: User;
}

View File

@ -0,0 +1,19 @@
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {Column} from "../../../../../src/decorator/columns/Column";
import {Embedded} from "../../../../../src/decorator/Embedded";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Counters} from "./Counters";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Embedded(() => Counters)
counters: Counters;
}

View File

@ -0,0 +1,13 @@
import {EmbeddableEntity} from "../../../../../src/decorator/entity/EmbeddableEntity";
import {Column} from "../../../../../src/decorator/columns/Column";
@EmbeddableEntity()
export class Subcounters {
@Column()
version: number;
@Column()
watches: number;
}

View File

@ -0,0 +1,19 @@
import {Column} from "../../../../../src/decorator/columns/Column";
import {PrimaryGeneratedColumn} from "../../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../../src/decorator/entity/Entity";
import {Post} from "./Post";
import {OneToOne} from "../../../../../src/decorator/relations/OneToOne";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(type => Post, post => post.counters.likedUser)
likedPost: Post;
}

View File

@ -0,0 +1,62 @@
import "reflect-metadata";
import {Post} from "./entity/Post";
import {Counters} from "./entity/Counters";
import {Connection} from "../../../src/connection/Connection";
import {expect} from "chai";
import {setupTestingConnections} from "../../utils/test-utils";
import {Subcounters} from "./entity/Subcounters";
import {User} from "./entity/User";
import {getConnectionManager} from "../../../src/index";
describe("entity-metadata > property-map", () => {
let connections: Connection[];
before(() => {
connections = setupTestingConnections({ entities: [__dirname + "/entity/*{.js,.ts}"] })
.map(options => getConnectionManager().create(options))
.map(connection => {
connection.buildMetadatas();
return connection;
});
});
it("should create correct property map object", () => Promise.all(connections.map(async connection => {
const user1 = new User();
user1.id = 1;
user1.name = "Alice";
const post1 = new Post();
post1.title = "About cars";
post1.counters = new Counters();
post1.counters.code = 1;
post1.counters.comments = 1;
post1.counters.favorites = 2;
post1.counters.likes = 3;
post1.counters.likedUsers = [user1];
post1.counters.subcounters = new Subcounters();
post1.counters.subcounters.version = 1;
post1.counters.subcounters.watches = 5;
post1.counters.subcounters.watchedUsers = [user1];
const postPropertiesMap = connection.getMetadata(Post).createPropertiesMap();
expect(postPropertiesMap.should.be.eql(
{
id: "id",
title: "title",
counters: {
code: "counters.code",
likes: "counters.likes",
comments: "counters.comments",
favorites: "counters.favorites",
subcounters: {
version: "counters.subcounters.version",
watches: "counters.subcounters.watches",
watchedUsers: "counters.subcounters.watchedUsers"
},
likedUsers: "counters.likedUsers"
}
}
));
})));
});

View File

@ -0,0 +1,31 @@
import {EmbeddableEntity} from "../../../../src/decorator/entity/EmbeddableEntity";
import {Column} from "../../../../src/decorator/columns/Column";
import {Embedded} from "../../../../src/decorator/Embedded";
import {ManyToMany} from "../../../../src/decorator/relations/ManyToMany";
import {JoinTable} from "../../../../src/decorator/relations/JoinTable";
import {Subcounters} from "./Subcounters";
import {User} from "./User";
@EmbeddableEntity()
export class Counters {
@Column()
code: number;
@Column()
likes: number;
@Column()
comments: number;
@Column()
favorites: number;
@Embedded(() => Subcounters)
subcounters: Subcounters;
@ManyToMany(type => User, user => user.likedPosts)
@JoinTable()
likedUsers: User[];
}

View File

@ -0,0 +1,19 @@
import {Entity} from "../../../../src/decorator/entity/Entity";
import {Column} from "../../../../src/decorator/columns/Column";
import {Embedded} from "../../../../src/decorator/Embedded";
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Counters} from "./Counters";
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Embedded(() => Counters)
counters: Counters;
}

View File

@ -0,0 +1,20 @@
import {EmbeddableEntity} from "../../../../src/decorator/entity/EmbeddableEntity";
import {Column} from "../../../../src/decorator/columns/Column";
import {ManyToMany} from "../../../../src/decorator/relations/ManyToMany";
import {JoinTable} from "../../../../src/decorator/relations/JoinTable";
import {User} from "./User";
@EmbeddableEntity()
export class Subcounters {
@Column()
version: number;
@Column()
watches: number;
@ManyToMany(type => User)
@JoinTable()
watchedUsers: User[];
}

View File

@ -0,0 +1,19 @@
import {Column} from "../../../../src/decorator/columns/Column";
import {PrimaryGeneratedColumn} from "../../../../src/decorator/columns/PrimaryGeneratedColumn";
import {Entity} from "../../../../src/decorator/entity/Entity";
import {ManyToMany} from "../../../../src/decorator/relations/ManyToMany";
import {Post} from "./Post";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(type => Post, post => post.counters.likedUsers)
likedPosts: Post[];
}