Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: oracle connector #310

Merged
merged 5 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gateway/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@azimutt/connector-mariadb": "^0.1.1",
"@azimutt/connector-mongodb": "^0.1.1",
"@azimutt/connector-mysql": "^0.1.2",
"@azimutt/connector-oracle": "workspace:^",
"@azimutt/connector-postgres": "^0.1.6",
"@azimutt/connector-snowflake": "^0.1.1",
"@azimutt/connector-sqlserver": "^0.1.1",
Expand Down
3 changes: 2 additions & 1 deletion gateway/src/services/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {mysql} from "@azimutt/connector-mysql"
import {postgres} from "@azimutt/connector-postgres"
import {snowflake} from "@azimutt/connector-snowflake"
import {sqlserver} from "@azimutt/connector-sqlserver"
import {oracle} from "@azimutt/connector-oracle"

const connectors: Record<DatabaseKind, Connector | undefined> = {
bigquery: bigquery,
Expand All @@ -17,7 +18,7 @@ const connectors: Record<DatabaseKind, Connector | undefined> = {
mariadb: mariadb,
mongodb: mongodb,
mysql: mysql,
oracle: undefined,
oracle: oracle,
postgres: postgres,
redis: undefined,
snowflake: snowflake,
Expand Down
5 changes: 5 additions & 0 deletions libs/connector-oracle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
out
local
src/**/*.js
*.tgz
7 changes: 7 additions & 0 deletions libs/connector-oracle/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
local
resources
src/*.js
src/*.test.ts
jest.config.js
tsconfig.json
*.tgz
75 changes: 75 additions & 0 deletions libs/connector-oracle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Oracle connector

This library allows to connect to [Oracle](https://www.oracle.com/database), extract its schema and more...
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar Improvement: Adjust phrasing for clarity.

Consider revising "This library allows to connect to Oracle" to "This library allows you to connect to Oracle" for grammatical correctness.

- This library allows to connect to [Oracle](https://www.oracle.com/database)
+ This library allows you to connect to [Oracle](https://www.oracle.com/database)
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
This library allows to connect to [Oracle](https://www.oracle.com/database), extract its schema and more...
This library allows you to connect to [Oracle](https://www.oracle.com/database), extract its schema and more...
Tools
LanguageTool

[grammar] ~3-~3: Did you mean “connecting”? Or maybe you should add a pronoun? In active voice, ‘allow’ + ‘to’ takes an object, usually a pronoun.
Context: ...# Oracle connector This library allows to connect to [Oracle](https://www.oracle.com/data...

(ALLOW_TO)


It lists all schemas, tables, columns, relations and types and format them in a JSON Schema.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar Improvement: Verb agreement.

The phrase "and format them in a JSON Schema" should be adjusted for proper verb agreement, perhaps to "and formats them into a JSON Schema."

- It lists all schemas, tables, columns, relations and types and format them in a JSON Schema.
+ It lists all schemas, tables, columns, relations, and types and formats them into a JSON Schema.
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
It lists all schemas, tables, columns, relations and types and format them in a JSON Schema.
It lists all schemas, tables, columns, relations, and types and formats them into a JSON Schema.


This library is made by [Azimutt](https://azimutt.app) to allow people to explore their Oracle database.
It's accessible through the [Desktop app](../../desktop) (soon), the [CLI](https://www.npmjs.com/package/azimutt) or even the website using the [gateway](../../gateway) server.

**Feel free to use it and even submit PR to improve it:**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style Improvement: Consider alternative phrasing.

The phrase "Feel free to use it" could be replaced with a more professional alternative like "You are encouraged to use it."

- **Feel free to use it and even submit PR to improve it:**
+ **You are encouraged to use it and even submit PRs to improve it:**
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Feel free to use it and even submit PR to improve it:**
**You are encouraged to use it and even submit PRs to improve it:**
Tools
LanguageTool

[style] ~10-~10: The phrase ‘feel free to’ is used quite frequently. Consider using a less frequent alternative to set your writing apart from others and make it sound more professional.
Context: ...the gateway server. **Feel free to use it and even submit PR to improve it...

(FEEL_FREE_TO_STYLE_ME)


## Publish

- update `package.json` version
- update lib versions (`pnpm -w run update` + manual)
- test with `pnpm run dry-publish` and check `azimutt-connector-oracle-x.y.z.tgz` content
- launch `pnpm publish --access public`

View it on [npm](https://www.npmjs.com/package/@azimutt/connector-oracle).

## Dev

If you need to develop on multiple libs at the same time (ex: want to update a connector and try it through the CLI), depend on local libs but publish & revert before commit.

- Depend on a local lib: `pnpm add <lib>`, ex: `pnpm add @azimutt/models`
- "Publish" lib locally by building it: `pnpm run build`

## Oracle Setup

### Run in Docker

You can use the free version of Oracle Database

```bash
docker pull container-registry.oracle.com/database/free:latest
```

To launch a container, the needed configuration is the `ORACLE_PWD` of the `SYS` user. You can also map the default 1521 port to your computer.

```bash
docker run -d --name oracle -p 1521:1521 -e ORACLE_PWD=oracle container-registry.oracle.com/database/free:latest
```

To connect, you can use a jdbc driver with the URL `jdbc:oracle:thin:<user>/<password>@//localhost:1521/FREE`

### Setup a user

Create a user

```sql
CREATE USER "C##AZIMUTT" IDENTIFIED BY "azimutt";
```

Grand permissions

```sql
GRANT CONNECT, RESOURCE, DBA TO "C##AZIMUTT";
```

Update user quota on `Users` tablespace

```sql
ALTER USER "C##AZIMUTT" QUOTA UNLIMITED ON USERS;
```

### Create a table

```sql
CREATE TABLE "C##AZIMUTT"."USERS"(
user_id NUMBER GENERATED BY DEFAULT AS IDENTITY,
first_name VARCHAR2(50) NOT NULL,
last_name VARCHAR2(50) NOT NULL,
PRIMARY KEY(user_id)
);
```
6 changes: 6 additions & 0 deletions libs/connector-oracle/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
transform: {'^.+\\.ts?$': 'ts-jest'},
testEnvironment: 'node',
testRegex: '/src/.+\\.test?\\.(ts|tsx)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
}
40 changes: 40 additions & 0 deletions libs/connector-oracle/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "@azimutt/connector-oracle",
"version": "0.1.0",
"description": "Connect to Oracle, extract schema, run analysis and queries",
"keywords": [],
"homepage": "https://azimutt.app",
"author": {
"name": "Anthony Ly",
"email": "anthonyly.dev@gmail.com",
"url": "https://anthonyly.dev"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/azimuttapp/azimutt.git",
"directory": "libs/connector-oracle"
},
"main": "./out/index.js",
"types": "./out/index.d.ts",
"scripts": {
"test": "jest",
"build": "rm -rf ./out && tsc",
"build:docker": "npx tsc",
"dry-publish": "pnpm run build && pnpm test && pnpm pack"
},
"dependencies": {
"@azimutt/models": "workspace:^",
"@azimutt/utils": "workspace:^",
"oracledb": "^6.5.1"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.5",
"@types/oracledb": "^6.5.1",
"jest": "^29.7.0",
"ts-jest": "^29.1.3",
"typescript": "^5.4.5"
}
}
30 changes: 30 additions & 0 deletions libs/connector-oracle/src/connect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, test } from "@jest/globals"
import { parseDatabaseUrl } from "@azimutt/models"
import { connect } from "./connect"
import { execQuery } from "./query"
import { application, logger } from "./constants.test"

// Use this test to troubleshoot database connection errors.
// If you don't succeed with the first one (Azimutt `connect`), try with the second one (raw node lib) and once you found a way, tell us how to fix ;)
// Of course, you can contact us (issues or contact@azimutt.app) to do it together.
// More documentation available at: https://azimutt.notion.site/Database-connection-troubleshooting-c4c19ed28c7040ef9aaaeec96ce6ba8d
describe("connect", () => {
// TODO 1: replace this with your own connection string, but don't commit it!
const url = "jdbc:oracle:thin:sys/oracle@//localhost:1521/FREE"

// TODO 2: write a valid query for your database
const query = "SELECT * FROM C##AZIMUTT.USERS"
const parameters: any[] = []

// TODO 3: unskip this test first and run it (`npm run test -- src/connect.test.ts`)
test.skip("Azimutt should connect", async () => {
const parsedUrl = parseDatabaseUrl(url)
const results = await connect(
application,
parsedUrl,
execQuery(query, parameters),
{ logger, logQueries: true }
)
console.log("results", results)
})
Comment on lines +7 to +29
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

Clarify TODO comments and consider enabling the test.

The test in libs/connector-oracle/src/connect.test.ts is skipped and contains several TODO comments, which are helpful but could be more specific in guiding the user on what needs to be done. Consider providing more detailed instructions or examples for the following TODO comments:

  • Replace the connection string with a valid one but ensure it is not committed.
  • Write a valid query for your database.
  • Unskip the test and run it using the provided command.

Additionally, evaluate whether this test can be enabled by default with environmental checks.

  • File: libs/connector-oracle/src/connect.test.ts
  • Lines: 7-29
Analysis chain

Clarify TODO comments and consider enabling the test.

The test is skipped and contains several TODO comments. These are helpful but could be more specific in guiding the user on what needs to be done. Consider providing more detailed instructions or examples. Also, evaluate whether this test can be enabled by default with environmental checks.

Scripts executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Check for guidance on enabling tests and handling TODOs.

# Test: Search for TODO comments and their handling in the codebase.
rg "TODO" --type typescript

Length of output: 62



Script:

#!/bin/bash
# Description: Check for guidance on enabling tests and handling TODOs.

# Test: Search for TODO comments and their handling in TypeScript files.
rg "TODO" --glob "*.ts"

Length of output: 15781

})
130 changes: 130 additions & 0 deletions libs/connector-oracle/src/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
Connection,
ConnectionAttributes,
getConnection,
SYSDBA,
} from "oracledb"
import { AnyError, errorToString } from "@azimutt/utils"
import {
AttributeValue,
ConnectorDefaultOpts,
DatabaseUrlParsed,
logQueryIfNeeded,
queryError,
} from "@azimutt/models"

export async function connect<T>(
application: string,
url: DatabaseUrlParsed,
exec: (c: Conn) => Promise<T>,
opts: ConnectorDefaultOpts
): Promise<T> {
const client = await createConnection(buildConfig(application, url)).catch(
(err) => Promise.reject(connectionError(err))
)
let queryCpt = 1
const conn: Conn = {
url,
query<T extends QueryResultRow>(
sql: string,
parameters: [] = [],
name?: string
): Promise<T[]> {
return logQueryIfNeeded(
queryCpt++,
name,
sql,
parameters,
(sql, parameters) => {
return client.execute<T>(sql, parameters).then(
(res) => res.rows ?? [],
(err) => Promise.reject(queryError(name, sql, err))
)
},
(r) => r?.length ?? 0,
opts
)
},
queryArrayMode(
sql: string,
parameters: any[] = [],
name?: string
): Promise<QueryResultArrayMode> {
return logQueryIfNeeded(
queryCpt++,
name,
sql,
parameters,
(sql, parameters) => {
return client.execute(sql, parameters).then(
(res) => {
const { metaData, rows } = res
const fields = metaData?.map((meta) => ({
name: meta.name,
}))
return { fields: fields ?? [], rows: (rows as any[]) ?? [] }
},
(err) => Promise.reject(queryError(name, sql, err))
)
},
(r) => r.rows.length,
opts
)
},
}
return exec(conn).then(
(res) => client.close().then((_) => res),
(err) => client.close().then((_) => Promise.reject(err))
)
}

export interface Conn {
url: DatabaseUrlParsed

query<T extends QueryResultRow>(
sql: string,
parameters?: any[],
name?: string
): Promise<T[]>

queryArrayMode(
sql: string,
parameters?: any[],
name?: string
): Promise<QueryResultArrayMode>
}

export type QueryResultValue = AttributeValue
export type QueryResultRow = QueryResultValue[]
export type QueryResultField = {
name: string
}
export type QueryResultRowArray = QueryResultValue[]
export type QueryResultArrayMode = {
fields: QueryResultField[]
rows: QueryResultRowArray[]
}

async function createConnection(
config: ConnectionAttributes
): Promise<Connection> {
const client = await getConnection(config)
return client
}

function buildConfig(
application: string,
url: DatabaseUrlParsed
): ConnectionAttributes {
return {
connectionString: `${url.host}:${url.port}/${url.db}`,
user: url.user,
password: url.pass || undefined,
privilege: SYSDBA,
}
}

function connectionError(err: AnyError): AnyError {
const msg = errorToString(err)
return err
}
14 changes: 14 additions & 0 deletions libs/connector-oracle/src/constants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {expect, test} from "@jest/globals";
import {Logger} from "@azimutt/utils";

export const logger: Logger = {
debug: (text: string): void => console.debug(text),
log: (text: string): void => console.log(text),
warn: (text: string): void => console.warn(text),
error: (text: string): void => console.error(text)
}
export const application = 'azimutt-tests'
Comment on lines +4 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove exports from test file

Exporting from test files can lead to unintended dependencies and should be avoided. Consider using a common setup file for shared test utilities instead.

- export const logger = {
-     debug: (text: string): void => console.debug(text),
-     log: (text: string): void => console.log(text),
-     warn: (text: string): void => console.warn(text),
-     error: (text: string): void => console.error(text)
- }
- export const application = 'azimutt-tests'
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const logger: Logger = {
debug: (text: string): void => console.debug(text),
log: (text: string): void => console.log(text),
warn: (text: string): void => console.warn(text),
error: (text: string): void => console.error(text)
}
export const application = 'azimutt-tests'
const logger: Logger = {
debug: (text: string): void => console.debug(text),
log: (text: string): void => console.log(text),
warn: (text: string): void => console.warn(text),
error: (text: string): void => console.error(text)
}
const application = 'azimutt-tests'
Tools
Biome

[error] 9-10: Do not export from a test file.

(lint/suspicious/noExportsInTest)


test('dummy', () => {
expect(application).toEqual('azimutt-tests')
})
16 changes: 16 additions & 0 deletions libs/connector-oracle/src/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, test } from "@jest/globals"
import { buildSqlColumn, buildSqlTable } from "./helpers"

describe("helpers", () => {
test("buildSqlTable", () => {
expect(buildSqlTable({ entity: "events" })).toEqual(`"events"`)
expect(buildSqlTable({ schema: "", entity: "events" })).toEqual(`"events"`)
expect(buildSqlTable({ schema: "public", entity: "events" })).toEqual(
`"public"."events"`
)
})
test("buildSqlColumn", () => {
expect(buildSqlColumn(["name"])).toEqual(`"name"`)
expect(buildSqlColumn(["data", "email"])).toEqual(`"data"->'email'`)
})
})
11 changes: 11 additions & 0 deletions libs/connector-oracle/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AttributePath, EntityRef, SqlFragment } from "@azimutt/models"

export function buildSqlTable(ref: EntityRef): SqlFragment {
const sqlSchema = ref.schema ? `"${ref.schema}".` : ""
return `${sqlSchema}"${ref.entity}"`
}

export function buildSqlColumn(path: AttributePath): SqlFragment {
const [head, ...tail] = path
return `"${head}"${tail.map((t) => `->'${t}'`).join("")}`
Comment on lines +8 to +10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

Potential SQL Injection Risk in buildSqlColumn Function

The buildSqlColumn function constructs SQL column references, including nested JSON attributes, without apparent sanitization of the AttributePath values. This could lead to SQL injection vulnerabilities if the AttributePath includes user input.

  • File: libs/connector-oracle/src/stats.ts

    • Lines: const sqlColumn = buildSqlColumn(ref.attribute)
    • Lines: const value = await (values.length > 0 ? Promise.resolve(values[0]) : getSampleValue(conn, sqlTable, buildSqlColumn([field.name])))
  • File: libs/connector-oracle/src/oracle.ts

    • Lines: const sqlColumn = buildSqlColumn(attribute)
  • File: libs/connector-sqlserver/src/stats.ts

    • Lines: const sqlColumn = buildSqlColumn(ref.attribute)
  • File: libs/connector-sqlserver/src/sqlserver.ts

    • Lines: const sqlColumn = buildSqlColumn(attribute)
  • File: libs/connector-mysql/src/stats.ts

    • Lines: const sqlColumn = buildSqlColumn(ref.attribute)
  • File: libs/connector-mysql/src/mysql.ts

    • Lines: const sqlColumn = buildSqlColumn(attribute)
  • File: libs/connector-snowflake/src/stats.ts

    • Lines: const sqlColumn = buildSqlColumn(ref.attribute)
  • File: libs/connector-postgres/src/stats.ts

    • Lines: const sqlColumn = buildSqlColumn(ref.attribute)
    • Lines: const value = await (values.length > 0 ? Promise.resolve(values[0]) : getSampleValue(conn, sqlTable, buildSqlColumn([field.name])))
  • File: libs/connector-postgres/src/postgres.ts

    • Lines: const sqlColumn = buildSqlColumn(attribute)
  • File: libs/connector-mariadb/src/stats.ts

    • Lines: const sqlColumn = buildSqlColumn(ref.attribute)
  • File: libs/connector-mariadb/src/mariadb.ts

    • Lines: const sqlColumn = buildSqlColumn(attribute)

Please ensure that AttributePath values are properly sanitized before being passed to the buildSqlColumn function to prevent SQL injection vulnerabilities.

Analysis chain

Review of buildSqlColumn function

This function handles the building of SQL column references, including nested JSON attributes using the -> operator. The spread operator and map are correctly used to handle multiple nested attributes. Ensure that all potential SQL injection vectors are considered, especially if AttributePath can come from user input.

Scripts executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Verify that AttributePath values are sanitized before being passed to buildSqlColumn.

# Test: Search for usages of buildSqlColumn and check for input sanitization.
rg --type typescript $'buildSqlColumn'

Length of output: 72



Script:

#!/bin/bash
# Description: Verify that AttributePath values are sanitized before being passed to buildSqlColumn.

# Test: Search for usages of buildSqlColumn and check for input sanitization.
rg 'buildSqlColumn' --glob '**/*.ts' --glob '**/*.tsx'

Length of output: 5785



Script:

#!/bin/bash
# Description: Extract lines around the calls to buildSqlColumn to check for input sanitization.

# Extract lines around the calls to buildSqlColumn in relevant files.
rg 'buildSqlColumn' --glob '**/*.ts' --glob '**/*.tsx' -A 3 -B 3

Length of output: 27106

}
Loading
Loading