diff --git a/lib/cmds/data-lake.js b/lib/cmds/data-lake.js new file mode 100644 index 0000000..6e5ef06 --- /dev/null +++ b/lib/cmds/data-lake.js @@ -0,0 +1,10 @@ +'use strict'; + +const options = require('../common-yargs'); + +exports.command = 'data-lake '; +exports.desc = 'Perform operations on the analytics data lake.'; +exports.builder = yargs => { + return options(yargs.commandDir('data_lake_cmds')); +}; +exports.handler = function (argv) {}; diff --git a/lib/cmds/data_lake_cmds/get-query.js b/lib/cmds/data_lake_cmds/get-query.js new file mode 100644 index 0000000..4a8f362 --- /dev/null +++ b/lib/cmds/data_lake_cmds/get-query.js @@ -0,0 +1,18 @@ +'use strict'; + +const { get } = require('../../api'); +const print = require('../../print'); + +exports.command = 'get-query '; +exports.desc = 'Fetch a single query execution.'; +exports.builder = yargs => { + yargs.positional('queryId', { + describe: 'Id of the query to fetch.', + type: 'string' + }); +}; + +exports.handler = async argv => { + const response = await get(argv, `/v1/analytics/query/${argv.queryId}`); + print(response.data, argv); +}; diff --git a/lib/cmds/data_lake_cmds/list-queries.js b/lib/cmds/data_lake_cmds/list-queries.js new file mode 100644 index 0000000..a7fb7d0 --- /dev/null +++ b/lib/cmds/data_lake_cmds/list-queries.js @@ -0,0 +1,37 @@ +'use strict'; + +const { list } = require('../../api'); +const print = require('../../print'); +const querystring = require('querystring'); + +exports.command = 'list-queries '; +exports.desc = 'List the query executions in the project.'; +exports.builder = yargs => { + yargs.positional('projectId', { + describe: 'The ID of the project to fetch queries of.', + type: 'string' + }).option('page-size', { + alias: 'n', + type: 'number', + default: 25, + describe: 'Maximum number of queries to return.' + }).option('next-page-token', { + alias: 't', + type: 'string', + describe: 'Token to retrieve the next page of results' + }); +}; + +exports.handler = async argv => { + const query = { + datasetId: argv.projectId, + pageSize: argv.pageSize + }; + + if (argv.nextPageToken) { + query.nextPageToken = argv.nextPageToken; + } + + const response = await list(argv, `/v1/analytics/query?${querystring.stringify(query)}`); + print(response.data, argv); +}; diff --git a/lib/cmds/data_lake_cmds/query.js b/lib/cmds/data_lake_cmds/query.js new file mode 100644 index 0000000..2b97ad8 --- /dev/null +++ b/lib/cmds/data_lake_cmds/query.js @@ -0,0 +1,39 @@ +'use strict'; + +const { post } = require('../../api'); +const print = require('../../print'); +const read = require('../../read'); + +exports.command = 'query '; +exports.desc = 'Submits a query to the Lifeomic data-lake API. A SQL query string can also be read from stdin.'; +exports.builder = yargs => { + yargs.positional('projectId', { + describe: 'The ID of the project to search within.', + type: 'string' + }).option('query', { + alias: 'q', + type: 'string', + describe: 'The SQL query to run.' + }).option('output-file-name', { + alias: 'o', + type: 'string', + describe: 'Name of the results file.', + demandOption: true + }); +}; + +exports.handler = async argv => { + const request = { + outputFileName: argv.outputFileName, + datasetId: argv.projectId + }; + + if (argv.query) { + request.query = argv.query; + } else { + request.query = await read(argv); + } + + const response = await post(argv, '/v1/analytics/query', request); + print(response.data, argv); +}; diff --git a/package.json b/package.json index 9289a09..c17d724 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@lifeomic/cli", - "version": "9.6.0", + "version": "9.7.0", "description": "CLI for interacting with the LifeOmic PHC API.", "main": "lo.js", "author": "LifeOmic ", @@ -43,6 +43,7 @@ "stream-chain": "^2.1.0", "stream-csv-as-json": "^1.0.1", "stream-json": "^1.2.1", + "uuid": "^3.3.2", "yargs": "^12.0.5" }, "devDependencies": { diff --git a/test/unit/commands/data-lake-test.js b/test/unit/commands/data-lake-test.js new file mode 100644 index 0000000..546dfd6 --- /dev/null +++ b/test/unit/commands/data-lake-test.js @@ -0,0 +1,133 @@ +'use strict'; + +const uuid = require('uuid'); +const yargs = require('yargs'); +const sinon = require('sinon'); +const test = require('ava'); +const proxyquire = require('proxyquire'); + +const getStub = sinon.stub(); +const postStub = sinon.stub(); +const listStub = sinon.stub(); +const printSpy = sinon.spy(); +const readStub = sinon.stub(); +let callback; + +const queryCmd = proxyquire('../../../lib/cmds/data_lake_cmds/query', { + '../../api': { + post: postStub + }, + '../../print': (data, opts) => { + printSpy(data, opts); + callback(); + }, + '../../read': async () => readStub() +}); + +const listCmd = proxyquire('../../../lib/cmds/data_lake_cmds/list-queries', { + '../../api': { + list: listStub + }, + '../../print': (data, opts) => { + printSpy(data, opts); + callback(); + } +}); + +const getCmd = proxyquire('../../../lib/cmds/data_lake_cmds/get-query', { + '../../api': { + get: getStub + }, + '../../print': (data, opts) => { + printSpy(data, opts); + callback(); + } +}); + +test.afterEach.always(t => { + getStub.resetHistory(); + postStub.resetHistory(); + listStub.resetHistory(); + printSpy.resetHistory(); + readStub.resetHistory(); + callback = null; +}); + +test.serial.cb('The "data-lake-query" command should accept a query as an optional argument', t => { + const query = "SELECT sample_id, gene, impact, amino_acid_change, histology FROM variant WHERE tumor_site='breast'"; + const datasetId = uuid(); + const outputFileName = 'data-lake-test'; + + postStub.onFirstCall().returns({}); + + callback = () => { + t.is(postStub.callCount, 1); + t.is(postStub.getCall(0).args[1], '/v1/analytics/query'); + t.deepEqual(postStub.getCall(0).args[2], { + query: query, + datasetId: datasetId, + outputFileName: outputFileName + }); + t.is(printSpy.callCount, 1); + t.end(); + }; + + yargs.command(queryCmd).parse(`query ${datasetId} -q "${query}" -o ${outputFileName}`); +}); + +test.serial.cb('The "data-lake-query" command should accept a query from stdin', t => { + const query = "SELECT sample_id, gene, impact, amino_acid_change, histology FROM variant WHERE tumor_site='breast'"; + const datasetId = uuid(); + const outputFileName = 'data-lake-test'; + + postStub.onFirstCall().returns({}); + readStub.onFirstCall().returns(query); + + callback = () => { + t.is(postStub.callCount, 1); + t.is(postStub.getCall(0).args[1], '/v1/analytics/query'); + t.deepEqual(postStub.getCall(0).args[2], { + query: query, + datasetId: datasetId, + outputFileName: outputFileName + }); + t.is(printSpy.callCount, 1); + t.end(); + }; + + yargs.command(queryCmd).parse(`query ${datasetId} -o ${outputFileName}`); +}); + +test.serial.cb('The "data-lake-list-queries" should accept page-size and next-page-token', t => { + const datasetId = uuid(); + const pageSize = 30; + const nextPageToken = uuid(); + + listStub.onFirstCall().returns({}); + const expectedPath = `/v1/analytics/query?datasetId=${datasetId}&pageSize=${pageSize}&nextPageToken=${nextPageToken}`; + + callback = () => { + t.is(listStub.callCount, 1); + t.is(listStub.getCall(0).args[1], expectedPath); + t.is(printSpy.callCount, 1); + t.end(); + }; + + yargs.command(listCmd).parse(`list-queries ${datasetId} -n ${pageSize} -t ${nextPageToken}`); +}); + +test.serial.cb('The "data-lake-get-query" should add query-id to path', t => { + const queryId = uuid(); + + getStub.onFirstCall().returns({}); + const expectedPath = `/v1/analytics/query/${queryId}`; + + callback = () => { + t.is(getStub.callCount, 1); + t.is(getStub.getCall(0).args[1], expectedPath); + t.is(printSpy.callCount, 1); + t.end(); + }; + + yargs.command(getCmd).parse(`get-query ${queryId}`); +});