diff --git a/src/Engine.ts b/src/Engine.ts index ee6d21d..ff9a358 100644 --- a/src/Engine.ts +++ b/src/Engine.ts @@ -160,6 +160,32 @@ export class Engine { Handlebars.registerHelper("toLowerCase", str => { return str.toLowerCase(); }); + Handlebars.registerHelper({ + eq: function(v1, v2) { + return v1 === v2; + }, + ne: function(v1, v2) { + return v1 !== v2; + }, + lt: function(v1, v2) { + return v1 < v2; + }, + gt: function(v1, v2) { + return v1 > v2; + }, + lte: function(v1, v2) { + return v1 <= v2; + }, + gte: function(v1, v2) { + return v1 >= v2; + }, + and: function(v1, v2) { + return v1 && v2; + }, + or: function(v1, v2) { + return v1 || v2; + } + }); } //TODO:Move to mustache template file diff --git a/src/drivers/AbstractDriver.ts b/src/drivers/AbstractDriver.ts index b69f656..29d6873 100644 --- a/src/drivers/AbstractDriver.ts +++ b/src/drivers/AbstractDriver.ts @@ -1,10 +1,71 @@ import { EntityInfo } from "./../models/EntityInfo"; import { DatabaseModel } from "./../models/DatabaseModel"; import * as TomgUtils from "./../Utils"; +import { RelationInfo } from "../models/RelationInfo"; +import { ColumnInfo } from "../models/ColumnInfo"; +import { ManyToMany } from "typeorm"; /** * AbstractDriver */ export abstract class AbstractDriver { + FindManyToManyRelations(dbModel: DatabaseModel) { + let manyToManyEntities = dbModel.entities.filter(entity => { + return ( + entity.Columns.filter(column => { + return ( + column.relations.length == 1 && + !column.relations[0].isOneToMany && + column.relations[0].isOwner + ); + }).length == entity.Columns.length + ); + }); + manyToManyEntities.map(entity => { + let relations: RelationInfo[] = []; + relations = entity.Columns.reduce((prev: RelationInfo[], curr) => { + return prev.concat(curr.relations); + }, relations); + //TODO: Composed keys + if (relations.length == 2) { + let relatedTable1 = dbModel.entities.filter( + v => v.EntityName == relations[0].relatedTable + )[0]; + relatedTable1.Columns = relatedTable1.Columns.filter( + v => v.name != entity.EntityName + ); + let relatedTable2 = dbModel.entities.filter( + v => v.EntityName == relations[1].relatedTable + )[0]; + relatedTable2.Columns = relatedTable2.Columns.filter( + v => v.name != entity.EntityName + ); + dbModel.entities = dbModel.entities.filter(ent => { + return ent.EntityName != entity.EntityName; + }); + + let column1 = new ColumnInfo(); + column1.name = relations[1].relatedTable; + let col1Rel = new RelationInfo(); + col1Rel.relatedTable = relations[1].relatedTable; + col1Rel.relatedColumn = relations[1].relatedTable; + col1Rel.relationType = "ManyToMany"; + col1Rel.isOwner = true; + col1Rel.ownerColumn = relations[0].relatedTable; + column1.relations.push(col1Rel); + relatedTable1.Columns.push(column1); + + let column2 = new ColumnInfo(); + column2.name = relations[0].relatedTable; + let col2Rel = new RelationInfo(); + col2Rel.relatedTable = relations[0].relatedTable; + col2Rel.relatedColumn = relations[1].relatedTable; + col2Rel.relationType = "ManyToMany"; + col2Rel.isOwner = false; + column2.relations.push(col2Rel); + relatedTable2.Columns.push(column2); + } + }); + } async GetDataFromServer( database: string, server: string, @@ -25,6 +86,7 @@ export abstract class AbstractDriver { sqlEscapedSchema ); await this.DisconnectFromServer(); + this.FindManyToManyRelations(dbModel); this.FindPrimaryColumnsFromIndexes(dbModel); return dbModel; } diff --git a/src/entity.mst b/src/entity.mst index f0698dc..e542077 100644 --- a/src/entity.mst +++ b/src/entity.mst @@ -1,4 +1,4 @@ -import {Index,Entity, PrimaryColumn, Column, OneToOne, OneToMany, ManyToOne, JoinColumn} from "typeorm"; +import {Index,Entity, PrimaryColumn, Column, OneToOne, OneToMany, ManyToOne, ManyToMany, JoinColumn, JoinTable} from "typeorm"; {{relationImports}}{{#each UniqueImports}}import {{curly true}}{{toEntityName this}}{{curly false}} from "./{{toFileName this}}"; {{/each}} @@ -23,8 +23,8 @@ import {Index,Entity, PrimaryColumn, Column, OneToOne, OneToMany, ManyToOne, Joi {{toPropertyName name}}:{{ts_type}}; {{/relations}}{{#relations}} @{{relationType}}(type=>{{toEntityName relatedTable}}, {{toPropertyName ../name}}=>{{toPropertyName ../name}}.{{#if isOwner}}{{toPropertyName ownerColumn}}{{else}}{{toPropertyName relatedColumn}}{{/if}}){{#isOwner}} - @JoinColumn({ name:'{{ ../name}}'}){{/isOwner}} - {{#if isOneToMany}}{{toPropertyName ../name}}:{{toEntityName relatedTable}}[]; + {{#if isManyToMany}}@JoinTable(){{else}}@JoinColumn({ name:'{{ ../name}}'}){{/if}}{{/isOwner}} + {{#if (or isOneToMany isManyToMany)}}{{toPropertyName ../name}}:{{toEntityName relatedTable}}[]; {{else}}{{toPropertyName ../name}}:{{toEntityName relatedTable}}; {{/if}}{{/relations}} {{/Columns}} diff --git a/src/models/RelationInfo.ts b/src/models/RelationInfo.ts index 88dd40a..8d895a2 100644 --- a/src/models/RelationInfo.ts +++ b/src/models/RelationInfo.ts @@ -2,7 +2,7 @@ export class RelationInfo { [x: string]: any; isOwner: boolean; - relationType: "OneToOne" | "OneToMany" | "ManyToOne"; + relationType: "OneToOne" | "OneToMany" | "ManyToOne" | "ManyToMany"; relatedTable: string; relatedColumn: string; ownerTable: string; @@ -13,4 +13,7 @@ export class RelationInfo { get isOneToMany(): boolean { return this.relationType == "OneToMany"; } + get isManyToMany(): boolean { + return this.relationType == "ManyToMany"; + } } diff --git a/test/integration/examples/sample4-many-to-many/entity/Post.ts b/test/integration/examples/sample4-many-to-many/entity/Post.ts new file mode 100644 index 0000000..0495570 --- /dev/null +++ b/test/integration/examples/sample4-many-to-many/entity/Post.ts @@ -0,0 +1,62 @@ +import { PrimaryGeneratedColumn, Column, Entity, OneToOne, OneToMany, ManyToOne, ManyToMany, JoinColumn, JoinTable } 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) + @ManyToMany(type => PostCategory, { + cascade: true + }) + @JoinTable() + PostCategory: 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 + @ManyToMany(type => PostDetails, details => details.Post, { + cascade: true + }) + @JoinTable() + PostDetails: 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 + @ManyToMany(type => PostImage, image => image.Post, { + cascade: true + }) + @JoinTable() + PostImage: 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 + @ManyToMany(type => PostMetadata, metadata => metadata.Post) + @JoinTable() + PostMetadata: PostMetadata[]; + + // post has relation with details. full cascades here + @ManyToMany(type => PostInformation, information => information.Post, { + cascade: true + }) + @JoinTable() + PostInformation: PostInformation[]; + + // post has relation with details. not cascades here. means cannot be persisted, updated or removed + @ManyToMany(type => PostAuthor, author => author.Post) + @JoinTable() + PostAuthor: PostAuthor[]; + +} diff --git a/test/integration/examples/sample4-many-to-many/entity/PostAuthor.ts b/test/integration/examples/sample4-many-to-many/entity/PostAuthor.ts new file mode 100644 index 0000000..abb6d03 --- /dev/null +++ b/test/integration/examples/sample4-many-to-many/entity/PostAuthor.ts @@ -0,0 +1,16 @@ +import { PrimaryGeneratedColumn, Column, Entity, OneToOne, OneToMany, ManyToOne, ManyToMany, JoinColumn, JoinTable } from "typeorm"; +import {Post} from "./Post"; + +@Entity("PostAuthor") +export class PostAuthor { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + + @ManyToMany(type => Post, post => post.PostAuthor) + Post: Post[]; + +} diff --git a/test/integration/examples/sample4-many-to-many/entity/PostCategory.ts b/test/integration/examples/sample4-many-to-many/entity/PostCategory.ts new file mode 100644 index 0000000..a0d3306 --- /dev/null +++ b/test/integration/examples/sample4-many-to-many/entity/PostCategory.ts @@ -0,0 +1,12 @@ +import { PrimaryGeneratedColumn, Column, Entity, OneToOne, OneToMany, ManyToOne, ManyToMany, JoinColumn, JoinTable } from "typeorm"; + +@Entity("PostCategory") +export class PostCategory { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + name: string; + +} diff --git a/test/integration/examples/sample4-many-to-many/entity/PostDetails.ts b/test/integration/examples/sample4-many-to-many/entity/PostDetails.ts new file mode 100644 index 0000000..2b570d8 --- /dev/null +++ b/test/integration/examples/sample4-many-to-many/entity/PostDetails.ts @@ -0,0 +1,22 @@ +import { PrimaryGeneratedColumn, Column, Entity, OneToOne, OneToMany, ManyToOne, ManyToMany, JoinColumn, JoinTable } from "typeorm"; +import {Post} from "./Post"; + +@Entity("PostDetails") +export class PostDetails { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + authorName: string; + + @Column() + comment: string; + + @Column() + metadata: string; + + @ManyToMany(type => Post, post => post.PostDetails) + Post: Post[]; + +} diff --git a/test/integration/examples/sample4-many-to-many/entity/PostImage.ts b/test/integration/examples/sample4-many-to-many/entity/PostImage.ts new file mode 100644 index 0000000..d758949 --- /dev/null +++ b/test/integration/examples/sample4-many-to-many/entity/PostImage.ts @@ -0,0 +1,16 @@ +import { PrimaryGeneratedColumn, Column, Entity, OneToOne, OneToMany, ManyToOne, ManyToMany, JoinColumn, JoinTable } from "typeorm"; +import {Post} from "./Post"; + +@Entity("PostImage") +export class PostImage { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + url: string; + + @ManyToMany(type => Post, post => post.PostImage) + Post: Post[]; + +} diff --git a/test/integration/examples/sample4-many-to-many/entity/PostInformation.ts b/test/integration/examples/sample4-many-to-many/entity/PostInformation.ts new file mode 100644 index 0000000..600fea7 --- /dev/null +++ b/test/integration/examples/sample4-many-to-many/entity/PostInformation.ts @@ -0,0 +1,16 @@ +import { PrimaryGeneratedColumn, Column, Entity, OneToOne, OneToMany, ManyToOne, ManyToMany, JoinColumn, JoinTable } from "typeorm"; +import {Post} from "./Post"; + +@Entity("PostInformation") +export class PostInformation { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + text: string; + + @ManyToMany(type => Post, post => post.PostInformation) + Post: Post[]; + +} diff --git a/test/integration/examples/sample4-many-to-many/entity/PostMetadata.ts b/test/integration/examples/sample4-many-to-many/entity/PostMetadata.ts new file mode 100644 index 0000000..283f2d4 --- /dev/null +++ b/test/integration/examples/sample4-many-to-many/entity/PostMetadata.ts @@ -0,0 +1,16 @@ +import { PrimaryGeneratedColumn, Column, Entity, OneToOne, OneToMany, ManyToOne, ManyToMany, JoinColumn, JoinTable } from "typeorm"; +import {Post} from "./Post"; + +@Entity("PostMetadata") +export class PostMetadata { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + description: string; + + @ManyToMany(type => Post, post => post.PostMetadata) + Post: Post[]; + +} diff --git a/test/utils/EntityFileToJson.ts b/test/utils/EntityFileToJson.ts index 1ec2c3a..90bf4bd 100644 --- a/test/utils/EntityFileToJson.ts +++ b/test/utils/EntityFileToJson.ts @@ -5,15 +5,15 @@ export class EntityFileToJson { let decoratorParameters = trimmedLine.slice(trimmedLine.indexOf('(') + 1, trimmedLine.lastIndexOf(')')) if (decoratorParameters.length > 0) { - if (decoratorParameters[0] == '"' && decoratorParameters.endsWith('"')) { + if (decoratorParameters[0] == '"' && decoratorParameters.endsWith('"')) { - } else { - 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] - } - ent.entityOptions = JSON.parse(badJSON.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": ')) + } else { + 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] } + ent.entityOptions = JSON.parse(badJSON.replace(/(['"])?([a-z0-9A-Z_]+)(['"])?:/g, '"$2": ')) + } } } getColumnOptionsAndType(trimmedLine: string, col: EntityColumn) { @@ -126,8 +126,8 @@ export class EntityFileToJson { priorPartOfMultilineStatement = trimmedLine; continue; } else { - let options = trimmedLine.substring(trimmedLine.lastIndexOf('{'), trimmedLine.lastIndexOf('}')+1).trim().toLowerCase() - this.getEntityOptions(trimmedLine,retVal); + let options = trimmedLine.substring(trimmedLine.lastIndexOf('{'), trimmedLine.lastIndexOf('}') + 1).trim().toLowerCase() + this.getEntityOptions(trimmedLine, retVal); continue; } } else if (trimmedLine.startsWith('export class')) { @@ -225,6 +225,18 @@ export class EntityFileToJson { column.relationType = "OneToMany" continue; } + } else if (trimmedLine.startsWith('@ManyToMany')) { + if (this.isPartOfMultilineStatement(trimmedLine)) { + isMultilineStatement = true; + priorPartOfMultilineStatement = trimmedLine; + continue; + } else { + isMultilineStatement = false; + let column = new EntityColumn() + retVal.columns.push(column) + column.relationType = "ManyToMany" + continue; + } } else if (trimmedLine.startsWith('@OneToOne')) { if (this.isPartOfMultilineStatement(trimmedLine)) { isMultilineStatement = true; @@ -247,6 +259,16 @@ export class EntityFileToJson { retVal.columns[retVal.columns.length - 1].isOwnerOfRelation = true; continue; } + } else if (trimmedLine.startsWith('@JoinTable')) { + if (this.isPartOfMultilineStatement(trimmedLine)) { + isMultilineStatement = true; + priorPartOfMultilineStatement = trimmedLine; + continue; + } else { + isMultilineStatement = false; + retVal.columns[retVal.columns.length - 1].isOwnerOfRelation = true; + continue; + } } else if (trimmedLine.startsWith('@Index')) { if (this.isPartOfMultilineStatement(trimmedLine)) { isMultilineStatement = true; @@ -332,7 +354,7 @@ class EntityColumn { columnName: string columnTypes: string[] = [] columnOptions: any = {} - relationType: "OneToOne" | "OneToMany" | "ManyToOne" | "None" = "None" + relationType: "OneToOne" | "OneToMany" | "ManyToOne" | "ManyToMany" | "None" = "None" isOwnerOfRelation: boolean = false; } class EntityIndex {