import _ from "lodash" ; import { AutoOptions } from "." ; import { FKSpec } from "./dialects/dialect-options" ; import { CaseOption , qNameJoin , qNameSplit , recase , Relation , TableData , singularize , pluralize } from "./types" ; /** Constructs entity relationships from TableData.foreignKeys and populates TableData.relations */ export class AutoRelater { caseModel : CaseOption ; caseProp : CaseOption ; singularize : boolean ; pkSuffixes : string [ ] ; relations : Relation [ ] ; private usedChildNames : Set < string > ; constructor ( options ) { this . caseModel = options . caseModel || 'o' ; this . caseProp = options . caseProp || 'o' ; this . singularize = options . singularize ; this . pkSuffixes = options . pkSuffixes || [ ] ; if ( ! this . pkSuffixes || this . pkSuffixes . length == 0 ) { this . pkSuffixes = [ "id" ] ; } this . relations = [ ] ; this . usedChildNames = new Set ( ) ; } /** Create Relations from the foreign keys, and add to TableData */ buildRelations ( td ) { const fkTables = _ . keys ( td . foreignKeys ) . sort ( ) ; fkTables . forEach ( t => { const fkFields = td . foreignKeys [ t ] ; const fkFieldNames = _ . keys ( fkFields ) ; fkFieldNames . forEach ( fkFieldName => { const spec = fkFields [ fkFieldName ] ; if ( spec . isForeignKey ) { this . addRelation ( t , fkFieldName , spec , fkFields ) ; } } ) ; } ) ; td . relations = _ . sortBy ( this . relations , [ 'parentTable' , 'childTable' ] ) ; return td ; } /** Create a Relation object for the given foreign key */ private addRelation ( table , fkFieldName , spec , fkFields ) { const [ schemaName , tableName ] = qNameSplit ( table ) ; const schema = schemaName as string ; const modelName = recase ( this . caseModel , tableName , this . singularize ) ; const targetModel = recase ( this . caseModel , spec . foreignSources . target_table as string , this . singularize ) ; const alias = this . getAlias ( fkFieldName , spec . foreignSources . target_table as string , spec . foreignSources . source_table as string ) ; const childAlias = this . getChildAlias ( fkFieldName , spec . foreignSources . source_table as string , spec . foreignSources . target_table as string ) ; const sourceProp = recase ( this . caseProp , fkFieldName ) ; const isOne = ( ( spec . isPrimaryKey && ! _ . some ( fkFields , f => f . isPrimaryKey && f . source_column !== fkFieldName ) || ( ! ! spec . isUnique && ! _ . some ( fkFields , f => f . isUnique === spec . isUnique && f . source_column !== fkFieldName ) ) ) ) ; this . relations . push ( { parentId : sourceProp , parentModel : targetModel , parentProp : alias , parentTable : qNameJoin ( spec . foreignSources . target_schema || schema , spec . foreignSources . target_table ) , childModel : modelName , childProp : isOne ? singularize ( childAlias ) : pluralize ( childAlias ) , childTable : qNameJoin ( spec . foreignSources . source_schema || schema , spec . foreignSources . source_table ) , isOne : isOne , isM2M : false } ) ; if ( spec . isPrimaryKey ) { const otherKeys = _ . filter ( fkFields , k => k . isForeignKey && k . isPrimaryKey && k . source_column !== fkFieldName ) ; if ( otherKeys . length === 1 ) { const otherKey = otherKeys [ 0 ] ; const otherModel = recase ( this . caseModel , otherKey . foreignSources . target_table as string , this . singularize ) ; const otherProp = this . getAlias ( otherKey . source_column , otherKey . foreignSources . target_table as string , otherKey . foreignSources . source_table as string , true ) ; const otherId = recase ( this . caseProp , otherKey . source_column ) ; this . relations . push ( { parentId : sourceProp , parentModel : targetModel , parentProp : pluralize ( alias ) , parentTable : qNameJoin ( spec . foreignSources . target_schema || schema , spec . foreignSources . target_table ) , childModel : otherModel , childProp : pluralize ( otherProp ) , childTable : qNameJoin ( otherKey . foreignSources . target_schema || schema , otherKey . foreignSources . target_table ) , childId : otherId , joinModel : modelName , isOne : isOne , isM2M : true } ) ; } } } /** Convert foreign key name into alias name for belongsTo relations */ private getAlias ( fkFieldName , modelName , targetModel , isM2M = false ) { let name = this . trimId ( fkFieldName ) ; if ( name === fkFieldName || isM2M ) { name = fkFieldName + "_" + modelName ; } let singleName = singularize ( name ) ; if ( isM2M ) { if ( this . usedChildNames . has ( modelName + "." + singleName ) ) { name = name + "_" + targetModel ; } this . usedChildNames . add ( modelName + "." + singularize ( name ) ) ; } else { if ( this . usedChildNames . has ( targetModel + "." + singleName ) ) { name = name + "_" + modelName ; } this . usedChildNames . add ( targetModel + "." + singularize ( name ) ) ; } return recase ( this . caseProp , name , true ) ; } /** Convert foreign key name into alias name for hasMany/hasOne relations */ private getChildAlias ( fkFieldName , modelName , targetModel ) { let name = modelName ; if ( this . usedChildNames . has ( targetModel + "." + singularize ( name ) ) ) { name = this . trimId ( fkFieldName ) ; name = name + "_" + modelName ; } name = singularize ( name ) ; this . usedChildNames . add ( targetModel + "." + name ) ; return recase ( this . caseProp , name , true ) ; } private trimId ( name ) { this . pkSuffixes . forEach ( suffix => { if ( name . length > ( suffix . length + 1 ) && name . toLowerCase ( ) . endsWith ( suffix . toLowerCase ( ) ) ) { name = name . substring ( 0 , name . length - suffix . length ) ; } } ) ; if ( name . endsWith ( "_" ) ) { name = name . substring ( 0 , name . length - 1 ) ; } return name ; } }