diff --git a/.travis.yml b/.travis.yml index 105057840..b2aee6d07 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ matrix: - os: osx osx_image: xcode9.0 language: node_js - node_js: "6" + node_js: "8" env: - ELECTRON_CACHE=$HOME/.cache/electron - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c4697dd3..9ce31921c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,46 +4,48 @@ Note that this section targets contributors and those who wish to set up and run If you're interested in using the distributed App, [download the latest release.](https://github.com/plotly/falcon-sql-client/releases) + ## Prerequisites -It is recommended to use node v6.12 with the latest electron-builder -## Install +Falcon development requires node v8 and yarn v1. Some connectors (e.g. the +Oracle connector) have additional requirements (see further below the section on +testing). -Start by cloning the repo via git: -```bash -git clone https://github.com/plotly/falcon-sql-client falcon-sql-client -``` - -And then install dependencies with **yarn**. +## Install -```bash +```sh +$ git clone https://github.com/plotly/falcon-sql-client falcon-sql-client $ cd falcon-sql-client $ yarn install +``` + + +## Build and Run the Electron App + +First, build the native dependencies against Electron: +```sh $ yarn run rebuild:modules:electron ``` -*Note: See package.json for the version of node that is required* +Then: -## Run as an Electron App -Run the app with -```bash +```sh $ yarn run build $ yarn run start ``` -## Run as a Server -Build and run the app: -```bash -$ yarn install -$ yarn run heroku-postbuild -$ yarn run start-headless -``` +## Build and Run the Web App -Build (after it was already built for electron desktop) and run the app: -```bash +If, last time, Falcon was built as an Electron app, then the native modules need +rebuilding against Node: +```sh $ yarn run rebuild:modules:node +``` + +To build and run the web app: +```sh $ yarn run heroku-postbuild $ yarn run start-headless ``` @@ -62,7 +64,8 @@ CORS_ALLOWED_ORIGINS: The database connector runs as a server by default as part of [Plotly On-Premise](https://plot.ly/products/on-premise). On Plotly On-Premise, every user who has access to the on-premise server also has access to the database connector, no extra installation or SSL configuration is necessary. If you would like to try out Plotly On-Premise at your company, please [get in touch with our team](https://plotly.typeform.com/to/seG7Vb), we'd love to help you out. -## Run as a docker image + +## Run as a Docker Container Build and run the docker image: ``` @@ -74,8 +77,11 @@ The web app will be accessible in your browser at `http://localhost:9494`. See the [Dockerfile](https://github.com/plotly/falcon-sql-client/blob/master/Dockerfile) for more information. + ## Developing +*([TODO] This section needs updating)* + Run watchers for the electron main process, the web bundle (the front-end), and the headless-bundle: ```bash $ yarn run watch-main @@ -89,7 +95,7 @@ $ yarn run watch-web $ yarn run watch-headless ``` -Then, view the the app in the electron window with: +Then, view the app in the electron window with: ```bash $ yarn run dev @@ -106,20 +112,79 @@ $ yarn start and in your web browser by visiting http://localhost:9494 + ## Testing -There are unit tests for the nodejs backend and integration tests to test the flow of the app. +Falcon is tested in three ways: -Run unit tests: -```bash -$ yarn run test-unit-all +- backend tests: stored under `test/backend` and run by `yarn run test-unit-all` +- frontend tests: stored under `test/app` and run by `yarn run test-jest` +- integration tests: stored in `test/integration_test.js` and run by `yarn run test-e2e` + +In some cases, we also provide `Dockerfile`s to build containers with a sample +database for testing. These can be found under `test/docker`. + + +### IBM DB2 Test Database + +In folder `test/docker/ibmdb2`, we provide a `Dockerfile` to setup an IBM DB2 +Express database for testing. + +To build the docker image, run `yarn run docker:db2:build`. + +And to start the docker container, run `yarn run docker:db2:start`. + +More details can be found in `test/docker/ibmdb2/README.md`. + + +### Oracle Test Database + +In folder `test/docker/oracle`, we provide a `Dockerfile` to setup an Oracle +Express database for testing. + +To build the docker image, run `yarn run docker:oracle:build`. + +And to start the docker container, run `yarn run docker:oracle:start`. + +More details can be found in `test/docker/oracle/README.md`. + + +#### Installation of Oracle Client Libraries + +Unlike IBM DB2's case and as of this writing, the Oracle bindings for Node.js, +[oracledb](https://www.npmjs.com/package/oracledb), are incomplete and users are +required to create an account on +[Oracle](https://login.oracle.com/mysso/signon.jsp) before downloading the +missing Oracle Client libraries. + +The installation procedure is very well documented +[here](https://github.com/oracle/node-oracledb/blob/master/INSTALL.md#instructions). + +The procedure for Ubuntu: + +1. Install requirements: `sudo apt-get -qq update && sudo apt-get --no-install-recommends -qq install alien bc libaio1` +2. Create an account on [Oracle](https://login.oracle.com/mysso/signon.jsp) +3. Download the Oracle Instant Client from [here](http://download.oracle.com/otn/linux/oracle11g/xe/oracle-xe-11.2.0-1.0.x86_64.rpm.zip) +4. Unzip `rpm` package: `unzip oracle-xe-11.2.0-1.0.x86_64.rpm.zip` +5. Convert `rpm` package into `deb`: `alien oracle-xe-11.2.0-1.0.x86_64.rpm` +6. Install `deb` package: `sudo dpkg -i oracle-instantclient12.2-basiclite_12.2.0.1.0-2_amd64.deb` + + +#### Running the Unit Tests for Oracle Connector + +First, open a terminal and start the container that runs test Oracle database: +```sh +$ yarn run docker:oracle:start ``` +and wait until the message `Ready` is shown. -Run integration tests: -```bash -$ yarn run test-e2e +Then, open another terminal and run: +```sh +$ export LD_LIBRARY_PATH=/usr/lib/oracle/12.2/client64/lib:$LD_LIBRARY_PATH +$ yarn run test-unit-oracle ``` + ## Builds and Releases - Update package.json with the new semver version @@ -129,13 +194,25 @@ $ yarn run test-e2e Builds are uploaded to https://github.com/plotly/falcon-sql-client/releases. + ## Troubleshooting -The Falcon Configuration information is installed in the user's home directory. -For example Unix and Mac (~/.plotly/connector) and for Windows (%userprofile%\.plotly\connector\). If you have tried the install -process and the app is still not running, this may be related to some corrupted -configuration files. You can try removing the existing configuration files and then -restarting the build process -```bash +Falcon keeps stores its configuration and logs in a folder under the user's home +directory: +- `%USERPROFILE%\.plotly\connector` (in Windows) +- `~/.plotly/connector` (in Mac and Linux) + +While developing a new connector, a common issue is that Falcon's configuration +gets corrupted. This often leads to a failure at start up. Until we address this +issue in more user-friendly manner (issue #342), the solution is to delete +Falcon's configuration folder. + +In Windows: +```sh +rmdir /s %USERPROFILE%\.plotly\connector +``` + +In Mac and Linux: +```sh rm -rf ~/.plotly/connector/ -``` \ No newline at end of file +``` diff --git a/app/components/Settings/ConnectButton/ConnectButton.react.js b/app/components/Settings/ConnectButton/ConnectButton.react.js index d8038b37a..58c3cd355 100644 --- a/app/components/Settings/ConnectButton/ConnectButton.react.js +++ b/app/components/Settings/ConnectButton/ConnectButton.react.js @@ -23,10 +23,10 @@ export default class ConnectButton extends Component { } /** - * @returns {boolean} true if waiting for a response to a connection request - */ - isConnecting() { - return this.props.connectRequest.status === 'loading'; + * @returns {boolean} true if waiting for a response to a connection request + */ + isConnecting() { + return this.props.connectRequest.status === 'loading'; } /** @@ -34,7 +34,7 @@ export default class ConnectButton extends Component { */ isConnected() { const status = Number(this.props.connectRequest.status); - return (status >= 200 || status < 300); + return (status >= 200 && status < 300); } /** @@ -56,7 +56,7 @@ export default class ConnectButton extends Component { */ isSaved() { const status = Number(this.props.saveConnectionsRequest.status); - return (status >= 200 || status < 300); + return (status >= 200 && status < 300); } /** @@ -89,18 +89,19 @@ export default class ConnectButton extends Component { buttonClick = connect; const connectErrorMessage = pathOr( - null, ['content', 'error'], connectRequest + null, ['content', 'error', 'message'], connectRequest ); const saveErrorMessage = pathOr( null, ['content', 'error', 'message'], saveConnectionsRequest ); const genericErrorMessage = 'Hm... had trouble connecting.'; - const errorMessage = connectErrorMessage || saveErrorMessage || genericErrorMessage; + const errorMessage = String(connectErrorMessage || saveErrorMessage || genericErrorMessage); error =
{errorMessage}
; } else if (this.isConnected() && this.isSaved()) { buttonText = 'Save changes'; buttonClick = connect; + } else { buttonText = 'Connect'; buttonClick = connect; diff --git a/app/components/Settings/Preview/TableTree.react.js b/app/components/Settings/Preview/TableTree.react.js index 3c2e2e213..9c39d29a0 100644 --- a/app/components/Settings/Preview/TableTree.react.js +++ b/app/components/Settings/Preview/TableTree.react.js @@ -35,6 +35,8 @@ class TableTree extends Component { return getPathNames(connectionObject.url)[2]; case DIALECTS.CSV: return connectionObject.label || connectionObject.id || connectionObject.database; + case DIALECTS.ORACLE: + return connectionObject.connectionString; default: return connectionObject.database; } diff --git a/app/components/Settings/Preview/code-editor.jsx b/app/components/Settings/Preview/code-editor.jsx index 3bab41022..030f18ed6 100644 --- a/app/components/Settings/Preview/code-editor.jsx +++ b/app/components/Settings/Preview/code-editor.jsx @@ -191,6 +191,7 @@ export default class CodeEditor extends React.Component { [DIALECTS.MYSQL]: 'text/x-mysql', [DIALECTS.SQLITE]: 'text/x-sqlite', [DIALECTS.MARIADB]: 'text/x-mariadb', + [DIALECTS.ORACLE]: 'text/x-plsql', [DIALECTS.POSTGRES]: 'text/x-pgsql', [DIALECTS.REDSHIFT]: 'text/x-pgsql', [DIALECTS.MSSQL]: 'text/x-mssql' diff --git a/app/components/Settings/Tabs/Tab.react.js b/app/components/Settings/Tabs/Tab.react.js index b907f4179..4bbefab00 100644 --- a/app/components/Settings/Tabs/Tab.react.js +++ b/app/components/Settings/Tabs/Tab.react.js @@ -38,6 +38,8 @@ export default class ConnectionTab extends Component { label = `Elasticsearch (${connectionObject.host})`; } else if (connectionObject.dialect === DIALECTS.ATHENA) { label = `Athena (${connectionObject.database})`; + } else if (connectionObject.dialect === DIALECTS.ORACLE) { + label = `${connectionObject.connectionString}`; } else if (connectionObject.dialect === DIALECTS.SQLITE) { label = connectionObject.storage; } else if (connectionObject.dialect === DIALECTS.DATA_WORLD) { diff --git a/app/constants/constants.js b/app/constants/constants.js index c24d8c3e7..2ec7bc479 100644 --- a/app/constants/constants.js +++ b/app/constants/constants.js @@ -5,6 +5,7 @@ import {concat} from 'ramda'; export const DIALECTS = { MYSQL: 'mysql', MARIADB: 'mariadb', + ORACLE: 'oracle', POSTGRES: 'postgres', REDSHIFT: 'redshift', ELASTICSEARCH: 'elasticsearch', @@ -23,6 +24,7 @@ export const DIALECTS = { export const SQL_DIALECTS_USING_EDITOR = [ 'mysql', 'mariadb', + 'oracle', 'postgres', 'redshift', 'mssql', @@ -139,6 +141,22 @@ export const CONNECTION_CONFIG = { } ] ), + [DIALECTS.ORACLE]: [ + {'label': 'Username', 'value': 'username', 'type': 'text'}, + {'label': 'Password', 'value': 'password', 'type': 'password'}, + { + 'label': 'Connection', + 'value': 'connectionString', + 'type': 'text', + 'description': ` + An Easy Connect string, + a Net Service Name from a local 'tnsnames.ora' file or an external naming service, + an SID of a local Oracle database instance, + or leave empty to connect to the local default database. + See https://oracle.github.io/node-oracledb/doc/api.html#connectionstrings for examples. + ` + } + ], [DIALECTS.POSTGRES]: commonSqlOptions, [DIALECTS.REDSHIFT]: commonSqlOptions, [DIALECTS.SQLITE]: [ @@ -245,6 +263,7 @@ export const LOGOS = { [DIALECTS.CSV]: 'images/csv-logo.png', [DIALECTS.IBM_DB2]: 'images/ibmdb2-logo.png', [DIALECTS.REDSHIFT]: 'images/redshift-logo.png', + [DIALECTS.ORACLE]: 'images/oracle-logo.png', [DIALECTS.POSTGRES]: 'images/postgres-logo.png', [DIALECTS.ELASTICSEARCH]: 'images/elastic-logo.png', [DIALECTS.MYSQL]: 'images/mysql-logo.png', @@ -263,6 +282,8 @@ export function PREVIEW_QUERY(connection, table, elasticsearchIndex) { return 'SELECT TOP 1000 * FROM ?'; case DIALECTS.IBM_DB2: return `SELECT * FROM ${table} FETCH FIRST 1000 ROWS ONLY`; + case DIALECTS.ORACLE: + return `SELECT * FROM ${table} WHERE ROWNUM <= 1000`; case DIALECTS.APACHE_IMPALA: case DIALECTS.APACHE_SPARK: case DIALECTS.MYSQL: @@ -382,6 +403,11 @@ export const SAMPLE_DBS = { host: 'db2.test.plotly.host', dialect: DIALECTS.IBM_DB2 }, + [DIALECTS.ORACLE]: { + username: 'XDB', + password: 'xdb', + connectionString: 'localhost/XE' + }, [DIALECTS.POSTGRES]: { username: 'masteruser', password: 'connecttoplotly', diff --git a/app/images/oracle-logo.png b/app/images/oracle-logo.png new file mode 100644 index 000000000..a901ac940 Binary files /dev/null and b/app/images/oracle-logo.png differ diff --git a/appveyor.yml b/appveyor.yml index e5be54013..c781c3aee 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,7 +2,7 @@ version: 0.4.{build} environment: matrix: - - nodejs_version: 6 + - nodejs_version: 8 POSTGRES_PATH: C:\Program Files\PostgreSQL\9.6 platform: diff --git a/backend/certificates.js b/backend/certificates.js index 8edfec671..3fe6746c2 100644 --- a/backend/certificates.js +++ b/backend/certificates.js @@ -1,6 +1,10 @@ import {getSetting, saveSetting} from './settings.js'; import * as fs from 'fs'; -import fetch from 'node-fetch'; + +const https = require('https'); +https.globalAgent.options.ecdhCurve = 'auto'; // fix default in node v8 +const fetch = require('node-fetch'); + import Logger from './logger'; import {fakeCerts} from '../test/backend/utils'; diff --git a/backend/persistent/datastores/Datastores.js b/backend/persistent/datastores/Datastores.js index d7bef60c1..e615de4da 100644 --- a/backend/persistent/datastores/Datastores.js +++ b/backend/persistent/datastores/Datastores.js @@ -1,3 +1,9 @@ +import Logger from '../../logger'; +function logError(error) { + Logger.log(`${error.constructor}: ${error.message}`); + throw error; +} + import * as Sql from './Sql'; import * as Elasticsearch from './Elasticsearch'; import * as S3 from './S3'; @@ -10,6 +16,7 @@ import * as DatastoreMock from './datastoremock'; import * as Athena from './athena'; const CSV = require('./csv'); +const Oracle = require('./oracle.js'); /* * Switchboard to all of the different types of connections @@ -56,6 +63,8 @@ function getDatastoreClient(connection) { return DataWorld; } else if (dialect === 'athena') { return Athena; + } else if (dialect === 'oracle') { + return Oracle; } return Sql; } @@ -71,7 +80,7 @@ function getDatastoreClient(connection) { * } */ export function query(queryStatement, connection) { - return getDatastoreClient(connection).query(queryStatement, connection); + return getDatastoreClient(connection).query(queryStatement, connection).catch(logError); } /** @@ -80,7 +89,7 @@ export function query(queryStatement, connection) { * @returns {Promise} that resolves when the connection succeeds */ export function connect(connection) { - return getDatastoreClient(connection).connect(connection); + return getDatastoreClient(connection).connect(connection).catch(logError); } /** @@ -91,7 +100,7 @@ export function connect(connection) { export function disconnect(connection) { const client = getDatastoreClient(connection); return (client.disconnect) ? - client.disconnect(connection) : + client.disconnect(connection).catch(logError) : Promise.resolve(connection); } @@ -107,7 +116,7 @@ export function disconnect(connection) { * } */ export function schemas(connection) { - return getDatastoreClient(connection).schemas(connection); + return getDatastoreClient(connection).schemas(connection).catch(logError); } /** @@ -118,7 +127,7 @@ export function schemas(connection) { * for elasticsearch, this means return the available "documents" per an "index" */ export function tables(connection) { - return getDatastoreClient(connection).tables(connection); + return getDatastoreClient(connection).tables(connection).catch(logError); } /* @@ -131,7 +140,7 @@ export function tables(connection) { // TODO - I think specificity is better here, just name this to "keys" // and if we ever add local file stuff, add a new function like "files". export function files(connection) { - return getDatastoreClient(connection).files(connection); + return getDatastoreClient(connection).files(connection).catch(logError); } @@ -141,7 +150,7 @@ export function files(connection) { * Return a list of configured Apache Drill storage plugins */ export function storage(connection) { - return getDatastoreClient(connection).storage(connection); + return getDatastoreClient(connection).storage(connection).catch(logError); } /* @@ -152,9 +161,9 @@ export function storage(connection) { * that plugin. */ export function listS3Files(connection) { - return getDatastoreClient(connection).listS3Files(connection); + return getDatastoreClient(connection).listS3Files(connection).catch(logError); } export function elasticsearchMappings(connection) { - return getDatastoreClient(connection).elasticsearchMappings(connection); + return getDatastoreClient(connection).elasticsearchMappings(connection).catch(logError); } diff --git a/backend/persistent/datastores/oracle.js b/backend/persistent/datastores/oracle.js new file mode 100644 index 000000000..a3776944f --- /dev/null +++ b/backend/persistent/datastores/oracle.js @@ -0,0 +1,98 @@ +module.exports = { + connect: connect, + tables: tables, + schemas: schemas, + query: query, + disconnect: disconnect +}; + +let oracledb; +try { + oracledb = require('oracledb'); +} catch (err) { + oracledb = err; +} + +const Pool = require('./pool.js'); +const pool = new Pool(newClient, sameConnection); + +function newClient(connection) { + if (oracledb instanceof Error) { + throw new Error(oracledb.message); + } + + return oracledb.getConnection({ + user: connection.username, + password: connection.password, + connectionString: connection.connectionString + }); +} + +function sameConnection(connection1, connection2) { + return ( + connection1.username === connection2.username && + connection1.password === connection2.password && + connection1.connectionString === connection2.connectionString + ); +} + +function connect(connection) { + return pool.getClient(connection); +} + +function disconnect(connection) { + return pool.remove(connection) + .then(client => client && client.close()); +} + +function tables(connection) { + const sqlQuery = ` + SELECT * FROM user_all_tables + WHERE + table_name NOT LIKE '%$%' AND + (table_type IS NULL OR table_type <> 'XMLTYPE') AND + (num_rows IS NULL OR num_rows > 0) AND + secondary = 'N' + `; + + return pool.getClient(connection) + .then(client => client.execute(sqlQuery)) + .then(result => { + return result.rows.map(row => row[0]); + }); +} + +function schemas(connection) { + const sqlQuery = ` + SELECT + c.table_name, + c.column_name, + c.data_type + FROM + user_tab_columns c, + user_all_tables t + WHERE + c.table_name = t.table_name AND + t.table_name NOT LIKE '%$%' AND + (t.table_type IS NULL OR t.table_type <> 'XMLTYPE') AND + (t.num_rows IS NULL OR t.num_rows > 0) AND + t.secondary = 'N' + `; + + return query(sqlQuery, connection); +} + +function query(queryString, connection) { + return pool.getClient(connection) + .then(client => client.execute(queryString)) + .then(result => { + const columnnames = result.metaData.map(column => column.name); + + // convert buffers into an hexadecimal string + const rows = result.rows.map( + row => row.map(value => (value instanceof Buffer) ? value.toString('hex') : value) + ); + + return {columnnames, rows}; + }); +} diff --git a/backend/persistent/datastores/pool.js b/backend/persistent/datastores/pool.js new file mode 100644 index 000000000..8d420d387 --- /dev/null +++ b/backend/persistent/datastores/pool.js @@ -0,0 +1,43 @@ +export default class Pool { + /** + * Pool keeps a list of clients indexed by connection objects + * + * @param {function} newClient Function that takes a connection object and creates a new client + * @param {function} sameConnection Function that returns whether two connection objects are the same + */ + constructor(newClient, sameConnection) { + this.newClient = newClient; + this.sameConnection = sameConnection; + this._pool = []; + } + + /** + * Get client indexed by connection (if no client found, a new client is created using newClient) + * @param {object} connection Connection object + * @returns {*} client for connection + */ + getClient(connection) { + for (let i = this._pool.length - 1; i >= 0; i--) { + if (this.sameConnection(connection, this._pool[i][0])) { + return this._pool[i][1]; + } + } + + const client = this.newClient(connection); + this._pool.push([connection, client]); + return client; + } + + /** + * Remove connection from pool + * @param {object} connection Connection object + * @returns {*} removed client (or undefined, if no client was found) + */ + remove(connection) { + for (let i = this._pool.length - 1; i >= 0; i--) { + if (this.sameConnection(connection, this._pool[i][0])) { + return this._pool.splice(i, 1)[0][1]; + } + } + } +} diff --git a/backend/routes.js b/backend/routes.js index 1741261ac..dddaa603a 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -467,11 +467,11 @@ export default class Servers { return next(); } Logger.log(validation, 2); - res.json(400, {error: validation.message}); + res.json(400, {error: {message: validation.message}}); return next(); }).catch(err => { Logger.log(err, 2); - res.json(400, {error: err.message}); + res.json(400, {error: {message: err.message}}); return next(); }); }); diff --git a/circle.yml b/circle.yml index 233ce3031..e56764871 100644 --- a/circle.yml +++ b/circle.yml @@ -2,7 +2,7 @@ version: 2 jobs: build: docker: - - image: circleci/node:6.13.0-browsers + - image: circleci/node:8-browsers - image: quay.io/plotly/falcon-test-spark - image: quay.io/plotly/falcon-test-db2 environment: diff --git a/package.json b/package.json index 0b70c86b5..510c95dd0 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "docker:db2:start": "docker run --rm -ti -p 50000:50000 pdc-db2", "docker:falcon:build": "docker build -t falcon-sql-client:local .", "docker:falcon:start": "docker run -ti --rm -p 9494:9494 -e PLOTLY_CONNECTOR_AUTH_ENABLED=$PLOTLY_CONNECTOR_AUTH_ENABLED -e PLOTLY_CONNECTOR_ALLOWED_USERS=$PLOTLY_CONNECTOR_ALLOWED_USERS falcon-sql-client:local", + "docker:oracle:build": "docker build test/docker/oracle -t falcon-test-oracle --no-cache", + "docker:oracle:start": "docker run --rm -ti -p 1521:1521 falcon-test-oracle", "rebuild:modules:electron": "cross-env FSEVENTS_BUILD_FROM_SOURCE=true node scripts/rebuild-modules.js --electron", "rebuild:modules:node": "cross-env FSEVENTS_BUILD_FROM_SOURCE=true node scripts/rebuild-modules.js", "fix:module:ibmdb": "node scripts/fix-module-ibmdb.js", @@ -39,6 +41,8 @@ "test-unit-livy": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.livy.spec.js", "test-unit-athena": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.athena.spec.js", "test-unit-oauth2": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/routes.oauth2.spec.js", + "test-unit-oracle": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.oracle.spec.js", + "test-unit-oracle:node": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.oracle.spec.js", "test-unit-plotly": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/PlotlyAPI.spec.js", "test-unit-scheduler": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/QueryScheduler.spec.js", "test-unit-routes": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/routes.spec.js", @@ -165,7 +169,7 @@ "css-loader": "^0.28.7", "del": "^3.0.0", "devtron": "^1.3.0", - "electron": "^1.7.8", + "electron": "2.0", "electron-builder": "^19.46.4", "electron-debug": "^1.4.0", "electron-mocha": "^4.0.3", @@ -234,6 +238,7 @@ "font-awesome": "^4.6.1", "ibm_db": "^2.3.0", "mysql": "^2.15.0", + "oracledb": "https://github.com/oracle/node-oracledb/releases/download/v2.2.0/oracledb-src-2.2.0.tgz", "papaparse": "^4.3.7", "pg": "^4.5.5", "pg-hstore": "^2.3.2", @@ -244,11 +249,11 @@ "tedious": "^2.1.4" }, "engines": { - "node": "6", + "node": "8", "yarn": "1" }, "devEngines": { - "node": "6", + "node": "8", "yarn": "1" } } diff --git a/test/app/components/Settings/ConnectButton/ConnectButton.test.js b/test/app/components/Settings/ConnectButton/ConnectButton.test.js index 1646d2045..3476df133 100644 --- a/test/app/components/Settings/ConnectButton/ConnectButton.test.js +++ b/test/app/components/Settings/ConnectButton/ConnectButton.test.js @@ -10,6 +10,66 @@ describe('Connect Button Test', () => { configure({ adapter: new Adapter() }); }); + it('should handle malformatted Connection Request Error', () => { + const connect = function() {}; + const connectRequest = { + status: 500, + content: {error: {message: {}}} + }; + const saveConnectionsRequest = { + }; + const editMode = true; + + const button = mount(); + + expect(button.find('.errorMessage').length).toBe(1); + }); + + it('should handle malformatted Save Connections Request Error', () => { + const connect = function() {}; + const connectRequest = { + status: 200 + }; + const saveConnectionsRequest = { + status: 500, + content: {error: {message: {}}} + }; + const editMode = true; + + const button = mount(); + + expect(button.find('.errorMessage').length).toBe(1); + }); + + it('should handle Connection Request Error Status 500', () => { + const connect = function() {}; + const connectRequest = { + status: 500 + }; + const saveConnectionsRequest = { + }; + const editMode = true; + + const button = mount(); + + expect(button.instance().connectionFailed()).toBe(true); + }); + it('should verify Connection Request Error', () => { const connect = function() {}; const connectRequest = { diff --git a/test/backend/datastores.oracle.spec.js b/test/backend/datastores.oracle.spec.js new file mode 100644 index 000000000..387479e80 --- /dev/null +++ b/test/backend/datastores.oracle.spec.js @@ -0,0 +1,68 @@ +import {assert} from 'chai'; + +import {DIALECTS} from '../../app/constants/constants.js'; + +import { + connect, + disconnect, + query, + schemas, + tables +} from '../../backend/persistent/datastores/Datastores.js'; + +const connection = { + dialect: DIALECTS.ORACLE, + username: 'XDB', + password: 'xdb', + connectionString: 'localhost/XE' +}; + +// Skip tests if there is no working installation of oracledb +let oracledb; +try { + oracledb = require('oracledb'); +} catch (err) { + if (!process.env.CIRCLECI) { + console.log('Skipping `datastores.oracle.spec.js`:', err); // eslint-disable-line + } +} + +((oracledb) ? describe : xdescribe)('Oracle:', function () { + it('connect succeeds', function() { + return connect(connection); + }); + + it('tables returns list of tables', function() { + return tables(connection).then(result => { + assert.include(result, 'CONSUMPTION2010', result); + }); + }); + + it('schemas returns schemas for all tables', function() { + return schemas(connection).then(results => { + assert.deepInclude(results.rows, ['CONSUMPTION2010', 'ALCOHOL', 'NUMBER']); + assert.deepInclude(results.rows, ['CONSUMPTION2010', 'LOCATION', 'VARCHAR2']); + assert.deepEqual(results.columnnames, ['TABLE_NAME', 'COLUMN_NAME', 'DATA_TYPE']); + }); + }); + + it('query returns rows and column names', function() { + return query( + 'SELECT * FROM CONSUMPTION2010 WHERE ROWNUM <= 5', + connection + ).then(results => { + assert.deepEqual(results.rows, [ + ['Belarus', 17.5], + ['Moldova', 16.8], + ['Lithuania', 15.4], + ['Russia', 15.1], + ['Romania', 14.4] + ]); + assert.deepEqual(results.columnnames, ['LOCATION', 'ALCOHOL']); + }); + }); + + it('disconnect succeeds', function() { + return disconnect(connection); + }); +}); diff --git a/test/backend/routes.spec.js b/test/backend/routes.spec.js index 6692807e1..d20badf6c 100644 --- a/test/backend/routes.spec.js +++ b/test/backend/routes.spec.js @@ -1578,7 +1578,7 @@ describe('Routes:', () => { .then(getResponseJson).then(json => { assert.deepEqual( json, - {error: 'password authentication failed for user "banana"'} + {error: {message: 'password authentication failed for user "banana"'}} ); assert.deepEqual( getConnections(), diff --git a/test/docker/oracle/Dockerfile b/test/docker/oracle/Dockerfile new file mode 100644 index 000000000..bd07ea8c1 --- /dev/null +++ b/test/docker/oracle/Dockerfile @@ -0,0 +1,10 @@ +FROM wnameless/oracle-xe-11g:16.04 + +EXPOSE 1521 + +ADD https://raw.githubusercontent.com/plotly/datasets/master/2010_alcohol_consumption_by_country.csv /2010_alcohol_consumption_by_country.csv +COPY setup.sql / +COPY setup.ctl / +COPY setup.sh /docker-entrypoint-initdb.d/ + +ENV ORACLE_ENABLE_XDB true diff --git a/test/docker/oracle/README.md b/test/docker/oracle/README.md new file mode 100644 index 000000000..d55b68f4d --- /dev/null +++ b/test/docker/oracle/README.md @@ -0,0 +1,39 @@ +The Dockerfile in this folder builds a Docker image that starts an instance of +Oracle Database 11g Express Edition with the logins `SYSTEM/oracle` and +`XDB/xdb`, and the sample database `consumption2010`. + + +# License + +This Dockerfile uses +[wnameless/oracle-xe-11g](https://hub.docker.com/r/wnameless/oracle-xe-11g/) as +a base image. + +Please, note that Oracle Database Express Edition is [licensed under the Oracle +Technology Network Developer License +Terms](http://www.oracle.com/technetwork/licenses/database-11g-express-license-459621.html). + + +# Usage + + +## Build + +Run the command below in the folder where the Dockerfile is located: + +```sh +docker build . -t falcon-test-oracle + +``` + + +## Run + +Run the command below inside a terminal, to start an instance listening on port +1521: + +```sh +docker run --rm -ti -p 1521:1521 falcon-test-oracle +``` + +To stop the container, just press `CTRL-C`. diff --git a/test/docker/oracle/setup.ctl b/test/docker/oracle/setup.ctl new file mode 100644 index 000000000..2d6ea4d2d --- /dev/null +++ b/test/docker/oracle/setup.ctl @@ -0,0 +1,11 @@ +OPTIONS(SKIP=1) + +LOAD DATA + INFILE "/2010_alcohol_consumption_by_country.csv" + + REPLACE + INTO TABLE consumption2010 + + FIELDS TERMINATED BY "," + OPTIONALLY ENCLOSED BY '"' + (location, alcohol) diff --git a/test/docker/oracle/setup.sh b/test/docker/oracle/setup.sh new file mode 100755 index 000000000..76e3b211e --- /dev/null +++ b/test/docker/oracle/setup.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +sqlplus XDB/xdb @/setup.sql +sqlldr userid=XDB/xdb control=/setup.ctl + +echo Ready diff --git a/test/docker/oracle/setup.sql b/test/docker/oracle/setup.sql new file mode 100644 index 000000000..234fc440c --- /dev/null +++ b/test/docker/oracle/setup.sql @@ -0,0 +1,5 @@ +CREATE TABLE consumption2010 ( + location varchar2(50), + alcohol number +); +QUIT diff --git a/webpack.config.base.js b/webpack.config.base.js index 471e7da12..f42560b58 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -35,6 +35,7 @@ export default { 'font-awesome': 'font-awesome', 'ibm_db': 'commonjs ibm_db', 'mysql': 'mysql', + 'oracledb': 'commonjs oracledb', 'pg': 'pg', 'pg-hstore': 'pg-hstore', 'restify': 'commonjs restify', diff --git a/yarn.lock b/yarn.lock index 663ffa2cf..66b223449 100644 --- a/yarn.lock +++ b/yarn.lock @@ -168,9 +168,9 @@ version "9.4.6" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.6.tgz#d8176d864ee48753d053783e4e463aec86b8d82e" -"@types/node@^7.0.18": - version "7.0.43" - resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.43.tgz#a187e08495a075f200ca946079c914e1a5fe962c" +"@types/node@^8.0.24": + version "8.10.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.14.tgz#a24767cfa22023f1bf7e751c0ead56a14c07ed45" "JSV@>= 4.0.x": version "4.0.2" @@ -3329,11 +3329,11 @@ electron-window@^0.8.0: dependencies: is-electron-renderer "^2.0.0" -electron@^1.7.8: - version "1.7.8" - resolved "https://registry.yarnpkg.com/electron/-/electron-1.7.8.tgz#27b791a6895171a7d52991b99442cdbd10a3539d" +electron@2.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-2.0.1.tgz#d9defcc187862143b9027378be78490eddbfabf4" dependencies: - "@types/node" "^7.0.18" + "@types/node" "^8.0.24" electron-download "^3.0.1" extract-zip "^1.0.3" @@ -6755,7 +6755,7 @@ nan@^2.3.0, nan@^2.3.3, nan@~2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" -nan@^2.7.0: +nan@^2.7.0, nan@~2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" @@ -7239,6 +7239,12 @@ options@>=0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" +"oracledb@https://github.com/oracle/node-oracledb/releases/download/v2.2.0/oracledb-src-2.2.0.tgz": + version "2.2.0" + resolved "https://github.com/oracle/node-oracledb/releases/download/v2.2.0/oracledb-src-2.2.0.tgz#44db77ad8db99c2646e611c2b33a6f8533e44e8e" + dependencies: + nan "~2.8.0" + orbit-camera-controller@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/orbit-camera-controller/-/orbit-camera-controller-4.0.0.tgz#6e2b36f0e7878663c330f50da9b7ce686c277005"