added relation tests - problems with distinguishing OneTwoMany from OneToOne without indexes

This commit is contained in:
Kononnable 2017-05-27 00:55:10 +02:00
parent 3ca6707a80
commit 642e57bf74
15 changed files with 335 additions and 75 deletions

View File

@ -1,22 +1,23 @@
import { AbstractDriver } from './AbstractDriver'
import * as MSSQL from 'mssql'
import {ColumnInfo} from './../models/ColumnInfo'
import {EntityInfo} from './../models/EntityInfo'
import {DatabaseModel} from './../models/DatabaseModel'
import { ColumnInfo } from './../models/ColumnInfo'
import { EntityInfo } from './../models/EntityInfo'
import { RelationInfo } from './../models/RelationInfo'
import { DatabaseModel } from './../models/DatabaseModel'
/**
* MssqlDriver
*/
export class MssqlDriver extends AbstractDriver {
FindPrimaryColumnsFromIndexes(dbModel: DatabaseModel) {
dbModel.entities.forEach(entity => {
let primaryIndex = entity.Indexes.find(v=>v.isPrimaryKey);
if (!primaryIndex){
let primaryIndex = entity.Indexes.find(v => v.isPrimaryKey);
if (!primaryIndex) {
console.error(`Table ${entity.EntityName} has no PK.`)
return;
}
let pIndex=primaryIndex //typescript error? pIndex:IndexInfo; primaryIndex:IndexInfo|undefined
entity.Columns.forEach(col=>{
if(pIndex.columns.some( cIndex=> cIndex.name==col.name)) col.isPrimary=true
let pIndex = primaryIndex //typescript error? pIndex:IndexInfo; primaryIndex:IndexInfo|undefined
entity.Columns.forEach(col => {
if (pIndex.columns.some(cIndex => cIndex.name == col.name)) col.isPrimary = true
})
});
}
@ -37,9 +38,11 @@ export class MssqlDriver extends AbstractDriver {
}
async GetCoulmnsFromEntity(entities: EntityInfo[]): Promise<EntityInfo[]> {
let request = new MSSQL.Request(this.Connection)
let response: { TABLE_NAME: string, COLUMN_NAME: string, COLUMN_DEFAULT: string,
IS_NULLABLE: string, DATA_TYPE: string, CHARACTER_MAXIMUM_LENGTH: number,
NUMERIC_PRECISION:number,NUMERIC_SCALE:number,IsIdentity:number }[]
let response: {
TABLE_NAME: string, COLUMN_NAME: string, COLUMN_DEFAULT: string,
IS_NULLABLE: string, DATA_TYPE: string, CHARACTER_MAXIMUM_LENGTH: number,
NUMERIC_PRECISION: number, NUMERIC_SCALE: number, IsIdentity: number
}[]
= await request.query(`SELECT TABLE_NAME,COLUMN_NAME,COLUMN_DEFAULT,IS_NULLABLE,
DATA_TYPE,CHARACTER_MAXIMUM_LENGTH,NUMERIC_PRECISION,NUMERIC_SCALE,
COLUMNPROPERTY(object_id(TABLE_NAME), COLUMN_NAME, 'IsIdentity') IsIdentity FROM INFORMATION_SCHEMA.COLUMNS`);
@ -116,8 +119,8 @@ export class MssqlDriver extends AbstractDriver {
case "decimal":
colInfo.ts_type = "number"
colInfo.sql_type = "decimal"
colInfo.numericPrecision=resp.NUMERIC_PRECISION
colInfo.numericScale=resp.NUMERIC_SCALE
colInfo.numericPrecision = resp.NUMERIC_PRECISION
colInfo.numericScale = resp.NUMERIC_SCALE
break;
case "xml":
colInfo.ts_type = "string"
@ -127,7 +130,7 @@ export class MssqlDriver extends AbstractDriver {
console.error("Unknown column type:" + resp.DATA_TYPE);
break;
}
colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH>0?resp.CHARACTER_MAXIMUM_LENGTH:null;
colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null;
if (colInfo.sql_type) ent.Columns.push(colInfo);
})
})
@ -192,7 +195,8 @@ ORDER BY
let response: {
TableWithForeignKey: string, FK_PartNo: number, ForeignKeyColumn: string,
TableReferenced: string, ForeignKeyColumnReferenced: string,
onDelete: "RESTRICT" | "CASCADE" | "SET NULL", object_id: number
onDelete: "RESTRICT" | "CASCADE" | "SET NULL",
onUpdate: "RESTRICT" | "CASCADE" | "SET NULL", object_id: number
}[]
= await request.query(`select
parentTable.name as TableWithForeignKey,
@ -201,6 +205,7 @@ ORDER BY
referencedTable.name as TableReferenced,
referencedColumn.name as ForeignKeyColumnReferenced,
fk.delete_referential_action_desc as onDelete,
fk.update_referential_action_desc as onUpdate,
fk.object_id
from
sys.foreign_keys fk
@ -228,6 +233,7 @@ order by
rels.ownerColumnsNames = [];
rels.referencedColumnsNames = [];
rels.actionOnDelete = resp.onDelete;
rels.actionOnUpdate = resp.onUpdate;
rels.object_id = resp.object_id;
rels.ownerTable = resp.TableWithForeignKey;
rels.referencedTable = resp.TableReferenced;
@ -236,66 +242,84 @@ order by
rels.ownerColumnsNames.push(resp.ForeignKeyColumn);
rels.referencedColumnsNames.push(resp.ForeignKeyColumnReferenced);
})
relationsTemp.forEach( (relationTmp)=>{
let ownerEntity = entities.find((entitity)=>{
return entitity.EntityName==relationTmp.ownerTable;
relationsTemp.forEach((relationTmp) => {
let ownerEntity = entities.find((entitity) => {
return entitity.EntityName == relationTmp.ownerTable;
})
if (!ownerEntity){
if (!ownerEntity) {
console.error(`Relation between tables ${relationTmp.ownerTable} and ${relationTmp.referencedTable} didn't found entity model ${relationTmp.ownerTable}.`)
return;
}
let referencedEntity = entities.find((entitity)=>{
return entitity.EntityName==relationTmp.referencedTable;
let referencedEntity = entities.find((entitity) => {
return entitity.EntityName == relationTmp.referencedTable;
})
if (!referencedEntity){
if (!referencedEntity) {
console.error(`Relation between tables ${relationTmp.ownerTable} and ${relationTmp.referencedTable} didn't found entity model ${relationTmp.referencedTable}.`)
return;
}
let ownerColumn = ownerEntity.Columns.find((column)=>{
return column.name==relationTmp.ownerColumnsNames[0];
let ownerColumn = ownerEntity.Columns.find((column) => {
return column.name == relationTmp.ownerColumnsNames[0];
})
if(!ownerColumn){
if (!ownerColumn) {
console.error(`Relation between tables ${relationTmp.ownerTable} and ${relationTmp.referencedTable} didn't found entity column ${relationTmp.ownerTable}.${ownerColumn}.`)
return;
}
let relatedColumn = referencedEntity.Columns.find((column)=>{
return column.name==relationTmp.referencedColumnsNames[0];
let relatedColumn = referencedEntity.Columns.find((column) => {
return column.name == relationTmp.referencedColumnsNames[0];
})
if(!relatedColumn){
if (!relatedColumn) {
console.error(`Relation between tables ${relationTmp.ownerTable} and ${relationTmp.referencedTable} didn't found entity column ${relationTmp.referencedTable}.${relatedColumn}.`)
return;
}
let ownColumn:ColumnInfo = ownerColumn;
let isOneToMany:boolean;
isOneToMany=false;
let ownColumn: ColumnInfo = ownerColumn;
let isOneToMany: boolean;
isOneToMany = false;
let index = ownerEntity.Indexes.find(
(index)=>{
return index.isUnique && index.columns.some(col=>{
return col.name==ownColumn.name
(index) => {
return index.isUnique && index.columns.some(col => {
return col.name == ownColumn.name
})
}
)
if (!index){
isOneToMany=true;
}else{
isOneToMany=false;
if (!index) {
isOneToMany = true;
} else {
isOneToMany = false;
}
let ownerRelation=new RelationInfo()
ownerRelation.actionOnDelete= relationTmp.actionOnDelete
ownerRelation.actionOnUpdate= relationTmp.actionOnUpdate
ownerRelation.isOwner= true
ownerRelation.relatedColumn= relatedColumn.name.toLowerCase()
ownerRelation.relatedTable= relationTmp.referencedTable
ownerRelation.relationType= isOneToMany ? "OneToMany" : "OneToOne"
ownerColumn.relations.push(ownerRelation)
if (isOneToMany) {
let col = new ColumnInfo()
col.name = ownerEntity.EntityName.toLowerCase() //+ 's'
let referencedRelation = new RelationInfo();
col.relations.push(referencedRelation)
referencedRelation.actionOnDelete= relationTmp.actionOnDelete
referencedRelation.actionOnUpdate= relationTmp.actionOnUpdate
referencedRelation.isOwner= false
referencedRelation.relatedColumn= ownerColumn.name
referencedRelation.relatedTable= relationTmp.ownerTable
referencedRelation.relationType= "ManyToOne"
referencedEntity.Columns.push(col)
} else {
let col = new ColumnInfo()
col.name = ownerEntity.EntityName.toLowerCase()
let referencedRelation = new RelationInfo();
col.relations.push(referencedRelation)
referencedRelation.actionOnDelete= relationTmp.actionOnDelete
referencedRelation.actionOnUpdate= relationTmp.actionOnUpdate
referencedRelation.isOwner= false
referencedRelation.relatedColumn= ownerColumn.name
referencedRelation.relatedTable= relationTmp.ownerTable
referencedRelation.relationType= "OneToOne"
referencedEntity.Columns.push(col)
}
ownerColumn.relations.push(<RelationInfo>{
actionOnDelete:relationTmp.actionOnDelete,
isOwner:true,
relatedColumn:relatedColumn.name,
relatedTable:relationTmp.referencedTable,
relationType:isOneToMany?"OneToMany":"OneToOne"
})
relatedColumn.relations.push(<RelationInfo>{
actionOnDelete:relationTmp.actionOnDelete,
isOwner:false,
relatedColumn:ownerColumn.name,
relatedTable:relationTmp.ownerTable,
relationType:isOneToMany?"ManyToOne":"OneToOne"
})
})
return entities;
}

View File

@ -1,4 +1,4 @@
import {Index,Entity, PrimaryColumn, Column, OneToOne, OneToMany, ManyToOne, JoinTable} from "typeorm";
import {Index,Entity, PrimaryColumn, Column, OneToOne, OneToMany, ManyToOne, JoinColumn} from "typeorm";
{{relationImports}}
@Entity()
@ -7,7 +7,7 @@ import {Index,Entity, PrimaryColumn, Column, OneToOne, OneToMany, ManyToOne, Joi
{{#Columns}}
@Column("{{sql_type}}",{ {{#is_generated}}
{{^relations}} @Column("{{sql_type}}",{ {{#is_generated}}
generated:true,{{/is_generated}}{{#is_nullable}}
nullable:true,{{/is_nullable}}{{^is_nullable}}
nullable:false,{{/is_nullable}}{{#char_max_lenght}}
@ -16,9 +16,13 @@ import {Index,Entity, PrimaryColumn, Column, OneToOne, OneToMany, ManyToOne, Joi
precision:{{numericPrecision}},{{/numericPrecision}}{{#numericScale}}
scale:{{numericScale}},{{/numericScale}}{{#isPrimary}}
primary:{{isPrimary}},{{/isPrimary}}
}){{#relations}}
@{{relationType}}(type=>{{relatedTable}},x=>x.{{relatedColumn}}){{#isOwner}}
@JoinTable(){{/isOwner}}{{/relations}}
})
{{name}}:{{ts_type}};
{{/relations}}{{#relations}}
@{{relationType}}(type=>{{relatedTable}},x=>x.{{relatedColumn}}){{#isOwner}}
@JoinColumn(){{/isOwner}}
{{#if isOneToMany}}{{../name}}:{{relatedTable}}[];
{{else}}{{../name}}:{{relatedTable}};
{{/if}}{{/relations}}
{{/Columns}}
}

View File

@ -1,3 +1,4 @@
import {RelationInfo} from './RelationInfo'
/**
* ColumnInfo
*/

View File

@ -1,7 +1,13 @@
interface RelationInfo {
isOwner: boolean,
relationType: "OneToOne", "OneToMany", "ManyToOne"
relatedTable: string,
relatedColumn: string,
actionOnDelete:"RESTRICT"|"CASCADE"|"SET NULL",
export class RelationInfo {
isOwner: boolean
relationType: "OneToOne" | "OneToMany" | "ManyToOne"
relatedTable: string
relatedColumn: string
actionOnDelete: "RESTRICT" | "CASCADE" | "SET NULL"
actionOnUpdate: "RESTRICT" | "CASCADE" | "SET NULL"
get isOneToMany(): boolean {
return this.relationType == "OneToMany"
}
}

View File

@ -4,5 +4,6 @@ interface RelationTempInfo{
referencedTable:string,
referencedColumnsNames:string[],
actionOnDelete:"RESTRICT"|"CASCADE"|"SET NULL",
actionOnUpdate:"RESTRICT"|"CASCADE"|"SET NULL",
object_id:number
}

View File

@ -4,6 +4,7 @@ import * as Sinon from 'sinon'
import * as MSSQL from 'mssql'
import { EntityInfo } from './../../src/models/EntityInfo'
import { ColumnInfo } from './../../src/models/ColumnInfo'
import { RelationInfo } from './../../src/models/RelationInfo'
describe('MssqlDriver', function () {

View File

@ -0,0 +1,68 @@
import {PrimaryGeneratedColumn, Column, Entity, OneToOne,JoinColumn} from "typeorm";
import {PostDetails} from "./PostDetails";
import {PostCategory} from "./PostCategory";
import {PostAuthor} from "./PostAuthor";
import {PostInformation} from "./PostInformation";
import {PostImage} from "./PostImage";
import {PostMetadata} from "./PostMetadata";
@Entity("Post")
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
text: string;
// post has relation with category, however inverse relation is not set (category does not have relation with post set)
@OneToOne(type => PostCategory, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
@JoinColumn()
category: PostCategory;
// post has relation with details. cascade inserts here means if new PostDetails instance will be set to this
// relation it will be inserted automatically to the db when you save this Post entity
@OneToOne(type => PostDetails, details => details.post, {
cascadeInsert: true
})
@JoinColumn()
details: PostDetails;
// post has relation with details. cascade update here means if new PostDetail instance will be set to this relation
// it will be inserted automatically to the db when you save this Post entity
@OneToOne(type => PostImage, image => image.post, {
cascadeUpdate: true
})
@JoinColumn()
image: PostImage;
// post has relation with details. cascade update here means if new PostDetail instance will be set to this relation
// it will be inserted automatically to the db when you save this Post entity
@OneToOne(type => PostMetadata, metadata => metadata.post, {
cascadeRemove: true
})
@JoinColumn()
metadata: PostMetadata|null;
// post has relation with details. full cascades here
@OneToOne(type => PostInformation, information => information.post, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
@JoinColumn()
information: PostInformation;
// post has relation with details. not cascades here. means cannot be persisted, updated or removed
@OneToOne(type => PostAuthor, author => author.post)
@JoinColumn()
author: PostAuthor;
}

View File

@ -0,0 +1,16 @@
import {PrimaryGeneratedColumn, Column, Entity, OneToOne,JoinColumn} from "typeorm";
import {Post} from "./Post";
@Entity("PostAuthor")
export class PostAuthor {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(type => Post, post => post.author)
post: Post;
}

View File

@ -0,0 +1,12 @@
import {PrimaryGeneratedColumn, Column, Entity, OneToOne,JoinColumn} from "typeorm";
@Entity("PostCategory")
export class PostCategory {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
}

View File

@ -0,0 +1,26 @@
import {PrimaryGeneratedColumn, Column, Entity, OneToOne,JoinColumn} from "typeorm";
import {Post} from "./Post";
@Entity("PostDetails")
export class PostDetails {
@PrimaryGeneratedColumn()
id: number;
@Column()
authorName: string;
@Column()
comment: string;
@Column()
metadata: string;
@OneToOne(type => Post, post => post.details, {
cascadeInsert: true,
cascadeUpdate: true,
cascadeRemove: true
})
post: Post;
}

View File

@ -0,0 +1,16 @@
import {PrimaryGeneratedColumn, Column, Entity, OneToOne,JoinColumn} from "typeorm";
import {Post} from "./Post";
@Entity("PostImage")
export class PostImage {
@PrimaryGeneratedColumn()
id: number;
@Column()
url: string;
@OneToOne(type => Post, post => post.image)
post: Post;
}

View File

@ -0,0 +1,18 @@
import {PrimaryGeneratedColumn, Column, Entity, OneToOne,JoinColumn} from "typeorm";
import {Post} from "./Post";
@Entity("PostInformation")
export class PostInformation {
@PrimaryGeneratedColumn()
id: number;
@Column()
text: string;
@OneToOne(type => Post, post => post.information, {
cascadeUpdate: true,
})
post: Post;
}

View File

@ -0,0 +1,16 @@
import {PrimaryGeneratedColumn, Column, Entity, OneToOne,JoinColumn} from "typeorm";
import {Post} from "./Post";
@Entity("PostMetadata")
export class PostMetadata {
@PrimaryGeneratedColumn()
id: number;
@Column()
description: string;
@OneToOne(type => Post, post => post.metadata)
post: Post;
}

View File

@ -80,7 +80,7 @@ describe("integration tests", async function () {
let entftj = new EntityFileToJson();
let jsonEntityOrg= entftj.convert(fs.readFileSync(path.resolve(filesOrgPath, file)))
let jsonEntityGen= entftj.convert(fs.readFileSync(path.resolve(filesGenPath, file)))
expect(jsonEntityGen).to.containSubset(jsonEntityOrg)
expect(jsonEntityGen,`Error in file ${file}`).to.containSubset(jsonEntityOrg)
}
});

View File

@ -4,7 +4,12 @@ export class EntityFileToJson {
if (decoratorParameters.length > 0) {
if (decoratorParameters.search(',') > 0) {
col.columnType = decoratorParameters.substring(0, decoratorParameters.indexOf(',')).trim()
col.columnTypes = decoratorParameters.substring(0, decoratorParameters.indexOf(',')).trim().split('|').map(function (x) {
if (!x.endsWith('[]')) {
x = x + '[]'// can't distinguish OneTwoMany from OneToOne without indexes
}
return x;
});
let badJSON = decoratorParameters.substring(decoratorParameters.indexOf(',') + 1).trim()
if (badJSON.lastIndexOf(',') == badJSON.length - 3) {
badJSON = badJSON.slice(0, badJSON.length - 3) + badJSON[badJSON.length - 2] + badJSON[badJSON.length - 1]
@ -12,7 +17,12 @@ export class EntityFileToJson {
col.columnOptions = JSON.parse(badJSON.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": '))
} else {
if (decoratorParameters[0] == '"' && decoratorParameters.endsWith('"')) {
col.columnType = decoratorParameters
col.columnTypes = decoratorParameters.split('|').map(function (x) {
if (!x.endsWith('[]')) {
x = x + '[]'// can't distinguish OneTwoMany from OneToOne without indexes
}
return x;
});
} else {
let badJSON = decoratorParameters.substring(decoratorParameters.indexOf(',') + 1).trim()
if (badJSON.lastIndexOf(',') == badJSON.length - 3) {
@ -34,6 +44,9 @@ export class EntityFileToJson {
let lines = entityFile.toString().replace('\r', '').split('\n');
for (let line of lines) {
let trimmedLine = line.trim();
if (trimmedLine.startsWith('//')) {
continue; //commented line
}
if (isMultilineStatement)
trimmedLine = priorPartOfMultilineStatement + ' ' + trimmedLine
if (trimmedLine.length == 0)
@ -97,8 +110,9 @@ export class EntityFileToJson {
continue;
} else {
isMultilineStatement = false;
retVal.columns.push(new EntityColumn())
//TODO:Options,relation options if declared
let column = new EntityColumn()
retVal.columns.push(column)
column.relationType = "OneToMany"//"ManyToOne" - can't distinguish OneTwoMany from OneToOne without indexes
continue;
}
} else if (trimmedLine.startsWith('@OneToMany')) {
@ -108,13 +122,47 @@ export class EntityFileToJson {
continue;
} else {
isMultilineStatement = false;
retVal.columns.push(new EntityColumn())
//TODO:Options, relation options if declared
let column = new EntityColumn()
retVal.columns.push(column)
column.relationType = "OneToMany"
continue;
}
} else if (trimmedLine.startsWith('@OneToOne')) {
if (this.isPartOfMultilineStatement(trimmedLine)) {
isMultilineStatement = true;
priorPartOfMultilineStatement = trimmedLine;
continue;
} else {
isMultilineStatement = false;
let column = new EntityColumn()
retVal.columns.push(column)
column.relationType = "OneToMany"//"OneToOne" - can't distinguish OneTwoMany from OneToOne without indexes
continue;
}
} else if (trimmedLine.startsWith('@JoinColumn')) {
if (this.isPartOfMultilineStatement(trimmedLine)) {
isMultilineStatement = true;
priorPartOfMultilineStatement = trimmedLine;
continue;
} else {
isMultilineStatement = false;
retVal.columns[retVal.columns.length - 1].isOwnerOfRelation = true;
continue;
}
} else if (trimmedLine.split(':').length - 1 > 0) {
retVal.columns[retVal.columns.length - 1].columnName = trimmedLine.split(':')[0].trim();
retVal.columns[retVal.columns.length - 1].columnType = trimmedLine.split(':')[1].split(';')[0].trim();
//TODO:Should check if null only column is nullable?
retVal.columns[retVal.columns.length - 1].columnTypes = trimmedLine.split(':')[1].split(';')[0].trim().split('|').map(function (x) {
if (!x.endsWith('[]')) {
x = x + '[]'// can't distinguish OneTwoMany from OneToOne without indexes
}
return x;
});
if (!retVal.columns[retVal.columns.length - 1].columnTypes.some(function (this, val, ind, arr) {
return val == "null" ? true : false;
})) retVal.columns[retVal.columns.length - 1].columnTypes.push('null[]')
continue
} else if (trimmedLine = '}') {
isInClassBody = false;
@ -122,7 +170,8 @@ export class EntityFileToJson {
}
}
console.log(`[EntityFileToJson:convert] Line not recognized: ${trimmedLine}`)
console.log(`[EntityFileToJson:convert] Line not recognized in entity ${retVal.entityName}:`)
console.log(`${trimmedLine}`)
}
return retVal;
}
@ -141,6 +190,8 @@ class EntityJson {
}
class EntityColumn {
columnName: string
columnType: string
columnTypes: string[]
columnOptions: any = {}
relationType: "OneToOne" | "OneToMany" | "ManyToOne" | "None" = "None"
isOwnerOfRelation: boolean = false;
}