|
|
|
@ -3,14 +3,28 @@ import { |
|
|
|
DatasourceFieldTypes, |
|
|
|
QueryTypes, |
|
|
|
SqlQuery, |
|
|
|
QueryJson, |
|
|
|
} from "../definitions/datasource" |
|
|
|
import { getSqlQuery } from "./utils" |
|
|
|
import oracledb, { ExecuteOptions, Result, Connection, ConnectionAttributes } from "oracledb" |
|
|
|
import { |
|
|
|
finaliseExternalTables, |
|
|
|
getSqlQuery, |
|
|
|
buildExternalTableId, |
|
|
|
convertType, |
|
|
|
} from "./utils" |
|
|
|
import oracledb, { |
|
|
|
ExecuteOptions, |
|
|
|
Result, |
|
|
|
Connection, |
|
|
|
ConnectionAttributes, |
|
|
|
BindParameters, |
|
|
|
} from "oracledb" |
|
|
|
import Sql from "./base/sql" |
|
|
|
import { Table } from "../definitions/common" |
|
|
|
import { DatasourcePlus } from "./base/datasourcePlus" |
|
|
|
import { FieldTypes } from "../constants" |
|
|
|
|
|
|
|
module OracleModule { |
|
|
|
|
|
|
|
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT; |
|
|
|
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT |
|
|
|
|
|
|
|
interface OracleConfig { |
|
|
|
host: string |
|
|
|
@ -22,8 +36,10 @@ module OracleModule { |
|
|
|
|
|
|
|
const SCHEMA: Integration = { |
|
|
|
docs: "https://github.com/oracle/node-oracledb", |
|
|
|
plus: true, |
|
|
|
friendlyName: "Oracle", |
|
|
|
description: "Oracle Database is an object-relational database management system developed by Oracle Corporation", |
|
|
|
description: |
|
|
|
"Oracle Database is an object-relational database management system developed by Oracle Corporation", |
|
|
|
datasource: { |
|
|
|
host: { |
|
|
|
type: DatasourceFieldTypes.STRING, |
|
|
|
@ -46,7 +62,7 @@ module OracleModule { |
|
|
|
password: { |
|
|
|
type: DatasourceFieldTypes.PASSWORD, |
|
|
|
required: true, |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
query: { |
|
|
|
create: { |
|
|
|
@ -63,64 +79,361 @@ module OracleModule { |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
class OracleIntegration extends Sql { |
|
|
|
|
|
|
|
const UNSUPPORTED_TYPES = ["BLOB", "CLOB", "NCLOB"] |
|
|
|
|
|
|
|
const TYPE_MAP = { |
|
|
|
long: FieldTypes.LONGFORM, |
|
|
|
number: FieldTypes.NUMBER, |
|
|
|
binary_float: FieldTypes.NUMBER, |
|
|
|
binary_double: FieldTypes.NUMBER, |
|
|
|
timestamp: FieldTypes.DATETIME, |
|
|
|
date: FieldTypes.DATETIME, |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Raw query response |
|
|
|
*/ |
|
|
|
interface ColumnsResponse { |
|
|
|
TABLE_NAME: string |
|
|
|
COLUMN_NAME: string |
|
|
|
DATA_TYPE: string |
|
|
|
DATA_DEFAULT: string | null |
|
|
|
COLUMN_ID: number |
|
|
|
CONSTRAINT_NAME: string | null |
|
|
|
CONSTRAINT_TYPE: string | null |
|
|
|
R_CONSTRAINT_NAME: string | null |
|
|
|
SEARCH_CONDITION: string | null |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* An oracle constraint |
|
|
|
*/ |
|
|
|
interface OracleConstraint { |
|
|
|
name: string |
|
|
|
type: string |
|
|
|
relatedConstraintName: string | null |
|
|
|
searchCondition: string | null |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* An oracle column and it's related constraints |
|
|
|
*/ |
|
|
|
interface OracleColumn { |
|
|
|
name: string |
|
|
|
type: string |
|
|
|
default: string | null |
|
|
|
id: number |
|
|
|
constraints: { [key: string]: OracleConstraint } |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* An oracle table and it's related columns |
|
|
|
*/ |
|
|
|
interface OracleTable { |
|
|
|
name: string |
|
|
|
columns: { [key: string]: OracleColumn } |
|
|
|
} |
|
|
|
|
|
|
|
const OracleContraintTypes = { |
|
|
|
PRIMARY: "P", |
|
|
|
NOT_NULL_OR_CHECK: "C", |
|
|
|
FOREIGN_KEY: "R", |
|
|
|
UNIQUE: "U", |
|
|
|
} |
|
|
|
|
|
|
|
class OracleIntegration extends Sql implements DatasourcePlus { |
|
|
|
private readonly config: OracleConfig |
|
|
|
|
|
|
|
public tables: Record<string, Table> = {} |
|
|
|
public schemaErrors: Record<string, string> = {} |
|
|
|
|
|
|
|
private readonly COLUMNS_SQL = ` |
|
|
|
SELECT |
|
|
|
tabs.table_name, |
|
|
|
cols.column_name, |
|
|
|
cols.data_type, |
|
|
|
cols.data_default, |
|
|
|
cols.column_id, |
|
|
|
cons.constraint_name, |
|
|
|
cons.constraint_type, |
|
|
|
cons.r_constraint_name, |
|
|
|
cons.search_condition |
|
|
|
FROM |
|
|
|
user_tables tabs |
|
|
|
JOIN |
|
|
|
user_tab_columns cols |
|
|
|
ON tabs.table_name = cols.table_name |
|
|
|
LEFT JOIN |
|
|
|
user_cons_columns col_cons |
|
|
|
ON cols.column_name = col_cons.column_name |
|
|
|
AND cols.table_name = col_cons.table_name |
|
|
|
LEFT JOIN |
|
|
|
user_constraints cons |
|
|
|
ON col_cons.constraint_name = cons.constraint_name |
|
|
|
AND cons.table_name = cols.table_name |
|
|
|
WHERE |
|
|
|
(cons.status = 'ENABLED' |
|
|
|
OR cons.status IS NULL) |
|
|
|
` |
|
|
|
constructor(config: OracleConfig) { |
|
|
|
super("oracle") |
|
|
|
super("oracledb") |
|
|
|
this.config = config |
|
|
|
} |
|
|
|
|
|
|
|
private query = async (query: SqlQuery): Promise<Result<any>> => { |
|
|
|
/** |
|
|
|
* Map the flat tabular columns and constraints data into a nested object |
|
|
|
*/ |
|
|
|
private mapColumns(result: Result<ColumnsResponse>): { |
|
|
|
[key: string]: OracleTable |
|
|
|
} { |
|
|
|
const oracleTables: { [key: string]: OracleTable } = {} |
|
|
|
|
|
|
|
if (result.rows) { |
|
|
|
result.rows.forEach(row => { |
|
|
|
const tableName = row.TABLE_NAME |
|
|
|
const columnName = row.COLUMN_NAME |
|
|
|
const dataType = row.DATA_TYPE |
|
|
|
const dataDefault = row.DATA_DEFAULT |
|
|
|
const columnId = row.COLUMN_ID |
|
|
|
const constraintName = row.CONSTRAINT_NAME |
|
|
|
const constraintType = row.CONSTRAINT_TYPE |
|
|
|
const relatedConstraintName = row.R_CONSTRAINT_NAME |
|
|
|
const searchCondition = row.SEARCH_CONDITION |
|
|
|
|
|
|
|
let table = oracleTables[tableName] |
|
|
|
if (!table) { |
|
|
|
table = { |
|
|
|
name: tableName, |
|
|
|
columns: {}, |
|
|
|
} |
|
|
|
oracleTables[tableName] = table |
|
|
|
} |
|
|
|
|
|
|
|
let column = table.columns[columnName] |
|
|
|
if (!column) { |
|
|
|
column = { |
|
|
|
name: columnName, |
|
|
|
type: dataType, |
|
|
|
default: dataDefault, |
|
|
|
id: columnId, |
|
|
|
constraints: {}, |
|
|
|
} |
|
|
|
table.columns[columnName] = column |
|
|
|
} |
|
|
|
|
|
|
|
if (constraintName && constraintType) { |
|
|
|
let constraint = column.constraints[constraintName] |
|
|
|
if (!constraint) { |
|
|
|
constraint = { |
|
|
|
name: constraintName, |
|
|
|
type: constraintType, |
|
|
|
relatedConstraintName: relatedConstraintName, |
|
|
|
searchCondition: searchCondition, |
|
|
|
} |
|
|
|
} |
|
|
|
column.constraints[constraintName] = constraint |
|
|
|
} |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
return oracleTables |
|
|
|
} |
|
|
|
|
|
|
|
private isSupportedColumn(column: OracleColumn) { |
|
|
|
if (UNSUPPORTED_TYPES.includes(column.type)) { |
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
private isAutoColumn(column: OracleColumn) { |
|
|
|
if (column.default && column.default.toLowerCase().includes("nextval")) { |
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* No native boolean in oracle. Best we can do is to check if a manual 1 or 0 number constraint has been set up |
|
|
|
* This matches the default behaviour for generating DDL used in knex. |
|
|
|
*/ |
|
|
|
private isBooleanType(column: OracleColumn): boolean { |
|
|
|
if ( |
|
|
|
column.type.toLowerCase() === "number" && |
|
|
|
Object.values(column.constraints).filter(c => { |
|
|
|
if ( |
|
|
|
c.type === OracleContraintTypes.NOT_NULL_OR_CHECK && |
|
|
|
c.searchCondition |
|
|
|
) { |
|
|
|
const condition = c.searchCondition |
|
|
|
.replace(/\s/g, "") // remove spaces
|
|
|
|
.replace(/[']+/g, "") // remove quotes
|
|
|
|
if ( |
|
|
|
condition.includes("in(0,1)") || |
|
|
|
condition.includes("in(1,0)") |
|
|
|
) { |
|
|
|
return true |
|
|
|
} |
|
|
|
} |
|
|
|
return false |
|
|
|
}).length > 0 |
|
|
|
) { |
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
private internalConvertType(column: OracleColumn): string { |
|
|
|
if (this.isBooleanType(column)) { |
|
|
|
return FieldTypes.BOOLEAN |
|
|
|
} |
|
|
|
|
|
|
|
return convertType(column.type, TYPE_MAP) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Fetches the tables from the oracle table and assigns them to the datasource. |
|
|
|
* @param {*} datasourceId - datasourceId to fetch |
|
|
|
* @param entities - the tables that are to be built |
|
|
|
*/ |
|
|
|
async buildSchema(datasourceId: string, entities: Record<string, Table>) { |
|
|
|
const columnsResponse = await this.internalQuery<ColumnsResponse>({ |
|
|
|
sql: this.COLUMNS_SQL, |
|
|
|
}) |
|
|
|
const oracleTables = this.mapColumns(columnsResponse) |
|
|
|
|
|
|
|
const tables: { [key: string]: Table } = {} |
|
|
|
|
|
|
|
// iterate each table
|
|
|
|
Object.values(oracleTables).forEach(oracleTable => { |
|
|
|
let table = tables[oracleTable.name] |
|
|
|
if (!table) { |
|
|
|
table = { |
|
|
|
_id: buildExternalTableId(datasourceId, oracleTable.name), |
|
|
|
primary: [], |
|
|
|
name: oracleTable.name, |
|
|
|
schema: {}, |
|
|
|
} |
|
|
|
tables[oracleTable.name] = table |
|
|
|
} |
|
|
|
|
|
|
|
// iterate each column on the table
|
|
|
|
Object.values(oracleTable.columns) |
|
|
|
// remove columns that we can't read / save
|
|
|
|
.filter(oracleColumn => this.isSupportedColumn(oracleColumn)) |
|
|
|
// match the order of the columns in the db
|
|
|
|
.sort((c1, c2) => c1.id - c2.id) |
|
|
|
.forEach(oracleColumn => { |
|
|
|
const columnName = oracleColumn.name |
|
|
|
let fieldSchema = table.schema[columnName] |
|
|
|
if (!fieldSchema) { |
|
|
|
fieldSchema = { |
|
|
|
autocolumn: this.isAutoColumn(oracleColumn), |
|
|
|
name: columnName, |
|
|
|
type: this.internalConvertType(oracleColumn), |
|
|
|
} |
|
|
|
table.schema[columnName] = fieldSchema |
|
|
|
} |
|
|
|
|
|
|
|
// iterate each constraint on the column
|
|
|
|
Object.values(oracleColumn.constraints).forEach( |
|
|
|
oracleConstraint => { |
|
|
|
if (oracleConstraint.type === OracleContraintTypes.PRIMARY) { |
|
|
|
table.primary!.push(columnName) |
|
|
|
} |
|
|
|
} |
|
|
|
) |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
const final = finaliseExternalTables(tables, entities) |
|
|
|
this.tables = final.tables |
|
|
|
this.schemaErrors = final.errors |
|
|
|
} |
|
|
|
|
|
|
|
private async internalQuery<T>(query: SqlQuery): Promise<Result<T>> { |
|
|
|
let connection |
|
|
|
try { |
|
|
|
connection = await this.getConnection() |
|
|
|
|
|
|
|
const options : ExecuteOptions = { autoCommit: true } |
|
|
|
const result: Result<any> = await connection.execute(query.sql, [], options) |
|
|
|
const options: ExecuteOptions = { autoCommit: true } |
|
|
|
const bindings: BindParameters = query.bindings || [] |
|
|
|
const result: Result<T> = await connection.execute<T>( |
|
|
|
query.sql, |
|
|
|
bindings, |
|
|
|
options |
|
|
|
) |
|
|
|
|
|
|
|
return result |
|
|
|
} finally { |
|
|
|
if (connection) { |
|
|
|
try { |
|
|
|
await connection.close(); |
|
|
|
} catch (err) { |
|
|
|
console.error(err); |
|
|
|
} |
|
|
|
} |
|
|
|
if (connection) { |
|
|
|
try { |
|
|
|
await connection.close() |
|
|
|
} catch (err) { |
|
|
|
console.error(err) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private getConnection = async (): Promise<Connection> => { |
|
|
|
//connectString : "(DESCRIPTION =(ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))(CONNECT_DATA =(SID= ORCL)))"
|
|
|
|
const connectString = `${this.config.host}:${this.config.port || 1521}/${this.config.database}` |
|
|
|
const connectString = `${this.config.host}:${this.config.port || 1521}/${ |
|
|
|
this.config.database |
|
|
|
}` |
|
|
|
const attributes: ConnectionAttributes = { |
|
|
|
user: this.config.user, |
|
|
|
password: this.config.user, |
|
|
|
connectString, |
|
|
|
} |
|
|
|
return oracledb.getConnection(attributes); |
|
|
|
return oracledb.getConnection(attributes) |
|
|
|
} |
|
|
|
|
|
|
|
async create(query: SqlQuery | string) { |
|
|
|
const response = await this.query(getSqlQuery(query)) |
|
|
|
return response.rows && response.rows.length ? response.rows : [{ created: true }] |
|
|
|
const response = await this.internalQuery(getSqlQuery(query)) |
|
|
|
return response.rows && response.rows.length |
|
|
|
? response.rows |
|
|
|
: [{ created: true }] |
|
|
|
} |
|
|
|
|
|
|
|
async read(query: SqlQuery | string) { |
|
|
|
const response = await this.query(getSqlQuery(query)) |
|
|
|
const response = await this.internalQuery(getSqlQuery(query)) |
|
|
|
return response.rows |
|
|
|
} |
|
|
|
|
|
|
|
async update(query: SqlQuery | string) { |
|
|
|
const response = await this.query(getSqlQuery(query)) |
|
|
|
return response.rows && response.rows.length ? response.rows : [{ updated: true }] |
|
|
|
const response = await this.internalQuery(getSqlQuery(query)) |
|
|
|
return response.rows && response.rows.length |
|
|
|
? response.rows |
|
|
|
: [{ updated: true }] |
|
|
|
} |
|
|
|
|
|
|
|
async delete(query: SqlQuery | string) { |
|
|
|
const response = await this.query(getSqlQuery(query)) |
|
|
|
return response.rows && response.rows.length ? response.rows : [{ deleted: true }] |
|
|
|
const response = await this.internalQuery(getSqlQuery(query)) |
|
|
|
return response.rows && response.rows.length |
|
|
|
? response.rows |
|
|
|
: [{ deleted: true }] |
|
|
|
} |
|
|
|
|
|
|
|
async query(json: QueryJson) { |
|
|
|
const operation = this._operation(json).toLowerCase() |
|
|
|
const input = this._query(json, { disableReturning: true }) |
|
|
|
if (Array.isArray(input)) { |
|
|
|
const responses = [] |
|
|
|
for (let query of input) { |
|
|
|
responses.push(await this.internalQuery(query)) |
|
|
|
} |
|
|
|
return responses |
|
|
|
} else { |
|
|
|
const response = await this.internalQuery(input) |
|
|
|
return response.rows && response.rows.length |
|
|
|
? response.rows |
|
|
|
: [{ [operation]: true }] |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|