This repository was archived by the owner on Aug 29, 2025. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 276
CSV connector #375
Merged
Merged
CSV connector #375
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| const alasql = require('alasql'); | ||
| import fetch from 'node-fetch'; | ||
| import papa from 'papaparse'; | ||
| import {type} from 'ramda'; | ||
|
|
||
| import {parseSQL} from '../../parse'; | ||
|
|
||
| /** | ||
| * @typedef {object} PapaError Papaparse error | ||
| * | ||
| * @property {string} type Error type | ||
| * @property {string} code Error code | ||
| * @property {string} message Error description | ||
| * @property {number} row Row index that triggered the error | ||
| */ | ||
|
|
||
| /** | ||
| * Error thrown by CSV connector | ||
| * @class | ||
| * @param {string} url URL of the CSV file that triggered the error | ||
| * @param {PapaError[]} errors List of errors returned by Papaparse | ||
| */ | ||
| export function CSVError(url, errors) { | ||
| /** | ||
| * Error class | ||
| * @type {string} | ||
| */ | ||
| this.name = 'CSVError'; | ||
|
|
||
| /** | ||
| * Error description | ||
| * @type {string} | ||
| */ | ||
| this.message = 'Failed to parse CSV file ' + url; | ||
|
|
||
| if (Error.captureStackTrace) { | ||
| Error.captureStackTrace(this, CSVError); | ||
| } else { | ||
| /** | ||
| * Error stack trace | ||
| */ | ||
| this.stack = new Error(this.message).stack; | ||
| } | ||
|
|
||
| /** | ||
| * URL to CSV file | ||
| * @type {string} | ||
| */ | ||
| this.url = url; | ||
|
|
||
| /** | ||
| * List of errors returned by Papaparse | ||
| * @type {PapaError[]} | ||
| */ | ||
| this.errors = errors; | ||
|
|
||
| if (errors && errors[0] && errors[0].message) { | ||
| this.message = errors[0].message; | ||
| } | ||
| } | ||
| CSVError.prototype = Object.create(Error.prototype); | ||
| CSVError.prototype.constructor = CSVError; | ||
|
|
||
| /** | ||
| * Store of CSV files parsed into JS objects and indexed by URL | ||
| * | ||
| * @const {Object.<string, object>} | ||
| */ | ||
| const connectionData = {}; | ||
| function getData(connection) { | ||
| return connectionData[connection.database]; | ||
| } | ||
| function putData(connection, data) { | ||
| connectionData[connection.database] = data; | ||
| } | ||
|
|
||
| export function connect(connection) { | ||
| const url = connection.database; | ||
|
|
||
| return fetch(url) | ||
| .then(res => res.text()) | ||
| .then(body => { | ||
| return new Promise(function(resolve) { | ||
| papa.parse(body, { | ||
| download: false, | ||
| dynamicTyping: true, | ||
| skipEmptyLines: true, | ||
| header: true, | ||
| worker: true, | ||
|
|
||
| complete: function({data, errors, meta}) { | ||
| if (errors.length) { | ||
| throw new CSVError(url, errors); | ||
| } | ||
|
|
||
| connection.meta = meta; | ||
|
|
||
| putData(connection, data); | ||
|
|
||
| resolve(connection); | ||
| } | ||
| }); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Table name used in SQL queries to refer to the data imported from a CSV file, | ||
| * so that we can take advantage of alaSQL's parser. | ||
| * @const {string} | ||
| */ | ||
| const TABLENAME = '?'; | ||
|
|
||
| export function tables() { | ||
| return Promise.resolve([TABLENAME]); | ||
| } | ||
|
|
||
| export function schemas(connection) { | ||
| const columnnames = ['TABNAME', 'COLNAME', 'TYPENAME']; | ||
| const rows = connection.meta.fields.map(columnName => { | ||
| return [TABLENAME, columnName, getType(columnName)]; | ||
| }); | ||
|
|
||
| return Promise.resolve({columnnames, rows}); | ||
|
|
||
| function getType(columnName) { | ||
| const data = getData(connection); | ||
|
|
||
| for (let i = 0; i < data.length; i++) { | ||
| const cell = data[i][columnName]; | ||
| if (cell) return type(cell); | ||
| } | ||
|
|
||
| // If we reach this point, the column is empty. | ||
| // Let's return 'String', as none of the cells can be converted to Number. | ||
| return 'String'; | ||
| } | ||
| } | ||
|
|
||
| export function query(queryString, connection) { | ||
| const data = getData(connection); | ||
|
|
||
| // In the query `SELECT * FROM ?`, alaSQL replaces ? with data | ||
| return alasql.promise(queryString, [data]).then(parseSQL); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ | |
| "test-unit-all-watch": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --bail --full-trace --timeout 90000 --compilers js:babel-register --recursive test/**/*.spec.js --watch", | ||
| "test-unit-watch": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --bail --full-trace --timeout 90000 --watch --compilers js:babel-register ", | ||
| "test-unit-certificates": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/certificates.spec.js", | ||
| "test-unit-csv": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.csv.spec.js", | ||
| "test-unit-datastores": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/Datastores.spec.js", | ||
| "test-unit-dataworld": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.dataworld.spec.js", | ||
| "test-unit-elasticsearch": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.elasticsearch.spec.js", | ||
|
|
@@ -36,7 +37,6 @@ | |
| "test-unit-ibmdb": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.ibmdb.spec.js", | ||
| "test-unit-impala": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.impala.spec.js", | ||
| "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-nock": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 electron-mocha --full-trace --timeout 90000 --compilers js:babel-register test/backend/datastores.dataworld.spec.js test/backend/datastores.elasticsearch*.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-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", | ||
|
|
@@ -215,10 +215,12 @@ | |
| "yamljs": "^0.3.0" | ||
| }, | ||
| "dependencies": { | ||
| "alasql": "^0.4.5", | ||
| "csv-parse": "^2.0.0", | ||
| "font-awesome": "^4.6.1", | ||
| "ibm_db": "git+https://[email protected]/n-riesco/node-ibm_db.git#patched-v2.2.1", | ||
| "mysql": "^2.15.0", | ||
| "papaparse": "^4.3.7", | ||
| "pg": "^4.5.5", | ||
| "pg-hstore": "^2.3.2", | ||
| "restify": "^4.3.2", | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // do not use import, otherwise other test units won't be able to reactivate nock | ||
| const nock = require('nock'); | ||
|
|
||
| import {assert} from 'chai'; | ||
|
|
||
| import { | ||
| connect, | ||
| query, | ||
| schemas, | ||
| tables | ||
| } from '../../backend/persistent/datastores/Datastores.js'; | ||
|
|
||
| const csvFile = [ | ||
| 'col1,col 2,"col 3",col 4', | ||
| '1,1.1,2018-01-10,UK', | ||
| '2,2.2,2019-02-20,ES', | ||
| '3,3.3,2020-03-30,PL', | ||
| '' // to test csv files with empty lines can be parsed | ||
| ].join('\n'); | ||
|
|
||
| const expected = { | ||
| columnnames: ['col1', 'col 2', 'col 3', 'col 4'], | ||
| rows: [ | ||
| [1, 1.1, '2018-01-10', 'UK'], | ||
| [2, 2.2, '2019-02-20', 'ES'], | ||
| [3, 3.3, '2020-03-30', 'PL'] | ||
| ], | ||
| schemas: [ | ||
| ['?', 'col1', 'Number'], | ||
| ['?', 'col 2', 'Number'], | ||
| ['?', 'col 3', 'String'], | ||
| ['?', 'col 4', 'String'] | ||
| ] | ||
| }; | ||
|
|
||
| const host = 'https://csv.example.com'; | ||
| const path = '/table.csv'; | ||
| const url = host + path; | ||
| const connection = { | ||
| dialect: 'csv', | ||
| database: url | ||
| }; | ||
|
|
||
| describe('CSV:', function () { | ||
| before(function() { | ||
| // Enable nock if it has been disabled by other specs | ||
| if (!nock.isActive()) nock.activate(); | ||
| }); | ||
|
|
||
| after(function() { | ||
| nock.restore(); | ||
| }); | ||
|
|
||
| it('connect succeeds', function() { | ||
| // mock connect response | ||
| nock(host) | ||
| .get(path) | ||
| .reply(200, csvFile); | ||
|
|
||
| return connect(connection) | ||
| .then(conn => { | ||
| assert.equal(conn.dialect, 'csv', 'Unexpected connection.dialect'); | ||
| assert.equal(conn.database, url, 'Unexpected connection.database'); | ||
| assert(conn.meta, 'Missing connection.meta'); | ||
| assert.deepEqual(conn.meta.fields, expected.columnnames, 'Unexpected connection.meta.fields'); | ||
| }); | ||
| }); | ||
|
|
||
| it('tables succeeds', function() { | ||
| return tables(connection) | ||
| .then(obtained => { | ||
| assert.deepEqual(obtained, ['?'], 'Unexpected list of tables'); | ||
| }); | ||
| }); | ||
|
|
||
| it('schemas succeeds', function() { | ||
| return schemas(connection) | ||
| .then(({columnnames, rows}) => { | ||
| assert.equal(columnnames.length, 3, 'Unexpected columnnames'); | ||
| assert.deepEqual(rows, expected.schemas, 'Unexpected rows'); | ||
| }); | ||
| }); | ||
|
|
||
| it('query succeeds', function() { | ||
| return query('SELECT * FROM ?', connection) | ||
| .then(({columnnames, rows}) => { | ||
| assert.deepEqual(columnnames, expected.columnnames, 'Unexpected columnnames'); | ||
| assert.deepEqual(rows, expected.rows, 'Unexpected rows'); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the url is bad, how will this be presented to the user on the Settings UI? will fetch throw an error and this will be gracefully passed to the UI on its own?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same behaviour for all the connectors, i.e.: fetch throws,
routes.jscatches the exception and replies to the connection request with a 500 status and the body{error: message: error.message}}.