import { Subject } from "../Subject";
import { OrmUtils } from "../../util/OrmUtils";
import { EntityMetadata } from "../../metadata/EntityMetadata";
/**
 * Builds operations needs to be executed for one-to-many relations of the given subjects.
 *
 * by example: post contains one-to-many relation with category in the property called "categories", e.g.
 *             @OneToMany(type => Category, category => category.post) categories: Category[]
 *             If user adds categories into the post and saves post we need to bind them.
 *             This operation requires updation of category table since its owner of the relation and contains a join column.
 *
 * note: this class shares lot of things with OneToOneInverseSideOperationBuilder, so when you change this class
 *       make sure to reflect changes there as well.
 */
export class OneToManySubjectBuilder {
    // ---------------------------------------------------------------------
    // Constructor
    // ---------------------------------------------------------------------
    constructor(subjects) {
        this.subjects = subjects;
    }
    // ---------------------------------------------------------------------
    // Public Methods
    // ---------------------------------------------------------------------
    /**
     * Builds all required operations.
     */
    build() {
        this.subjects.forEach((subject) => {
            subject.metadata.oneToManyRelations.forEach((relation) => {
                // skip relations for which persistence is disabled
                if (relation.persistenceEnabled === false)
                    return;
                this.buildForSubjectRelation(subject, relation);
            });
        });
    }
    // ---------------------------------------------------------------------
    // Protected Methods
    // ---------------------------------------------------------------------
    /**
     * Builds operations for a given subject and relation.
     *
     * by example: subject is "post" entity we are saving here and relation is "categories" inside it here.
     */
    buildForSubjectRelation(subject, relation) {
        // prepare objects (relation id maps) for the database entity
        // by example: since subject is a post, we are expecting to get all post's categories saved in the database here,
        //             particularly their relation ids, e.g. category ids stored in the database
        // in most cases relatedEntityDatabaseValues will contain only the entity key properties.
        // this is because subject.databaseEntity contains relations with loaded relation ids only.
        // however if the entity uses the afterLoad hook to calculate any properties, the fetched "key object" might include ADDITIONAL properties.
        // to handle such situations, we pass the data to relation.inverseEntityMetadata.getEntityIdMap to extract the key without any other properties.
        let relatedEntityDatabaseRelationIds = [];
        if (subject.databaseEntity) {
            // related entities in the database can exist only if this entity (post) is saved
            const relatedEntityDatabaseRelation = relation.getEntityValue(subject.databaseEntity);
            if (relatedEntityDatabaseRelation) {
                relatedEntityDatabaseRelationIds =
                    relatedEntityDatabaseRelation.map((entity) => relation.inverseEntityMetadata.getEntityIdMap(entity));
            }
        }
        // get related entities of persisted entity
        // by example: get categories from the passed to persist post entity
        let relatedEntities = relation.getEntityValue(subject.entity);
        if (relatedEntities === null)
            // we treat relations set to null as removed, so we don't skip it
            relatedEntities = [];
        if (relatedEntities === undefined)
            // if relation is undefined then nothing to update
            return;
        // extract only relation ids from the related entities, since we only need them for comparison
        // by example: extract from categories only relation ids (category id, or let's say category title, depend on join column options)
        const relatedPersistedEntityRelationIds = [];
        relatedEntities.forEach((relatedEntity) => {
            // by example: relatedEntity is a category here
            let relationIdMap = relation.inverseEntityMetadata.getEntityIdMap(relatedEntity); // by example: relationIdMap is category.id map here, e.g. { id: ... }
            // try to find a subject of this related entity, maybe it was loaded or was marked for persistence
            let relatedEntitySubject = this.subjects.find((subject) => {
                return subject.entity === relatedEntity;
            });
            // if subject with entity was found take subject identifier as relation id map since it may contain extra properties resolved
            if (relatedEntitySubject)
                relationIdMap = relatedEntitySubject.identifier;
            // if relationIdMap is undefined then it means user binds object which is not saved in the database yet
            // by example: if post contains categories which does not have ids yet (because they are new)
            //             it means they are always newly inserted and relation update operation always must be created for them
            //             it does not make sense to perform difference operation for them for both add and remove actions
            if (!relationIdMap) {
                // we decided to remove this error because it brings complications when saving object with non-saved entities
                // if (!relatedEntitySubject)
                //     throw new TypeORMError(`One-to-many relation "${relation.entityMetadata.name}.${relation.propertyPath}" contains ` +
                //         `entities which do not exist in the database yet, thus they cannot be bind in the database. ` +
                //         `Please setup cascade insertion or save entities before binding it.`);
                if (!relatedEntitySubject)
                    return;
                // okay, so related subject exist and its marked for insertion, then add a new change map
                // by example: this will tell category to insert into its post relation our post we are working with
                //             relatedEntitySubject is newly inserted CategorySubject
                //             relation.inverseRelation is ManyToOne relation inside Category
                //             subject is Post needs to be inserted into Category
                relatedEntitySubject.changeMaps.push({
                    relation: relation.inverseRelation,
                    value: subject,
                });
                return;
            }
            // check if this binding really exist in the database
            // by example: find our category if its already bind in the database
            const relationIdInDatabaseSubjectRelation = relatedEntityDatabaseRelationIds.find((relatedDatabaseEntityRelationId) => {
                return OrmUtils.compareIds(relationIdMap, relatedDatabaseEntityRelationId);
            });
            // if relationIdMap DOES NOT exist in the subject's relation in the database it means its a new relation and we need to "bind" them
            // by example: this will tell category to insert into its post relation our post we are working with
            //             relatedEntitySubject is newly inserted CategorySubject
            //             relation.inverseRelation is ManyToOne relation inside Category
            //             subject is Post needs to be inserted into Category
            if (!relationIdInDatabaseSubjectRelation) {
                // if there is no relatedEntitySubject then it means "category" wasn't persisted,
                // but since we are going to update "category" table (since its an owning side of relation with join column)
                // we create a new subject here:
                if (!relatedEntitySubject) {
                    relatedEntitySubject = new Subject({
                        metadata: relation.inverseEntityMetadata,
                        parentSubject: subject,
                        canBeUpdated: true,
                        identifier: relationIdMap,
                    });
                    this.subjects.push(relatedEntitySubject);
                }
                relatedEntitySubject.changeMaps.push({
                    relation: relation.inverseRelation,
                    value: subject,
                });
            }
            // if related entity has relation id then we add it to the list of relation ids
            // this list will be used later to compare with database relation ids to find a difference
            // what exist in this array and does not exist in the database are newly inserted relations
            // what does not exist in this array, but exist in the database are removed relations
            // removed relations are set to null from inverse side of relation
            relatedPersistedEntityRelationIds.push(relationIdMap);
        });
        // find what related entities were added and what were removed based on difference between what we save and what database has
        if (relation.inverseRelation?.orphanedRowAction !== "disable") {
            EntityMetadata.difference(relatedEntityDatabaseRelationIds, relatedPersistedEntityRelationIds).forEach((removedRelatedEntityRelationId) => {
                // by example: removedRelatedEntityRelationId is category that was bind in the database before, but now its unbind
                // todo: probably we can improve this in the future by finding entity with column those values,
                // todo: maybe it was already in persistence process. This is possible due to unique requirements of join columns
                // we create a new subject which operations will be executed in subject operation executor
                const removedRelatedEntitySubject = new Subject({
                    metadata: relation.inverseEntityMetadata,
                    parentSubject: subject,
                    identifier: removedRelatedEntityRelationId,
                });
                if (!relation.inverseRelation ||
                    relation.inverseRelation.orphanedRowAction === "nullify") {
                    removedRelatedEntitySubject.canBeUpdated = true;
                    removedRelatedEntitySubject.changeMaps = [
                        {
                            relation: relation.inverseRelation,
                            value: null,
                        },
                    ];
                }
                else if (relation.inverseRelation.orphanedRowAction === "delete") {
                    removedRelatedEntitySubject.mustBeRemoved = true;
                }
                else if (relation.inverseRelation.orphanedRowAction === "soft-delete") {
                    removedRelatedEntitySubject.canBeSoftRemoved = true;
                }
                this.subjects.push(removedRelatedEntitySubject);
            });
        }
    }
}

//# sourceMappingURL=OneToManySubjectBuilder.js.map
