import { CannotCreateEntityIdMapError } from "../error/CannotCreateEntityIdMapError";
import { OrmUtils } from "../util/OrmUtils";
import { EntityPropertyNotFoundError } from "../error/EntityPropertyNotFoundError";
import { ObjectUtils } from "../util/ObjectUtils";
import { shorten } from "../util/StringUtils";
/**
 * Contains all entity metadata.
 */
export class EntityMetadata {
    // ---------------------------------------------------------------------
    // Constructor
    // ---------------------------------------------------------------------
    constructor(options) {
        this["@instanceof"] = Symbol.for("EntityMetadata");
        /**
         * Children entity metadatas. Used in inheritance patterns.
         */
        this.childEntityMetadatas = [];
        /**
         * All "inheritance tree" from a target entity.
         * For example for target Post < ContentModel < Unit it will be an array of [Post, ContentModel, Unit].
         * It also contains child entities for single table inheritance.
         */
        this.inheritanceTree = [];
        /**
         * Table type. Tables can be closure, junction, etc.
         */
        this.tableType = "regular";
        /**
         * Enables Sqlite "WITHOUT ROWID" modifier for the "CREATE TABLE" statement
         */
        this.withoutRowid = false;
        /**
         * Indicates if schema will be synchronized for this entity or not.
         */
        this.synchronize = true;
        /**
         * Checks if there any non-nullable column exist in this entity.
         */
        this.hasNonNullableRelations = false;
        /**
         * Indicates if this entity metadata of a junction table, or not.
         * Junction table is a table created by many-to-many relationship.
         *
         * Its also possible to understand if entity is junction via tableType.
         */
        this.isJunction = false;
        /**
         * Indicates if the entity should be instantiated using the constructor
         * or via allocating a new object via `Object.create()`.
         */
        this.isAlwaysUsingConstructor = true;
        /**
         * Checks if this table is a junction table of the closure table.
         * This type is for tables that contain junction metadata of the closure tables.
         */
        this.isClosureJunction = false;
        /**
         * Checks if entity's table has multiple primary columns.
         */
        this.hasMultiplePrimaryKeys = false;
        /**
         * Indicates if this entity metadata has uuid generated columns.
         */
        this.hasUUIDGeneratedColumns = false;
        /**
         * Entity's column metadatas defined by user.
         */
        this.ownColumns = [];
        /**
         * Columns of the entity, including columns that are coming from the embeddeds of this entity.
         */
        this.columns = [];
        /**
         * Ancestor columns used only in closure junction tables.
         */
        this.ancestorColumns = [];
        /**
         * Descendant columns used only in closure junction tables.
         */
        this.descendantColumns = [];
        /**
         * All columns except for virtual columns.
         */
        this.nonVirtualColumns = [];
        /**
         * In the case if this entity metadata is junction table's entity metadata,
         * this will contain all referenced columns of owner entity.
         */
        this.ownerColumns = [];
        /**
         * In the case if this entity metadata is junction table's entity metadata,
         * this will contain all referenced columns of inverse entity.
         */
        this.inverseColumns = [];
        /**
         * Gets the column with generated flag.
         */
        this.generatedColumns = [];
        /**
         * Gets the primary columns.
         */
        this.primaryColumns = [];
        /**
         * Entity's relation metadatas.
         */
        this.ownRelations = [];
        /**
         * Relations of the entity, including relations that are coming from the embeddeds of this entity.
         */
        this.relations = [];
        /**
         * List of eager relations this metadata has.
         */
        this.eagerRelations = [];
        /**
         * List of eager relations this metadata has.
         */
        this.lazyRelations = [];
        /**
         * Gets only one-to-one relations of the entity.
         */
        this.oneToOneRelations = [];
        /**
         * Gets only owner one-to-one relations of the entity.
         */
        this.ownerOneToOneRelations = [];
        /**
         * Gets only one-to-many relations of the entity.
         */
        this.oneToManyRelations = [];
        /**
         * Gets only many-to-one relations of the entity.
         */
        this.manyToOneRelations = [];
        /**
         * Gets only many-to-many relations of the entity.
         */
        this.manyToManyRelations = [];
        /**
         * Gets only owner many-to-many relations of the entity.
         */
        this.ownerManyToManyRelations = [];
        /**
         * Gets only owner one-to-one and many-to-one relations.
         */
        this.relationsWithJoinColumns = [];
        /**
         * Entity's relation id metadatas.
         */
        this.relationIds = [];
        /**
         * Entity's relation id metadatas.
         */
        this.relationCounts = [];
        /**
         * Entity's foreign key metadatas.
         */
        this.foreignKeys = [];
        /**
         * Entity's embedded metadatas.
         */
        this.embeddeds = [];
        /**
         * All embeddeds - embeddeds from this entity metadata and from all child embeddeds, etc.
         */
        this.allEmbeddeds = [];
        /**
         * Entity's own indices.
         */
        this.ownIndices = [];
        /**
         * Entity's index metadatas.
         */
        this.indices = [];
        /**
         * Entity's unique metadatas.
         */
        this.uniques = [];
        /**
         * Entity's own uniques.
         */
        this.ownUniques = [];
        /**
         * Entity's check metadatas.
         */
        this.checks = [];
        /**
         * Entity's exclusion metadatas.
         */
        this.exclusions = [];
        /**
         * Entity's own listener metadatas.
         */
        this.ownListeners = [];
        /**
         * Entity listener metadatas.
         */
        this.listeners = [];
        /**
         * Listener metadatas with "AFTER LOAD" type.
         */
        this.afterLoadListeners = [];
        /**
         * Listener metadatas with "BEFORE INSERT" type.
         */
        this.beforeInsertListeners = [];
        /**
         * Listener metadatas with "AFTER INSERT" type.
         */
        this.afterInsertListeners = [];
        /**
         * Listener metadatas with "BEFORE UPDATE" type.
         */
        this.beforeUpdateListeners = [];
        /**
         * Listener metadatas with "AFTER UPDATE" type.
         */
        this.afterUpdateListeners = [];
        /**
         * Listener metadatas with "BEFORE REMOVE" type.
         */
        this.beforeRemoveListeners = [];
        /**
         * Listener metadatas with "BEFORE SOFT REMOVE" type.
         */
        this.beforeSoftRemoveListeners = [];
        /**
         * Listener metadatas with "BEFORE RECOVER" type.
         */
        this.beforeRecoverListeners = [];
        /**
         * Listener metadatas with "AFTER REMOVE" type.
         */
        this.afterRemoveListeners = [];
        /**
         * Listener metadatas with "AFTER SOFT REMOVE" type.
         */
        this.afterSoftRemoveListeners = [];
        /**
         * Listener metadatas with "AFTER RECOVER" type.
         */
        this.afterRecoverListeners = [];
        this.connection = options.connection;
        this.inheritanceTree = options.inheritanceTree || [];
        this.inheritancePattern = options.inheritancePattern;
        this.treeType = options.tableTree ? options.tableTree.type : undefined;
        this.treeOptions = options.tableTree
            ? options.tableTree.options
            : undefined;
        this.parentClosureEntityMetadata = options.parentClosureEntityMetadata;
        this.tableMetadataArgs = options.args;
        this.target = this.tableMetadataArgs.target;
        this.tableType = this.tableMetadataArgs.type;
        this.expression = this.tableMetadataArgs.expression;
        this.withoutRowid = this.tableMetadataArgs.withoutRowid;
        this.dependsOn = this.tableMetadataArgs.dependsOn;
    }
    // -------------------------------------------------------------------------
    // Public Methods
    // -------------------------------------------------------------------------
    /**
     * Creates a new entity.
     */
    create(queryRunner, options) {
        const pojo = options && options.pojo === true ? true : false;
        // if target is set to a function (e.g. class) that can be created then create it
        let ret;
        if (typeof this.target === "function" && !pojo) {
            if (!options?.fromDeserializer || this.isAlwaysUsingConstructor) {
                ret = new this.target();
            }
            else {
                ret = Object.create(this.target.prototype);
            }
        }
        else {
            // otherwise simply return a new empty object
            ret = {};
        }
        // add "typename" property
        if (this.connection.options.typename) {
            ret[this.connection.options.typename] = this.targetName;
        }
        this.lazyRelations.forEach((relation) => this.connection.relationLoader.enableLazyLoad(relation, ret, queryRunner));
        return ret;
    }
    /**
     * Checks if given entity has an id.
     */
    hasId(entity) {
        if (!entity)
            return false;
        return this.primaryColumns.every((primaryColumn) => {
            const value = primaryColumn.getEntityValue(entity);
            return value !== null && value !== undefined && value !== "";
        });
    }
    /**
     * Checks if given entity / object contains ALL primary keys entity must have.
     * Returns true if it contains all of them, false if at least one of them is not defined.
     */
    hasAllPrimaryKeys(entity) {
        return this.primaryColumns.every((primaryColumn) => {
            const value = primaryColumn.getEntityValue(entity);
            return value !== null && value !== undefined;
        });
    }
    /**
     * Ensures that given object is an entity id map.
     * If given id is an object then it means its already id map.
     * If given id isn't an object then it means its a value of the id column
     * and it creates a new id map with this value and name of the primary column.
     */
    ensureEntityIdMap(id) {
        if (ObjectUtils.isObject(id))
            return id;
        if (this.hasMultiplePrimaryKeys)
            throw new CannotCreateEntityIdMapError(this, id);
        return this.primaryColumns[0].createValueMap(id);
    }
    /**
     * Gets primary keys of the entity and returns them in a literal object.
     * For example, for Post{ id: 1, title: "hello" } where id is primary it will return { id: 1 }
     * For multiple primary keys it returns multiple keys in object.
     * For primary keys inside embeds it returns complex object literal with keys in them.
     */
    getEntityIdMap(entity) {
        if (!entity)
            return undefined;
        return EntityMetadata.getValueMap(entity, this.primaryColumns, {
            skipNulls: true,
        });
    }
    /**
     * Creates a "mixed id map".
     * If entity has multiple primary keys (ids) then it will return just regular id map, like what getEntityIdMap returns.
     * But if entity has a single primary key then it will return just value of the id column of the entity, just value.
     * This is called mixed id map.
     */
    getEntityIdMixedMap(entity) {
        if (!entity)
            return entity;
        const idMap = this.getEntityIdMap(entity);
        if (this.hasMultiplePrimaryKeys) {
            return idMap;
        }
        else if (idMap) {
            return this.primaryColumns[0].getEntityValue(idMap); // todo: what about parent primary column?
        }
        return idMap;
    }
    /**
     * Compares two different entities by their ids.
     * Returns true if they match, false otherwise.
     */
    compareEntities(firstEntity, secondEntity) {
        const firstEntityIdMap = this.getEntityIdMap(firstEntity);
        if (!firstEntityIdMap)
            return false;
        const secondEntityIdMap = this.getEntityIdMap(secondEntity);
        if (!secondEntityIdMap)
            return false;
        return OrmUtils.compareIds(firstEntityIdMap, secondEntityIdMap);
    }
    /**
     * Finds column with a given property name.
     */
    findColumnWithPropertyName(propertyName) {
        return this.columns.find((column) => column.propertyName === propertyName);
    }
    /**
     * Finds column with a given database name.
     */
    findColumnWithDatabaseName(databaseName) {
        return this.columns.find((column) => column.databaseName === databaseName);
    }
    /**
     * Checks if there is a column or relationship with a given property path.
     */
    hasColumnWithPropertyPath(propertyPath) {
        const hasColumn = this.columns.some((column) => column.propertyPath === propertyPath);
        return hasColumn || this.hasRelationWithPropertyPath(propertyPath);
    }
    /**
     * Finds column with a given property path.
     */
    findColumnWithPropertyPath(propertyPath) {
        const column = this.columns.find((column) => column.propertyPath === propertyPath);
        if (column)
            return column;
        // in the case if column with property path was not found, try to find a relation with such property path
        // if we find relation and it has a single join column then its the column user was seeking
        const relation = this.relations.find((relation) => relation.propertyPath === propertyPath);
        if (relation && relation.joinColumns.length === 1)
            return relation.joinColumns[0];
        return undefined;
    }
    /**
     * Finds column with a given property path.
     * Does not search in relation unlike findColumnWithPropertyPath.
     */
    findColumnWithPropertyPathStrict(propertyPath) {
        return this.columns.find((column) => column.propertyPath === propertyPath);
    }
    /**
     * Finds columns with a given property path.
     * Property path can match a relation, and relations can contain multiple columns.
     */
    findColumnsWithPropertyPath(propertyPath) {
        const column = this.columns.find((column) => column.propertyPath === propertyPath);
        if (column)
            return [column];
        // in the case if column with property path was not found, try to find a relation with such property path
        // if we find relation and it has a single join column then its the column user was seeking
        const relation = this.findRelationWithPropertyPath(propertyPath);
        if (relation && relation.joinColumns)
            return relation.joinColumns;
        return [];
    }
    /**
     * Checks if there is a relation with the given property path.
     */
    hasRelationWithPropertyPath(propertyPath) {
        return this.relations.some((relation) => relation.propertyPath === propertyPath);
    }
    /**
     * Finds relation with the given property path.
     */
    findRelationWithPropertyPath(propertyPath) {
        return this.relations.find((relation) => relation.propertyPath === propertyPath);
    }
    /**
     * Checks if there is an embedded with a given property path.
     */
    hasEmbeddedWithPropertyPath(propertyPath) {
        return this.allEmbeddeds.some((embedded) => embedded.propertyPath === propertyPath);
    }
    /**
     * Finds embedded with a given property path.
     */
    findEmbeddedWithPropertyPath(propertyPath) {
        return this.allEmbeddeds.find((embedded) => embedded.propertyPath === propertyPath);
    }
    /**
     * Returns an array of databaseNames mapped from provided propertyPaths
     */
    mapPropertyPathsToColumns(propertyPaths) {
        return propertyPaths.map((propertyPath) => {
            const column = this.findColumnWithPropertyPath(propertyPath);
            if (column == null) {
                throw new EntityPropertyNotFoundError(propertyPath, this);
            }
            return column;
        });
    }
    /**
     * Iterates through entity and finds and extracts all values from relations in the entity.
     * If relation value is an array its being flattened.
     */
    extractRelationValuesFromEntity(entity, relations) {
        const relationsAndValues = [];
        relations.forEach((relation) => {
            const value = relation.getEntityValue(entity);
            if (Array.isArray(value)) {
                value.forEach((subValue) => relationsAndValues.push([
                    relation,
                    subValue,
                    EntityMetadata.getInverseEntityMetadata(subValue, relation),
                ]));
            }
            else if (value) {
                relationsAndValues.push([
                    relation,
                    value,
                    EntityMetadata.getInverseEntityMetadata(value, relation),
                ]);
            }
        });
        return relationsAndValues;
    }
    /**
     * In the case of SingleTableInheritance, find the correct metadata
     * for a given value.
     *
     * @param value The value to find the metadata for.
     * @returns The found metadata for the entity or the base metadata if no matching metadata
     *          was found in the whole inheritance tree.
     */
    findInheritanceMetadata(value) {
        // Check for single table inheritance and find the correct metadata in that case.
        // Goal is to use the correct discriminator as we could have a repository
        // for an (abstract) base class and thus the target would not match.
        if (this.inheritancePattern === "STI" &&
            this.childEntityMetadatas.length > 0) {
            // There could be a column on the base class that can manually be set to override the type.
            let manuallySetDiscriminatorValue;
            if (this.discriminatorColumn) {
                manuallySetDiscriminatorValue =
                    value[this.discriminatorColumn.propertyName];
            }
            return (this.childEntityMetadatas.find((meta) => manuallySetDiscriminatorValue ===
                meta.discriminatorValue ||
                value.constructor === meta.target) || this);
        }
        return this;
    }
    // -------------------------------------------------------------------------
    // Private Static Methods
    // -------------------------------------------------------------------------
    static getInverseEntityMetadata(value, relation) {
        return relation.inverseEntityMetadata.findInheritanceMetadata(value);
    }
    // -------------------------------------------------------------------------
    // Public Static Methods
    // -------------------------------------------------------------------------
    /**
     * Creates a property paths for a given entity.
     *
     * @deprecated
     */
    static createPropertyPath(metadata, entity, prefix = "") {
        const paths = [];
        Object.keys(entity).forEach((key) => {
            // check for function is needed in the cases when createPropertyPath used on values contain a function as a value
            // example: .update().set({ name: () => `SUBSTR('', 1, 2)` })
            const parentPath = prefix ? prefix + "." + key : key;
            if (metadata.hasEmbeddedWithPropertyPath(parentPath)) {
                const subPaths = this.createPropertyPath(metadata, entity[key], parentPath);
                paths.push(...subPaths);
            }
            else {
                const path = prefix ? prefix + "." + key : key;
                paths.push(path);
            }
        });
        return paths;
    }
    /**
     * Finds difference between two entity id maps.
     * Returns items that exist in the first array and absent in the second array.
     */
    static difference(firstIdMaps, secondIdMaps) {
        return firstIdMaps.filter((firstIdMap) => {
            return !secondIdMaps.find((secondIdMap) => OrmUtils.compareIds(firstIdMap, secondIdMap));
        });
    }
    /**
     * Creates value map from the given values and columns.
     * Examples of usages are primary columns map and join columns map.
     */
    static getValueMap(entity, columns, options) {
        return columns.reduce((map, column) => {
            const value = column.getEntityValueMap(entity, options);
            // make sure that none of the values of the columns are not missing
            if (map === undefined || value === null || value === undefined)
                return undefined;
            return OrmUtils.mergeDeep(map, value);
        }, {});
    }
    // ---------------------------------------------------------------------
    // Public Builder Methods
    // ---------------------------------------------------------------------
    build() {
        const namingStrategy = this.connection.namingStrategy;
        const entityPrefix = this.connection.options.entityPrefix;
        const entitySkipConstructor = this.connection.options.entitySkipConstructor;
        this.engine = this.tableMetadataArgs.engine;
        this.database =
            this.tableMetadataArgs.type === "entity-child" &&
                this.parentEntityMetadata
                ? this.parentEntityMetadata.database
                : this.tableMetadataArgs.database;
        if (this.tableMetadataArgs.schema) {
            this.schema = this.tableMetadataArgs.schema;
        }
        else if (this.tableMetadataArgs.type === "entity-child" &&
            this.parentEntityMetadata) {
            this.schema = this.parentEntityMetadata.schema;
        }
        else if (this.connection.options?.hasOwnProperty("schema")) {
            this.schema = this.connection.options.schema;
        }
        this.givenTableName =
            this.tableMetadataArgs.type === "entity-child" &&
                this.parentEntityMetadata
                ? this.parentEntityMetadata.givenTableName
                : this.tableMetadataArgs.name;
        this.synchronize =
            this.tableMetadataArgs.synchronize === false ? false : true;
        this.targetName =
            typeof this.tableMetadataArgs.target === "function"
                ? this.tableMetadataArgs.target.name
                : this.tableMetadataArgs.target;
        if (this.tableMetadataArgs.type === "closure-junction") {
            this.tableNameWithoutPrefix =
                namingStrategy.closureJunctionTableName(this.givenTableName);
        }
        else if (this.tableMetadataArgs.type === "entity-child" &&
            this.parentEntityMetadata) {
            this.tableNameWithoutPrefix = namingStrategy.tableName(this.parentEntityMetadata.targetName, this.parentEntityMetadata.givenTableName);
        }
        else {
            this.tableNameWithoutPrefix = namingStrategy.tableName(this.targetName, this.givenTableName);
            if (this.tableMetadataArgs.type === "junction" &&
                this.connection.driver.maxAliasLength &&
                this.connection.driver.maxAliasLength > 0 &&
                this.tableNameWithoutPrefix.length >
                    this.connection.driver.maxAliasLength) {
                // note: we are not using DriverUtils.buildAlias here because we would like to avoid
                // hashed table names. However, current algorithm also isn't perfect, but we cannot
                // change it, since it's a big breaking change. Planned to 0.4.0
                this.tableNameWithoutPrefix = shorten(this.tableNameWithoutPrefix, { separator: "_", segmentLength: 3 });
            }
        }
        this.tableName = entityPrefix
            ? namingStrategy.prefixTableName(entityPrefix, this.tableNameWithoutPrefix)
            : this.tableNameWithoutPrefix;
        this.target = this.target ? this.target : this.tableName;
        this.name = this.targetName ? this.targetName : this.tableName;
        this.expression = this.tableMetadataArgs.expression;
        this.withoutRowid =
            this.tableMetadataArgs.withoutRowid === true ? true : false;
        this.tablePath = this.connection.driver.buildTableName(this.tableName, this.schema, this.database);
        this.orderBy =
            typeof this.tableMetadataArgs.orderBy === "function"
                ? this.tableMetadataArgs.orderBy(this.propertiesMap)
                : this.tableMetadataArgs.orderBy; // todo: is propertiesMap available here? Looks like its not
        if (entitySkipConstructor !== undefined) {
            this.isAlwaysUsingConstructor = !entitySkipConstructor;
        }
        this.isJunction =
            this.tableMetadataArgs.type === "closure-junction" ||
                this.tableMetadataArgs.type === "junction";
        this.isClosureJunction =
            this.tableMetadataArgs.type === "closure-junction";
        this.comment = this.tableMetadataArgs.comment;
    }
    /**
     * Registers a new column in the entity and recomputes all depend properties.
     */
    registerColumn(column) {
        if (this.ownColumns.indexOf(column) !== -1)
            return;
        this.ownColumns.push(column);
        this.columns = this.embeddeds.reduce((columns, embedded) => columns.concat(embedded.columnsFromTree), this.ownColumns);
        this.primaryColumns = this.columns.filter((column) => column.isPrimary);
        this.hasMultiplePrimaryKeys = this.primaryColumns.length > 1;
        this.hasUUIDGeneratedColumns =
            this.columns.filter((column) => column.isGenerated || column.generationStrategy === "uuid").length > 0;
        this.propertiesMap = this.createPropertiesMap();
        if (this.childEntityMetadatas)
            this.childEntityMetadatas.forEach((entityMetadata) => entityMetadata.registerColumn(column));
    }
    /**
     * Creates a special object - all columns and relations of the object (plus columns and relations from embeds)
     * in a special format - { propertyName: propertyName }.
     *
     * 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() {
        const map = {};
        this.columns.forEach((column) => OrmUtils.mergeDeep(map, column.createValueMap(column.propertyPath)));
        this.relations.forEach((relation) => OrmUtils.mergeDeep(map, relation.createValueMap(relation.propertyPath)));
        return map;
    }
    /**
     * Checks if entity has any column which rely on returning data,
     * e.g. columns with auto generated value, DEFAULT values considered as dependant of returning data.
     * For example, if we need to have RETURNING after INSERT (or we need returned id for DBs not supporting RETURNING),
     * it means we cannot execute bulk inserts in some cases.
     */
    getInsertionReturningColumns() {
        return this.columns.filter((column) => {
            return (column.default !== undefined ||
                column.asExpression !== undefined ||
                column.isGenerated ||
                column.isCreateDate ||
                column.isUpdateDate ||
                column.isDeleteDate ||
                column.isVersion);
        });
    }
}

//# sourceMappingURL=EntityMetadata.js.map
