From 0572b984f96f1b96bbd3c65f4c278f3eed536cce Mon Sep 17 00:00:00 2001 From: Sarah Allen Date: Mon, 19 Aug 2019 15:39:25 -0500 Subject: [PATCH 1/2] Reorganize READMEs. Support query env for POST and PUT --- packages/lib-panoptes-js/README.md | 2 +- packages/lib-panoptes-js/docs/CHANGELOG.md | 2 +- .../lib-panoptes-js/{docs => src}/README.md | 46 +++++++++++---- packages/lib-panoptes-js/src/panoptes.js | 56 ++++++++++--------- packages/lib-panoptes-js/src/panoptes.spec.js | 56 +++++++++++++++++-- .../resources/media/README.md} | 0 .../resources/projects/README.md} | 0 .../resources/subjects/README.md} | 0 .../resources/tutorials/README.md} | 0 .../resources/users/README.md} | 0 10 files changed, 116 insertions(+), 46 deletions(-) rename packages/lib-panoptes-js/{docs => src}/README.md (74%) rename packages/lib-panoptes-js/{docs/media.md => src/resources/media/README.md} (100%) rename packages/lib-panoptes-js/{docs/projects.md => src/resources/projects/README.md} (100%) rename packages/lib-panoptes-js/{docs/subjects.md => src/resources/subjects/README.md} (100%) rename packages/lib-panoptes-js/{docs/tutorials.md => src/resources/tutorials/README.md} (100%) rename packages/lib-panoptes-js/{docs/users.md => src/resources/users/README.md} (100%) diff --git a/packages/lib-panoptes-js/README.md b/packages/lib-panoptes-js/README.md index 2c136242ca..bd8b2ddf4f 100644 --- a/packages/lib-panoptes-js/README.md +++ b/packages/lib-panoptes-js/README.md @@ -32,7 +32,7 @@ import { panoptes } from '@zooniverse/panoptes-js'; ## Documentation -Full API documentation is avialable at [](). +The repository contains readme files under each sub-folder. ## Tests diff --git a/packages/lib-panoptes-js/docs/CHANGELOG.md b/packages/lib-panoptes-js/docs/CHANGELOG.md index e180efb1d3..6638658b85 100644 --- a/packages/lib-panoptes-js/docs/CHANGELOG.md +++ b/packages/lib-panoptes-js/docs/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added - Base helpers for HTTP requests GET, PUT, POST, and DELETE - Client configuration file for which API host to use depending on environment -- Project resource request helpers +- Project, Subject, Tutorial, and Collection resource request helpers - Media resource test mocks - User resource test mocks - Tests diff --git a/packages/lib-panoptes-js/docs/README.md b/packages/lib-panoptes-js/src/README.md similarity index 74% rename from packages/lib-panoptes-js/docs/README.md rename to packages/lib-panoptes-js/src/README.md index 4961504bce..bbc1c25b54 100644 --- a/packages/lib-panoptes-js/docs/README.md +++ b/packages/lib-panoptes-js/src/README.md @@ -2,11 +2,13 @@ All of the following examples will be using ES6. -## Configuring your environment +The client has a base set of request functions built on top of superagent for HTTP GET, POST, PUT, and DELETE. Each function returns a promise that returns the request response. The client does not do error handling and that is up to the consuming script or app either by using a catch on the promise or wrapping async/await in a try/catch statement. The function will error, though, if an argument is an incorrect type or if a parameter is missing that is necessary to the request. -There are currently options for running the client against the staging and production environments, which determine which endpoints are used for requests handled by the module. There are a few different ways to set which you want to use - **staging is the default environment**. +## Request Environment and Host -Note: There is a test environment specified in the config file, but it is set to use the same API hosts as staging. The test environment is specifically for automated test running. +The environment used cascades from the environment set via query param, then node process environment variable, then to a default of `'staging'`. Depending on the environment, an appropriate Panoptes host is used. For the environments of `'test'`, `'development'`, `'staging'`, the host `'https://panoptes-staging.zooniverse.org/api'` is used. For `'production'`, the host `'https://www.zooniverse.org/api'` is used. + +Each of the request functions will take a query param for `env` which then will return the correctly matched host and use that as the destination to make the HTTP request. The `env` param then gets deleted from the param object before it is then passed along as params to the request itself. The currently set environment is available from the config file exported as `env`. @@ -28,6 +30,17 @@ If you're running an app using hash history, you'll need to add `?env=` before t http://localhost:3000?env=production#/classify ``` +Then in your app, you'll have to retrieve the environment set in the window.location.search string and pass the environment into the request in the query params argument. A package like `query-string` is helpful for parsing the search string. + + +``` javascript +import panoptes from '@zoonivere/panoptes-js'; +import queryString from 'query-string'; + +const { env } = queryString.parse(window.location.search); +const response = panoptes.get('/projects', { env }) +``` + ### Setting the environment via `PANOPTES_ENV` This lets you choose a Panoptes environment in isolation from your Node environment, so you can use the production Panoptes API in development, for example. @@ -56,6 +69,10 @@ The available host configurations are: host: 'https://panoptes-staging.zooniverse.org/api', oauth: 'https://panoptes-staging.zooniverse.org' }, + development: { + host: 'https://panoptes-staging.zooniverse.org/api', + oauth: 'https://panoptes-staging.zooniverse.org' + }, staging: { host: 'https://panoptes-staging.zooniverse.org/api', oauth: 'https://panoptes-staging.zooniverse.org' @@ -106,7 +123,7 @@ panoptes.get(endpoint, query, authorization, host) **Example** -Get many projects: +Get next page of projects: ``` javascript panoptes.get('/projects', { page: 2 }).then((response) => { @@ -127,7 +144,7 @@ panoptes.get('/projects/1104', { include: 'avatar,background,owners' }).then((re **Function** ``` javascript -panoptes.post(endpoint, data, authorization, host) +panoptes.post(endpoint, data, authorization, query, host) ``` **Arguments** @@ -135,6 +152,7 @@ panoptes.post(endpoint, data, authorization, host) - endpoint _(string)_ - the API endpoint for the request. Required. - data _(object)_ - an object of data to send with the request. Optional. - authorization _(string)_ - a string of the authorization type and token, i.e. `'Bearer 12345abcde'`. Optional. +- query _(object)_ - an object of query parameters to send with the request. Optional. - host _(string)_ - available to specify a different API host. Defaults to the hosts defined in the `config.js` file. Optional. **Returns** @@ -156,14 +174,15 @@ panoptes.get('/projects', { private: true }).then((response) => { **Function** ``` javascript -panoptes.post(endpoint, data, authorization, host) +panoptes.put(endpoint, data, authorization, query, host) ``` **Arguments** - endpoint _(string)_ - the API endpoint for the request. Required. -- data _(object)_ - an object of data to send with the request. Optional. +- data _(object)_ - an object of data to send with the request. Required. - authorization _(string)_ - a string of the authorization type and token, i.e. `'Bearer 12345abcde'`. Optional. +- query _(object)_ - an object of query parameters to send with the request. Optional. - host _(string)_ - available to specify a different API host. Defaults to the hosts defined in the `config.js` file. Optional. **Returns** @@ -175,7 +194,7 @@ panoptes.post(endpoint, data, authorization, host) Update a project: ``` javascript -panoptes.put('/projects/1104', { display_name: 'Super Zoo' }).then((response) => { +panoptes.put('/projects/1104', { projects: { display_name: 'Super Zoo' }}, { authorization: 'Bearer 12345' }).then((response) => { // Do something with the response }); ``` @@ -212,9 +231,12 @@ panoptes.del('/projects/1104').then((response) => { Using helper functions for a defined Panoptes resource in a React component. These resources have functions defined: -- [Projects](projects.md) -- [Subjects](subjects.md) -- [Tutorials](tutorials.md) +- Collections +- Projects +- Subjects +- Tutorials + +A readme on specific use is available in the folder for each resource type. The API for resource helpers will include: @@ -263,4 +285,4 @@ class MyComponent extends React.Component { ) } } -``` +``` \ No newline at end of file diff --git a/packages/lib-panoptes-js/src/panoptes.js b/packages/lib-panoptes-js/src/panoptes.js index a064c22d00..88ef070a8a 100644 --- a/packages/lib-panoptes-js/src/panoptes.js +++ b/packages/lib-panoptes-js/src/panoptes.js @@ -24,79 +24,83 @@ function determineHost (query, host) { return config.host } +function getQueryParams (query) { + const defaultParams = { admin: checkForAdminFlag(), http_cache: true } + + if (query && Object.keys(query).length > 0) { + if (query && query.env) delete query.env + const fullQuery = Object.assign({}, query, defaultParams) + return fullQuery + } else { + return defaultParams + } +} + // TODO: Consider how to integrate a GraphQL option function get (endpoint, query = {}, headers = {}, host) { - const defaultParams = { admin: checkForAdminFlag(), http_cache: true } if (!endpoint) return handleMissingParameter('Request needs a defined resource endpoint') + if (typeof query !== 'object') return Promise.reject(new TypeError('Query must be an object')) + const apiHost = determineHost(query, host) const request = superagent.get(`${apiHost}${endpoint}`) .set('Content-Type', 'application/json') .set('Accept', 'application/vnd.api+json; version=1') if (headers && headers.authorization) request.set('Authorization', headers.authorization) + const queryParams = getQueryParams(query) - if (query && Object.keys(query).length > 0) { - if (typeof query !== 'object') return Promise.reject(new TypeError('Query must be an object')) - if (query && query.env) delete query.env - const fullQuery = Object.assign({}, query, defaultParams) - request.query(fullQuery) - } else { - request.query(defaultParams) - } - - return request.then(response => response) + return request.query(queryParams).then(response => response) } -// TODO support env query -function post (endpoint, data, headers = {}, host) { - const defaultParams = { admin: checkForAdminFlag(), http_cache: true } - +function post(endpoint, data, headers = {}, query = {}, host) { if (!endpoint) return handleMissingParameter('Request needs a defined resource endpoint') - const apiHost = host || config.host + if (typeof query !== 'object') return Promise.reject(new TypeError('Query must be an object')) + const apiHost = determineHost(query, host) const request = superagent.post(`${apiHost}${endpoint}`) .set('Content-Type', 'application/json') .set('Accept', 'application/vnd.api+json; version=1') if (headers && headers.authorization) request.set('Authorization', headers.authorization) + const queryParams = getQueryParams(query) - return request.query(defaultParams) + return request.query(queryParams) .send(data) .then(response => response) } -// TODO: support env query -function put (endpoint, data, headers = {}, host) { - const defaultParams = { admin: checkForAdminFlag(), http_cache: true } +function put(endpoint, data, headers = {}, query = {}, host) { if (!endpoint) return handleMissingParameter('Request needs a defined resource endpoint') if (!data) return handleMissingParameter('Request needs a defined data for update') - const apiHost = host || config.host + if (typeof query !== 'object') return Promise.reject(new TypeError('Query must be an object')) + const apiHost = determineHost(query, host) const request = superagent.put(`${apiHost}${endpoint}`) .set('Content-Type', 'application/json') .set('Accept', 'application/vnd.api+json; version=1') if (headers && headers.authorization) request.set('Authorization', headers.authorization) if (headers && headers.etag) request.set('If-Match', headers.etag) + const queryParams = getQueryParams(query) - return request.query(defaultParams) + return request.query(queryParams) .send(data) .then(response => response) } function del (endpoint, query = {}, headers = {}, host) { - const defaultParams = { admin: checkForAdminFlag(), http_cache: true } - if (!endpoint) return handleMissingParameter('Request needs a defined resource endpoint') - const apiHost = determineHost(query, host) + if (typeof query !== 'object') return Promise.reject(new TypeError('Query must be an object')) + const apiHost = determineHost(query, host) const request = superagent.delete(`${apiHost}${endpoint}`) .set('Content-Type', 'application/json') .set('Accept', 'application/vnd.api+json; version=1') if (headers && headers.authorization) request.set('Authorization', headers.authorization) + const queryParams = getQueryParams(query) - return request.query(defaultParams) + return request.query(queryParams) .then(response => response) } diff --git a/packages/lib-panoptes-js/src/panoptes.spec.js b/packages/lib-panoptes-js/src/panoptes.spec.js index 26c4d564f1..53527738fc 100644 --- a/packages/lib-panoptes-js/src/panoptes.spec.js +++ b/packages/lib-panoptes-js/src/panoptes.spec.js @@ -1,7 +1,7 @@ const { expect } = require('chai') const nock = require('nock') -const { config } = require('./config') +const { baseConfig, config } = require('./config') const panoptes = require('./panoptes') describe('panoptes.js', function () { @@ -28,6 +28,7 @@ describe('panoptes.js', function () { testAcceptHeader('get', endpoint) testAuthHeader('get', endpoint) testHttpCache('get', endpoint) + testEnvParam('get', endpoint, expectedResponse) testAdminParam('get', endpoint) testNoEndpoint('get') @@ -69,13 +70,14 @@ describe('panoptes.js', function () { testAcceptHeader('post', endpoint) testAuthHeader('post', endpoint) testHttpCache('post', endpoint) + testEnvParam('post', endpoint, expectedResponse) testAdminParam('post', endpoint) testNoEndpoint('post') it('should send any data params if defined', async function () { - const params = { display_name: 'My project' } - const response = await panoptes.post(endpoint, params) - expect(response.request._data).to.deep.equal(params) + const data = { display_name: 'My project' } + const response = await panoptes.post(endpoint, data) + expect(response.request._data).to.deep.equal(data) }) }) @@ -103,6 +105,7 @@ describe('panoptes.js', function () { testAcceptHeader('put', endpoint, update) testAuthHeader('put', endpoint, update) testHttpCache('put', endpoint, update) + testEnvParam('put', endpoint, expectedResponse, update) testAdminParam('put', endpoint, update) testNoEndpoint('put') @@ -141,8 +144,22 @@ describe('panoptes.js', function () { testAcceptHeader('del', endpoint) testAuthHeader('del', endpoint) testHttpCache('del', endpoint) + testEnvParam('del', endpoint, expectedResponse) testAdminParam('del', endpoint) testNoEndpoint('del') + + it('should use the env query param to set the host only', async function () { + const queryParams = { env: 'production' } + const { host } = baseConfig[queryParams.env] + + nock(host).delete(uri => uri.includes(endpoint)) + .query(true) + .reply(200, expectedResponse) + + const response = await panoptes.del(endpoint, queryParams) + expect(response.request.url.includes(host)).to.be.true() + expect(response.req.path.includes('env=production')).to.be.false() + }) }) function testExpectedResponse (method, endpoint, expectedResponse, update = null) { @@ -152,14 +169,18 @@ describe('panoptes.js', function () { }) } - function testHostArg (method, endpoint, expectedResponse, update = null) { + function testHostArg (method, endpoint, expectedResponse, update = null, query = null) { it('should use the host from the function call if defined', async function () { const mockAPIHost = 'https://my-api.com' const isDel = method === 'del' + const isPost = method === 'post' + const isPut = method === 'put' // Nock calls it 'delete', panoptes-js calls it 'del' const nockMethod = isDel ? 'delete' : method - const methodArgs = [endpoint, update, null, mockAPIHost] + const methodArgs = (isPost || isPut) ? + [endpoint, update, null, query, mockAPIHost] : + [endpoint, update, null, mockAPIHost] nock(mockAPIHost)[nockMethod](uri => uri.includes(endpoint)) .query(true) @@ -208,6 +229,29 @@ describe('panoptes.js', function () { }) } + function testEnvParam (method, endpoint, expectedResponse) { + it('should use the env query param to set the host only', async function () { + const envParams = { env: 'production' } + const { host } = baseConfig[envParams.env] + const isDel = method === 'del' + const isPost = method === 'post' + const isPut = method === 'put' + // Nock calls it 'delete', panoptes-js calls it 'del' + const nockMethod = isDel ? 'delete' : method + const methodArgs = (isPost || isPut) ? + [endpoint, {}, '', envParams] : + [endpoint, envParams] + + nock(host)[nockMethod](uri => uri.includes(endpoint)) + .query(true) + .reply(200, expectedResponse) + + const response = await panoptes[method].apply(this, methodArgs) + expect(response.request.url.includes(host)).to.be.true() + expect(response.req.path.includes('env=production')).to.be.false() + }) + } + function testAdminParam (method, endpoint, update = null) { it('should add the admin default query param if flag is found in local storage', async function () { localStorage.setItem('adminFlag', true) diff --git a/packages/lib-panoptes-js/docs/media.md b/packages/lib-panoptes-js/src/resources/media/README.md similarity index 100% rename from packages/lib-panoptes-js/docs/media.md rename to packages/lib-panoptes-js/src/resources/media/README.md diff --git a/packages/lib-panoptes-js/docs/projects.md b/packages/lib-panoptes-js/src/resources/projects/README.md similarity index 100% rename from packages/lib-panoptes-js/docs/projects.md rename to packages/lib-panoptes-js/src/resources/projects/README.md diff --git a/packages/lib-panoptes-js/docs/subjects.md b/packages/lib-panoptes-js/src/resources/subjects/README.md similarity index 100% rename from packages/lib-panoptes-js/docs/subjects.md rename to packages/lib-panoptes-js/src/resources/subjects/README.md diff --git a/packages/lib-panoptes-js/docs/tutorials.md b/packages/lib-panoptes-js/src/resources/tutorials/README.md similarity index 100% rename from packages/lib-panoptes-js/docs/tutorials.md rename to packages/lib-panoptes-js/src/resources/tutorials/README.md diff --git a/packages/lib-panoptes-js/docs/users.md b/packages/lib-panoptes-js/src/resources/users/README.md similarity index 100% rename from packages/lib-panoptes-js/docs/users.md rename to packages/lib-panoptes-js/src/resources/users/README.md From f46a7e57d993fe7ad257ef46002169876c8d6a0d Mon Sep 17 00:00:00 2001 From: Sarah Allen Date: Mon, 19 Aug 2019 16:02:21 -0500 Subject: [PATCH 2/2] Remove tutorial include req --- .../src/resources/tutorials/README.md | 43 ------------- .../tutorials/commonRequests.spec.js | 64 ------------------- .../src/resources/tutorials/index.js | 3 +- 3 files changed, 1 insertion(+), 109 deletions(-) diff --git a/packages/lib-panoptes-js/src/resources/tutorials/README.md b/packages/lib-panoptes-js/src/resources/tutorials/README.md index 923699d74e..a9dc565251 100644 --- a/packages/lib-panoptes-js/src/resources/tutorials/README.md +++ b/packages/lib-panoptes-js/src/resources/tutorials/README.md @@ -7,7 +7,6 @@ [Other common requests](#common-requests) - [Get Attached Images](#get-attached-images) -- [Get with Images](#get-with-images) - [Get Tutorials](#get-tutorials) - [Get Mini-courses](#get-mini-courses) @@ -107,48 +106,6 @@ tutorials.getAttachedImages({ id: '10' }).then((response) => { }); ``` -### Get with Images - -A tutorials get request uses a default include query param for attached images. It then uses the tutorials' [`get`](#get request helper so this also requires either a tutorial id or workflow id. Note, there is a known bug with the include request, so this will not return what we expect at the moment: - -https://github.com/zooniverse/Panoptes/issues/2279 - -**Function** - -``` javascript -const params = { id: '1', authorization: authorization }; - -tutorials.getWithImages(params) - -// or with additional query params -const params = { workflowId: '1000', query: { page: '2' } } - -tutorials.getWithImages(params) -``` - -**Arguments** - -- params _(object)_ - An object that should include the tutorial id _(string)_ or workflow id _(string)_ and optionally additional query params. Also can take an authorization _(string)_ property that must be set to a string including type and token, i.e. `{ authorization: 'Bearer 12345' }`. - -**Returns** - -- Promise _(object)_ resolves to the API response with the resource, meta, links, and linked media resources or the request error. - -**Example** - -``` javascript -// Get request -tutorials.getWithImages({ id: '1' }).then((response) => { - // The media will have to be matched up with the step it is for... - this.setState({ - tutorial: response.body.tutorials[0], - tutorialMedia: response.body.linked - }); -}).catch((error) => { - if (error.statusCode === 404) return null; // If you don't care about catching a 404 -}); -``` - ### Get Tutorials A tutorials get request that filters the response to only be tutorials with the property `kind` set to `tutorial` or null values (for backwards compatibility). It then uses the tutorials' [`get`](#get) request helper so this also requires either a tutorial id or workflow id. diff --git a/packages/lib-panoptes-js/src/resources/tutorials/commonRequests.spec.js b/packages/lib-panoptes-js/src/resources/tutorials/commonRequests.spec.js index 37046d4ab6..e02fdb25ae 100644 --- a/packages/lib-panoptes-js/src/resources/tutorials/commonRequests.spec.js +++ b/packages/lib-panoptes-js/src/resources/tutorials/commonRequests.spec.js @@ -47,70 +47,6 @@ describe('Tutorials resource common requests', function () { }) }) - describe('getWithImages', function () { - describe('a single tutorial', function () { - const expectedGetResponse = responses.get.tutorialWithImages - const scope = nock(config.host) - - beforeEach(function () { - scope - .get(`${endpoint}/1`) - .query(true) - .reply(200, expectedGetResponse) - }) - - after(function () { - nock.cleanAll() - }) - - it('should use a default include query param', async function () { - const response = await tutorials.getWithImages({ id: '1' }) - expect(response.req.path.includes('include=attached_images')).to.be.true() - }) - - it('should include any other query params if defined', async function () { - const response = await tutorials.getWithImages({ id: '1', query: { page: '2' } }) - expect(response.req.path.includes('page=2')).to.be.true() - }) - - it('should return the expected response', async function () { - const response = await tutorials.getWithImages({ id: '1' }) - expect(response.body).to.eql(expectedGetResponse) - }) - }) - - describe('many tutorials', function () { - const expectedGetResponse = responses.get.tutorialsWithImages - const scope = nock(config.host) - - beforeEach(function () { - scope - .get(endpoint) - .query(true) - .reply(200, expectedGetResponse) - }) - - after(function () { - nock.cleanAll() - }) - - it('should use a default include query param', async function () { - const response = await tutorials.getWithImages({ workflowId: '10' }) - expect(response.req.path.includes('include=attached_images')).to.be.true() - }) - - it('should include any other query params if defined', async function () { - const response = await tutorials.getWithImages({ workflowId: '10', query: { page: '2' } }) - expect(response.req.path.includes('page=2')).to.be.true() - }) - - it('should return the expected response', async function () { - const response = await tutorials.getWithImages({ workflowId: '10' }) - expect(response.body).to.eql(expectedGetResponse) - }) - }) - }) - describe('getTutorials', function () { describe('by tutorial id', function () { const scope = nock(config.host) diff --git a/packages/lib-panoptes-js/src/resources/tutorials/index.js b/packages/lib-panoptes-js/src/resources/tutorials/index.js index 0cf3c28329..d3d7b845f2 100644 --- a/packages/lib-panoptes-js/src/resources/tutorials/index.js +++ b/packages/lib-panoptes-js/src/resources/tutorials/index.js @@ -1,5 +1,5 @@ const { create, get, update, del } = require('./rest') -const { getAttachedImages, getMinicourses, getTutorials, getWithImages } = require('./commonRequests') +const { getAttachedImages, getMinicourses, getTutorials } = require('./commonRequests') const { endpoint } = require('./helpers') const mocks = require('./mocks') @@ -11,7 +11,6 @@ module.exports = { getAttachedImages, getMinicourses, getTutorials, - getWithImages, endpoint, mocks }