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"