import { AnyEntity } from "../entity/any-entity.type"; import { FieldDiscriminator } from "../field/field-discriminator.class"; import { RelationField } from "../field/field-names/relation-field.type"; import { Collection } from "../field/field-values/collection.class"; import { EmptyValue } from "../field/field-values/empty-value.type"; import { META } from "../symbols"; import { BerryOrm } from "./berry-orm.class"; export class EntityRelationManager { constructor(private orm: BerryOrm) {} /** * Destruct any bilateral relation on the specified field of the entity. * @param entity * @param field */ clearRelations( entity: Entity, field: RelationField, ): this { this.invokeOnRelationField( entity, field, (relationEntity) => { if (!relationEntity) return; this.destructRelation(entity, field, relationEntity); return undefined; }, (relationEntities) => { relationEntities.forEach((relationEntity) => this.destructRelation(entity, field, relationEntity), ); return relationEntities; }, ); return this; } /** * Construct a bilateral relation with the target entity on the specified * field of the entity. * @param entity * @param field * @param targetEntity */ constructRelation( entity: Entity, field: RelationField, targetEntity: AnyEntity, ): this { this.invokeOnRelationFieldBilateral( entity, field, targetEntity, (targetEntity) => targetEntity, (targetEntity, entities) => entities.add(targetEntity), ); return this; } /** * Destruct the bilateral relation with the target entity on the specified * field of the entity if exists. * @param entity * @param field * @param targetEntity */ destructRelation( entity: Entity, field: RelationField, targetEntity: AnyEntity, ): this { this.invokeOnRelationFieldBilateral( entity, field, targetEntity, (targetEntity, entity) => (entity == targetEntity ? undefined : entity), (targetEntity, entities) => { entities?.delete(targetEntity); return entities; }, ); return this; } /** * A wrap of {@link EntityManager.invokeOnRelationField} which makes it * easier to operate on the both sides of the relation. * @param entity * @param field * @param targetEntity * @param onToOne * @param onToMany */ private invokeOnRelationFieldBilateral( entity: AnyEntity, field: string, targetEntity: AnyEntity, onToOne?: ( targetEntity: AnyEntity, entity: AnyEntity | EmptyValue, ) => AnyEntity | EmptyValue, onToMany?: ( targetEntity: AnyEntity, entities: Collection, ) => Collection, ) { const wrappedInvoke = ( entity: AnyEntity, field: string, targetEntity: AnyEntity, ) => this.invokeOnRelationField( entity, field, onToOne ? (entity) => onToOne(targetEntity, entity) : undefined, onToMany ? (entities) => onToMany(targetEntity, entities) : undefined, ); const relationMeta = entity[META].fields[field].relation!; wrappedInvoke(entity, field, targetEntity); wrappedInvoke(targetEntity, relationMeta.inverse, entity); return this; } /** * Invoke a callback based on the field's relation type. * @param entity * @param field * @param onToOne - The callback to be invoked on a to-one relation field. * The return value will be set as the value of the field. * @param onToMany - The callback to be invoked on a to-many relation field. */ private invokeOnRelationField( entity: AnyEntity, field: string, onToOne?: (entity: AnyEntity | EmptyValue) => AnyEntity | EmptyValue, onToMany?: (entities: Collection) => void, ) { if (FieldDiscriminator.isRelationFieldToMany(entity, field)) { if (!onToMany) return; const relationEntities = entity[field] as Collection; onToMany(relationEntities); } else { if (!onToOne) return; const relationEntity = entity[field] as AnyEntity | EmptyValue; const processed = onToOne(relationEntity); entity[field] = processed; } return this; } }