Skip to content

Commit 9e70780

Browse files
committed
Support for non-string and nested link params
Signed-off-by: Alan Cha <[email protected]>
1 parent fbefbac commit 9e70780

File tree

9 files changed

+6217
-5887
lines changed

9 files changed

+6217
-5887
lines changed

packages/openapi-to-graphql/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"graphql-upload": "^13.0.0",
9292
"json-ptr": "^2.2.0",
9393
"jsonpath-plus": "^6.0.1",
94+
"jsonpointer": "^5.0.0",
9495
"oas-validator": "^5.0.2",
9596
"pluralize": "^8.0.0",
9697
"swagger2openapi": "^7.0.2",

packages/openapi-to-graphql/src/resolver_builder.ts

+26-33
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { FileUpload } from 'graphql-upload'
2121
import stream from 'stream'
2222
import * as Oas3Tools from './oas_3_tools'
2323
import { JSONPath } from 'jsonpath-plus'
24+
import * as JSONPointer from 'jsonpointer'
2425
import { debug } from 'debug'
2526
import { GraphQLError, GraphQLFieldResolver } from 'graphql'
2627
import formurlencoded from 'form-urlencoded'
@@ -1157,26 +1158,26 @@ function getAuthReqAndProtcolName<TSource, TContext, TArgs>(
11571158
*/
11581159
function resolveRuntimeExpression(
11591160
paramName: string,
1160-
value: string,
1161+
runtimeExpression: string,
11611162
resolveData: any,
11621163
root: any,
11631164
args: any
11641165
): any {
1165-
if (value === '$url') {
1166+
if (runtimeExpression === '$url') {
11661167
return resolveData.url
1167-
} else if (value === '$method') {
1168+
} else if (runtimeExpression === '$method') {
11681169
return resolveData.usedRequestOptions.method
1169-
} else if (value === '$statusCode') {
1170+
} else if (runtimeExpression === '$statusCode') {
11701171
return resolveData.usedStatusCode
1171-
} else if (value.startsWith('$request.')) {
1172+
} else if (runtimeExpression.startsWith('$request.')) {
11721173
// CASE: parameter is previous body
1173-
if (value === '$request.body') {
1174+
if (runtimeExpression === '$request.body') {
11741175
return resolveData.usedPayload
11751176

11761177
// CASE: parameter in previous body
1177-
} else if (value.startsWith('$request.body#')) {
1178+
} else if (runtimeExpression.startsWith('$request.body#')) {
11781179
const tokens = JSONPath({
1179-
path: value.split('body#/')[1],
1180+
path: runtimeExpression.split('body#/')[1],
11801181
json: resolveData.usedPayload
11811182
})
11821183
if (Array.isArray(tokens) && tokens.length > 0) {
@@ -1186,36 +1187,36 @@ function resolveRuntimeExpression(
11861187
}
11871188

11881189
// CASE: parameter in previous query parameter
1189-
} else if (value.startsWith('$request.query')) {
1190+
} else if (runtimeExpression.startsWith('$request.query')) {
11901191
return resolveData.usedParams[
11911192
Oas3Tools.sanitize(
1192-
value.split('query.')[1],
1193+
runtimeExpression.split('query.')[1],
11931194
Oas3Tools.CaseStyle.camelCase
11941195
)
11951196
]
11961197

11971198
// CASE: parameter in previous path parameter
1198-
} else if (value.startsWith('$request.path')) {
1199+
} else if (runtimeExpression.startsWith('$request.path')) {
11991200
return resolveData.usedParams[
12001201
Oas3Tools.sanitize(
1201-
value.split('path.')[1],
1202+
runtimeExpression.split('path.')[1],
12021203
Oas3Tools.CaseStyle.camelCase
12031204
)
12041205
]
12051206

12061207
// CASE: parameter in previous header parameter
1207-
} else if (value.startsWith('$request.header')) {
1208-
return resolveData.usedRequestOptions.headers[value.split('header.')[1]]
1208+
} else if (runtimeExpression.startsWith('$request.header')) {
1209+
return resolveData.usedRequestOptions.headers[runtimeExpression.split('header.')[1]]
12091210
}
1210-
} else if (value.startsWith('$response.')) {
1211+
} else if (runtimeExpression.startsWith('$response.')) {
12111212
/**
12121213
* CASE: parameter is body
12131214
*
12141215
* NOTE: may not be used because it implies that the operation does not
12151216
* return a JSON object and OpenAPI-to-GraphQL does not create GraphQL
12161217
* objects for non-JSON data and links can only exists between objects.
12171218
*/
1218-
if (value === '$response.body') {
1219+
if (runtimeExpression === '$response.body') {
12191220
const result = JSON.parse(JSON.stringify(root))
12201221
/**
12211222
* _openAPIToGraphQL contains data used by OpenAPI-to-GraphQL to create the GraphQL interface
@@ -1225,45 +1226,37 @@ function resolveRuntimeExpression(
12251226
return result
12261227

12271228
// CASE: parameter in body
1228-
} else if (value.startsWith('$response.body#')) {
1229-
const tokens = JSONPath({
1230-
path: value.split('body#/')[1],
1231-
json: root
1232-
})
1233-
if (Array.isArray(tokens) && tokens.length > 0) {
1234-
return tokens[0]
1235-
} else {
1236-
httpLog(`Warning: could not extract parameter '${paramName}' from link`)
1237-
}
1229+
} else if (runtimeExpression.startsWith('$response.body#')) {
1230+
return JSONPointer.get(root, runtimeExpression.split('body#')[1])
12381231

12391232
// CASE: parameter in query parameter
1240-
} else if (value.startsWith('$response.query')) {
1233+
} else if (runtimeExpression.startsWith('$response.query')) {
12411234
// NOTE: handled the same way $request.query is handled
12421235
return resolveData.usedParams[
12431236
Oas3Tools.sanitize(
1244-
value.split('query.')[1],
1237+
runtimeExpression.split('query.')[1],
12451238
Oas3Tools.CaseStyle.camelCase
12461239
)
12471240
]
12481241

12491242
// CASE: parameter in path parameter
1250-
} else if (value.startsWith('$response.path')) {
1243+
} else if (runtimeExpression.startsWith('$response.path')) {
12511244
// NOTE: handled the same way $request.path is handled
12521245
return resolveData.usedParams[
12531246
Oas3Tools.sanitize(
1254-
value.split('path.')[1],
1247+
runtimeExpression.split('path.')[1],
12551248
Oas3Tools.CaseStyle.camelCase
12561249
)
12571250
]
12581251

12591252
// CASE: parameter in header parameter
1260-
} else if (value.startsWith('$response.header')) {
1261-
return resolveData.responseHeaders[value.split('header.')[1]]
1253+
} else if (runtimeExpression.startsWith('$response.header')) {
1254+
return resolveData.responseHeaders[runtimeExpression.split('header.')[1]]
12621255
}
12631256
}
12641257

12651258
throw new Error(
1266-
`Cannot create link because '${value}' is an invalid runtime expression.`
1259+
`Cannot resolve link because '${runtimeExpression}' is an invalid runtime expression.`
12671260
)
12681261
}
12691262

packages/openapi-to-graphql/test/example_api6.test.ts

+49
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,52 @@ test('Handle no response schema', () => {
347347
})
348348
})
349349
})
350+
351+
352+
/**
353+
* GET /testLinkWithNonStringParam has a link object that has a non-string
354+
* parameter
355+
*/
356+
test('Handle no response schema', () => {
357+
const query = `{
358+
testLinkWithNonStringParam {
359+
hello
360+
return5
361+
}
362+
}`
363+
364+
return graphql(createdSchema, query).then((result) => {
365+
expect(result.data).toEqual({
366+
testLinkWithNonStringParam: {
367+
hello: "world",
368+
return5: "5"
369+
}
370+
})
371+
})
372+
})
373+
374+
/**
375+
* GET /testLinkwithNestedParam has a link object that has a nested
376+
* parameter
377+
*/
378+
test('Handle no response schema', () => {
379+
const query = `{
380+
testLinkwithNestedParam{
381+
nesting1 {
382+
nesting2
383+
}
384+
returnNestedNumber
385+
}
386+
}`
387+
388+
return graphql(createdSchema, query).then((result) => {
389+
expect(result.data).toEqual({
390+
testLinkwithNestedParam: {
391+
nesting1: {
392+
nesting2: 5
393+
},
394+
returnNestedNumber: "5"
395+
}
396+
})
397+
})
398+
})

packages/openapi-to-graphql/test/example_api6_server.js

+13
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ function startServer(PORT) {
9090
res.set('Content-Type', 'text/plain').send('Hello world')
9191
})
9292

93+
app.get('/api/returnNumber', (req, res) => {
94+
res.set('Content-Type', 'text/plain').send(req.headers.number)
95+
})
96+
97+
app.get('/api/testLinkWithNonStringParam', (req, res) => {
98+
res.send({"hello": "world"})
99+
})
100+
101+
app.get('/api/testLinkwithNestedParam', (req, res) => {
102+
res.send({"nesting1": {"nesting2": 5} })
103+
})
104+
105+
93106
return new Promise((resolve) => {
94107
server = app.listen(PORT, () => {
95108
console.log(`Example API accessible on port ${PORT}`)

packages/openapi-to-graphql/test/example_gql_server.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ const openAPIToGraphQL = require('../dist/index')
1616
// const oas = require('./fixtures/example_oas3.json')
1717
// const oas = require('./fixtures/example_oas4.json')
1818
// const oas = require('./fixtures/example_oas5.json')
19-
// const oas = require('./fixtures/example_oas6.json')
20-
const oas = require('./fixtures/file_upload.json')
19+
const oas = require('./fixtures/example_oas6.json')
20+
// const oas = require('./fixtures/file_upload.json')
2121

2222
// const oas = require('./fixtures/github.json')
2323
// const oas = require('./fixtures/instagram.json')

packages/openapi-to-graphql/test/fixtures/example_oas.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@
475475
"get": {
476476
"x-graphql-field-name": "fetchAllOfficesWithFormStyleAndExplodeTrue",
477477
"operationId": "returnAllOffices",
478-
"description": "returns all offices",
478+
"description": "Used to test query parameters with form style and explode",
479479
"parameters": [{
480480
"name": "parameters",
481481
"in": "query",

packages/openapi-to-graphql/test/fixtures/example_oas6.json

+98
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,96 @@
326326
}
327327
}
328328
}
329+
},
330+
"/returnNumber": {
331+
"get": {
332+
"operationId": "returnNumber",
333+
"description": "Return a number from the request header.",
334+
"parameters": [
335+
{
336+
"name": "number",
337+
"in": "header",
338+
"required": true,
339+
"schema": {
340+
"type": "number"
341+
}
342+
}
343+
],
344+
"responses": {
345+
"200": {
346+
"description": "Success",
347+
"content": {
348+
"text/plain": {
349+
"schema": {
350+
"type": "number"
351+
}
352+
}
353+
}
354+
}
355+
}
356+
}
357+
},
358+
"/testLinkWithNonStringParam": {
359+
"get": {
360+
"description": "Test link object with non-string parameter.",
361+
"responses": {
362+
"200": {
363+
"description": "Success",
364+
"content": {
365+
"application/json": {
366+
"schema": {
367+
"type": "object",
368+
"properties": {
369+
"hello": {
370+
"type": "string"
371+
}
372+
}
373+
}
374+
}
375+
},
376+
"links": {
377+
"return5": {
378+
"$ref": "#/components/links/Return5"
379+
}
380+
}
381+
}
382+
}
383+
}
384+
},
385+
"/testLinkwithNestedParam": {
386+
"get": {
387+
"description": "Test link object with nested parameter.",
388+
"responses": {
389+
"200": {
390+
"description": "Success",
391+
"content": {
392+
"application/json": {
393+
"schema": {
394+
"type": "object",
395+
"properties": {
396+
"nesting1": {
397+
"type": "object",
398+
"properties": {
399+
"nesting2": {
400+
"type": "number"
401+
}
402+
}
403+
}
404+
}
405+
}
406+
}
407+
},
408+
"links": {
409+
"returnNestedNumber": {
410+
"operationId": "returnNumber",
411+
"parameters": {
412+
"number": "$response.body#/nesting1/nesting2"
413+
}
414+
}
415+
}
416+
}
417+
}
418+
}
329419
}
330420
},
331421
"components": {
@@ -376,6 +466,14 @@
376466
}
377467
}
378468
}
469+
},
470+
"links": {
471+
"Return5": {
472+
"operationId": "returnNumber",
473+
"parameters": {
474+
"number": 5
475+
}
476+
}
379477
}
380478
}
381479
}

0 commit comments

Comments
 (0)