import { FindOptionsUtils } from "../find-options/FindOptionsUtils";
/**
 * Wraps entities and creates getters/setters for their relations
 * to be able to lazily load relations when accessing these relations.
 */
export class RelationLoader {
    // -------------------------------------------------------------------------
    // Constructor
    // -------------------------------------------------------------------------
    constructor(connection) {
        this.connection = connection;
    }
    // -------------------------------------------------------------------------
    // Public Methods
    // -------------------------------------------------------------------------
    /**
     * Loads relation data for the given entity and its relation.
     */
    load(relation, entityOrEntities, queryRunner, queryBuilder) {
        // todo: check all places where it uses non array
        if (queryRunner && queryRunner.isReleased)
            queryRunner = undefined; // get new one if already closed
        if (relation.isManyToOne || relation.isOneToOneOwner) {
            return this.loadManyToOneOrOneToOneOwner(relation, entityOrEntities, queryRunner, queryBuilder);
        }
        else if (relation.isOneToMany || relation.isOneToOneNotOwner) {
            return this.loadOneToManyOrOneToOneNotOwner(relation, entityOrEntities, queryRunner, queryBuilder);
        }
        else if (relation.isManyToManyOwner) {
            return this.loadManyToManyOwner(relation, entityOrEntities, queryRunner, queryBuilder);
        }
        else {
            // many-to-many non owner
            return this.loadManyToManyNotOwner(relation, entityOrEntities, queryRunner, queryBuilder);
        }
    }
    /**
     * Loads data for many-to-one and one-to-one owner relations.
     *
     * (ow) post.category<=>category.post
     * loaded: category from post
     * example: SELECT category.id AS category_id, category.name AS category_name FROM category category
     *              INNER JOIN post Post ON Post.category=category.id WHERE Post.id=1
     */
    loadManyToOneOrOneToOneOwner(relation, entityOrEntities, queryRunner, queryBuilder) {
        const entities = Array.isArray(entityOrEntities)
            ? entityOrEntities
            : [entityOrEntities];
        const joinAliasName = relation.entityMetadata.name;
        const qb = queryBuilder
            ? queryBuilder
            : this.connection
                .createQueryBuilder(queryRunner)
                .select(relation.propertyName) // category
                .from(relation.type, relation.propertyName);
        const mainAlias = qb.expressionMap.mainAlias.name;
        const columns = relation.entityMetadata.primaryColumns;
        const joinColumns = relation.isOwning
            ? relation.joinColumns
            : relation.inverseRelation.joinColumns;
        const conditions = joinColumns
            .map((joinColumn) => {
            return `${relation.entityMetadata.name}.${joinColumn.propertyName} = ${mainAlias}.${joinColumn.referencedColumn.propertyName}`;
        })
            .join(" AND ");
        qb.innerJoin(relation.entityMetadata.target, joinAliasName, conditions);
        if (columns.length === 1) {
            qb.where(`${joinAliasName}.${columns[0].propertyPath} IN (:...${joinAliasName + "_" + columns[0].propertyName})`);
            qb.setParameter(joinAliasName + "_" + columns[0].propertyName, entities.map((entity) => columns[0].getEntityValue(entity, true)));
        }
        else {
            const condition = entities
                .map((entity, entityIndex) => {
                return columns
                    .map((column, columnIndex) => {
                    const paramName = joinAliasName +
                        "_entity_" +
                        entityIndex +
                        "_" +
                        columnIndex;
                    qb.setParameter(paramName, column.getEntityValue(entity, true));
                    return (joinAliasName +
                        "." +
                        column.propertyPath +
                        " = :" +
                        paramName);
                })
                    .join(" AND ");
            })
                .map((condition) => "(" + condition + ")")
                .join(" OR ");
            qb.where(condition);
        }
        FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias.metadata);
        return qb.getMany();
        // return qb.getOne(); todo: fix all usages
    }
    /**
     * Loads data for one-to-many and one-to-one not owner relations.
     *
     * SELECT post
     * FROM post post
     * WHERE post.[joinColumn.name] = entity[joinColumn.referencedColumn]
     */
    loadOneToManyOrOneToOneNotOwner(relation, entityOrEntities, queryRunner, queryBuilder) {
        const entities = Array.isArray(entityOrEntities)
            ? entityOrEntities
            : [entityOrEntities];
        const columns = relation.inverseRelation.joinColumns;
        const qb = queryBuilder
            ? queryBuilder
            : this.connection
                .createQueryBuilder(queryRunner)
                .select(relation.propertyName)
                .from(relation.inverseRelation.entityMetadata.target, relation.propertyName);
        const aliasName = qb.expressionMap.mainAlias.name;
        if (columns.length === 1) {
            qb.where(`${aliasName}.${columns[0].propertyPath} IN (:...${aliasName + "_" + columns[0].propertyName})`);
            qb.setParameter(aliasName + "_" + columns[0].propertyName, entities.map((entity) => columns[0].referencedColumn.getEntityValue(entity, true)));
        }
        else {
            const condition = entities
                .map((entity, entityIndex) => {
                return columns
                    .map((column, columnIndex) => {
                    const paramName = aliasName +
                        "_entity_" +
                        entityIndex +
                        "_" +
                        columnIndex;
                    qb.setParameter(paramName, column.referencedColumn.getEntityValue(entity, true));
                    return (aliasName +
                        "." +
                        column.propertyPath +
                        " = :" +
                        paramName);
                })
                    .join(" AND ");
            })
                .map((condition) => "(" + condition + ")")
                .join(" OR ");
            qb.where(condition);
        }
        FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias.metadata);
        return qb.getMany();
        // return relation.isOneToMany ? qb.getMany() : qb.getOne(); todo: fix all usages
    }
    /**
     * Loads data for many-to-many owner relations.
     *
     * SELECT category
     * FROM category category
     * INNER JOIN post_categories post_categories
     * ON post_categories.postId = :postId
     * AND post_categories.categoryId = category.id
     */
    loadManyToManyOwner(relation, entityOrEntities, queryRunner, queryBuilder) {
        const entities = Array.isArray(entityOrEntities)
            ? entityOrEntities
            : [entityOrEntities];
        const parameters = relation.joinColumns.reduce((parameters, joinColumn) => {
            parameters[joinColumn.propertyName] = entities.map((entity) => joinColumn.referencedColumn.getEntityValue(entity, true));
            return parameters;
        }, {});
        const qb = queryBuilder
            ? queryBuilder
            : this.connection
                .createQueryBuilder(queryRunner)
                .select(relation.propertyName)
                .from(relation.type, relation.propertyName);
        const mainAlias = qb.expressionMap.mainAlias.name;
        const joinAlias = relation.junctionEntityMetadata.tableName;
        const joinColumnConditions = relation.joinColumns.map((joinColumn) => {
            return `${joinAlias}.${joinColumn.propertyName} IN (:...${joinColumn.propertyName})`;
        });
        const inverseJoinColumnConditions = relation.inverseJoinColumns.map((inverseJoinColumn) => {
            return `${joinAlias}.${inverseJoinColumn.propertyName}=${mainAlias}.${inverseJoinColumn.referencedColumn.propertyName}`;
        });
        qb.innerJoin(joinAlias, joinAlias, [...joinColumnConditions, ...inverseJoinColumnConditions].join(" AND ")).setParameters(parameters);
        FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias.metadata);
        return qb.getMany();
    }
    /**
     * Loads data for many-to-many not owner relations.
     *
     * SELECT post
     * FROM post post
     * INNER JOIN post_categories post_categories
     * ON post_categories.postId = post.id
     * AND post_categories.categoryId = post_categories.categoryId
     */
    loadManyToManyNotOwner(relation, entityOrEntities, queryRunner, queryBuilder) {
        const entities = Array.isArray(entityOrEntities)
            ? entityOrEntities
            : [entityOrEntities];
        const qb = queryBuilder
            ? queryBuilder
            : this.connection
                .createQueryBuilder(queryRunner)
                .select(relation.propertyName)
                .from(relation.type, relation.propertyName);
        const mainAlias = qb.expressionMap.mainAlias.name;
        const joinAlias = relation.junctionEntityMetadata.tableName;
        const joinColumnConditions = relation.inverseRelation.joinColumns.map((joinColumn) => {
            return `${joinAlias}.${joinColumn.propertyName} = ${mainAlias}.${joinColumn.referencedColumn.propertyName}`;
        });
        const inverseJoinColumnConditions = relation.inverseRelation.inverseJoinColumns.map((inverseJoinColumn) => {
            return `${joinAlias}.${inverseJoinColumn.propertyName} IN (:...${inverseJoinColumn.propertyName})`;
        });
        const parameters = relation.inverseRelation.inverseJoinColumns.reduce((parameters, joinColumn) => {
            parameters[joinColumn.propertyName] = entities.map((entity) => joinColumn.referencedColumn.getEntityValue(entity, true));
            return parameters;
        }, {});
        qb.innerJoin(joinAlias, joinAlias, [...joinColumnConditions, ...inverseJoinColumnConditions].join(" AND ")).setParameters(parameters);
        FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias.metadata);
        return qb.getMany();
    }
    /**
     * Wraps given entity and creates getters/setters for its given relation
     * to be able to lazily load data when accessing this relation.
     */
    enableLazyLoad(relation, entity, queryRunner) {
        const relationLoader = this;
        const dataIndex = "__" + relation.propertyName + "__"; // in what property of the entity loaded data will be stored
        const promiseIndex = "__promise_" + relation.propertyName + "__"; // in what property of the entity loading promise will be stored
        const resolveIndex = "__has_" + relation.propertyName + "__"; // indicates if relation data already was loaded or not, we need this flag if loaded data is empty
        const setData = (entity, value) => {
            entity[dataIndex] = value;
            entity[resolveIndex] = true;
            delete entity[promiseIndex];
            return value;
        };
        const setPromise = (entity, value) => {
            delete entity[resolveIndex];
            delete entity[dataIndex];
            entity[promiseIndex] = value;
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
            value.then(
            // ensure different value is not assigned yet
            (result) => entity[promiseIndex] === value
                ? setData(entity, result)
                : result);
            return value;
        };
        Object.defineProperty(entity, relation.propertyName, {
            get: function () {
                if (this[resolveIndex] === true ||
                    this[dataIndex] !== undefined)
                    // if related data already was loaded then simply return it
                    return Promise.resolve(this[dataIndex]);
                if (this[promiseIndex])
                    // if related data is loading then return a promise relationLoader loads it
                    return this[promiseIndex];
                // nothing is loaded yet, load relation data and save it in the model once they are loaded
                const loader = relationLoader
                    .load(relation, this, queryRunner)
                    .then((result) => relation.isOneToOne || relation.isManyToOne
                    ? result.length === 0
                        ? null
                        : result[0]
                    : result);
                return setPromise(this, loader);
            },
            set: function (value) {
                if (value instanceof Promise) {
                    // if set data is a promise then wait for its resolve and save in the object
                    // eslint-disable-next-line @typescript-eslint/no-floating-promises
                    setPromise(this, value);
                }
                else {
                    // if its direct data set (non promise, probably not safe-typed)
                    setData(this, value);
                }
            },
            configurable: true,
            enumerable: false,
        });
    }
}

//# sourceMappingURL=RelationLoader.js.map
