-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add BaseGraphqlService, support [github] V4 API (#3763)
* add base class for Graphql APIs * add GithubAuthV4Service + updates to GH token pool * update github forks to use GithubAuthV4Service * rename GithubAuthService to GithubAuthV3Service
- Loading branch information
Showing
36 changed files
with
755 additions
and
119 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
'use strict' | ||
|
||
const { print } = require('graphql/language/printer') | ||
const BaseService = require('./base') | ||
const { InvalidResponse, ShieldsRuntimeError } = require('./errors') | ||
const { parseJson } = require('./json') | ||
|
||
function defaultTransformErrors(errors) { | ||
return new InvalidResponse({ prettyMessage: errors[0].message }) | ||
} | ||
|
||
class BaseGraphqlService extends BaseService { | ||
_parseJson(buffer) { | ||
return parseJson(buffer) | ||
} | ||
|
||
async _requestGraphql({ | ||
schema, | ||
url, | ||
query, | ||
variables = {}, | ||
options = {}, | ||
httpErrorMessages = {}, | ||
transformErrors = defaultTransformErrors, | ||
}) { | ||
const mergedOptions = { | ||
...{ headers: { Accept: 'application/json' } }, | ||
...options, | ||
} | ||
mergedOptions.method = 'POST' | ||
mergedOptions.body = JSON.stringify({ query: print(query), variables }) | ||
const { buffer } = await this._request({ | ||
url, | ||
options: mergedOptions, | ||
errorMessages: httpErrorMessages, | ||
}) | ||
const json = this._parseJson(buffer) | ||
if (json.errors) { | ||
const exception = transformErrors(json.errors) | ||
if (exception instanceof ShieldsRuntimeError) { | ||
throw exception | ||
} else { | ||
throw Error( | ||
`transformErrors() must return a ShieldsRuntimeError; got ${exception}` | ||
) | ||
} | ||
} | ||
return this.constructor._validate(json, schema) | ||
} | ||
} | ||
|
||
module.exports = BaseGraphqlService |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
'use strict' | ||
|
||
const Joi = require('@hapi/joi') | ||
const { expect } = require('chai') | ||
const gql = require('graphql-tag') | ||
const sinon = require('sinon') | ||
const BaseGraphqlService = require('./base-graphql') | ||
const { InvalidResponse } = require('./errors') | ||
|
||
const dummySchema = Joi.object({ | ||
requiredString: Joi.string().required(), | ||
}).required() | ||
|
||
class DummyGraphqlService extends BaseGraphqlService { | ||
static get category() { | ||
return 'cat' | ||
} | ||
|
||
static get route() { | ||
return { | ||
base: 'foo', | ||
} | ||
} | ||
|
||
async handle() { | ||
const { requiredString } = await this._requestGraphql({ | ||
schema: dummySchema, | ||
url: 'http://example.com/graphql', | ||
query: gql` | ||
query { | ||
requiredString | ||
} | ||
`, | ||
}) | ||
return { message: requiredString } | ||
} | ||
} | ||
|
||
describe('BaseGraphqlService', function() { | ||
describe('Making requests', function() { | ||
let sendAndCacheRequest | ||
beforeEach(function() { | ||
sendAndCacheRequest = sinon.stub().returns( | ||
Promise.resolve({ | ||
buffer: '{"some": "json"}', | ||
res: { statusCode: 200 }, | ||
}) | ||
) | ||
}) | ||
|
||
it('invokes _sendAndCacheRequest', async function() { | ||
await DummyGraphqlService.invoke( | ||
{ sendAndCacheRequest }, | ||
{ handleInternalErrors: false } | ||
) | ||
|
||
expect(sendAndCacheRequest).to.have.been.calledOnceWith( | ||
'http://example.com/graphql', | ||
{ | ||
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}', | ||
headers: { Accept: 'application/json' }, | ||
method: 'POST', | ||
} | ||
) | ||
}) | ||
|
||
it('forwards options to _sendAndCacheRequest', async function() { | ||
class WithOptions extends DummyGraphqlService { | ||
async handle() { | ||
const { value } = await this._requestGraphql({ | ||
schema: dummySchema, | ||
url: 'http://example.com/graphql', | ||
query: gql` | ||
query { | ||
requiredString | ||
} | ||
`, | ||
options: { qs: { queryParam: 123 } }, | ||
}) | ||
return { message: value } | ||
} | ||
} | ||
|
||
await WithOptions.invoke( | ||
{ sendAndCacheRequest }, | ||
{ handleInternalErrors: false } | ||
) | ||
|
||
expect(sendAndCacheRequest).to.have.been.calledOnceWith( | ||
'http://example.com/graphql', | ||
{ | ||
body: '{"query":"{\\n requiredString\\n}\\n","variables":{}}', | ||
headers: { Accept: 'application/json' }, | ||
method: 'POST', | ||
qs: { queryParam: 123 }, | ||
} | ||
) | ||
}) | ||
}) | ||
|
||
describe('Making badges', function() { | ||
it('handles valid json responses', async function() { | ||
const sendAndCacheRequest = async () => ({ | ||
buffer: '{"requiredString": "some-string"}', | ||
res: { statusCode: 200 }, | ||
}) | ||
expect( | ||
await DummyGraphqlService.invoke( | ||
{ sendAndCacheRequest }, | ||
{ handleInternalErrors: false } | ||
) | ||
).to.deep.equal({ | ||
message: 'some-string', | ||
}) | ||
}) | ||
|
||
it('handles json responses which do not match the schema', async function() { | ||
const sendAndCacheRequest = async () => ({ | ||
buffer: '{"unexpectedKey": "some-string"}', | ||
res: { statusCode: 200 }, | ||
}) | ||
expect( | ||
await DummyGraphqlService.invoke( | ||
{ sendAndCacheRequest }, | ||
{ handleInternalErrors: false } | ||
) | ||
).to.deep.equal({ | ||
isError: true, | ||
color: 'lightgray', | ||
message: 'invalid response data', | ||
}) | ||
}) | ||
|
||
it('handles unparseable json responses', async function() { | ||
const sendAndCacheRequest = async () => ({ | ||
buffer: 'not json', | ||
res: { statusCode: 200 }, | ||
}) | ||
expect( | ||
await DummyGraphqlService.invoke( | ||
{ sendAndCacheRequest }, | ||
{ handleInternalErrors: false } | ||
) | ||
).to.deep.equal({ | ||
isError: true, | ||
color: 'lightgray', | ||
message: 'unparseable json response', | ||
}) | ||
}) | ||
}) | ||
|
||
describe('Error handling', function() { | ||
it('handles generic error', async function() { | ||
const sendAndCacheRequest = async () => ({ | ||
buffer: '{ "errors": [ { "message": "oh noes!!" } ] }', | ||
res: { statusCode: 200 }, | ||
}) | ||
expect( | ||
await DummyGraphqlService.invoke( | ||
{ sendAndCacheRequest }, | ||
{ handleInternalErrors: false } | ||
) | ||
).to.deep.equal({ | ||
isError: true, | ||
color: 'lightgray', | ||
message: 'oh noes!!', | ||
}) | ||
}) | ||
|
||
it('handles custom error', async function() { | ||
class WithErrorHandler extends DummyGraphqlService { | ||
async handle() { | ||
const { requiredString } = await this._requestGraphql({ | ||
schema: dummySchema, | ||
url: 'http://example.com/graphql', | ||
query: gql` | ||
query { | ||
requiredString | ||
} | ||
`, | ||
transformErrors: function(errors) { | ||
if (errors[0].message === 'oh noes!!') { | ||
return new InvalidResponse({ | ||
prettyMessage: 'a terrible thing has happened', | ||
}) | ||
} | ||
}, | ||
}) | ||
return { message: requiredString } | ||
} | ||
} | ||
|
||
const sendAndCacheRequest = async () => ({ | ||
buffer: '{ "errors": [ { "message": "oh noes!!" } ] }', | ||
res: { statusCode: 200 }, | ||
}) | ||
expect( | ||
await WithErrorHandler.invoke( | ||
{ sendAndCacheRequest }, | ||
{ handleInternalErrors: false } | ||
) | ||
).to.deep.equal({ | ||
isError: true, | ||
color: 'lightgray', | ||
message: 'a terrible thing has happened', | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
'use strict' | ||
/** | ||
* @module | ||
*/ | ||
|
||
/** | ||
* Utility function to merge two graphql queries together | ||
* This is basically copied from | ||
* [graphql-query-merge](https://www.npmjs.com/package/graphql-query-merge) | ||
* but can't use that due to incorrect packaging. | ||
* | ||
* @param {...object} queries queries to merge | ||
* @returns {object} merged query | ||
*/ | ||
function mergeQueries(...queries) { | ||
const merged = { | ||
kind: 'Document', | ||
definitions: [ | ||
{ | ||
directives: [], | ||
operation: 'query', | ||
variableDefinitions: [], | ||
kind: 'OperationDefinition', | ||
selectionSet: { kind: 'SelectionSet', selections: [] }, | ||
}, | ||
], | ||
} | ||
|
||
queries.forEach(query => { | ||
const parsedQuery = query | ||
parsedQuery.definitions.forEach(definition => { | ||
merged.definitions[0].directives = [ | ||
...merged.definitions[0].directives, | ||
...definition.directives, | ||
] | ||
|
||
merged.definitions[0].variableDefinitions = [ | ||
...merged.definitions[0].variableDefinitions, | ||
...definition.variableDefinitions, | ||
] | ||
|
||
merged.definitions[0].selectionSet.selections = [ | ||
...merged.definitions[0].selectionSet.selections, | ||
...definition.selectionSet.selections, | ||
] | ||
}) | ||
}) | ||
|
||
return merged | ||
} | ||
|
||
module.exports = { mergeQueries } |
Oops, something went wrong.