openapi-to-graphql-nullable
Version:
Fork of openapi-to-graphql that fixes nullable handling. Published to NPM since there's no other easy way to consume a Lerna package. Unmaintained.
1,330 lines (1,196 loc) • 40.9 kB
text/typescript
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: openapi-to-graphql
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
/**
* Functions to create resolve functions.
*/
// Type imports:
import { SchemaObject, ParameterObject } from './types/oas3'
import { ConnectOptions } from './types/options'
import { Operation } from './types/operation'
import { SubscriptionContext } from './types/graphql'
import { PreprocessingData } from './types/preprocessing_data'
import * as NodeRequest from 'request'
import { RequestOptions } from './types/options'
// Imports:
import * as Oas3Tools from './oas_3_tools'
import * as querystring from 'querystring'
import * as JSONPath from 'jsonpath-plus'
import { debug } from 'debug'
import { GraphQLError, GraphQLFieldResolver } from 'graphql'
import formurlencoded from 'form-urlencoded'
import { PubSub } from 'graphql-subscriptions'
import { IncomingHttpHeaders } from 'http'
const pubsub = new PubSub()
const translationLog = debug('translation')
const httpLog = debug('http')
const pubsubLog = debug('pubsub')
// OAS runtime expression reference locations
const RUNTIME_REFERENCES = ['header.', 'query.', 'path.', 'body']
export const OPENAPI_TO_GRAPHQL = '_openAPIToGraphQL'
// Type definitions & exports:
type AuthReqAndProtcolName = {
authRequired: boolean
securityRequirement?: string
sanitizedSecurityRequirement?: string
}
type AuthOptions = {
authHeaders: { [key: string]: string }
authQs: { [key: string]: string }
authCookie: NodeRequest.Cookie
}
type GetResolverParams<TSource, TContext, TArgs> = {
operation: Operation
argsFromLink?: { [key: string]: string }
payloadName?: string
responseName?: string
data: PreprocessingData<TSource, TContext, TArgs>
baseUrl?: string
requestOptions?: RequestOptions<TSource, TContext, TArgs>
}
type GetSubscribeParams<TSource, TContext, TArgs> = {
operation: Operation
argsFromLink?: { [key: string]: string }
payloadName?: string
data: PreprocessingData<TSource, TContext, TArgs>
baseUrl?: string
connectOptions?: ConnectOptions
}
type ResolveData<TSource, TContext, TArgs> = {
/**
* TODO: Determine type
*
* Is it related to TArgs?
*/
usedParams: any
usedPayload: any
usedRequestOptions: RequestOptions<TSource, TContext, TArgs>
usedStatusCode: string
responseHeaders: IncomingHttpHeaders
}
// TODO: Determine better name
type OpenAPIToGraphQLRoot<TSource, TContext, TArgs> = {
data?: {
[identifier: string]: ResolveData<TSource, TContext, TArgs>
}
/**
* TODO: We can define more specific types. See getProcessedSecuritySchemes().
*
* Is it related TArgs?
*/
security: { [saneProtocolName: string]: any }
}
// TODO: Determine better name
type OpenAPIToGraphQLSource<TSource, TContext, TArgs> = {
_openAPIToGraphQL: OpenAPIToGraphQLRoot<TSource, TContext, TArgs>
}
/*
* If the operation type is Subscription, create and return a resolver object
* that contains subscribe to perform subscription and resolve to execute
* payload transformation
*/
export function getSubscribe<TSource, TContext, TArgs>({
operation,
payloadName,
data,
baseUrl,
connectOptions
}: GetSubscribeParams<TSource, TContext, TArgs>): GraphQLFieldResolver<
TSource,
SubscriptionContext,
TArgs
> {
// Determine the appropriate URL:
if (typeof baseUrl === 'undefined') {
baseUrl = Oas3Tools.getBaseUrl(operation)
}
// Return custom resolver if it is defined
const customResolvers = data.options.customSubscriptionResolvers
const title = operation.oas.info.title
const path = operation.path
const method = operation.method
if (
typeof customResolvers === 'object' &&
typeof customResolvers[title] === 'object' &&
typeof customResolvers[title][path] === 'object' &&
typeof customResolvers[title][path][method] === 'object' &&
typeof customResolvers[title][path][method].subscribe === 'function'
) {
translationLog(
`Use custom publish resolver for ${operation.operationString}`
)
return customResolvers[title][path][method].subscribe
}
return (root, args, context, info) => {
/**
* Determine possible topic(s) by resolving callback path
*
* GraphQL produces sanitized payload names, so we have to sanitize before
* lookup here
*/
const paramName = Oas3Tools.sanitize(
payloadName,
Oas3Tools.CaseStyle.camelCase
)
let resolveData: any = {}
if (payloadName && typeof payloadName === 'string') {
// The option genericPayloadArgName will change the payload name to "requestBody"
const sanePayloadName = data.options.genericPayloadArgName
? 'requestBody'
: Oas3Tools.sanitize(payloadName, Oas3Tools.CaseStyle.camelCase)
if (sanePayloadName in args) {
if (typeof args[sanePayloadName] === 'object') {
const rawPayload = Oas3Tools.desanitizeObjectKeys(
args[sanePayloadName],
data.saneMap
)
resolveData.usedPayload = rawPayload
} else {
const rawPayload = JSON.parse(args[sanePayloadName])
resolveData.usedPayload = rawPayload
}
}
}
if (connectOptions) {
resolveData.usedRequestOptions = connectOptions
} else {
resolveData.usedRequestOptions = {
method: resolveData.usedPayload.method
? resolveData.usedPayload.method
: method.toUpperCase()
}
}
pubsubLog(`Subscription schema: ${JSON.stringify(resolveData.usedPayload)}`)
let value = path
let paramNameWithoutLocation = paramName
if (paramName.indexOf('.') !== -1) {
paramNameWithoutLocation = paramName.split('.')[1]
}
// See if the callback path contains constants expression
if (value.search(/{|}/) === -1) {
args[paramNameWithoutLocation] = isRuntimeExpression(value)
? resolveRuntimeExpression(paramName, value, resolveData, root, args)
: value
} else {
// Replace callback expression with appropriate values
const cbParams = value.match(/{([^}]*)}/g)
pubsubLog(`Analyzing subscription path: ${cbParams.toString()}`)
cbParams.forEach((cbParam) => {
value = value.replace(
cbParam,
resolveRuntimeExpression(
paramName,
cbParam.substring(1, cbParam.length - 1),
resolveData,
root,
args
)
)
})
args[paramNameWithoutLocation] = value
}
const topic = args[paramNameWithoutLocation] || 'test'
pubsubLog(`Subscribing to: ${topic}`)
return context.pubsub
? context.pubsub.asyncIterator(topic)
: pubsub.asyncIterator(topic)
}
}
/*
* If the operation type is Subscription, create and return a resolver function
* triggered after a message has been published to the corresponding subscribe
* topic(s) to execute payload transformation
*/
export function getPublishResolver<TSource, TContext, TArgs>({
operation,
responseName,
data
}: GetResolverParams<TSource, TContext, TArgs>): GraphQLFieldResolver<
TSource,
TContext,
TArgs
> {
// Return custom resolver if it is defined
const customResolvers = data.options.customSubscriptionResolvers
const title = operation.oas.info.title
const path = operation.path
const method = operation.method
if (
typeof customResolvers === 'object' &&
typeof customResolvers[title] === 'object' &&
typeof customResolvers[title][path] === 'object' &&
typeof customResolvers[title][path][method] === 'object' &&
typeof customResolvers[title][path][method].resolve === 'function'
) {
translationLog(
`Use custom publish resolver for ${operation.operationString}`
)
return customResolvers[title][path][method].resolve
}
return (payload, args, context, info) => {
// Validate and format based on operation.responseDefinition
const typeOfResponse = operation.responseDefinition.targetGraphQLType
pubsubLog(
`Message received: ${responseName}, ${typeOfResponse}, ${JSON.stringify(
payload
)}`
)
let responseBody
let saneData
if (typeof payload === 'object') {
if (typeOfResponse === 'object') {
if (Buffer.isBuffer(payload)) {
try {
responseBody = JSON.parse(payload.toString())
} catch (e) {
const errorString =
`Cannot JSON parse payload` +
`operation ${operation.operationString} ` +
`even though it has content-type 'application/json'`
pubsubLog(errorString)
return null
}
} else {
responseBody = payload
}
saneData = Oas3Tools.sanitizeObjectKeys(payload)
} else if (
(Buffer.isBuffer(payload) || Array.isArray(payload)) &&
typeOfResponse === 'string'
) {
saneData = payload.toString()
}
} else if (typeof payload === 'string') {
if (typeOfResponse === 'object') {
try {
responseBody = JSON.parse(payload)
saneData = Oas3Tools.sanitizeObjectKeys(responseBody)
} catch (e) {
const errorString =
`Cannot JSON parse payload` +
`operation ${operation.operationString} ` +
`even though it has content-type 'application/json'`
pubsubLog(errorString)
return null
}
} else if (typeOfResponse === 'string') {
saneData = payload
}
}
pubsubLog(
`Message forwarded: ${JSON.stringify(saneData ? saneData : payload)}`
)
return saneData ? saneData : payload
}
}
/**
* If the operation type is Query or Mutation, create and return a resolver
* function that performs API requests for the given GraphQL query
*/
export function getResolver<TSource, TContext, TArgs>({
operation,
argsFromLink = {},
payloadName,
data,
baseUrl,
requestOptions
}: GetResolverParams<TSource, TContext, TArgs>): GraphQLFieldResolver<
TSource & OpenAPIToGraphQLSource<TSource, TContext, TArgs>,
TContext,
TArgs
> {
// Determine the appropriate URL:
if (typeof baseUrl === 'undefined') {
baseUrl = Oas3Tools.getBaseUrl(operation)
}
// Return custom resolver if it is defined
const customResolvers = data.options.customResolvers
const title = operation.oas.info.title
const path = operation.path
const method = operation.method
if (
typeof customResolvers === 'object' &&
typeof customResolvers[title] === 'object' &&
typeof customResolvers[title][path] === 'object' &&
typeof customResolvers[title][path][method] === 'function'
) {
translationLog(`Use custom resolver for ${operation.operationString}`)
return customResolvers[title][path][method]
}
// Return resolve function :
return (source, args, context, info) => {
/**
* Fetch resolveData from possibly existing _openAPIToGraphQL
*
* NOTE: _openAPIToGraphQL is an object used to pass security info and data
* from previous resolvers
*/
let resolveData: Partial<ResolveData<TSource, TContext, TArgs>> = {}
if (
source &&
typeof source === 'object' &&
typeof source[OPENAPI_TO_GRAPHQL] === 'object' &&
typeof source[OPENAPI_TO_GRAPHQL].data === 'object'
) {
const parentIdentifier = getParentIdentifier(info)
if (
!(parentIdentifier.length === 0) &&
parentIdentifier in source[OPENAPI_TO_GRAPHQL].data
) {
/**
* Resolving link params may change the usedParams, but these changes
* should not be present in the parent _openAPIToGraphQL, therefore copy
* the object
*/
resolveData = JSON.parse(
JSON.stringify(source[OPENAPI_TO_GRAPHQL].data[parentIdentifier])
)
}
}
if (typeof resolveData.usedParams === 'undefined') {
resolveData.usedParams = {}
}
/**
* Handle default values of parameters, if they have not yet been defined by
* the user.
*/
operation.parameters.forEach((param) => {
const paramName = Oas3Tools.sanitize(
param.name,
!data.options.simpleNames
? Oas3Tools.CaseStyle.camelCase
: Oas3Tools.CaseStyle.simple
)
if (
typeof args[paramName] === 'undefined' &&
param.schema &&
typeof param.schema === 'object'
) {
let schema = param.schema
if (schema && schema.$ref && typeof schema.$ref === 'string') {
schema = Oas3Tools.resolveRef(schema.$ref, operation.oas)
}
if (
schema &&
(schema as SchemaObject).default &&
typeof (schema as SchemaObject).default !== 'undefined'
) {
args[paramName] = (schema as SchemaObject).default
}
}
})
// Handle arguments provided by links
for (const paramName in argsFromLink) {
const saneParamName = Oas3Tools.sanitize(
paramName,
!data.options.simpleNames
? Oas3Tools.CaseStyle.camelCase
: Oas3Tools.CaseStyle.simple
)
let value = argsFromLink[paramName]
/**
* see if the link parameter contains constants that are appended to the link parameter
*
* e.g. instead of:
* $response.body#/employerId
*
* it could be:
* abc_{$response.body#/employerId}
*/
if (value.search(/{|}/) === -1) {
args[saneParamName] = isRuntimeExpression(value)
? resolveRuntimeExpression(
paramName,
value,
resolveData,
source,
args
)
: value
} else {
// Replace link parameters with appropriate values
const linkParams = value.match(/{([^}]*)}/g)
linkParams.forEach((linkParam) => {
value = value.replace(
linkParam,
resolveRuntimeExpression(
paramName,
linkParam.substring(1, linkParam.length - 1),
resolveData,
source,
args
)
)
})
args[saneParamName] = value
}
}
// Stored used parameters to future requests:
resolveData.usedParams = Object.assign(resolveData.usedParams, args)
// Build URL (i.e., fill in path parameters):
const { path, qs, headers } = extractRequestDataFromArgs(
operation.path,
operation.parameters,
args,
data
)
const url = baseUrl + path
/**
* The Content-Type and Accept property should not be changed because the
* object type has already been created and unlike these properties, it
* cannot be easily changed
*
* NOTE: This may cause the user to encounter unexpected changes
*/
if (operation.method !== Oas3Tools.HTTP_METHODS.get) {
headers['content-type'] =
typeof operation.payloadContentType !== 'undefined'
? operation.payloadContentType
: 'application/json'
}
headers['accept'] =
typeof operation.responseContentType !== 'undefined'
? operation.responseContentType
: 'application/json'
let options: NodeRequest.OptionsWithUrl
if (requestOptions) {
options = {
...requestOptions,
method: operation.method,
url // Must be after the requestOptions spread as url is a mandatory field so undefined may be used
}
options.headers = {} // Handle requestOptions.header later if applicable
options.qs = {} // Handle requestOptions.qs later if applicable
if (requestOptions.headers) {
// requestOptions.headers may be either an object or a function
if (typeof requestOptions.headers === 'object') {
Object.assign(options.headers, headers, requestOptions.headers)
} else if (typeof requestOptions.headers === 'function') {
const headers = requestOptions.headers(method, path, title, {
source,
args,
context,
info
})
if (typeof headers === 'object') {
Object.assign(options.headers, headers, headers)
}
}
} else {
options.headers = headers
}
if (requestOptions.qs) {
Object.assign(options.qs, qs, requestOptions.qs)
} else {
options.qs = qs
}
} else {
options = {
method: operation.method,
url,
headers,
qs
}
}
/**
* Determine possible payload
*
* GraphQL produces sanitized payload names, so we have to sanitize before
* lookup here
*/
resolveData.usedPayload = undefined
if (typeof payloadName === 'string') {
// The option genericPayloadArgName will change the payload name to "requestBody"
const sanePayloadName = data.options.genericPayloadArgName
? 'requestBody'
: Oas3Tools.sanitize(payloadName, Oas3Tools.CaseStyle.camelCase)
let rawPayload
if (operation.payloadContentType === 'application/json') {
rawPayload = JSON.stringify(
Oas3Tools.desanitizeObjectKeys(args[sanePayloadName], data.saneMap)
)
} else if (
operation.payloadContentType === 'application/x-www-form-urlencoded'
) {
rawPayload = formurlencoded(
Oas3Tools.desanitizeObjectKeys(args[sanePayloadName], data.saneMap)
)
} else {
// Payload is not an object
rawPayload = args[sanePayloadName]
}
options.body = rawPayload
resolveData.usedPayload = rawPayload
}
/**
* Pass on OpenAPI-to-GraphQL options
*/
if (typeof data.options === 'object') {
// Headers:
if (typeof data.options.headers === 'object') {
Object.assign(options.headers, data.options.headers)
} else if (typeof data.options.headers === 'function') {
const headers = data.options.headers(method, path, title, {
source,
args,
context,
info
})
if (typeof headers === 'object') {
Object.assign(options.headers, headers)
}
}
// Query string:
if (typeof data.options.qs === 'object') {
Object.assign(options.qs, data.options.qs)
}
}
// Get authentication headers and query parameters
if (
source &&
typeof source === 'object' &&
typeof source[OPENAPI_TO_GRAPHQL] === 'object'
) {
const { authHeaders, authQs, authCookie } = getAuthOptions(
operation,
source[OPENAPI_TO_GRAPHQL],
data
)
// ...and pass them to the options
Object.assign(options.headers, authHeaders)
Object.assign(options.qs, authQs)
// Add authentication cookie if created
if (authCookie !== null) {
const j = NodeRequest.jar()
j.setCookie(authCookie, options.url)
options.jar = j
}
}
// Extract OAuth token from context (if available)
if (data.options.sendOAuthTokenInQuery) {
const oauthQueryObj = createOAuthQS(data, context)
Object.assign(options.qs, oauthQueryObj)
} else {
const oauthHeader = createOAuthHeader(data, context)
Object.assign(options.headers, oauthHeader)
}
resolveData.usedRequestOptions = options
resolveData.usedStatusCode = operation.statusCode
// Make the call
httpLog(
`Call ${options.method.toUpperCase()} ${
options.url
}?${querystring.stringify(options.qs)}\n` +
`headers: ${JSON.stringify(options.headers)}\n` +
`request body: ${options.body}`
)
return new Promise((resolve, reject) => {
NodeRequest(options, (err, response, body) => {
if (err) {
httpLog(err)
reject(err)
} else if (response.statusCode < 200 || response.statusCode > 299) {
httpLog(`${response.statusCode} - ${Oas3Tools.trim(body, 100)}`)
const errorString = `Could not invoke operation ${operation.operationString}`
if (data.options.provideErrorExtensions) {
let responseBody
try {
responseBody = JSON.parse(body)
} catch (e) {
responseBody = body
}
const extensions = {
method: operation.method,
path: operation.path,
statusCode: response.statusCode,
responseHeaders: response.headers,
responseBody
}
reject(graphQLErrorWithExtensions(errorString, extensions))
} else {
reject(new Error(errorString))
}
// Successful response code 200-299
} else {
httpLog(`${response.statusCode} - ${Oas3Tools.trim(body, 100)}`)
if (response.headers['content-type']) {
/**
* Throw warning if the non-application/json content does not
* match the OAS.
*
* Use an inclusion test in case of charset
*
* i.e. text/plain; charset=utf-8
*/
if (
!(
response.headers['content-type'].includes(
operation.responseContentType
) ||
operation.responseContentType.includes(
response.headers['content-type']
)
)
) {
const errorString =
`Operation ` +
`${operation.operationString} ` +
`should have a content-type '${operation.responseContentType}' ` +
`but has '${response.headers['content-type']}' instead`
httpLog(errorString)
reject(errorString)
} else {
/**
* If the response body is type JSON, then parse it
*
* content-type may not be necessarily 'application/json' it can be
* 'application/json; charset=utf-8' for example
*/
if (
response.headers['content-type'].includes('application/json')
) {
let responseBody
try {
responseBody = JSON.parse(body)
} catch (e) {
const errorString =
`Cannot JSON parse response body of ` +
`operation ${operation.operationString} ` +
`even though it has content-type 'application/json'`
httpLog(errorString)
reject(errorString)
}
resolveData.responseHeaders = response.headers
// Deal with the fact that the server might send unsanitized data
let saneData = Oas3Tools.sanitizeObjectKeys(
responseBody,
!data.options.simpleNames
? Oas3Tools.CaseStyle.camelCase
: Oas3Tools.CaseStyle.simple
)
// Pass on _openAPIToGraphQL to subsequent resolvers
if (saneData && typeof saneData === 'object') {
if (Array.isArray(saneData)) {
saneData.forEach((element) => {
if (typeof element[OPENAPI_TO_GRAPHQL] === 'undefined') {
element[OPENAPI_TO_GRAPHQL] = {
data: {}
}
}
if (
source &&
typeof source === 'object' &&
typeof source[OPENAPI_TO_GRAPHQL] === 'object'
) {
Object.assign(
element[OPENAPI_TO_GRAPHQL],
source[OPENAPI_TO_GRAPHQL]
)
}
element[OPENAPI_TO_GRAPHQL].data[
getIdentifier(info)
] = resolveData
})
} else {
if (typeof saneData[OPENAPI_TO_GRAPHQL] === 'undefined') {
saneData[OPENAPI_TO_GRAPHQL] = {
data: {}
}
}
if (
source &&
typeof source === 'object' &&
typeof source[OPENAPI_TO_GRAPHQL] === 'object'
) {
Object.assign(
saneData[OPENAPI_TO_GRAPHQL],
source[OPENAPI_TO_GRAPHQL]
)
}
saneData[OPENAPI_TO_GRAPHQL].data[
getIdentifier(info)
] = resolveData
}
}
// Apply limit argument
if (
data.options.addLimitArgument &&
/**
* NOTE: Does not differentiate between autogenerated args and
* preexisting args
*
* Ensure that there is not preexisting 'limit' argument
*/
!operation.parameters.find((parameter) => {
return parameter.name === 'limit'
}) &&
// Only array data
Array.isArray(saneData) &&
// Only array of objects/arrays
saneData.some((data) => {
return typeof data === 'object'
})
) {
let arraySaneData = saneData
if ('limit' in args) {
const limit = args['limit']
if (limit >= 0) {
arraySaneData = arraySaneData.slice(0, limit)
} else {
reject(
new Error(
`Auto-generated 'limit' argument must be greater than or equal to 0`
)
)
}
} else {
reject(
new Error(
`Cannot get value for auto-generated 'limit' argument`
)
)
}
saneData = arraySaneData
}
resolve(saneData)
} else {
// TODO: Handle YAML
resolve(body)
}
}
} else {
/**
* Check to see if there is not supposed to be a response body,
* if that is the case, that would explain why there is not
* a content-type
*/
const { responseContentType } = Oas3Tools.getResponseObject(
operation,
operation.statusCode,
operation.oas
)
if (responseContentType === null) {
resolve(null)
} else {
const errorString =
'Response does not have a Content-Type property'
httpLog(errorString)
reject(errorString)
}
}
}
})
})
}
}
/**
* Attempts to create an object to become an OAuth query string by extracting an
* OAuth token from the context based on the JSON path provided in the options.
*/
function createOAuthQS<TSource, TContext, TArgs>(
data: PreprocessingData<TSource, TContext, TArgs>,
context: TContext
): { [key: string]: string } {
return typeof data.options.tokenJSONpath !== 'string'
? {}
: extractToken(data, context)
}
function extractToken<TSource, TContext, TArgs>(
data: PreprocessingData<TSource, TContext, TArgs>,
context: TContext
) {
const tokenJSONpath = data.options.tokenJSONpath
const tokens = JSONPath.JSONPath({
path: tokenJSONpath,
json: (context as unknown) as object
})
if (Array.isArray(tokens) && tokens.length > 0) {
const token = tokens[0]
return {
access_token: token
}
} else {
httpLog(
`Warning: could not extract OAuth token from context at '${tokenJSONpath}'`
)
return {}
}
}
/**
* Attempts to create an OAuth authorization header by extracting an OAuth token
* from the context based on the JSON path provided in the options.
*/
function createOAuthHeader<TSource, TContext, TArgs>(
data: PreprocessingData<TSource, TContext, TArgs>,
context: TContext
): { [key: string]: string } {
if (typeof data.options.tokenJSONpath !== 'string') {
return {}
}
// Extract token
const tokenJSONpath = data.options.tokenJSONpath
const tokens = JSONPath.JSONPath({
path: tokenJSONpath,
json: (context as unknown) as object
})
if (Array.isArray(tokens) && tokens.length > 0) {
const token = tokens[0]
return {
Authorization: `Bearer ${token}`,
'User-Agent': 'openapi-to-graphql'
}
} else {
httpLog(
`Warning: could not extract OAuth token from context at ` +
`'${tokenJSONpath}'`
)
return {}
}
}
/**
* Return the headers and query strings to authenticate a request (if any).
* Return authHeader and authQs, which hold headers and query parameters
* respectively to authentication a request.
*/
function getAuthOptions<TSource, TContext, TArgs>(
operation: Operation,
_openAPIToGraphQL: OpenAPIToGraphQLRoot<TSource, TContext, TArgs>,
data: PreprocessingData<TSource, TContext, TArgs>
): AuthOptions {
const authHeaders = {}
const authQs = {}
let authCookie = null
/**
* Determine if authentication is required, and which protocol (if any) we can
* use
*/
const {
authRequired,
securityRequirement,
sanitizedSecurityRequirement
} = getAuthReqAndProtcolName(operation, _openAPIToGraphQL)
// Possibly, we don't need to do anything:
if (!authRequired) {
return { authHeaders, authQs, authCookie }
}
// If authentication is required, but we can't fulfill the protocol, throw:
if (authRequired && typeof securityRequirement !== 'string') {
throw new Error(`Missing information to authenticate API request.`)
}
if (typeof securityRequirement === 'string') {
const security = data.security[securityRequirement]
switch (security.def.type) {
case 'apiKey':
const apiKey =
_openAPIToGraphQL.security[sanitizedSecurityRequirement].apiKey
if ('in' in security.def) {
if (typeof security.def.name === 'string') {
if (security.def.in === 'header') {
authHeaders[security.def.name] = apiKey
} else if (security.def.in === 'query') {
authQs[security.def.name] = apiKey
} else if (security.def.in === 'cookie') {
authCookie = NodeRequest.cookie(`${security.def.name}=${apiKey}`)
}
} else {
throw new Error(
`Cannot send API key in '${JSON.stringify(security.def.in)}'`
)
}
}
break
case 'http':
switch (security.def.scheme) {
case 'basic':
const username =
_openAPIToGraphQL.security[sanitizedSecurityRequirement].username
const password =
_openAPIToGraphQL.security[sanitizedSecurityRequirement].password
const credentials = `${username}:${password}`
authHeaders['Authorization'] = `Basic ${Buffer.from(
credentials
).toString('base64')}`
break
default:
throw new Error(
`Cannot recognize http security scheme ` +
`'${JSON.stringify(security.def.scheme)}'`
)
}
break
case 'oauth2':
break
case 'openIdConnect':
break
default:
throw new Error(`Cannot recognize security type '${security.def.type}'`)
}
}
return { authHeaders, authQs, authCookie }
}
/**
* Determines whether a given operation requires authentication, and which of
* the (possibly multiple) authentication protocols can be used based on the
* data present in the given context.
*/
function getAuthReqAndProtcolName<TSource, TContext, TArgs>(
operation: Operation,
_openAPIToGraphQL: OpenAPIToGraphQLRoot<TSource, TContext, TArgs>
): AuthReqAndProtcolName {
let authRequired = false
if (
Array.isArray(operation.securityRequirements) &&
operation.securityRequirements.length > 0
) {
authRequired = true
for (let securityRequirement of operation.securityRequirements) {
const sanitizedSecurityRequirement = Oas3Tools.sanitize(
securityRequirement,
Oas3Tools.CaseStyle.camelCase
)
if (
typeof _openAPIToGraphQL.security[sanitizedSecurityRequirement] ===
'object'
) {
return {
authRequired,
securityRequirement,
sanitizedSecurityRequirement
}
}
}
}
return {
authRequired
}
}
/**
* Given a link parameter or callback path, determine the value from the runtime
* expression
*
* The link parameter or callback path is a reference to data contained in the
* url/method/statuscode or response/request body/query/path/header
*/
function resolveRuntimeExpression(
paramName: string,
value: string,
resolveData: any,
root: any,
args: any
): any {
if (value === '$url') {
return resolveData.usedRequestOptions.url
} else if (value === '$method') {
return resolveData.usedRequestOptions.method
} else if (value === '$statusCode') {
return resolveData.usedStatusCode
} else if (value.startsWith('$request.')) {
// CASE: parameter is previous body
if (value === '$request.body') {
return resolveData.usedPayload
// CASE: parameter in previous body
} else if (value.startsWith('$request.body#')) {
const tokens = JSONPath.JSONPath({
path: value.split('body#/')[1],
json: resolveData.usedPayload
})
if (Array.isArray(tokens) && tokens.length > 0) {
return tokens[0]
} else {
httpLog(`Warning: could not extract parameter '${paramName}' from link`)
}
// CASE: parameter in previous query parameter
} else if (value.startsWith('$request.query')) {
return resolveData.usedParams[
Oas3Tools.sanitize(
value.split('query.')[1],
Oas3Tools.CaseStyle.camelCase
)
]
// CASE: parameter in previous path parameter
} else if (value.startsWith('$request.path')) {
return resolveData.usedParams[
Oas3Tools.sanitize(
value.split('path.')[1],
Oas3Tools.CaseStyle.camelCase
)
]
// CASE: parameter in previous header parameter
} else if (value.startsWith('$request.header')) {
return resolveData.usedRequestOptions.headers[value.split('header.')[1]]
}
} else if (value.startsWith('$response.')) {
/**
* CASE: parameter is body
*
* NOTE: may not be used because it implies that the operation does not
* return a JSON object and OpenAPI-to-GraphQL does not create GraphQL
* objects for non-JSON data and links can only exists between objects.
*/
if (value === '$response.body') {
const result = JSON.parse(JSON.stringify(root))
/**
* _openAPIToGraphQL contains data used by OpenAPI-to-GraphQL to create the GraphQL interface
* and should not be exposed
*/
result._openAPIToGraphQL = undefined
return result
// CASE: parameter in body
} else if (value.startsWith('$response.body#')) {
const tokens = JSONPath.JSONPath({
path: value.split('body#/')[1],
json: root
})
if (Array.isArray(tokens) && tokens.length > 0) {
return tokens[0]
} else {
httpLog(`Warning: could not extract parameter '${paramName}' from link`)
}
// CASE: parameter in query parameter
} else if (value.startsWith('$response.query')) {
// NOTE: handled the same way $request.query is handled
return resolveData.usedParams[
Oas3Tools.sanitize(
value.split('query.')[1],
Oas3Tools.CaseStyle.camelCase
)
]
// CASE: parameter in path parameter
} else if (value.startsWith('$response.path')) {
// NOTE: handled the same way $request.path is handled
return resolveData.usedParams[
Oas3Tools.sanitize(
value.split('path.')[1],
Oas3Tools.CaseStyle.camelCase
)
]
// CASE: parameter in header parameter
} else if (value.startsWith('$response.header')) {
return resolveData.responseHeaders[value.split('header.')[1]]
}
}
throw new Error(
`Cannot create link because '${value}' is an invalid runtime expression`
)
}
/**
* Check if a string is a runtime expression in the context of link parameters
*/
function isRuntimeExpression(str: string): boolean {
if (str === '$url' || str === '$method' || str === '$statusCode') {
return true
} else if (str.startsWith('$request.')) {
for (let i = 0; i < RUNTIME_REFERENCES.length; i++) {
if (str.startsWith(`$request.${RUNTIME_REFERENCES[i]}`)) {
return true
}
}
} else if (str.startsWith('$response.')) {
for (let i = 0; i < RUNTIME_REFERENCES.length; i++) {
if (str.startsWith(`$response.${RUNTIME_REFERENCES[i]}`)) {
return true
}
}
}
return false
}
/**
* From the info object provided by the resolver, get a unique identifier, which
* is the path formed from the nested field names (or aliases if provided)
*
* Used to store and retrieve the _openAPIToGraphQL of parent field
*/
function getIdentifier(info): string {
return getIdentifierRecursive(info.path)
}
/**
* From the info object provided by the resolver, get the unique identifier of
* the parent object
*/
function getParentIdentifier(info): string {
return getIdentifierRecursive(info.path.prev)
}
/**
* Get the path of nested field names (or aliases if provided)
*/
function getIdentifierRecursive(path): string {
return typeof path.prev === 'undefined'
? path.key
: /**
* Check if the identifier contains array indexing, if so remove.
*
* i.e. instead of 0/friends/1/friends/2/friends/user, create
* friends/friends/friends/user
*/
isNaN(parseInt(path.key))
? `${path.key}/${getIdentifierRecursive(path.prev)}`
: getIdentifierRecursive(path.prev)
}
/**
* Create a new GraphQLError with an extensions field
*/
function graphQLErrorWithExtensions(
message: string,
extensions: { [key: string]: any }
): GraphQLError {
return new GraphQLError(message, null, null, null, null, null, extensions)
}
/**
* Extracts data from the GraphQL arguments of a particular field
*
* Replaces the path parameter in the given path with values in the given args.
* Furthermore adds the query parameters for a request.
*/
export function extractRequestDataFromArgs<TSource, TContext, TArgs>(
path: string,
parameters: ParameterObject[],
args: TArgs, // NOTE: argument keys are sanitized!
data: PreprocessingData<TSource, TContext, TArgs>
): {
path: string
qs: { [key: string]: string }
headers: { [key: string]: string }
} {
const qs = {}
const headers = {}
// Iterate parameters:
for (const param of parameters) {
const sanitizedParamName = Oas3Tools.sanitize(
param.name,
!data.options.simpleNames
? Oas3Tools.CaseStyle.camelCase
: Oas3Tools.CaseStyle.simple
)
if (sanitizedParamName && sanitizedParamName in args) {
switch (param.in) {
// Path parameters
case 'path':
path = path.replace(`{${param.name}}`, args[sanitizedParamName])
break
// Query parameters
case 'query':
qs[param.name] = args[sanitizedParamName]
break
// Header parameters
case 'header':
headers[param.name] = args[sanitizedParamName]
break
// Cookie parameters
case 'cookie':
if (!('cookie' in headers)) {
headers['cookie'] = ''
}
headers['cookie'] += `${param.name}=${args[sanitizedParamName]}; `
break
default:
httpLog(
`Warning: The parameter location '${param.in}' in the ` +
`parameter '${param.name}' of operation '${path}' is not ` +
`supported`
)
}
}
}
return { path, qs, headers }
}