import { EntityMetadata } from "./EntityMetadata" import { IndexMetadataArgs } from "../metadata-args/IndexMetadataArgs" import { NamingStrategyInterface } from "../naming-strategy/NamingStrategyInterface" import { ColumnMetadata } from "./ColumnMetadata" import { EmbeddedMetadata } from "./EmbeddedMetadata" import { TypeORMError } from "../error" /** * Index metadata contains all information about table's index. */ export class IndexMetadata { // --------------------------------------------------------------------- // Public Properties // --------------------------------------------------------------------- /** * Entity metadata of the class to which this index is applied. */ entityMetadata: EntityMetadata /** * Embedded metadata if this index was applied on embedded. */ embeddedMetadata?: EmbeddedMetadata /** * Indicates if this index must be unique. */ isUnique: boolean = false /** * The SPATIAL modifier indexes the entire column and does not allow indexed columns to contain NULL values. * Works only in MySQL. */ isSpatial: boolean = false /** * The FULLTEXT modifier indexes the entire column and does not allow prefixing. * Works only in MySQL. */ isFulltext: boolean = false /** * NULL_FILTERED indexes are particularly useful for indexing sparse columns, where most rows contain a NULL value. * In these cases, the NULL_FILTERED index can be considerably smaller and more efficient to maintain than * a normal index that includes NULL values. * * Works only in Spanner. */ isNullFiltered: boolean = false /** * Fulltext parser. * Works only in MySQL. */ parser?: string /** * Indicates if this index must synchronize with database index. */ synchronize: boolean = true /** * If true, the index only references documents with the specified field. * These indexes use less space but behave differently in some situations (particularly sorts). * This option is only supported for mongodb database. */ isSparse?: boolean /** * Builds the index in the background so that building an index an does not block other database activities. * This option is only supported for mongodb database. */ isBackground?: boolean /** * Specifies a time to live, in seconds. * This option is only supported for mongodb database. */ expireAfterSeconds?: number /** * Target class to which metadata is applied. */ target?: Function | string /** * Indexed columns. */ columns: ColumnMetadata[] = [] /** * User specified index name. */ givenName?: string /** * User specified column names. */ givenColumnNames?: | ((object?: any) => any[] | { [key: string]: number }) | string[] /** * Final index name. * If index name was given by a user then it stores normalized (by naming strategy) givenName. * If index name was not given then its generated. */ name: string /** * Index filter condition. */ where?: string /** * Map of column names with order set. * Used only by MongoDB driver. */ columnNamesWithOrderingMap: { [key: string]: number } = {} // --------------------------------------------------------------------- // Constructor // --------------------------------------------------------------------- constructor(options: { entityMetadata: EntityMetadata embeddedMetadata?: EmbeddedMetadata columns?: ColumnMetadata[] args?: IndexMetadataArgs }) { this.entityMetadata = options.entityMetadata this.embeddedMetadata = options.embeddedMetadata if (options.columns) this.columns = options.columns if (options.args) { this.target = options.args.target if ( options.args.synchronize !== null && options.args.synchronize !== undefined ) this.synchronize = options.args.synchronize this.isUnique = !!options.args.unique this.isSpatial = !!options.args.spatial this.isFulltext = !!options.args.fulltext this.isNullFiltered = !!options.args.nullFiltered this.parser = options.args.parser this.where = options.args.where this.isSparse = options.args.sparse this.isBackground = options.args.background this.expireAfterSeconds = options.args.expireAfterSeconds this.givenName = options.args.name this.givenColumnNames = options.args.columns } } // --------------------------------------------------------------------- // Public Build Methods // --------------------------------------------------------------------- /** * Builds some depend index properties. * Must be called after all entity metadata's properties map, columns and relations are built. */ build(namingStrategy: NamingStrategyInterface): this { if (this.synchronize === false) { this.name = this.givenName! return this } const map: { [key: string]: number } = {} // if columns already an array of string then simply return it if (this.givenColumnNames) { let columnPropertyPaths: string[] = [] if (Array.isArray(this.givenColumnNames)) { columnPropertyPaths = this.givenColumnNames.map( (columnName) => { if (this.embeddedMetadata) return ( this.embeddedMetadata.propertyPath + "." + columnName ) return columnName.trim() }, ) columnPropertyPaths.forEach( (propertyPath) => (map[propertyPath] = 1), ) } else { // todo: indices in embeds are not implemented in this syntax. deprecate this syntax? // if columns is a function that returns array of field names then execute it and get columns names from it const columnsFnResult = this.givenColumnNames( this.entityMetadata.propertiesMap, ) if (Array.isArray(columnsFnResult)) { columnPropertyPaths = columnsFnResult.map((i: any) => String(i), ) columnPropertyPaths.forEach((name) => (map[name] = 1)) } else { columnPropertyPaths = Object.keys(columnsFnResult).map( (i: any) => String(i), ) Object.keys(columnsFnResult).forEach( (columnName) => (map[columnName] = columnsFnResult[columnName]), ) } } this.columns = columnPropertyPaths .map((propertyPath) => { const columnWithSameName = this.entityMetadata.columns.find( (column) => column.propertyPath === propertyPath, ) if (columnWithSameName) { return [columnWithSameName] } const relationWithSameName = this.entityMetadata.relations.find( (relation) => relation.isWithJoinColumn && relation.propertyName === propertyPath, ) if (relationWithSameName) { return relationWithSameName.joinColumns } const indexName = this.givenName ? '"' + this.givenName + '" ' : "" const entityName = this.entityMetadata.targetName throw new TypeORMError( `Index ${indexName}contains column that is missing in the entity (${entityName}): ` + propertyPath, ) }) .reduce((a, b) => a.concat(b)) } this.columnNamesWithOrderingMap = Object.keys(map).reduce( (updatedMap, key) => { const column = this.entityMetadata.columns.find( (column) => column.propertyPath === key, ) if (column) updatedMap[column.databasePath] = map[key] return updatedMap }, {} as { [key: string]: number }, ) this.name = this.givenName ? this.givenName : namingStrategy.indexName( this.entityMetadata.tableName, this.columns.map((column) => column.databaseName), this.where, ) return this } }