budibase/packages/server/src/integrations/oracle.ts

462 lines
13 KiB
TypeScript
Raw Normal View History

import {
DatasourceFieldTypes,
2022-03-02 18:40:50 +01:00
Integration,
Operation,
QueryJson,
QueryTypes,
SqlQuery,
Table,
DatasourcePlus,
} from "@budibase/types"
2021-11-18 14:35:22 +01:00
import {
buildExternalTableId,
convertSqlType,
2022-03-02 18:40:50 +01:00
finaliseExternalTables,
getSqlQuery,
2021-11-23 14:27:38 +01:00
SqlClients,
2021-11-18 14:35:22 +01:00
} from "./utils"
import oracledb, {
2022-03-02 18:40:50 +01:00
BindParameters,
2021-11-18 14:35:22 +01:00
Connection,
ConnectionAttributes,
2022-03-02 18:40:50 +01:00
ExecuteOptions,
Result,
2021-11-18 14:35:22 +01:00
} from "oracledb"
import Sql from "./base/sql"
2021-11-17 15:34:16 +01:00
import { FieldTypes } from "../constants"
2021-11-09 18:52:26 +01:00
module OracleModule {
2021-11-18 14:35:22 +01:00
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT
2021-11-09 18:52:26 +01:00
interface OracleConfig {
host: string
port: number
database: string
user: string
password: string
}
const SCHEMA: Integration = {
2021-11-08 23:08:47 +01:00
docs: "https://github.com/oracle/node-oracledb",
2021-11-17 15:34:16 +01:00
plus: true,
friendlyName: "Oracle",
2022-06-23 12:35:57 +02:00
type: "Relational",
2021-11-18 14:35:22 +01:00
description:
"Oracle Database is an object-relational database management system developed by Oracle Corporation",
datasource: {
host: {
type: DatasourceFieldTypes.STRING,
default: "localhost",
required: true,
},
port: {
type: DatasourceFieldTypes.NUMBER,
required: true,
default: 1521,
},
database: {
type: DatasourceFieldTypes.STRING,
required: true,
},
user: {
type: DatasourceFieldTypes.STRING,
required: true,
},
password: {
type: DatasourceFieldTypes.PASSWORD,
required: true,
2021-11-18 14:35:22 +01:00
},
},
query: {
create: {
type: QueryTypes.SQL,
},
read: {
type: QueryTypes.SQL,
},
update: {
type: QueryTypes.SQL,
},
delete: {
type: QueryTypes.SQL,
},
},
}
2021-11-17 15:34:16 +01:00
2021-11-18 14:35:22 +01:00
const UNSUPPORTED_TYPES = ["BLOB", "CLOB", "NCLOB"]
2021-11-17 17:41:00 +01:00
2021-11-17 15:34:16 +01:00
/**
* Raw query response
*/
interface ColumnsResponse {
TABLE_NAME: string
COLUMN_NAME: string
DATA_TYPE: string
2021-11-18 11:49:53 +01:00
DATA_DEFAULT: string | null
2021-11-17 15:34:16 +01:00
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
2021-11-18 14:35:22 +01:00
type: string
2021-11-17 15:34:16 +01:00
relatedConstraintName: string | null
searchCondition: string | null
}
/**
* An oracle column and it's related constraints
*/
interface OracleColumn {
name: string
type: string
2021-11-18 11:49:53 +01:00
default: string | null
2021-11-17 15:34:16 +01:00
id: number
2021-11-18 14:35:22 +01:00
constraints: { [key: string]: OracleConstraint }
2021-11-17 15:34:16 +01:00
}
/**
* An oracle table and it's related columns
*/
interface OracleTable {
name: string
2021-11-18 14:35:22 +01:00
columns: { [key: string]: OracleColumn }
2021-11-17 15:34:16 +01:00
}
const OracleContraintTypes = {
PRIMARY: "P",
NOT_NULL_OR_CHECK: "C",
FOREIGN_KEY: "R",
2021-11-18 14:35:22 +01:00
UNIQUE: "U",
2021-11-17 15:34:16 +01:00
}
class OracleIntegration extends Sql implements DatasourcePlus {
private readonly config: OracleConfig
private index: number = 1
2021-11-17 15:34:16 +01:00
public tables: Record<string, Table> = {}
public schemaErrors: Record<string, string> = {}
private readonly COLUMNS_SQL = `
SELECT
tabs.table_name,
cols.column_name,
cols.data_type,
2021-11-18 11:49:53 +01:00
cols.data_default,
2021-11-17 15:34:16 +01:00
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) {
2021-11-23 14:27:38 +01:00
super(SqlClients.ORACLE)
this.config = config
}
getBindingIdentifier(): string {
return `:${this.index++}`
}
getStringConcat(parts: string[]): string {
return parts.join(" || ")
}
2021-11-17 15:34:16 +01:00
/**
2021-11-18 14:35:22 +01:00
* Map the flat tabular columns and constraints data into a nested object
2021-11-17 15:34:16 +01:00
*/
2021-11-18 14:35:22 +01:00
private mapColumns(result: Result<ColumnsResponse>): {
[key: string]: OracleTable
} {
2021-11-17 15:34:16 +01:00
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
2021-11-18 11:49:53 +01:00
const dataDefault = row.DATA_DEFAULT
2021-11-17 15:34:16 +01:00
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
2021-11-08 23:08:47 +01:00
2021-11-17 15:34:16 +01:00
let table = oracleTables[tableName]
if (!table) {
table = {
name: tableName,
2021-11-18 14:35:22 +01:00
columns: {},
2021-11-17 15:34:16 +01:00
}
oracleTables[tableName] = table
}
let column = table.columns[columnName]
if (!column) {
column = {
name: columnName,
type: dataType,
2021-11-18 14:35:22 +01:00
default: dataDefault,
2021-11-17 15:34:16 +01:00
id: columnId,
2021-11-18 14:35:22 +01:00
constraints: {},
2021-11-17 15:34:16 +01:00
}
table.columns[columnName] = column
}
if (constraintName && constraintType) {
let constraint = column.constraints[constraintName]
if (!constraint) {
constraint = {
name: constraintName,
type: constraintType,
relatedConstraintName: relatedConstraintName,
2021-11-18 14:35:22 +01:00
searchCondition: searchCondition,
2021-11-17 15:34:16 +01:00
}
}
column.constraints[constraintName] = constraint
}
})
}
return oracleTables
}
2022-03-02 18:40:50 +01:00
private static isSupportedColumn(column: OracleColumn) {
return !UNSUPPORTED_TYPES.includes(column.type)
2021-11-17 17:41:00 +01:00
}
2022-03-02 18:40:50 +01:00
private static isAutoColumn(column: OracleColumn) {
return !!(
column.default && column.default.toLowerCase().includes("nextval")
)
2021-11-18 11:49:53 +01:00
}
2021-11-18 14:35:22 +01:00
/**
* 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 {
2022-03-02 18:40:50 +01:00
return (
2021-11-18 14:35:22 +01:00
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
2022-03-02 18:40:50 +01:00
)
2021-11-18 14:35:22 +01:00
}
private internalConvertType(column: OracleColumn): { type: string } {
2021-11-18 14:35:22 +01:00
if (this.isBooleanType(column)) {
return { type: FieldTypes.BOOLEAN }
2021-11-18 14:35:22 +01:00
}
return convertSqlType(column.type)
2021-11-18 14:35:22 +01:00
}
2021-11-17 15:34:16 +01:00
/**
* 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
2021-11-18 14:35:22 +01:00
*/
2021-11-17 15:34:16 +01:00
async buildSchema(datasourceId: string, entities: Record<string, Table>) {
2021-11-18 14:35:22 +01:00
const columnsResponse = await this.internalQuery<ColumnsResponse>({
sql: this.COLUMNS_SQL,
})
2021-11-17 15:34:16 +01:00
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)
2021-11-18 14:35:22 +01:00
// remove columns that we can't read / save
2022-03-02 18:40:50 +01:00
.filter(oracleColumn =>
OracleIntegration.isSupportedColumn(oracleColumn)
)
2021-11-18 14:35:22 +01:00
// 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 = {
2022-03-02 18:40:50 +01:00
autocolumn: OracleIntegration.isAutoColumn(oracleColumn),
2021-11-18 14:35:22 +01:00
name: columnName,
...this.internalConvertType(oracleColumn),
2021-11-18 14:35:22 +01:00
}
table.schema[columnName] = fieldSchema
2021-11-17 15:34:16 +01:00
}
2021-11-18 14:35:22 +01:00
// iterate each constraint on the column
Object.values(oracleColumn.constraints).forEach(
oracleConstraint => {
if (oracleConstraint.type === OracleContraintTypes.PRIMARY) {
table.primary!.push(columnName)
}
}
)
2021-11-17 15:34:16 +01:00
})
})
const final = finaliseExternalTables(tables, entities)
this.tables = final.tables
this.schemaErrors = final.errors
}
2021-11-26 16:02:03 +01:00
private async internalQuery<T>(query: SqlQuery): Promise<Result<T>> {
2021-11-18 14:35:22 +01:00
let connection
try {
this.index = 1
2021-11-18 14:35:22 +01:00
connection = await this.getConnection()
2021-11-17 15:34:16 +01:00
const options: ExecuteOptions = { autoCommit: true }
const bindings: BindParameters = query.bindings || []
2021-11-23 14:27:38 +01:00
2022-03-02 18:40:50 +01:00
return await connection.execute<T>(query.sql, bindings, options)
2021-11-08 23:08:47 +01:00
} finally {
2021-11-18 14:35:22 +01:00
if (connection) {
try {
await connection.close()
} catch (err) {
console.error(err)
}
}
2021-11-08 23:08:47 +01:00
}
}
private getConnection = async (): Promise<Connection> => {
//connectString : "(DESCRIPTION =(ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))(CONNECT_DATA =(SID= ORCL)))"
2021-11-18 14:35:22 +01:00
const connectString = `${this.config.host}:${this.config.port || 1521}/${
this.config.database
}`
2021-11-08 23:08:47 +01:00
const attributes: ConnectionAttributes = {
user: this.config.user,
2021-12-03 11:48:47 +01:00
password: this.config.password,
2021-11-08 23:08:47 +01:00
connectString,
}
2021-11-18 14:35:22 +01:00
return oracledb.getConnection(attributes)
}
async create(query: SqlQuery | string): Promise<any[]> {
const response = await this.internalQuery<any>(getSqlQuery(query))
2021-11-18 14:35:22 +01:00
return response.rows && response.rows.length
? response.rows
: [{ created: true }]
}
async read(query: SqlQuery | string): Promise<any[]> {
const response = await this.internalQuery<any>(getSqlQuery(query))
return response.rows ? response.rows : []
2021-11-08 23:08:47 +01:00
}
2021-11-25 18:14:07 +01:00
async update(query: SqlQuery | string): Promise<any[]> {
2021-11-17 15:34:16 +01:00
const response = await this.internalQuery(getSqlQuery(query))
2021-11-18 14:35:22 +01:00
return response.rows && response.rows.length
? response.rows
: [{ updated: true }]
2021-11-08 23:08:47 +01:00
}
2021-11-25 18:14:07 +01:00
async delete(query: SqlQuery | string): Promise<any[]> {
2021-11-17 15:34:16 +01:00
const response = await this.internalQuery(getSqlQuery(query))
2021-11-18 14:35:22 +01:00
return response.rows && response.rows.length
? response.rows
: [{ deleted: true }]
}
2021-11-17 15:34:16 +01:00
async query(json: QueryJson) {
2021-11-26 17:50:15 +01:00
const operation = this._operation(json)
2021-11-26 16:02:03 +01:00
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 {
2021-11-26 17:50:15 +01:00
// read the row to be deleted up front for the return
let deletedRows
if (operation === Operation.DELETE) {
const queryFn = (query: any) => this.internalQuery(query)
deletedRows = await this.getReturningRow(queryFn, json)
}
// run the query
2021-11-26 16:02:03 +01:00
const response = await this.internalQuery(input)
2021-11-26 17:50:15 +01:00
// get the results or return the created / updated / deleted row
if (deletedRows?.rows?.length) {
return deletedRows.rows
} else if (response.rows?.length) {
2021-11-26 16:02:03 +01:00
return response.rows
} else {
// get the last row that was updated
if (
response.lastRowid &&
json.endpoint?.entityId &&
2021-11-26 17:50:15 +01:00
operation !== Operation.DELETE
2021-11-26 16:02:03 +01:00
) {
const lastRow = await this.internalQuery({
sql: `SELECT * FROM \"${json.endpoint.entityId}\" WHERE ROWID = '${response.lastRowid}'`,
})
return lastRow.rows
} else {
2021-12-02 15:54:47 +01:00
return [{ [operation.toLowerCase()]: true }]
2021-11-26 16:02:03 +01:00
}
}
}
2021-11-17 15:34:16 +01:00
}
}
module.exports = {
schema: SCHEMA,
integration: OracleIntegration,
}
}