diff --git a/packages/server/src/integrations/oracle.ts b/packages/server/src/integrations/oracle.ts index 5c9f6a1a9a..34f01c8180 100644 --- a/packages/server/src/integrations/oracle.ts +++ b/packages/server/src/integrations/oracle.ts @@ -3,10 +3,14 @@ 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 { @@ -22,6 +26,7 @@ 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", datasource: { @@ -63,22 +68,237 @@ module OracleModule { }, }, } - class OracleIntegration extends Sql { + + const TYPE_MAP = { + text: FieldTypes.LONGFORM, + blob: FieldTypes.LONGFORM, + enum: FieldTypes.STRING, + varchar: FieldTypes.STRING, + float: FieldTypes.NUMBER, + int: FieldTypes.NUMBER, + numeric: FieldTypes.NUMBER, + bigint: FieldTypes.NUMBER, + mediumint: FieldTypes.NUMBER, + decimal: FieldTypes.NUMBER, + dec: FieldTypes.NUMBER, + double: FieldTypes.NUMBER, + real: FieldTypes.NUMBER, + fixed: FieldTypes.NUMBER, + smallint: FieldTypes.NUMBER, + timestamp: FieldTypes.DATETIME, + date: FieldTypes.DATETIME, + datetime: FieldTypes.DATETIME, + time: FieldTypes.DATETIME, + tinyint: FieldTypes.BOOLEAN, + json: DatasourceFieldTypes.JSON, + } + + /** + * Raw query response + */ + interface ColumnsResponse { + TABLE_NAME: string + COLUMN_NAME: string + DATA_TYPE: string + 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 + 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 = {} + public schemaErrors: Record = {} + + private readonly COLUMNS_SQL = ` + SELECT + tabs.table_name, + cols.column_name, + cols.data_type, + 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> => { - let connection - try { - connection = await this.getConnection() + /** + * Map the flat tabular columns and constraints data into a nested object + */ + private mapColumns(result: Result): { [key: string]: OracleTable } { + const oracleTables: { [key: string]: OracleTable } = {} - const options : ExecuteOptions = { autoCommit: true } - const result: Result = await connection.execute(query.sql, [], options) + if (result.rows) { + result.rows.forEach(row => { + const tableName = row.TABLE_NAME + const columnName = row.COLUMN_NAME + const dataType = row.DATA_TYPE + 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, + 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 + } + + /** + * 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) { + const columnsResponse = await this.internalQuery({ 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) + // 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: false, + name: columnName, + type: convertType(oracleColumn.type, TYPE_MAP), + } + 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(query: SqlQuery): Promise> { + let connection + try { + connection = await this.getConnection() + + const options: ExecuteOptions = { autoCommit: true } + const bindings: BindParameters = query.bindings || [] + const result: Result = await connection.execute(query.sql, bindings, options) return result } finally { @@ -104,24 +324,39 @@ module OracleModule { } async create(query: SqlQuery | string) { - const response = await this.query(getSqlQuery(query)) + 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)) + 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)) + 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) + 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 }] + } + } } module.exports = { diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts index 4d139fa28b..129f3f8d80 100644 --- a/packages/server/src/integrations/utils.ts +++ b/packages/server/src/integrations/utils.ts @@ -125,7 +125,7 @@ function copyExistingPropsOver( } export function finaliseExternalTables(tables: { [key: string]: any }, entities: { [key: string]: any }) { - const finalTables: { [key: string]: any } = {} + let finalTables: { [key: string]: any } = {} const errors: { [key: string]: string } = {} for (let [name, table] of Object.entries(tables)) { // make sure every table has a key @@ -136,5 +136,9 @@ export function finaliseExternalTables(tables: { [key: string]: any }, entities: // make sure all previous props have been added back finalTables[name] = copyExistingPropsOver(name, table, entities) } + // sort the tables by name + finalTables = Object.entries(finalTables) + .sort(([a,],[b,]) => a.localeCompare(b)) + .reduce((r, [k, v]) => ({ ...r, [k]: v }), {}); return { tables: finalTables, errors } }