From 1c19611317e27efcd9547bd041cd79a7eb642bc6 Mon Sep 17 00:00:00 2001 From: Anugerah Erlaut Date: Wed, 31 Mar 2021 21:36:39 +0700 Subject: [PATCH 1/4] get samples --- src/api/routes/samples.js | 11 ++ src/specs/api.yaml | 141 +++++++++--------- src/specs/models/SampleResponse.v1.yaml | 13 ++ .../models/api-body-schemas/Sample.v1.yaml | 6 + 4 files changed, 103 insertions(+), 68 deletions(-) create mode 100644 src/api/routes/samples.js create mode 100644 src/specs/models/SampleResponse.v1.yaml create mode 100644 src/specs/models/api-body-schemas/Sample.v1.yaml diff --git a/src/api/routes/samples.js b/src/api/routes/samples.js new file mode 100644 index 000000000..432add1b7 --- /dev/null +++ b/src/api/routes/samples.js @@ -0,0 +1,11 @@ +const SamplesService = require('../route-services/samples'); + +const samplesService = new SamplesService(); + +module.exports = { + 'samples#get': (req, res, next) => { + samplesService.getSamples(req.params.experimentId) + .then((data) => res.json(data)) + .catch(next); + }, +}; diff --git a/src/specs/api.yaml b/src/specs/api.yaml index 740c405c9..06b4d8903 100644 --- a/src/specs/api.yaml +++ b/src/specs/api.yaml @@ -5,7 +5,7 @@ info: description: The main Node.js pipeline API. license: name: MIT - url: 'https://github.com/biomage-ltd/api/blob/master/LICENSE' + url: "https://github.com/biomage-ltd/api/blob/master/LICENSE" contact: name: Biomage Ltd. servers: @@ -26,10 +26,10 @@ paths: - heartbeat summary: API health check operationId: checkHealth - x-eov-operation-id: 'health#check' + x-eov-operation-id: health#check x-eov-operation-handler: routes/health responses: - '200': + "200": description: API is available. content: application/json: @@ -61,10 +61,10 @@ paths: /workResults: post: operationId: receiveWork - x-eov-operation-id: 'work#response' + x-eov-operation-id: work#response x-eov-operation-handler: routes/work requestBody: - description: The results from the execution of a work request, sent via SNS. + description: "The results from the execution of a work request, sent via SNS." required: true content: text/plain: @@ -77,14 +77,14 @@ paths: properties: {} examples: {} responses: - '200': - description: 'A JSON-parseable was received by the server, *irrespective of whether it was correct/acceptable or not*.' + "200": + description: "A JSON-parseable was received by the server, *irrespective of whether it was correct/acceptable or not*." content: text/plain: schema: type: string pattern: ok - '500': + "500": description: The data sent by the server could not be parsed as JSON. content: text/plain: @@ -104,10 +104,10 @@ paths: /pipelineResults: post: operationId: receivePipelineResponse - x-eov-operation-id: 'pipelines#response' + x-eov-operation-id: pipelines#response x-eov-operation-handler: routes/pipelines requestBody: - description: The results from the execution of a pipeline step, sent via SNS. + description: "The results from the execution of a pipeline step, sent via SNS." required: true content: text/plain: @@ -120,14 +120,14 @@ paths: properties: {} examples: {} responses: - '200': - description: 'A JSON-parseable was received by the server, *irrespective of whether it was correct/acceptable or not*.' + "200": + description: "A JSON-parseable was received by the server, *irrespective of whether it was correct/acceptable or not*." content: text/plain: schema: type: string pattern: ok - '500': + "500": description: The data sent by the server could not be parsed as JSON. content: text/plain: @@ -144,23 +144,23 @@ paths: tags: - work parameters: [] - '/experiments/{experimentId}': + "/experiments/{experimentId}": get: tags: - experiments summary: Get experiment details description: Returns the main details of the experiment. operationId: getExperimentById - x-eov-operation-id: 'experiment#findByID' + x-eov-operation-id: experiment#findByID x-eov-operation-handler: routes/experiment responses: - '200': - description: successful operation + "200": + description: "Fetch successful, response below" content: application/json: schema: $ref: ./models/api-body-schemas/Experiment.v1.yaml - '404': + "404": description: Experiment not found content: application/json: @@ -173,18 +173,18 @@ paths: in: path required: true description: ID of the experiment to find. - '/experiments/{experimentId}/cellSets': + "/experiments/{experimentId}/cellSets": get: tags: - experiments summary: Get cell sets for experiment description: Returns a hirearchical view of cell sets in the experiment. operationId: getExperimentCellSetsById - x-eov-operation-id: 'experiment#getCellSets' + x-eov-operation-id: experiment#getCellSets x-eov-operation-handler: routes/experiment responses: - '200': - description: 'Request successful, hierarchy returned below.' + "200": + description: "Request successful, hierarchy returned below." content: application/json: schema: @@ -194,7 +194,7 @@ paths: type: array items: $ref: ./models/api-body-schemas/CellSets.v1.yaml - '404': + "404": description: Experiment not found. content: application/json: @@ -208,12 +208,12 @@ paths: schema: type: string put: - summary: '' + summary: "" operationId: updateExperimentCellSetsById - x-eov-operation-id: 'experiment#updateCellSets' + x-eov-operation-id: experiment#updateCellSets x-eov-operation-handler: routes/experiment responses: - '200': + "200": description: Update to object in response successful. content: application/json: @@ -233,21 +233,21 @@ paths: - $ref: ./models/api-body-schemas/CellSets.v1.yaml tags: - experiments - '/experiments/{experimentId}/processingConfig': + "/experiments/{experimentId}/processingConfig": get: summary: Retrieve processing configuration description: Returns a hirearchical view of processing configuration used in the experiment. operationId: getExperimentProcessingConfigById - x-eov-operation-id: 'experiment#getProcessingConfig' + x-eov-operation-id: experiment#getProcessingConfig x-eov-operation-handler: routes/experiment responses: - '200': - description: 'Fetch successful, response below.' + "200": + description: "Fetch successful, response below." content: application/json: schema: $ref: ./models/ProcessingConfig.v1.yaml - '404': + "404": description: Experiment not found. content: application/json: @@ -263,12 +263,12 @@ paths: schema: type: string put: - summary: '' + summary: "" operationId: updateExperimentProcessingConfigById - x-eov-operation-id: 'experiment#updateProcessingConfig' + x-eov-operation-id: experiment#updateProcessingConfig x-eov-operation-handler: routes/experiment responses: - '200': + "200": description: Update to object in response successful. content: application/json: @@ -296,7 +296,7 @@ paths: description: Updates the keys specified by `name` with the body specified by `body`. A standard DynamoDB UpdateItem request should be called on the update. tags: - processing-config - '/experiments/{experimentId}/plots-tables/{plotUuid}': + "/experiments/{experimentId}/plots-tables/{plotUuid}": parameters: - schema: type: string @@ -309,24 +309,24 @@ paths: in: path required: true put: - summary: '' + summary: "" operationId: updatePlotTable - x-eov-operation-id: 'plots-tables#update' + x-eov-operation-id: plots-tables#update x-eov-operation-handler: routes/plots-tables responses: - '200': + "200": description: Update to object in response successful. content: application/json: schema: $ref: ./models/api-body-schemas/PlotTableConfig.v1.yaml - '201': + "201": description: New resource created. content: application/json: schema: $ref: ./models/api-body-schemas/PlotTableConfig.v1.yaml - '404': + "404": description: Invalid experiment ID specified. description: Updates a plot and table for a given experiment with the data specified. requestBody: @@ -336,12 +336,12 @@ paths: $ref: ./models/api-body-schemas/PlotTableConfig.v1.yaml description: The new configuration to update the old one by. get: - summary: '' + summary: "" operationId: getPlotTable - x-eov-operation-id: 'plots-tables#read' + x-eov-operation-id: plots-tables#read x-eov-operation-handler: routes/plots-tables responses: - '200': + "200": description: OK content: application/json: @@ -349,15 +349,15 @@ paths: $ref: ./models/api-body-schemas/PlotTableConfig.v1.yaml description: Reads a plot and table for a given experiment with the data specified. delete: - summary: '' + summary: "" operationId: deletePlotTable - x-eov-operation-id: 'plots-tables#delete' + x-eov-operation-id: plots-tables#delete x-eov-operation-handler: routes/plots-tables responses: - '200': + "200": description: OK description: Deletes a plot and table for a given experiment with the data specified. - '/experiments/{experimentId}/pipelines': + "/experiments/{experimentId}/pipelines": parameters: - schema: type: string @@ -367,10 +367,10 @@ paths: post: summary: Create a new pipeline for taks execution operationId: createNewPipeline - x-eov-operation-id: 'pipelines#create' + x-eov-operation-id: pipelines#create x-eov-operation-handler: routes/pipelines responses: - '200': + "200": description: OK content: application/json: @@ -379,37 +379,42 @@ paths: properties: {} description: This path will create a new pipeline that can run a state machine with different bioinformatics tasks. get: - summary: '' + summary: "" operationId: get-experiments-experimentId-pipelines - x-eov-operation-id: 'pipelines#get' + x-eov-operation-id: pipelines#get x-eov-operation-handler: routes/pipelines responses: - '200': + "200": description: OK content: application/json: schema: type: object properties: {} - '/experiments/{experimentId}/mitochondrialContent': + "/experiments/{experimentId}/samples": parameters: - schema: type: string name: experimentId in: path required: true - '/experiments/{experimentId}/doubletScores': - parameters: - - schema: - type: string - name: experimentId - in: path - required: true -components: - schemas: - dummy: - title: dummy - type: object - properties: - id: - type: string + get: + summary: Your GET endpoint + operationId: getSamples + x-eov-operation-id: samples#get + x-eov-operation-handler: routes/samples + tags: [] + responses: + "200": + description: "Fetch successful, samples returned below." + content: + application/json: + schema: + $ref: ./models/SampleResponse.v1.yaml + "400": + description: Samples not found. + content: + application/json: + schema: + $ref: ./models/HTTPError.v1.yaml + description: Get all samples for an experiment diff --git a/src/specs/models/SampleResponse.v1.yaml b/src/specs/models/SampleResponse.v1.yaml new file mode 100644 index 000000000..b101431f3 --- /dev/null +++ b/src/specs/models/SampleResponse.v1.yaml @@ -0,0 +1,13 @@ +title: SampleResponse.v1 +type: object +description: Response containing the samples +properties: + experimentId: + type: string + samples: + type: object + properties: + ids: + type: array + items: + type: string diff --git a/src/specs/models/api-body-schemas/Sample.v1.yaml b/src/specs/models/api-body-schemas/Sample.v1.yaml new file mode 100644 index 000000000..af7e1d89c --- /dev/null +++ b/src/specs/models/api-body-schemas/Sample.v1.yaml @@ -0,0 +1,6 @@ +type: object +title: '' +description: Schema for Samples object +properties: + experimentId: + type: string From 6b8adfc65bd41fa4c5b2d5762033cbe08ab2f280 Mon Sep 17 00:00:00 2001 From: Anugerah Erlaut Date: Wed, 31 Mar 2021 21:48:49 +0700 Subject: [PATCH 2/4] modify API response for samples --- src/specs/models/SampleResponse.v1.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/specs/models/SampleResponse.v1.yaml b/src/specs/models/SampleResponse.v1.yaml index b101431f3..393b180a9 100644 --- a/src/specs/models/SampleResponse.v1.yaml +++ b/src/specs/models/SampleResponse.v1.yaml @@ -2,12 +2,14 @@ title: SampleResponse.v1 type: object description: Response containing the samples properties: - experimentId: - type: string samples: type: object + required: + - ids properties: ids: type: array items: type: string +required: + - samples From 27f3642071af6513b6d11ddbb949ddfa07fb3c97 Mon Sep 17 00:00:00 2001 From: Anugerah Erlaut Date: Wed, 31 Mar 2021 22:52:32 +0700 Subject: [PATCH 3/4] add tests for samples --- src/api/route-services/__mocks__/samples.js | 25 ++++++++ tests/api/route-services/samples.test.js | 68 +++++++++++++++++++++ tests/api/routes/samples.test.js | 42 +++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 src/api/route-services/__mocks__/samples.js create mode 100644 tests/api/route-services/samples.test.js create mode 100644 tests/api/routes/samples.test.js diff --git a/src/api/route-services/__mocks__/samples.js b/src/api/route-services/__mocks__/samples.js new file mode 100644 index 000000000..e65346587 --- /dev/null +++ b/src/api/route-services/__mocks__/samples.js @@ -0,0 +1,25 @@ +const mockGetSamples = jest.fn(() => new Promise((resolve) => { + resolve({ + samples: { + ids: ['sample-1'], + 'sample-1': { + name: 'sample-1', + }, + }, + }); +})); + +const mockGetSampleIds = jest.fn(() => new Promise((resolve) => { + resolve({ + samples: { + ids: ['sample-1', 'sample-2'], + }, + }); +})); + +const mock = jest.fn().mockImplementation(() => ({ + getSamples: mockGetSamples, + getSampleIds: mockGetSampleIds, +})); + +module.exports = mock; diff --git a/tests/api/route-services/samples.test.js b/tests/api/route-services/samples.test.js new file mode 100644 index 000000000..69efafc1a --- /dev/null +++ b/tests/api/route-services/samples.test.js @@ -0,0 +1,68 @@ +const AWSMock = require('aws-sdk-mock'); +const AWS = require('../../../src/utils/requireAWS'); + +const SamplesService = require('../../../src/api/route-services/samples'); + +describe('tests for the samples service', () => { + afterEach(() => { + AWSMock.restore('DynamoDB'); + }); + + const mockDynamoGetItem = (jsData) => { + const dynamodbData = { + Item: AWS.DynamoDB.Converter.marshall(jsData), + }; + const getItemSpy = jest.fn((x) => x); + AWSMock.setSDKInstance(AWS); + AWSMock.mock('DynamoDB', 'getItem', (params, callback) => { + getItemSpy(params); + callback(null, dynamodbData); + }); + return getItemSpy; + }; + + it('Get samples works', async (done) => { + const jsData = { + samples: { + ids: ['sample-1'], + 'sample-1': { + name: 'sample-1', + }, + }, + }; + + const getItemSpy = mockDynamoGetItem(jsData); + + (new SamplesService()).getSamples('12345') + .then((data) => { + expect(data).toEqual(jsData); + expect(getItemSpy).toHaveBeenCalledWith({ + TableName: 'samples-test', + Key: { experimentId: { S: '12345' } }, + ProjectionExpression: 'samples', + }); + }) + .then(() => done()); + }); + + it('Get sampleIds works', async (done) => { + const jsData = { + samples: { + ids: ['sample-1', 'sample-2'], + }, + }; + + const getItemSpy = mockDynamoGetItem(jsData); + + (new SamplesService()).getSampleIds('12345') + .then((data) => { + expect(data).toEqual(jsData); + expect(getItemSpy).toHaveBeenCalledWith({ + TableName: 'samples-test', + Key: { experimentId: { S: '12345' } }, + ProjectionExpression: 'samples.ids', + }); + }) + .then(() => done()); + }); +}); diff --git a/tests/api/routes/samples.test.js b/tests/api/routes/samples.test.js new file mode 100644 index 000000000..9d020b9ec --- /dev/null +++ b/tests/api/routes/samples.test.js @@ -0,0 +1,42 @@ +const express = require('express'); +const request = require('supertest'); +const expressLoader = require('../../../src/loaders/express'); + +jest.mock('../../../src/api/route-services/samples'); + +describe('tests for samples route', () => { + let app = null; + + beforeEach(async () => { + const mockApp = await expressLoader(express()); + app = mockApp.app; + }); + + afterEach(() => { + /** + * Most important since b'coz of caching, the mocked implementations sometimes does not reset + */ + jest.resetModules(); + jest.restoreAllMocks(); + }); + + it('Get samples by experimentId works', async (done) => { + request(app) + .get('/v1/experiments/someId/samples') + .expect(200) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res.body).toEqual({ + samples: { + ids: ['sample-1'], + 'sample-1': { + name: 'sample-1', + }, + }, + }); + return done(); + }); + }); +}); From e4588c884f09dc147c7f01bc9cb2e40a7e36e879 Mon Sep 17 00:00:00 2001 From: Anugerah Erlaut Date: Thu, 1 Apr 2021 20:49:49 +0700 Subject: [PATCH 4/4] add auto keys to API schema --- ...ocessingConfigCellSizeDistribution.v1.yaml | 25 ++++++++++--------- .../ProcessingConfigDoubletScores.v1.yaml | 2 ++ ...ocessingConfigMitochondrialContent.v1.yaml | 2 ++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/specs/models/processing-config-bodies/ProcessingConfigCellSizeDistribution.v1.yaml b/src/specs/models/processing-config-bodies/ProcessingConfigCellSizeDistribution.v1.yaml index 21f76b4ab..09e2e7564 100644 --- a/src/specs/models/processing-config-bodies/ProcessingConfigCellSizeDistribution.v1.yaml +++ b/src/specs/models/processing-config-bodies/ProcessingConfigCellSizeDistribution.v1.yaml @@ -1,15 +1,5 @@ title: Cell Size Distribution type: object -properties: - enabled: - type: boolean - filterSettings: - type: object - properties: - minCellSize: - type: number - binStep: - type: number additionalProperties: type: object properties: @@ -20,7 +10,18 @@ additionalProperties: type: number binStep: type: number - +description: Processing config body for Cell Size Distribution filter. +properties: + enabled: + type: boolean + auto: + type: boolean + filterSettings: + type: object + properties: + minCellSize: + type: number + binStep: + type: number required: - filterSettings -description: Processing config body for Cell Size Distribution filter. \ No newline at end of file diff --git a/src/specs/models/processing-config-bodies/ProcessingConfigDoubletScores.v1.yaml b/src/specs/models/processing-config-bodies/ProcessingConfigDoubletScores.v1.yaml index 05b00e573..62549d1ca 100644 --- a/src/specs/models/processing-config-bodies/ProcessingConfigDoubletScores.v1.yaml +++ b/src/specs/models/processing-config-bodies/ProcessingConfigDoubletScores.v1.yaml @@ -3,6 +3,8 @@ type: object properties: enabled: type: boolean + auto: + type: boolean filterSettings: type: object properties: diff --git a/src/specs/models/processing-config-bodies/ProcessingConfigMitochondrialContent.v1.yaml b/src/specs/models/processing-config-bodies/ProcessingConfigMitochondrialContent.v1.yaml index 6f7c14a45..9c40e6800 100644 --- a/src/specs/models/processing-config-bodies/ProcessingConfigMitochondrialContent.v1.yaml +++ b/src/specs/models/processing-config-bodies/ProcessingConfigMitochondrialContent.v1.yaml @@ -3,6 +3,8 @@ type: object properties: enabled: type: boolean + auto: + type: boolean filterSettings: type: object required: