import { EmbeddedMetadata } from "./EmbeddedMetadata" import { EntityMetadata } from "./EntityMetadata" import { NamingStrategyInterface } from "../naming-strategy/NamingStrategyInterface" import { ColumnMetadata } from "./ColumnMetadata" import { UniqueMetadataArgs } from "../metadata-args/UniqueMetadataArgs" import { TypeORMError } from "../error" import { DeferrableType } from "./types/DeferrableType" /** * Unique metadata contains all information about table's unique constraints. */ export class UniqueMetadata { // --------------------------------------------------------------------- // Public Properties // --------------------------------------------------------------------- /** * Entity metadata of the class to which this unique constraint is applied. */ entityMetadata: EntityMetadata /** * Embedded metadata if this unique was applied on embedded. */ embeddedMetadata?: EmbeddedMetadata /** * Target class to which metadata is applied. */ target?: Function | string /** * Unique columns. */ columns: ColumnMetadata[] = [] /** * Indicate if unique constraints can be deferred. */ deferrable?: DeferrableType /** * User specified unique constraint name. */ givenName?: string /** * User specified column names. */ givenColumnNames?: | ((object?: any) => any[] | { [key: string]: number }) | string[] /** * Final unique constraint name. * If unique constraint name was given by a user then it stores normalized (by naming strategy) givenName. * If unique constraint name was not given then its generated. */ name: 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?: UniqueMetadataArgs }) { this.entityMetadata = options.entityMetadata this.embeddedMetadata = options.embeddedMetadata if (options.columns) this.columns = options.columns if (options.args) { this.target = options.args.target this.givenName = options.args.name this.givenColumnNames = options.args.columns this.deferrable = options.args.deferrable } } // --------------------------------------------------------------------- // Public Build Methods // --------------------------------------------------------------------- /** * Builds some depend unique constraint properties. * Must be called after all entity metadata's properties map, columns and relations are built. */ build(namingStrategy: NamingStrategyInterface): 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 { // 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((propertyName) => { const columnWithSameName = this.entityMetadata.columns.find( (column) => column.propertyPath === propertyName, ) if (columnWithSameName) { return [columnWithSameName] } const relationWithSameName = this.entityMetadata.relations.find( (relation) => relation.isWithJoinColumn && relation.propertyName === propertyName, ) if (relationWithSameName) { return relationWithSameName.joinColumns } const indexName = this.givenName ? '"' + this.givenName + '" ' : "" const entityName = this.entityMetadata.targetName throw new TypeORMError( `Unique constraint ${indexName}contains column that is missing in the entity (${entityName}): ` + propertyName, ) }) .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.uniqueConstraintName( this.entityMetadata.tableName, this.columns.map((column) => column.databaseName), ) return this } }