From 3ebea166b2099b1d731e50b1f225d7c2f25d8abc Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Thu, 28 Feb 2019 09:24:22 +0000 Subject: [PATCH] feat(http2): basic HTTP2 support for event-stream (#1009) --- package.json | 5 +- ...ostGraphileHttpRequestHandler-test.js.snap | 48 +++ ...eatePostGraphileHttpRequestHandler-test.js | 99 ++++-- .../http/__tests__/supertest/index.ts | 35 ++ .../http/__tests__/supertest/lib/agent.ts | 65 ++++ .../http/__tests__/supertest/lib/test.ts | 330 ++++++++++++++++++ .../http/setupServerSentEvents.ts | 7 +- src/postgraphile/postgraphile.ts | 2 +- yarn.lock | 99 +++--- 9 files changed, 624 insertions(+), 66 deletions(-) create mode 100644 src/postgraphile/http/__tests__/supertest/index.ts create mode 100644 src/postgraphile/http/__tests__/supertest/lib/agent.ts create mode 100644 src/postgraphile/http/__tests__/supertest/lib/test.ts diff --git a/package.json b/package.json index 12dde7599f..18fac0cf25 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "prettier": "^1.15.2", "source-map-support": "^0.4.6", "style-loader": "^0.23.0", - "supertest": "^2.0.1", + "superagent": "^4.1.0", "ts-node": "^2.0.0", "tslint": "^5.10.0", "tslint-config-prettier": "^1.14.0", @@ -112,7 +112,8 @@ }, "moduleFileExtensions": [ "ts", - "js" + "js", + "json" ], "setupFiles": [ "/resources/jest-setup.js" diff --git a/src/postgraphile/http/__tests__/__snapshots__/createPostGraphileHttpRequestHandler-test.js.snap b/src/postgraphile/http/__tests__/__snapshots__/createPostGraphileHttpRequestHandler-test.js.snap index 7773f4317f..8b7259317e 100644 --- a/src/postgraphile/http/__tests__/__snapshots__/createPostGraphileHttpRequestHandler-test.js.snap +++ b/src/postgraphile/http/__tests__/__snapshots__/createPostGraphileHttpRequestHandler-test.js.snap @@ -238,6 +238,54 @@ Array [ ] `; +exports[`fastify-http2 will report an extended error when extendedErrors is enabled 1`] = ` +Array [ + Object { + "detail": "test detail", + "errcode": "12345", + "extensions": Object { + "exception": Object { + "detail": "test detail", + "errcode": "12345", + "hint": "test hint", + }, + "testingExtensions": true, + }, + "hint": "test hint", + "locations": Array [ + Object { + "column": 2, + "line": 1, + }, + ], + "message": "test message", + "path": Array [ + "testError", + ], + }, +] +`; + +exports[`fastify-http2 will report standard error when extendedErrors is not enabled 1`] = ` +Array [ + Object { + "extensions": Object { + "testingExtensions": true, + }, + "locations": Array [ + Object { + "column": 2, + "line": 1, + }, + ], + "message": "test message", + "path": Array [ + "testError", + ], + }, +] +`; + exports[`http will report an extended error when extendedErrors is enabled 1`] = ` Array [ Object { diff --git a/src/postgraphile/http/__tests__/createPostGraphileHttpRequestHandler-test.js b/src/postgraphile/http/__tests__/createPostGraphileHttpRequestHandler-test.js index 694213b4db..2a60adb2a6 100644 --- a/src/postgraphile/http/__tests__/createPostGraphileHttpRequestHandler-test.js +++ b/src/postgraphile/http/__tests__/createPostGraphileHttpRequestHandler-test.js @@ -1,15 +1,20 @@ import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql'; import { $$pgClient } from '../../../postgres/inventory/pgClientFromContext'; import createPostGraphileHttpRequestHandler from '../createPostGraphileHttpRequestHandler'; +import request from './supertest'; + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const http = require('http'); -const request = require('supertest'); +const http2 = require('http2'); const connect = require('connect'); const express = require('express'); const compress = require('koa-compress'); const koa = require('koa'); const koaMount = require('koa-mount'); const fastify = require('fastify'); +// tslint:disable-next-line variable-name +const EventEmitter = require('events'); const shortString = 'User_Running_These_Tests'; // Default bodySizeLimit is 100kB @@ -130,33 +135,61 @@ const serverCreators = new Map([ return server; }, ], + [ + 'koa', + (handler, options = {}, subpath) => { + const app = new koa(); + if (options.onPreCreate) options.onPreCreate(app); + if (subpath) { + app.use(koaMount(subpath, handler)); + } else { + app.use(handler); + } + return http.createServer(app.callback()); + }, + ], + [ + 'fastify-http2', + async (handler, _options, subpath) => { + let server; + function serverFactory(fastlyHandler, opts) { + if (server) throw new Error('Factory called twice'); + server = http2.createServer({}, (req, res) => { + fastlyHandler(req, res); + }); + return server; + } + const app = fastify({ serverFactory, http2: true }); + if (subpath) { + throw new Error('Fastify does not support subpath at this time'); + } else { + app.use(handler); + } + await app.ready(); + if (!server) { + throw new Error('Fastify server not created!'); + } + server._http2 = true; + return server; + }, + ], ]); -serverCreators.set('koa', (handler, options = {}, subpath) => { - const app = new koa(); - if (options.onPreCreate) options.onPreCreate(app); - if (subpath) { - app.use(koaMount(subpath, handler)); - } else { - app.use(handler); - } - return http.createServer(app.callback()); -}); - const toTest = []; for (const [name, createServerFromHandler] of Array.from(serverCreators)) { toTest.push({ name, createServerFromHandler }); - if (name !== 'http' && name !== 'fastify') { + if (name !== 'http' && name !== 'fastify' && name !== 'fastify-http2') { toTest.push({ name, createServerFromHandler, subpath: '/path/to/mount' }); } } for (const { name, createServerFromHandler, subpath = '' } of toTest) { - const createServer = (handlerOptions, serverOptions) => - createServerFromHandler( + const createServer = async (handlerOptions, serverOptions) => { + const _emitter = new EventEmitter(); + const server = await createServerFromHandler( createPostGraphileHttpRequestHandler( Object.assign( - {}, + { _emitter }, subpath ? { externalUrlBase: subpath, @@ -169,6 +202,9 @@ for (const { name, createServerFromHandler, subpath = '' } of toTest) { serverOptions, subpath, ); + server._emitter = _emitter; + return server; + }; describe(name + (subpath ? ` (@${subpath})` : ''), () => { test('will 404 for route other than that specified 1', async () => { @@ -248,6 +284,7 @@ for (const { name, createServerFromHandler, subpath = '' } of toTest) { const server = await createServer({ enableCors: true }); await request(server) .post(`${subpath}/graphql`) + .expect(400) .expect('Access-Control-Allow-Origin', '*') .expect('Access-Control-Allow-Methods', 'HEAD, GET, POST') .expect('Access-Control-Allow-Headers', /Accept, Authorization, X-Apollo-Tracing/) @@ -852,6 +889,22 @@ for (const { name, createServerFromHandler, subpath = '' } of toTest) { .expect(405); }); + test('will return an event-stream', async () => { + const server = await createServer({ graphiql: true, watchPg: true }); + const promise = request(server) + .get(`${subpath}/graphql/stream`) + .set('Accept', 'text/event-stream') + .expect(200) + .expect('event: open\n\nevent: change\ndata: schema\n\n') + .then(res => res); // Trick superagent into finishing + await sleep(200); + server._emitter.emit('schemas:changed'); + await sleep(100); + server._emitter.emit('test:close'); + + return await promise; + }); + test('will render GraphiQL if enabled', async () => { const server1 = await createServer(); const server2 = await createServer({ graphiql: true }); @@ -1010,12 +1063,14 @@ for (const { name, createServerFromHandler, subpath = '' } of toTest) { .expect('Content-Type', /json/) .expect({ data: { hello: 'foo' } }); expect(additionalGraphQLContextFromRequest).toHaveBeenCalledTimes(1); - expect(additionalGraphQLContextFromRequest.mock.calls[0][0]).toBeInstanceOf( - http.IncomingMessage, - ); - expect(additionalGraphQLContextFromRequest.mock.calls[0][1]).toBeInstanceOf( - http.ServerResponse, - ); + const [req, res] = additionalGraphQLContextFromRequest.mock.calls[0]; + if (req.httpVersionMajor > 1) { + expect(req).toBeInstanceOf(http2.Http2ServerRequest); + expect(res).toBeInstanceOf(http2.Http2ServerResponse); + } else { + expect(req).toBeInstanceOf(http.IncomingMessage); + expect(res).toBeInstanceOf(http.ServerResponse); + } }); if (name === 'koa') { diff --git a/src/postgraphile/http/__tests__/supertest/index.ts b/src/postgraphile/http/__tests__/supertest/index.ts new file mode 100644 index 0000000000..95be6e1cd9 --- /dev/null +++ b/src/postgraphile/http/__tests__/supertest/index.ts @@ -0,0 +1,35 @@ +// tslint:disable no-any +import * as http from 'http'; +import Test from './lib/test'; +import agent from './lib/agent'; + +const methods = http.METHODS.map(m => m.toLowerCase()); + +/** + * Test against the given `app`, + * returning a new `Test`. + * + * @param {Function|Server} app + * @return {Test} + * @api public + */ +export default (app: any) => { + const obj: any = {}; + + if (typeof app === 'function') { + app = http.createServer(app); // eslint-disable-line no-param-reassign + } + + methods.forEach(method => { + obj[method] = (url: string) => { + return new Test(app, method, url); + }; + }); + + // Support previous use of del + obj.del = obj.delete; + + return obj; +}; + +export { Test, agent }; diff --git a/src/postgraphile/http/__tests__/supertest/lib/agent.ts b/src/postgraphile/http/__tests__/supertest/lib/agent.ts new file mode 100644 index 0000000000..c85ebebdeb --- /dev/null +++ b/src/postgraphile/http/__tests__/supertest/lib/agent.ts @@ -0,0 +1,65 @@ +// tslint:disable +/** + * Module dependencies. + */ + +import { agent as Agent } from 'superagent'; +import * as http from 'http'; +import Test from './test'; + +const methods = http.METHODS.map(m => m.toLowerCase()); + +/** + * Initialize a new `TestAgent`. + * + * @param {Function|Server} app + * @param {Object} options + * @api public + */ + +function TestAgent(app: any, options: any) { + // @ts-ignore + if (!(this instanceof TestAgent)) return new TestAgent(app, options); + if (typeof app === 'function') app = http.createServer(app); // eslint-disable-line no-param-reassign + if (options) { + this._ca = options.ca; + this._key = options.key; + this._cert = options.cert; + } + Agent.call(this); + this.app = app; +} + +/** + * Inherits from `Agent.prototype`. + */ + +Object.setPrototypeOf(TestAgent.prototype, Agent.prototype); + +// set a host name +TestAgent.prototype.host = function(host: any) { + this._host = host; + return this; +}; + +// override HTTP verb methods +methods.forEach(method => { + TestAgent.prototype[method] = function(url: any, _fn: any) { + // eslint-disable-line no-unused-vars + var req = new Test(this.app, method.toUpperCase(), url, this._host); + req.ca(this._ca); + req.cert(this._cert); + req.key(this._key); + + req.on('response', this._saveCookies.bind(this)); + req.on('redirect', this._saveCookies.bind(this)); + req.on('redirect', this._attachCookies.bind(this, req)); + this._attachCookies(req); + + return req; + }; +}); + +TestAgent.prototype.del = TestAgent.prototype.delete; + +export default TestAgent; diff --git a/src/postgraphile/http/__tests__/supertest/lib/test.ts b/src/postgraphile/http/__tests__/supertest/lib/test.ts new file mode 100644 index 0000000000..c69c09f574 --- /dev/null +++ b/src/postgraphile/http/__tests__/supertest/lib/test.ts @@ -0,0 +1,330 @@ +// tslint:disable +/** + * Module dependencies. + */ + +import * as request from 'superagent'; +import * as util from 'util'; +import * as http from 'http'; +import * as https from 'https'; +import * as assert from 'assert'; + +// @ts-ignore +const Request: any = request.Request; + +/** + * Initialize a new `Test` with the given `app`, + * request `method` and `path`. + * + * @param {Server} app + * @param {String} method + * @param {String} path + * @api public + */ + +class Test extends Request { + constructor(app: any, method: any, path: any, host?: any) { + super(method.toUpperCase(), path); + this._enableHttp2 = app._http2; + this.redirects(0); + this.buffer(); + this.app = app; + this._asserts = []; + this.url = typeof app === 'string' ? app + path : this.serverAddress(app, path, host); + + // Make awaiting work + const oldThen = this.then; + const donePromise = new Promise(resolve => { + this.completeCallback = resolve; + }); + let promise; + this.then = (cb, ecb) => { + if (!promise) + promise = oldThen.call( + this, + () => donePromise, + e => { + if (this.expectedStatus >= 400 && e.status === this.expectedStatus) { + return donePromise; + } else { + console.error(e); + return Promise.reject(e); + } + }, + ); + return promise.then(cb, ecb); + }; + } + /** + * Returns a URL, extracted from a server. + * + * @param {Server} app + * @param {String} path + * @returns {String} URL address + * @api private + */ + + serverAddress(app: any, path: any, host: any) { + var addr = app.address(); + var port; + var protocol; + + if (!addr) this._server = app.listen(0); + port = app.address().port; + protocol = app instanceof https.Server ? 'https' : 'http'; + return protocol + '://' + (host || '127.0.0.1') + ':' + port + path; + } + + /** + * Expectations: + * + * .expect(200) + * .expect(200, fn) + * .expect(200, body) + * .expect('Some body') + * .expect('Some body', fn) + * .expect('Content-Type', 'application/json') + * .expect('Content-Type', 'application/json', fn) + * .expect(fn) + * + * @return {Test} + * @api public + */ + + expect(a: any, b: any, c: any) { + // callback + if (typeof a === 'function') { + this._asserts.push(a); + return this; + } + if (typeof b === 'function') this.end(b); + if (typeof c === 'function') this.end(c); + + // status + if (typeof a === 'number') { + this.expectedStatus = a; + this._asserts.push(this._assertStatus.bind(this, a)); + // body + if (typeof b !== 'function' && arguments.length > 1) { + this._asserts.push(this._assertBody.bind(this, b)); + } + return this; + } + + // header field + if (typeof b === 'string' || typeof b === 'number' || b instanceof RegExp) { + this._asserts.push(this._assertHeader.bind(this, { name: '' + a, value: b })); + return this; + } + + // body + this._asserts.push(this._assertBody.bind(this, a)); + + return this; + } + + /** + * Defer invoking superagent's `.end()` until + * the server is listening. + * + * @param {Function} fn + * @api public + */ + + end(fn: any) { + var self = this; + var server = this._server; + var end = Request.prototype.end; + + end.call(this, (err: any, res: any) => { + if (server && server._handle) return server.close(localAssert); + + localAssert(); + + function localAssert() { + self.assert(err, res, fn); + } + }); + + return this; + } + + /** + * Perform assertions and invoke `fn(err, res)`. + * + * @param {?Error} resError + * @param {Response} res + * @param {Function} fn + * @api private + */ + + assert(resError: any, res: any, fn: any) { + this.completeCallback(res); + var error; + var i; + + // check for unexpected network errors or server not running/reachable errors + // when there is no response and superagent sends back a System Error + // do not check further for other asserts, if any, in such case + // https://nodejs.org/api/errors.html#errors_common_system_errors + var sysErrors = { + ECONNREFUSED: 'Connection refused', + ECONNRESET: 'Connection reset by peer', + EPIPE: 'Broken pipe', + ETIMEDOUT: 'Operation timed out', + }; + + if (!res && resError) { + if ( + resError instanceof Error && + resError['syscall'] === 'connect' && + Object.getOwnPropertyNames(sysErrors).indexOf(resError['code']) >= 0 + ) { + error = new Error(resError['code'] + ': ' + sysErrors[resError['code']]); + } else { + error = resError; + } + } + + // asserts + for (i = 0; i < this._asserts.length && !error; i += 1) { + error = this._assertFunction(this._asserts[i], res); + } + + // set unexpected superagent error if no other error has occurred. + if (!error && resError instanceof Error && (!res || resError['status'] !== res.status)) { + error = resError; + } + + fn.call(this, error || null, res); + } + + /** + * Perform assertions on a response body and return an Error upon failure. + * + * @param {Mixed} body + * @param {Response} res + * @return {?Error} + * @api private + */ + + _assertBody(body: any, res: any) { + var isregexp = body instanceof RegExp; + var a; + var b; + + // parsed + if (typeof body === 'object' && !isregexp) { + try { + assert.deepStrictEqual(body, res.body); + } catch (err) { + a = util.inspect(body); + b = util.inspect(res.body); + return error('expected ' + a + ' response body, got ' + b, body, res.body); + } + } else if (body !== res.text) { + // string + a = util.inspect(body); + b = util.inspect(res.text); + + // regexp + if (isregexp) { + if (!body.test(res.text)) { + return error('expected body ' + b + ' to match ' + body, body, res.body); + } + } else { + return error('expected ' + a + ' response body, got ' + b, body, res.body); + } + } + } + + /** + * Perform assertions on a response header and return an Error upon failure. + * + * @param {Object} header + * @param {Response} res + * @return {?Error} + * @api private + */ + + _assertHeader(header: any, res: any) { + var field = header.name; + var actual = res.header[field.toLowerCase()]; + var fieldExpected = header.value; + + if (typeof actual === 'undefined') return new Error('expected "' + field + '" header field'); + // This check handles header values that may be a String or single element Array + if ( + (Array.isArray(actual) && actual.toString() === fieldExpected) || + fieldExpected === actual + ) { + return; + } + if (fieldExpected instanceof RegExp) { + if (!fieldExpected.test(actual)) { + return new Error( + 'expected "' + field + '" matching ' + fieldExpected + ', got "' + actual + '"', + ); + } + } else { + return new Error('expected "' + field + '" of "' + fieldExpected + '", got "' + actual + '"'); + } + } + + /** + * Perform assertions on the response status and return an Error upon failure. + * + * @param {Number} status + * @param {Response} res + * @return {?Error} + * @api private + */ + + _assertStatus(status: any, res: any) { + var a; + var b; + if (res.status !== status) { + a = http.STATUS_CODES[status]; + b = http.STATUS_CODES[res.status]; + return new Error('expected ' + status + ' "' + a + '", got ' + res.status + ' "' + b + '"'); + } + } + + /** + * Performs an assertion by calling a function and return an Error upon failure. + * + * @param {Function} fn + * @param {Response} res + * @return {?Error} + * @api private + */ + _assertFunction(fn: any, res: any) { + var err; + try { + err = fn(res); + } catch (e) { + err = e; + } + if (err instanceof Error) return err; + } +} + +/** + * Return an `Error` with `msg` and results properties. + * + * @param {String} msg + * @param {Mixed} expected + * @param {Mixed} actual + * @return {Error} + * @api private + */ + +function error(msg: any, expected: any, actual: any) { + var err: any = new Error(msg); + err.expected = expected; + err.actual = actual; + err.showDiff = true; + return err; +} + +export default Test; diff --git a/src/postgraphile/http/setupServerSentEvents.ts b/src/postgraphile/http/setupServerSentEvents.ts index cbd3abe7a7..d0c4adfb3e 100644 --- a/src/postgraphile/http/setupServerSentEvents.ts +++ b/src/postgraphile/http/setupServerSentEvents.ts @@ -19,7 +19,11 @@ export default function setupServerSentEvents( res.statusCode = 200; res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); - res.setHeader('Connection', 'keep-alive'); + if (req.httpVersionMajor >= 2) { + // NOOP + } else { + res.setHeader('Connection', 'keep-alive'); + } const koaCtx = (req as object)['_koaCtx']; const isKoa = !!koaCtx; const stream = isKoa ? new PassThrough() : null; @@ -60,4 +64,5 @@ export default function setupServerSentEvents( req.on('close', cleanup); req.on('finish', cleanup); req.on('error', cleanup); + _emitter.on('test:close', cleanup); } diff --git a/src/postgraphile/postgraphile.ts b/src/postgraphile/postgraphile.ts index 7af4f4767f..f798e8586f 100644 --- a/src/postgraphile/postgraphile.ts +++ b/src/postgraphile/postgraphile.ts @@ -59,7 +59,7 @@ export function getPostgraphileSchemaBuilder( // Creates the Postgres schemas array. const pgSchemas: Array = Array.isArray(schema) ? schema : [schema]; - const _emitter = new EventEmitter(); + const _emitter = options._emitter || new EventEmitter(); // Creates a promise which will resolve to a GraphQL schema. Connects a // client from our pool to introspect the database. diff --git a/yarn.lock b/yarn.lock index 423592d7a7..faa73aeca7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1299,7 +1299,7 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== -async@^1.4.0, async@^1.5.2: +async@^1.4.0: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= @@ -2030,13 +2030,20 @@ colors@1.0.3: resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= -combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5: +combined-stream@1.0.6, combined-stream@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" integrity sha1-cj599ugBrFYTETp+RFqbactjKBg= dependencies: delayed-stream "~1.0.0" +combined-stream@^1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + dependencies: + delayed-stream "~1.0.0" + commander@2.16.x, commander@^2.12.1, commander@~2.16.0: version "2.16.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" @@ -2165,7 +2172,7 @@ cookie@0.3.1: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= -cookiejar@^2.0.6: +cookiejar@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== @@ -2371,6 +2378,13 @@ debug@^4.0.1: dependencies: ms "^2.1.1" +debug@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + decamelize@^1.0.0, decamelize@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -2879,7 +2893,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.1: +extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" integrity sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ= @@ -3124,14 +3138,14 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= -form-data@1.0.0-rc4: - version "1.0.0-rc4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.0-rc4.tgz#05ac6bc22227b43e4461f488161554699d4f8b5e" - integrity sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14= +form-data@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== dependencies: - async "^1.5.2" - combined-stream "^1.0.5" - mime-types "^2.1.10" + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" form-data@~2.3.1: version "2.3.2" @@ -3142,7 +3156,7 @@ form-data@~2.3.1: combined-stream "1.0.6" mime-types "^2.1.12" -formidable@^1.0.17: +formidable@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" integrity sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg== @@ -4889,7 +4903,7 @@ merge@^1.2.0: resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" integrity sha1-dTHjnUlJwoGma4xabgJl6LBYlNo= -methods@1.x, methods@^1.1.1, methods@~1.1.2: +methods@^1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= @@ -4958,7 +4972,7 @@ mime-db@~1.33.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== -mime-types@^2.1.10, mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.17, mime-types@~2.1.18: +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.17, mime-types@~2.1.18: version "2.1.18" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== @@ -4970,10 +4984,10 @@ mime@1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== -mime@^1.3.4: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6" + integrity sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w== minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: version "1.0.1" @@ -5958,11 +5972,16 @@ qs@6.5.1: resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== -qs@6.5.2, qs@^6.1.0, qs@~6.5.1: +qs@6.5.2, qs@~6.5.1: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== +qs@^6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.6.0.tgz#a99c0f69a8d26bf7ef012f871cdabb0aee4424c2" + integrity sha512-KIJqT9jQJDQx5h5uAVPimw6yVg2SekOKu959OCtktD3FjzbpvaPr8i4zzg07DOMz+igA4W/aNM7OV8H37pFYfA== + querystring-es3@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -6056,7 +6075,7 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== @@ -6088,6 +6107,15 @@ readable-stream@^3.0.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^3.0.6: + version "3.1.1" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.1.1.tgz#ed6bbc6c5ba58b090039ff18ce670515795aeb06" + integrity sha512-DkN66hPyqDhnIQ6Jcsvx9bFjhw214O4poMBcIMgPVpQvNy9a0e0Uhg5SqySyDKAmUlwt8LonTBz1ezOnM8pUdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" @@ -6830,29 +6858,20 @@ subscriptions-transport-ws@^0.9.15: symbol-observable "^1.0.4" ws "^5.2.0" -superagent@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-2.3.0.tgz#703529a0714e57e123959ddefbce193b2e50d115" - integrity sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU= +superagent@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-4.1.0.tgz#c465c2de41df2b8d05c165cbe403e280790cdfd5" + integrity sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag== dependencies: component-emitter "^1.2.0" - cookiejar "^2.0.6" - debug "^2.2.0" - extend "^3.0.0" - form-data "1.0.0-rc4" - formidable "^1.0.17" + cookiejar "^2.1.2" + debug "^4.1.0" + form-data "^2.3.3" + formidable "^1.2.0" methods "^1.1.1" - mime "^1.3.4" - qs "^6.1.0" - readable-stream "^2.0.5" - -supertest@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-2.0.1.tgz#a058081d788f1515d4700d7502881e6b759e44cd" - integrity sha1-oFgIHXiPFRXUcA11Aogea3WeRM0= - dependencies: - methods "1.x" - superagent "^2.0.0" + mime "^2.4.0" + qs "^6.6.0" + readable-stream "^3.0.6" supports-color@^2.0.0: version "2.0.0"