diff --git a/app/components/Settings/Preview/TableTree.react.js b/app/components/Settings/Preview/TableTree.react.js index b267ab05d..2d268fa5b 100644 --- a/app/components/Settings/Preview/TableTree.react.js +++ b/app/components/Settings/Preview/TableTree.react.js @@ -4,6 +4,7 @@ import TreeView from 'react-treeview'; import {isEmpty, has} from 'ramda'; import {DIALECTS} from '../../../constants/constants'; +import {getPathNames} from '../../../utils/utils'; const BASENAME_RE = /[^\\/]+$/; @@ -26,6 +27,17 @@ class TableTree extends Component { }) } + getLabel(connectionObject) { + switch (connectionObject.dialect) { + case DIALECTS.SQLITE: + return BASENAME_RE.exec(connectionObject.storage)[0] || connectionObject.storage; + case DIALECTS.DATA_WORLD: + return getPathNames(connectionObject.url)[2]; + default: + return connectionObject.database; + } + } + storeSchemaTree() { const {schemaRequest, getSqlSchema, updatePreview} = this.props; @@ -84,9 +96,7 @@ class TableTree extends Component { return (
{'Updating'}
); } - const label = (this.props.connectionObject.dialect === DIALECTS.SQLITE) ? - BASENAME_RE.exec(this.props.connectionObject.storage)[0] || this.props.connectionObject.storage : - this.props.connectionObject.database; + const label = this.getLabel(this.props.connectionObject); const labelNode = {label}; return ( diff --git a/app/components/Settings/Settings.react.js b/app/components/Settings/Settings.react.js index 2b15aff0f..e22abdfbb 100644 --- a/app/components/Settings/Settings.react.js +++ b/app/components/Settings/Settings.react.js @@ -199,7 +199,8 @@ class Settings extends Component { DIALECTS.APACHE_SPARK, DIALECTS.IBM_DB2, DIALECTS.MYSQL, DIALECTS.MARIADB, DIALECTS.POSTGRES, - DIALECTS.REDSHIFT, DIALECTS.MSSQL, DIALECTS.SQLITE + DIALECTS.REDSHIFT, DIALECTS.MSSQL, DIALECTS.SQLITE, + DIALECTS.DATA_WORLD ])) { if (connectRequest.status === 200 && !tablesRequest.status) { this.setState({editMode: false}); diff --git a/app/components/Settings/Tabs/Tab.react.js b/app/components/Settings/Tabs/Tab.react.js index 70d3b6b94..21a65d5ff 100644 --- a/app/components/Settings/Tabs/Tab.react.js +++ b/app/components/Settings/Tabs/Tab.react.js @@ -2,6 +2,7 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import {LOGOS, DIALECTS} from '../../../constants/constants'; +import {getPathNames} from '../../../utils/utils'; export default class ConnectionTab extends Component { constructor(props) { @@ -35,6 +36,13 @@ export default class ConnectionTab extends Component { label = `Elasticsearch (${connectionObject.host})`; } else if (connectionObject.dialect === DIALECTS.SQLITE) { label = connectionObject.storage; + } else if (connectionObject.dialect === DIALECTS.DATA_WORLD) { + const pathNames = getPathNames(connectionObject.url); + if (pathNames.length >= 3) { + label = `data.world (${pathNames[1]}/${pathNames[2]})`; + } else { + label = 'data.world (/)'; + } } else { label = `${connectionObject.database} (${connectionObject.username}@${connectionObject.host})`; } diff --git a/app/constants/constants.js b/app/constants/constants.js index df4ac8079..e233a4e88 100644 --- a/app/constants/constants.js +++ b/app/constants/constants.js @@ -14,7 +14,8 @@ export const DIALECTS = { IBM_DB2: 'ibm db2', APACHE_SPARK: 'apache spark', APACHE_IMPALA: 'apache impala', - APACHE_DRILL: 'apache drill' + APACHE_DRILL: 'apache drill', + DATA_WORLD: 'data.world' }; export const SQL_DIALECTS_USING_EDITOR = [ @@ -26,7 +27,8 @@ export const SQL_DIALECTS_USING_EDITOR = [ 'sqlite', 'ibm db2', 'apache spark', - 'apache impala' + 'apache impala', + 'data.world' ]; const commonSqlOptions = [ @@ -178,7 +180,21 @@ export const CONNECTION_CONFIG = { 'value': 'secretAccessKey', 'type': 'password' } - ] // TODO - password options for apache drill + ], // TODO - password options for apache drill + [DIALECTS.DATA_WORLD]: [ + { + 'label': 'Dataset/Project URL', + 'value': 'url', + 'type': 'text', + 'description': 'The URL of the dataset or project on data.world' + }, + { + 'label': 'Read/Write API Token', + 'value': 'token', + 'type': 'password', + 'description': 'Your data.world read/write token. It can be obtained from https://data.world/settings/advanced' + } + ] }; @@ -194,7 +210,8 @@ export const LOGOS = { [DIALECTS.MSSQL]: 'images/mssql-logo.png', [DIALECTS.SQLITE]: 'images/sqlite-logo.png', [DIALECTS.S3]: 'images/s3-logo.png', - [DIALECTS.APACHE_DRILL]: 'images/apache_drill-logo.png' + [DIALECTS.APACHE_DRILL]: 'images/apache_drill-logo.png', + [DIALECTS.DATA_WORLD]: 'images/dataworld-logo.png' }; export function PREVIEW_QUERY (dialect, table, database = '') { @@ -207,6 +224,7 @@ export function PREVIEW_QUERY (dialect, table, database = '') { case DIALECTS.SQLITE: case DIALECTS.MARIADB: case DIALECTS.POSTGRES: + case DIALECTS.DATA_WORLD: case DIALECTS.REDSHIFT: return `SELECT * FROM ${table} LIMIT 1000`; case DIALECTS.MSSQL: @@ -378,5 +396,8 @@ export const SAMPLE_DBS = { sqlite: { dialect: 'sqlite', storage: `${__dirname}/plotly_datasets.db` + }, + [DIALECTS.DATA_WORLD]: { + url: 'https://data.world/rflprr/reported-lyme-disease-cases-by-state' } }; diff --git a/app/images/dataworld-logo.png b/app/images/dataworld-logo.png new file mode 100644 index 000000000..3ca880384 Binary files /dev/null and b/app/images/dataworld-logo.png differ diff --git a/app/utils/utils.js b/app/utils/utils.js index e3d4f6488..5b030271e 100644 --- a/app/utils/utils.js +++ b/app/utils/utils.js @@ -49,3 +49,11 @@ export function homeUrl() { '/external-data-connector' : ''; } + +export function getPathNames(url) { + const parser = document.createElement('a'); + parser.href = url; + const pathNames = parser.pathname.split('/'); + + return pathNames; +} diff --git a/backend/persistent/datastores/Datastores.js b/backend/persistent/datastores/Datastores.js index 075660593..17be6cad8 100644 --- a/backend/persistent/datastores/Datastores.js +++ b/backend/persistent/datastores/Datastores.js @@ -5,6 +5,7 @@ import * as ApacheDrill from './ApacheDrill'; import * as IbmDb2 from './ibmdb2'; import * as ApacheLivy from './livy'; import * as ApacheImpala from './impala'; +import * as DataWorld from './dataworld'; import * as DatastoreMock from './datastoremock'; /* @@ -45,6 +46,8 @@ function getDatastoreClient(connection) { return ApacheImpala; } else if (dialect === 'ibm db2') { return IbmDb2; + } else if (dialect === 'data.world') { + return DataWorld; } return Sql; } diff --git a/backend/persistent/datastores/dataworld.js b/backend/persistent/datastores/dataworld.js new file mode 100644 index 000000000..00e336c64 --- /dev/null +++ b/backend/persistent/datastores/dataworld.js @@ -0,0 +1,111 @@ +import fetch from 'node-fetch'; +import url from 'url'; + +import Logger from '../../logger'; + +function parseUrl(datasetUrl) { + const pathnameArray = url.parse(datasetUrl).pathname.split('/'); + return { + owner: pathnameArray[1], + id: pathnameArray[2] + }; +} + +export function connect(connection) { + const { owner, id } = parseUrl(connection.url); + return fetch(`https://api.data.world/v0/datasets/${owner}/${id}/`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${connection.token}`, + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'User-Agent': `Falcon/Plotly - ${process.env.npm_package_version}` + } + }) + .then(res => res.json()) + .then(json => { + // json.code is defined only when there is an error + if (json.code) { + throw new Error(JSON.stringify(json)); + } + }) + .catch(err => { + Logger.log(err); + throw err; + }); +} + +export function tables(connection) { + return query('SELECT * FROM Tables', connection).then((res) => { + const allTables = res.rows.map((table) => { + return table[0].replace(/-/g, '_'); + }); + + return allTables; + }) + .catch(err => { + Logger.log(err); + throw err; + }); +} + +export function schemas(connection) { + return query('SELECT * FROM TableColumns', connection).then((res) => { + const rows = res.rows.map((table) => { + const tableName = table[0].replace(/-/g, '_'); + const columnName = table[3]; + // Extract the datatype from datatype url e.g. http://www.w3.org/2001/XMLSchema#integer + const columnDataType = /#(.*)/.exec(table[6])[1]; + + return [ + tableName, + columnName, + columnDataType + ]; + }); + + return ({ + columnNames: [ 'tablename', 'column_name', 'data_type' ], + rows + }); + }) + .catch(err => { + Logger.log(err); + throw err; + }); +} + +export function query(queryString, connection) { + const { owner, id } = parseUrl(connection.url); + const params = `${encodeURIComponent('query')}=${encodeURIComponent(queryString)}`; + + return fetch(`https://api.data.world/v0/sql/${owner}/${id}?includeTableSchema=true`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${connection.token}`, + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'User-Agent': `Falcon/Plotly - ${process.env.npm_package_version}` + }, + body: params + }) + .then(res => { + return res.json(); + }) + .then(json => { + const fields = json[0].fields; + const columnnames = fields.map((field) => { + return field.name; + }); + const rows = json.slice(1).map((row) => { + return Object.values(row); + }); + + return ({ + columnnames, + rows + }); + }) + .catch(err => { + Logger.log(err); + throw err; + }); +} diff --git a/package.json b/package.json index e6a502aea..3bffdbd17 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,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-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-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", @@ -168,6 +169,7 @@ "json-loader": "^0.5.4", "minimist": "^1.2.0", "mkdirp": "^0.5.1", + "nock": "^9.1.5", "node-fetch": "^1.7.2", "node-impala": "^2.0.4", "plotly.js": "^1.31.2", diff --git a/test/backend/datastores.dataworld.spec.js b/test/backend/datastores.dataworld.spec.js new file mode 100644 index 000000000..b21932be0 --- /dev/null +++ b/test/backend/datastores.dataworld.spec.js @@ -0,0 +1,109 @@ +import nock from 'nock'; +import {assert} from 'chai'; + +import { + dataWorldConnection as connection, + dataWorldTablesResponse, + dataWorldQueryResponse, + dataWorldColumnsResponse +} from './utils.js'; +import {connect, tables, query, schemas} from '../../backend/persistent/datastores/dataworld'; + +// Mock dataset GET request +nock('https://api.data.world/v0/datasets/falcon/test-dataset', { + reqheaders: { + 'Authorization': 'Bearer token', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + } +}) +.get('/') +.reply(200, { + owner: 'falcon', + id: 'test-dataset', + title: 'test-dataset', + tags: [], + visibility: 'PUBLIC', + files: [] +}); + +// Mock table query POST request +nock('https://api.data.world', { + reqheaders: { + 'Authorization': 'Bearer token', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + } +}) +.post('/v0/sql/falcon/test-dataset', 'query=SELECT%20*%20FROM%20Tables') +.query({ + includeTableSchema: 'true' +}) +.reply(200, dataWorldTablesResponse); + +// Mock query POST request +nock('https://api.data.world', { + reqheaders: { + 'Authorization': 'Bearer token', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + } +}) +.post('/v0/sql/falcon/test-dataset', 'query=SELECT%20*%20FROM%20sampletable%20LIMIT%205') +.query({ + includeTableSchema: 'true' +}) +.reply(200, dataWorldQueryResponse); + +// Mock table columns query POST request +nock('https://api.data.world', { + reqheaders: { + 'Authorization': 'Bearer token', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' + } +}) +.post('/v0/sql/falcon/test-dataset', 'query=SELECT%20*%20FROM%20TableColumns') +.query({ + includeTableSchema: 'true' +}) +.reply(200, dataWorldColumnsResponse); + +describe('Data World:', function () { + + it('connect succeeds', function() { + return connect(connection); + }); + + it('tables returns list of tables', function() { + return tables(connection).then(result => { + assert.deepEqual(result, ['sampletable']); + }); + }); + + it('query returns rows and column names', function() { + return query('SELECT * FROM sampletable LIMIT 5', connection).then(result => { + assert.deepEqual( + result.columnnames, + [ 'stringcolumn', 'datecolumn', 'decimalcolumn' ] + ); + assert.deepEqual(result.rows, [ + [ 'First column', '2017-05-24', 1 ], + [ 'Second column', '2017-05-25', 2 ], + [ 'Third column', '2017-05-26', 3 ], + [ 'Fourth column', '2017-05-27', 4 ], + [ 'Fifth column', '2017-05-28', 5 ] + ]); + }); + }); + + it('schemas returns table schemas', function() { + return schemas(connection).then(result => { + assert.deepEqual( + result.columnNames, + [ 'tablename', 'column_name', 'data_type' ] + ); + assert.deepEqual(result.rows, [ + [ 'sampletable', 'stringcolumn', 'string' ], + [ 'sampletable', 'datecolumn', 'date' ], + [ 'sampletable', 'decimalcolumn', 'decimal' ] + ]); + }); + }); +}); diff --git a/test/backend/utils.js b/test/backend/utils.js index 7c63d8552..6fb07ed17 100644 --- a/test/backend/utils.js +++ b/test/backend/utils.js @@ -296,6 +296,10 @@ export const apacheImpalaConnection = { port: 21000, database: 'plotly' }; +export const dataWorldConnection = { + url: 'https://data.world/falcon/test-dataset', + token: 'token' +}; // TODO - Add sqlite here // TODO - Add postgis in here @@ -602,3 +606,188 @@ export const apacheDrillStorage = [ } } ]; + +export const dataWorldTablesResponse = [ + { + 'fields': [ + { + 'name': 'tableId', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'tableName', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'tableTitle', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'tableDescription', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'owner', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'dataset', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + } + ] + }, + { + 'tableId': 'sampletable', + 'tableName': 'sampletable', + 'tableTitle': 'sampletable', + 'tableDescription': null, + 'owner': 'falcon', + 'dataset': 'sample-dataset' + } +]; + +export const dataWorldQueryResponse = [ + { + 'fields': [ + { + 'name': 'stringcolumn', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'datecolumn', + 'type': 'date', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#date' + }, + { + 'name': 'decimalcolumn', + 'type': 'decimal', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#decimal' + } + ] + }, + { + 'stringcolumn': 'First column', + 'datecolumn': '2017-05-24', + 'decimalcolumn': 1 + }, + { + 'stringcolumn': 'Second column', + 'datecolumn': '2017-05-25', + 'decimalcolumn': 2 + }, + { + 'stringcolumn': 'Third column', + 'datecolumn': '2017-05-26', + 'decimalcolumn': 3 + }, + { + 'stringcolumn': 'Fourth column', + 'datecolumn': '2017-05-27', + 'decimalcolumn': 4 + }, + { + 'stringcolumn': 'Fifth column', + 'datecolumn': '2017-05-28', + 'decimalcolumn': 5 + } +]; + +export const dataWorldColumnsResponse = [ + { + 'fields': [ + { + 'name': 'tableId', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'tableName', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'columnIndex', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'columnName', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'columnTitle', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'columnDescription', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'columnDatatype', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'columnNullable', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'owner', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + }, + { + 'name': 'dataset', + 'type': 'string', + 'rdfType': 'http://www.w3.org/2001/XMLSchema#string' + } + ] + }, + { + 'tableId': 'sampletable', + 'tableName': 'sampletable', + 'columnIndex': 1, + 'columnName': 'stringcolumn', + 'columnTitle': 'stringcolumn', + 'columnDescription': null, + 'columnDatatype': 'http://www.w3.org/2001/XMLSchema#string', + 'columnNullable': false, + 'owner': 'falcon', + 'dataset': 'test-dataset' + }, + { + 'tableId': 'sampletable', + 'tableName': 'sampletable', + 'columnIndex': 2, + 'columnName': 'datecolumn', + 'columnTitle': 'datecolumn', + 'columnDescription': null, + 'columnDatatype': 'http://www.w3.org/2001/XMLSchema#date', + 'columnNullable': false, + 'owner': 'falcon', + 'dataset': 'test-dataset' + }, + { + 'tableId': 'sampletable', + 'tableName': 'sampletable', + 'columnIndex': 3, + 'columnName': 'decimalcolumn', + 'columnTitle': 'decimalcolumn', + 'columnDescription': null, + 'columnDatatype': 'http://www.w3.org/2001/XMLSchema#decimal', + 'columnNullable': false, + 'owner': 'falcon', + 'dataset': 'test-dataset' + } +]; diff --git a/yarn.lock b/yarn.lock index a17679578..1d4cbc115 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1659,7 +1659,7 @@ chai-spies@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/chai-spies/-/chai-spies-0.7.1.tgz#343d99f51244212e8b17e64b93996ff7b2c2a9b1" -chai@^3.5.0: +"chai@>=1.9.2 <4.0.0", chai@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/chai/-/chai-3.5.0.tgz#4d02637b067fe958bdbfdd3a40ec56fef7373247" dependencies: @@ -5008,7 +5008,7 @@ json-stable-stringify@^1.0.1: dependencies: jsonify "~0.0.0" -json-stringify-safe@~5.0.1: +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -5338,7 +5338,7 @@ lodash@4.12.0: version "4.12.0" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.12.0.tgz#2bd6dc46a040f59e686c972ed21d93dc59053258" -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1: +lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.2: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -5830,6 +5830,20 @@ nextafter@^1.0.0: dependencies: double-bits "^1.1.0" +nock@^9.1.5: + version "9.1.5" + resolved "https://registry.yarnpkg.com/nock/-/nock-9.1.5.tgz#9e4878e0e1c050bdd93ae1e326e89461ea15618b" + dependencies: + chai ">=1.9.2 <4.0.0" + debug "^2.2.0" + deep-equal "^1.0.0" + json-stringify-safe "^5.0.1" + lodash "~4.17.2" + mkdirp "^0.5.0" + propagate "0.4.0" + qs "^6.5.1" + semver "^5.3.0" + node-abi@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.1.2.tgz#4da6caceb6685fcd31e7dd1994ef6bb7d0a9c0b2" @@ -6814,6 +6828,10 @@ prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, loose-envify "^1.3.1" object-assign "^4.1.1" +propagate@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-0.4.0.tgz#f3fcca0a6fe06736a7ba572966069617c130b481" + protocol-buffers-schema@^2.0.2: version "2.2.0" resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-2.2.0.tgz#d29c6cd73fb655978fb6989691180db844119f61" @@ -6874,7 +6892,7 @@ q@~1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" -qs@^6.2.1, qs@~6.5.1: +qs@^6.2.1, qs@^6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"