From 5d3dac6be7ccc69d47135025613d5e847aa50f7b Mon Sep 17 00:00:00 2001 From: Kononnable Date: Sun, 17 Dec 2017 11:38:25 +0100 Subject: [PATCH] adding oracle support - part 1 --- docker-compose.yml | 28 ++- package-lock.json | 14 ++ package.json | 2 + src/drivers/OracleDriver.ts | 471 ++++++++++++++++++++++++++++++++++++ src/index.ts | 9 +- 5 files changed, 513 insertions(+), 11 deletions(-) create mode 100644 src/drivers/OracleDriver.ts diff --git a/docker-compose.yml b/docker-compose.yml index a2b2149..5068342 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,13 +28,23 @@ services: environment: POSTGRES_PASSWORD: "!Passw0rd" - # # mssql - # mssql: - # image: "microsoft/mssql-server-linux:2017-GA" - # container_name: "typeorm-mg-mssql" - # ports: - # - "1433:1433" - # environment: - # ACCEPT_EULA: "Y" - # SA_PASSWORD: "!Passw0rd" + # mssql + mssql: + image: "microsoft/mssql-server-linux:2017-GA" + container_name: "typeorm-mg-mssql" + ports: + - "1433:1433" + environment: + ACCEPT_EULA: "Y" + SA_PASSWORD: "!Passw0rd" + + # oracle + oracle: + image: "store/oracle/database-enterprise:12.2.0.1" + container_name: "typeorm-mg-oracle" + ports: + - "1521:1521" + # environment: + # DB_SID: "ORCLCDB" + # SYS_PASSWORD: "Oradoc_db1" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f324466..0bf8d84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,6 +73,15 @@ "integrity": "sha512-54Dm6NwYeiSQmRB1BLXKr5GELi0wFapR1npi8bnZhEcu84d/yQKqnwwXQ56hZ0RUbTG6L5nqDZaN3dgByQXQRQ==", "dev": true }, + "@types/oracledb": { + "version": "1.11.34", + "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-1.11.34.tgz", + "integrity": "sha512-7cgZaKEfYcPPTScxxCoYoLxmmhM/PBobGBfxE3RGzRJl8YKhkyGKyExFu8fTOpF2cPgdfh83NGKBVX7prWzb+Q==", + "dev": true, + "requires": { + "@types/node": "8.0.53" + } + }, "@types/pg": { "version": "6.1.45", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-6.1.45.tgz", @@ -2443,6 +2452,11 @@ } } }, + "oracledb": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/oracledb/-/oracledb-2.0.15.tgz", + "integrity": "sha1-9+IBtp+ngjUIFV6fNKumXVdCbx0=" + }, "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", diff --git a/package.json b/package.json index f59778c..2dffad6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "handlebars": "^4.0.11", "mssql": "^4.0.4", "mysql": "^2.15.0", + "oracledb": "^2.0.15", "pg": "^6.4.2", "reflect-metadata": "^0.1.10", "typeorm": "^0.1.3", @@ -44,6 +45,7 @@ "@types/mssql": "^4.0.4", "@types/mysql": "0.0.34", "@types/node": "^8.0.53", + "@types/oracledb": "^1.11.34", "@types/pg": "^6.1.45", "@types/sinon": "^2.3.7", "chai": "^4.1.2", diff --git a/src/drivers/OracleDriver.ts b/src/drivers/OracleDriver.ts new file mode 100644 index 0000000..6c4606b --- /dev/null +++ b/src/drivers/OracleDriver.ts @@ -0,0 +1,471 @@ +import { AbstractDriver } from './AbstractDriver' +import * as MSSQL from 'mssql' +import { ColumnInfo } from './../models/ColumnInfo' +import { EntityInfo } from './../models/EntityInfo' +import { RelationInfo } from './../models/RelationInfo' +import { DatabaseModel } from './../models/DatabaseModel' +/** + * OracleDriver + */ +export class OracleDriver extends AbstractDriver { + FindPrimaryColumnsFromIndexes(dbModel: DatabaseModel) { + dbModel.entities.forEach(entity => { + let primaryIndex = entity.Indexes.find(v => v.isPrimaryKey); + if (!primaryIndex) { + console.error(`Table ${entity.EntityName} has no PK.`) + return; + } + entity.Columns.forEach(col => { + if (primaryIndex!.columns.some(cIndex => cIndex.name == col.name)) col.isPrimary = true + }) + }); + } + + async GetAllTables(schema: string): Promise { + let request = new MSSQL.Request(this.Connection) + let response: { TABLE_SCHEMA: string, TABLE_NAME: string }[] + = (await request.query(`SELECT TABLE_SCHEMA,TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' and TABLE_SCHEMA='${schema}'`)).recordset; + let ret: EntityInfo[] = []; + response.forEach((val) => { + let ent: EntityInfo = new EntityInfo(); + ent.EntityName = val.TABLE_NAME; + ent.Columns = []; + ent.Indexes = []; + ret.push(ent); + }) + return ret; + } + async GetCoulmnsFromEntity(entities: EntityInfo[], schema: string): Promise { + 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 + }[] + = (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 where TABLE_SCHEMA='${schema}'`)).recordset; + entities.forEach((ent) => { + response.filter((filterVal) => { + return filterVal.TABLE_NAME == ent.EntityName; + }).forEach((resp) => { + let colInfo: ColumnInfo = new ColumnInfo(); + colInfo.name = resp.COLUMN_NAME; + colInfo.is_nullable = resp.IS_NULLABLE == 'YES' ? true : false; + colInfo.is_generated = resp.IsIdentity == 1 ? true : false; + colInfo.default = resp.COLUMN_DEFAULT; + switch (resp.DATA_TYPE) { + case "int": + colInfo.ts_type = "number" + colInfo.sql_type = "int" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "tinyint": + colInfo.ts_type = "number" + colInfo.sql_type = "smallint" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "smallint": + colInfo.ts_type = "number" + colInfo.sql_type = "smallint" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "bit": + colInfo.ts_type = "boolean" + colInfo.sql_type = "boolean" + break; + case "float": + colInfo.ts_type = "number" + colInfo.sql_type = "float" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + colInfo.numericPrecision = resp.NUMERIC_PRECISION + break; + case "bigint": + colInfo.ts_type = "string" + colInfo.sql_type = "bigint" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "date": + colInfo.ts_type = "Date" + colInfo.sql_type = "date" + break; + case "time": + colInfo.ts_type = "Date" + colInfo.sql_type = "time" + break; + case "datetime": + colInfo.ts_type = "Date"; + colInfo.sql_type = "datetime" + break; + case "char": + colInfo.ts_type = "string" + colInfo.sql_type = "char" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "nchar": + colInfo.ts_type = "string" + colInfo.sql_type = "nchar" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "text": + colInfo.ts_type = "string" + colInfo.sql_type = "text" + break; + case "ntext": + colInfo.ts_type = "string" + colInfo.sql_type = "ntext" + break; + case "uniqueidentifier": + colInfo.ts_type = "string" + colInfo.sql_type = "uniqueidentifier" + break; + case "varchar": + colInfo.ts_type = "string" + colInfo.sql_type = "varchar" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "binary": + colInfo.ts_type = "Buffer" + colInfo.sql_type = "binary" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "varbinary": + colInfo.ts_type = "Buffer" + colInfo.sql_type = "varbinary" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "image": + colInfo.ts_type = "Buffer" + colInfo.sql_type = "image" + break; + case "nvarchar": + colInfo.ts_type = "string" + colInfo.sql_type = "nvarchar" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "money": + colInfo.ts_type = "number" + colInfo.sql_type = "decimal" + break; + case "smallmoney": + colInfo.ts_type = "number" + colInfo.sql_type = "smallmoney" + break; + case "real": + colInfo.ts_type = "number" + colInfo.sql_type = "double" + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "decimal": + colInfo.ts_type = "number" + colInfo.sql_type = "decimal" + colInfo.numericPrecision = resp.NUMERIC_PRECISION + colInfo.numericScale = resp.NUMERIC_SCALE + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "numeric": + colInfo.ts_type = "number" + colInfo.sql_type = "numeric" + colInfo.numericPrecision = resp.NUMERIC_PRECISION + colInfo.numericScale = resp.NUMERIC_SCALE + colInfo.char_max_lenght = resp.CHARACTER_MAXIMUM_LENGTH > 0 ? resp.CHARACTER_MAXIMUM_LENGTH : null; + break; + case "datetime2": + colInfo.ts_type = "Date" + colInfo.sql_type = "datetime2" + colInfo.numericPrecision = resp.NUMERIC_PRECISION + break; + case "time": + colInfo.ts_type = "Date" + colInfo.sql_type = "time" + colInfo.numericPrecision = resp.NUMERIC_PRECISION + break; + case "datetimeoffset": + colInfo.ts_type = "Date" + colInfo.sql_type = "datetimeoffset" + colInfo.numericPrecision = resp.NUMERIC_PRECISION + break; + case "smalldatetime": + colInfo.ts_type = "Date" + colInfo.sql_type = "smalldatetime" + break; + case "xml": + colInfo.ts_type = "string" + colInfo.sql_type = "text" + break; + default: + console.error("Unknown column type:" + resp.DATA_TYPE); + break; + } + + if (colInfo.sql_type) ent.Columns.push(colInfo); + }) + }) + return entities; + } + async GetIndexesFromEntity(entities: EntityInfo[], schema: string): Promise { + let request = new MSSQL.Request(this.Connection) + let response: { + TableName: string, IndexName: string, ColumnName: string, is_unique: number, + is_primary_key: number//, is_descending_key: number//, is_included_column: number + }[] + = (await request.query(`SELECT + TableName = t.name, + IndexName = ind.name, + ColumnName = col.name, + ind.is_unique, + ind.is_primary_key + -- ,ic.is_descending_key, + -- ic.is_included_column +FROM + sys.indexes ind +INNER JOIN + sys.index_columns ic ON ind.object_id = ic.object_id and ind.index_id = ic.index_id +INNER JOIN + sys.columns col ON ic.object_id = col.object_id and ic.column_id = col.column_id +INNER JOIN + sys.tables t ON ind.object_id = t.object_id +INNER JOIN + sys.schemas s on s.schema_id=t.schema_id +WHERE + t.is_ms_shipped = 0 and s.name='${schema}' +ORDER BY + t.name, ind.name, ind.index_id, ic.key_ordinal;`)).recordset; + entities.forEach((ent) => { + response.filter((filterVal) => { + return filterVal.TableName == ent.EntityName; + }).forEach((resp) => { + let indexInfo: IndexInfo = {}; + let indexColumnInfo: IndexColumnInfo = {}; + if (ent.Indexes.filter((filterVal) => { + return filterVal.name == resp.IndexName + }).length > 0) { + indexInfo = ent.Indexes.filter((filterVal) => { + return filterVal.name == resp.IndexName + })[0]; + } else { + indexInfo.columns = []; + indexInfo.name = resp.IndexName; + indexInfo.isUnique = resp.is_unique == 1 ? true : false; + indexInfo.isPrimaryKey = resp.is_primary_key == 1 ? true : false; + ent.Indexes.push(indexInfo); + } + indexColumnInfo.name = resp.ColumnName; + // indexColumnInfo.isIncludedColumn = resp.is_included_column == 1 ? true : false; + // indexColumnInfo.isDescending = resp.is_descending_key == 1 ? true : false; + indexInfo.columns.push(indexColumnInfo); + + }) + }) + + return entities; + } + async GetRelations(entities: EntityInfo[], schema: string): Promise { + let request = new MSSQL.Request(this.Connection) + let response: { + TableWithForeignKey: string, FK_PartNo: number, ForeignKeyColumn: string, + TableReferenced: string, ForeignKeyColumnReferenced: string, + onDelete: "RESTRICT" | "CASCADE" | "SET NULL", + onUpdate: "RESTRICT" | "CASCADE" | "SET NULL", object_id: number + }[] + = (await request.query(`select + parentTable.name as TableWithForeignKey, + fkc.constraint_column_id as FK_PartNo, + parentColumn.name as ForeignKeyColumn, + 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 +inner join + sys.foreign_key_columns as fkc on fkc.constraint_object_id=fk.object_id +inner join + sys.tables as parentTable on fkc.parent_object_id = parentTable.object_id +inner join + sys.columns as parentColumn on fkc.parent_object_id = parentColumn.object_id and fkc.parent_column_id = parentColumn.column_id +inner join + sys.tables as referencedTable on fkc.referenced_object_id = referencedTable.object_id +inner join + sys.columns as referencedColumn on fkc.referenced_object_id = referencedColumn.object_id and fkc.referenced_column_id = referencedColumn.column_id +inner join + sys.schemas as parentSchema on parentSchema.schema_id=parentTable.schema_id +where + fk.is_disabled=0 and fk.is_ms_shipped=0 and parentSchema.name='${schema}' +order by + TableWithForeignKey, FK_PartNo`)).recordset; + let relationsTemp: RelationTempInfo[] = []; + response.forEach((resp) => { + let rels = relationsTemp.find((val) => { + return val.object_id == resp.object_id; + }) + if (rels == undefined) { + rels = {}; + 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; + relationsTemp.push(rels); + } + rels.ownerColumnsNames.push(resp.ForeignKeyColumn); + rels.referencedColumnsNames.push(resp.ForeignKeyColumnReferenced); + }) + relationsTemp.forEach((relationTmp) => { + let ownerEntity = entities.find((entitity) => { + return entitity.EntityName == relationTmp.ownerTable; + }) + 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; + }) + 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]; + }) + 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]; + }) + 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 index = ownerEntity.Indexes.find( + (index) => { + return index.isUnique && index.columns.some(col => { + return col.name == ownerColumn!.name + }) + } + ) + if (!index) { + isOneToMany = true; + } else { + isOneToMany = false; + } + let ownerRelation = new RelationInfo() + let columnName = ownerEntity.EntityName.toLowerCase() + (isOneToMany ? 's' : '') + if (referencedEntity.Columns.filter((filterVal) => { + return filterVal.name == columnName; + }).length>0){ + for (let i=2;i<=ownerEntity.Columns.length;i++){ + columnName = ownerEntity.EntityName.toLowerCase() + (isOneToMany ? 's' : '')+i.toString(); + if (referencedEntity.Columns.filter((filterVal) => { + return filterVal.name == columnName; + }).length==0) break; + } + } + ownerRelation.actionOnDelete = relationTmp.actionOnDelete + ownerRelation.actionOnUpdate = relationTmp.actionOnUpdate + ownerRelation.isOwner = true + ownerRelation.relatedColumn = relatedColumn.name.toLowerCase() + ownerRelation.relatedTable = relationTmp.referencedTable + ownerRelation.ownerTable = relationTmp.ownerTable + ownerRelation.ownerColumn = columnName + ownerRelation.relationType = isOneToMany ? "ManyToOne" : "OneToOne" + ownerColumn.relations.push(ownerRelation) + if (isOneToMany) { + let col = new ColumnInfo() + col.name = columnName + 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.ownerTable = relationTmp.referencedTable + referencedRelation.ownerColumn = relatedColumn.name.toLowerCase() + referencedRelation.relationType = "OneToMany" + referencedEntity.Columns.push(col) + } else { + let col = new ColumnInfo() + col.name = columnName + 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.ownerTable = relationTmp.referencedTable + referencedRelation.ownerColumn = relatedColumn.name.toLowerCase() + referencedRelation.relationType = "OneToOne" + + referencedEntity.Columns.push(col) + } + }) + return entities; + } + async DisconnectFromServer() { + if (this.Connection) + await this.Connection.close(); + } + + private Connection: MSSQL.ConnectionPool; + async ConnectToServer(database: string, server: string, port: number, user: string, password: string, ssl: boolean) { + let config: MSSQL.config = { + database: database, + server: server, + port: port, + user: user, + password: password, + options: { + encrypt: ssl, // Use this if you're on Windows Azure + appName: 'typeorm-model-generator' + } + } + + + let promise = new Promise( + (resolve, reject) => { + this.Connection = new MSSQL.ConnectionPool(config, (err) => { + if (!err) { + //Connection successfull + resolve(true) + } + else { + console.error('Error connecting to MSSQL Server.') + console.error(err.message) + process.abort() + reject(err) + } + }); + } + ) + + await promise; + } + async CreateDB(dbName: string) { + let request = new MSSQL.Request(this.Connection); + let resp = await request.query(`CREATE DATABASE ${dbName}; `) + } + async UseDB(dbName: string) { + let request = new MSSQL.Request(this.Connection); + let resp = await request.query(`USE ${dbName}; `) + } + async DropDB(dbName: string) { + let request = new MSSQL.Request(this.Connection); + let resp = await request.query(`DROP DATABASE ${dbName}; `) + } + async CheckIfDBExists(dbName: string): Promise { + let request = new MSSQL.Request(this.Connection); + let resp = await request.query(`SELECT name FROM master.sys.databases WHERE name = N'${dbName}' `) + return resp.recordset.length > 0; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index e0f9090..5e3638b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { MssqlDriver } from './drivers/MssqlDriver'; import { PostgresDriver } from "./drivers/PostgresDriver"; import { MysqlDriver } from "./drivers/MysqlDriver"; import { MariaDbDriver } from "./drivers/MariaDbDriver"; +import { OracleDriver } from "./drivers/OracleDriver"; import { Engine } from './Engine' import * as Yargs from 'yargs' import path = require('path') @@ -38,7 +39,7 @@ var argv = Yargs .option('e', { alias: 'engine', describe: 'Database engine.', - choices: ['mssql', 'postgres', 'mysql', 'mariadb'], + choices: ['mssql', 'postgres', 'mysql', 'mariadb','oracle'], default: 'mssql' }) .option('o', { @@ -75,10 +76,14 @@ switch (argv.e) { driver = new MysqlDriver(); standardPort = 3306; break; - case 'mariadb': + case 'mariadb': driver = new MysqlDriver(); standardPort = 3306; break; + case 'oracle': + driver = new OracleDriver(); + standardPort = 1521; + break; default: console.error('Database engine not recognized.') process.abort();