├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── docs └── subscriptions.md ├── package.json ├── src ├── auth_builder.ts ├── graphql_tools.ts ├── index.ts ├── oas_3_tools.ts ├── preprocessor.ts ├── resolver_builder.ts ├── schema_builder.ts ├── types │ ├── graphql.ts │ ├── oas2.ts │ ├── oas3.ts │ ├── operation.ts │ ├── options.ts │ ├── preprocessing_data.ts │ └── request.ts └── utils.ts ├── test ├── README.md ├── authentication.test.ts ├── cloudfunction.test.ts ├── docusign.test.ts ├── evaluation │ ├── README.md │ ├── eval_apis_guru.js │ ├── load_apis_guru.js │ └── results │ │ ├── errors_breakdown.json │ │ ├── results.json │ │ ├── warnings_breakdown.csv │ │ ├── warnings_breakdown.json │ │ ├── warnings_per_api.csv │ │ └── warnings_per_api.json ├── example_api.test.ts ├── example_api2.test.ts ├── example_api2_server.js ├── example_api3.test.ts ├── example_api3_server.js ├── example_api4.test.ts ├── example_api5.test.ts ├── example_api5_server.js ├── example_api6.test.ts ├── example_api6_server.js ├── example_api_server.js ├── extensions.test.ts ├── fixtures │ ├── cloudfunction.json │ ├── docusign.json │ ├── example_oas.json │ ├── example_oas2.json │ ├── example_oas3.json │ ├── example_oas4.json │ ├── example_oas5.json │ ├── example_oas6.json │ ├── example_oas7.json │ ├── extensions.json │ ├── extensions_error1.json │ ├── extensions_error2.json │ ├── extensions_error3.json │ ├── extensions_error4.json │ ├── extensions_error5.json │ ├── extensions_error6.json │ ├── extensions_error7.json │ ├── github.json │ ├── government_social_work.json │ ├── ibm_language_translator.json │ ├── instagram.json │ ├── stripe.json │ └── weather_underground.json ├── government_social_work.test.ts ├── httprequest.ts ├── ibm_language_translator.test.ts ├── instagram.test.ts ├── oas_3_tools.test.ts ├── stripe.test.ts └── weather_underground.test.ts ├── tsconfig.json └── tslint.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: npm 9 | directory: '/' 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '*.md' 8 | pull_request: 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | services: 21 | redis: 22 | image: redis 23 | ports: 24 | - 6379:6379 25 | options: --entrypoint redis-server 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - name: Use Node.js 30 | uses: actions/setup-node@v3.2.0 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | 34 | - name: Install Dependencies 35 | run: | 36 | npm i --ignore-scripts 37 | 38 | - name: Run Tests 39 | run: | 40 | npm run test 41 | 42 | automerge: 43 | needs: test 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: fastify/github-action-merge-dependabot@v3.1.7 47 | with: 48 | github-token: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | lib 106 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | *.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Matteo Collina 4 | Copyright (c) IBM Corp. 2012,2019. All Rights Reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /docs/subscriptions.md: -------------------------------------------------------------------------------- 1 | # Subscriptions with OpenAPI-to-GraphQL 2 | Since version 2.1.0, OpenAPI-to-GraphQL supports [GraphQL _subscription_ operations](http://spec.graphql.org/draft/#sec-Subscription). In GraphQL, using a subscription query, clients subscribe to updates on the data defined in the query. In this scenario, when data changes, the server publishes these changes to all clients that have active subscriptions for that data. 3 | 4 | The OpenAPI specification can define similar behavior using [callbacks](https://swagger.io/specification/#callbackObject): a callback defines a request that the server may initiate in response to receiving another request. Callbacks can thus be used to model publish/subscribe behavior. I.e., when the server receives a request to update some data, it can then itself issue callback requests (outside of the first request/response cycle) to any number of subscribed clients to inform about the new data. 5 | 6 | When the [`createSubscriptionsFromCallbacks` option](https://github.com/IBM/openapi-to-graphql/tree/master/packages/openapi-to-graphql#options) is enabled, OpenAPI-to-GraphQL creates subscription fields from an operation's callback objects. In such cases, OpenAPI-to-GraphQL creates a [subscribe](http://spec.graphql.org/draft/#Subscribe()) function responsible to subscribing clients to receive results of callbacks being executed, and a special form of a [resolve](http://spec.graphql.org/draft/#ResolveFieldEventStream()) function, which pushes data updates to subscribed clients. 7 | 8 | To create these two functions, OpenAPI-to-GraphQL relies on the popular [graphql-subscriptions](https://github.com/apollographql/graphql-subscriptions) package, which provides a unified API to support different network transports (like WebSockets, MQTT, Redis etc. - see this [list of supported transports](https://github.com/apollographql/graphql-subscriptions#pubsub-implementations)). 9 | 10 | A typical example of using OpenAPI-to-GraphQL to create a GraphQL server supporting subscriptions may look like this: 11 | 12 | ### Creating PubSub instance 13 | 14 | First, initialize a PubSub instance to spread events between your API and the GraphQL Server, in a `pubsub.js` file. 15 | 16 | ```javascript 17 | import { EventEmitter2 } from 'eventEmitter2'; 18 | import { PubSub } = from 'graphql-subscriptions' 19 | 20 | const eventEmitter = new EventEmitter2({ 21 | wildcard: true, 22 | delimiter: '/' 23 | }); 24 | 25 | // Create the PubSub instance (here by wrapping an EventEmitter client) 26 | const pubsub = new PubSub() 27 | 28 | export default pubsub 29 | ``` 30 | 31 | PubSub could also wrap an MQTT client connected to a broker, like in this [example API](../test/example_api5_server.js). 32 | 33 | ```javascript 34 | import { connect } = from 'mqtt' 35 | import { MQTTPubSub } = from 'graphql-mqtt-subscriptions' 36 | 37 | const MQTT_PORT = 1883 38 | 39 | // Create a PubSub instance (here by wrapping a MQTT client) 40 | const client = connect(`mqtt://localhost:${MQTT_PORT}`) 41 | 42 | const pubsub = new MQTTPubSub({ 43 | client 44 | }) 45 | 46 | export default pubsub 47 | ``` 48 | 49 | ## GraphQL server 50 | 51 | Create GraphQL schema, resolvers and endpoints. 52 | 53 | ```javascript 54 | import { createGraphQLSchema } from 'openapi-to-graphql' 55 | import express from 'express' 56 | import { graphqlExpress } from 'apollo-server-express' 57 | import { execute, printSchema, subscribe } from 'graphql' 58 | import { SubscriptionServer } from 'subscriptions-transport-ws' 59 | import { createServer } from 'http' 60 | import { pubsub } from './pubsub' 61 | 62 | const HTTP_PORT = 3000 63 | 64 | const init = async () => { 65 | // Let OpenAPI-to-GraphQL create the schema 66 | const schema = await createGraphQLSchema(oasWithCallbackObjects, { 67 | createSubscriptionsFromCallbacks: true 68 | }) 69 | 70 | // Log GraphQL schema... 71 | const myGraphQLSchema = printSchema(schema) 72 | console.log(myGraphQLSchema) 73 | 74 | // Set up GraphQL server using Express.js 75 | const app = express() 76 | app.use('/graphql', graphqlExpress({ schema })) 77 | 78 | // Wrap the Express server... 79 | const wsServer = createServer(app) 80 | 81 | // ...and set up the WebSocket for handling GraphQL subscriptions 82 | wsServer.listen(HTTP_PORT, () => { 83 | new SubscriptionServer( 84 | { 85 | execute, 86 | subscribe, 87 | schema, 88 | onConnect: (params, socket, ctx) => { 89 | // Add pubsub to context to be used by GraphQL subscribe field 90 | return { pubsub } 91 | } 92 | }, 93 | { 94 | server: wsServer, 95 | path: '/subscriptions' 96 | } 97 | ) 98 | }) 99 | } 100 | 101 | init() 102 | ``` 103 | 104 | ## API server 105 | 106 | A simple example could be the following, when an HTTP client tries to create a device (via `post('/api/devices')` route) an event is published by the PubSub instance. 107 | If a callback like [#/components/callbacks/DevicesEvent](../test/fixtures/example_oas5.json) is declared in your OpenAPI schema and used in path `/devices` for the `post` Operation, a subscription field will be generated by OpenAPI-to-GraphQL. 108 | 109 | 110 | ```javascript 111 | import express from 'express' 112 | import bodyParser from 'body-parser' 113 | import pubsub from './pubsub' 114 | 115 | const HTTP_PORT = 4000 116 | 117 | const Devices = { 118 | 'Audio-player': { 119 | name: 'Audio-player', 120 | userName: 'johnny' 121 | }, 122 | Drone: { 123 | name: 'Drone', 124 | userName: 'eric' 125 | } 126 | } 127 | 128 | const startServer = () => { 129 | const app = express() 130 | 131 | app.use(bodyParser.json()) 132 | 133 | const httpServer = app.listen(HTTP_PORT, () => { 134 | app.get('/api/devices', (req, res) => { 135 | res.status(200).send(Object.values(Devices)) 136 | }) 137 | 138 | app.post('/api/devices', (req, res) => { 139 | if (req.body.userName && req.body.name) { 140 | const device = req.body 141 | Devices[device.name] = device 142 | const packet = { 143 | topic: `/api/${device.userName}/devices/${req.method.toUpperCase()}/${ 144 | device.name 145 | }`, 146 | payload: Buffer.from(JSON.stringify(device)) 147 | } 148 | 149 | // Use pubsub to publish the event 150 | pubsub.publish(packet) 151 | 152 | res.status(200).send(device) 153 | } else { 154 | res.status(404).send({ 155 | message: 'Wrong device schema' 156 | }) 157 | } 158 | }) 159 | 160 | app.get('/api/devices/:deviceName', (req, res) => { 161 | if (req.params.deviceName in Devices) { 162 | res.status(200).send(Devices[req.params.deviceName]) 163 | } else { 164 | res.status(404).send({ 165 | message: 'Wrong device ID.' 166 | }) 167 | } 168 | }) 169 | 170 | }) 171 | } 172 | 173 | startServer() 174 | ``` 175 | 176 | ## GrapQL client 177 | 178 | If any GraphQL (WS) client subscribed to the route defined by the callback (`#/components/callbacks/DevicesEvent`), it will get the content transfered by PubSub. 179 | 180 | ```javascript 181 | import axios from 'axios' 182 | import { SubscriptionClient } from 'subscriptions-transport-ws' 183 | import pubsub from './pubsub' 184 | 185 | const GRAPHQL_HTTP_PORT = 3000 186 | const REST_HTTP_PORT = 4000 187 | 188 | const device = { 189 | userName: 'Carlos', 190 | name: 'Bot' 191 | } 192 | 193 | const startClient = () => { 194 | // Generate subscription via GraphQL WS API... 195 | const client = new SubscriptionClient( 196 | `ws://localhost:${GRAPHQL_HTTP_PORT}/subscriptions` 197 | ) 198 | 199 | client.request({ 200 | query: `subscription watchDevice($topicInput: TopicInput!) { 201 | devicesEventListener(topicInput: $topicInput) { 202 | name 203 | userName 204 | status 205 | } 206 | }`, 207 | operationName: 'watchDevice', 208 | variables: { 209 | topicInput: { 210 | method: 'POST', 211 | userName: `${device.userName}` 212 | } 213 | } 214 | }) 215 | .subscribe({ 216 | next: {data} => { 217 | console.log('Device created', data) 218 | }, 219 | }) 220 | 221 | // ...or directly via PubSub instance like OpenAPI-to-GraphQL would do 222 | pubsub.subscribe(`/api/${device.userName}/devices/POST/*`, (...args) => { 223 | console.log('Device created', args) 224 | }) 225 | 226 | 227 | // Trigger device creation via GraphQL HTTP API... 228 | axios({ 229 | url: `http://localhost:${GRAPHQL_HTTP_PORT}/graphql`, 230 | method: 'POST', 231 | json: true, 232 | data: { 233 | query: `mutation($deviceInput: DeviceInput!) { 234 | createDevice(deviceInput: $deviceInput) { 235 | name 236 | userName 237 | } 238 | }`, 239 | variables: device, 240 | }, 241 | }) 242 | 243 | // ...or via REST API like OpenAPI-to-GraphQL would do 244 | axios({ 245 | url: `http://localhost:${REST_HTTP_PORT}/api/devices`, 246 | method: 'POST', 247 | json: true, 248 | data: device, 249 | }) 250 | } 251 | 252 | startClient() 253 | ``` 254 | 255 | In this example, we rely on the [`subscriptions-transport-ws` package](https://github.com/apollographql/subscriptions-transport-ws) to create a `SubscriptionServer` that manages WebSockets connections between the GraphQL clients and our server. We also rely on the `graphqlExpress` server provided by the [`apollo-server-express` package](https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-express) to serve GraphQL from Express.js. 256 | 257 | 258 | Concerning callbacks, as you can see in the example, the path (runtime expression) `/api/{$request.body#/userName}/devices/{$request.body#/method}/+` is delimited by `/` and ends with `+`, these symbols are interpreted as delimiters and wildcard when using MQTT topics. 259 | It needs to be adapted accordingly to the client wrapped in your PubSub instance, for eventEmitter2 you can use `*` and define your own delimiter. 260 | A helper might be provided in the future, to simplify this process. 261 | 262 | ## Examples 263 | 264 | You can also run the example provided in this project. 265 | 266 | Start REST API server (HTTP and MQTT) : 267 | ```sh 268 | npm run api_sub 269 | ``` 270 | 271 | Start GRAPHQL server (HTTP and WS) : 272 | ```sh 273 | npm run start_dev_sub 274 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openapi-graphql", 3 | "version": "2.0.0", 4 | "description": "Generates a GraphQL schema for a given OpenAPI Specification (OAS)", 5 | "contributors": [ 6 | "Matteo Collina", 7 | "Alan Cha", 8 | "Erik Wittern" 9 | ], 10 | "engines": { 11 | "node": ">=12" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/mcollina/openapi-graphql" 16 | }, 17 | "homepage": "https://github.com/mcollina/openapi-graphql", 18 | "keywords": [ 19 | "oas", 20 | "openapi specification", 21 | "graphql", 22 | "translation", 23 | "wrap", 24 | "create", 25 | "rest", 26 | "restful", 27 | "api", 28 | "apiharmony" 29 | ], 30 | "license": "MIT", 31 | "standard": { 32 | "ignore": [ 33 | "*.js" 34 | ] 35 | }, 36 | "main": "lib/index.js", 37 | "types": "lib/index.d.ts", 38 | "scripts": { 39 | "api": "nodemon test/example_api_server.js", 40 | "api_sub": "nodemon test/example_api5_server.js", 41 | "dev": "tsc -w", 42 | "start_dev": "DEBUG=preprocessing,translation,http nodemon test/example_gql_server.js", 43 | "start_dev_ws": "DEBUG=preprocessing,translation,http,pubsub nodemon test/example_gql_server_ws.js", 44 | "build": "tsc", 45 | "guru-load": "node test/evaluation/load_apis_guru.js", 46 | "guru-test": "DEBUG=preprocessing,translation node test/evaluation/eval_apis_guru.js", 47 | "test": "npm run build && jest --runInBand" 48 | }, 49 | "jest": { 50 | "moduleFileExtensions": [ 51 | "ts", 52 | "tsx", 53 | "js" 54 | ], 55 | "transform": { 56 | "\\.(ts|tsx)$": "ts-jest" 57 | }, 58 | "testRegex": "/test/.*\\.test\\.(ts|tsx|js)$" 59 | }, 60 | "dependencies": { 61 | "debug": "^4.2.0", 62 | "deep-equal": "^2.0.1", 63 | "form-urlencoded": "^6.0.4", 64 | "graphql-type-json": "^0.3.2", 65 | "json-ptr": "^3.0.1", 66 | "jsonpath-plus": "^6.0.1", 67 | "oas-validator": "^5.0.2", 68 | "pluralize": "^8.0.0", 69 | "swagger2openapi": "^7.0.2" 70 | }, 71 | "peerDependencies": { 72 | "graphql": "^16.0.0" 73 | }, 74 | "devDependencies": { 75 | "@types/node": "^18.6.3", 76 | "aedes-persistence": "^9.0.0", 77 | "body-parser": "^1.18.3", 78 | "cookie-parser": "^1.4.5", 79 | "express": "^4.16.4", 80 | "glob": "^8.0.1", 81 | "graphql": "^16.0.0", 82 | "husky": "^8.0.1", 83 | "isomorphic-git": "^1.7.8", 84 | "jest": "^27.0.0", 85 | "js-yaml": "^4.1.0", 86 | "nodemon": "^2.0.2", 87 | "prettier": "^2.1.2", 88 | "pretty-quick": "^3.0.2", 89 | "qs": "^6.10.1", 90 | "rimraf": "^3.0.1", 91 | "simple-statistics": "^7.3.0", 92 | "standard": "^17.0.0", 93 | "ts-jest": "^27.0.0", 94 | "tslint": "^6.0.0", 95 | "tslint-config-standard": "^9.0.0", 96 | "typescript": "^4.0.3", 97 | "undici": "^5.0.0" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/auth_builder.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Functions to create viewers that allow users to pass credentials to resolve 8 | * functions used by OpenAPI-to-GraphQL. 9 | */ 10 | 11 | // Type imports: 12 | import { 13 | GraphQLString, 14 | GraphQLObjectType, 15 | GraphQLNonNull, 16 | GraphQLFieldConfigMap, 17 | GraphQLFieldResolver, 18 | GraphQLFieldConfig 19 | } from 'graphql' 20 | import { Args, GraphQLOperationType } from './types/graphql' 21 | import { 22 | PreprocessingData, 23 | ProcessedSecurityScheme 24 | } from './types/preprocessing_data' 25 | 26 | // Imports: 27 | import { getGraphQLType } from './schema_builder' 28 | import * as Oas3Tools from './oas_3_tools' 29 | import debug from 'debug' 30 | import { handleWarning, sortObject, MitigationTypes } from './utils' 31 | import { createDataDef } from './preprocessor' 32 | 33 | const translationLog = debug('translation') 34 | 35 | /** 36 | * Load the field object in the appropriate root object 37 | * 38 | * i.e. inside either rootQueryFields/rootMutationFields or inside 39 | * rootQueryFields/rootMutationFields for further processing 40 | */ 41 | export function createAndLoadViewer( 42 | queryFields: object, 43 | operationType: GraphQLOperationType, 44 | data: PreprocessingData 45 | ): { [key: string]: GraphQLFieldConfig } { 46 | const results = {} 47 | /** 48 | * To ensure that viewers have unique names, we add a numerical postfix. 49 | * 50 | * This object keeps track of what the postfix should be. 51 | * 52 | * The key is the security scheme type and the value is 53 | * the current highest postfix used for viewers of that security scheme type. 54 | */ 55 | const viewerNamePostfix: { [key: string]: number } = {} 56 | 57 | /** 58 | * Used to collect all fields in the given querFields object, no matter which 59 | * protocol. Used to populate anyAuthViewer. 60 | */ 61 | const anyAuthFields = {} 62 | 63 | for (let protocolName in queryFields) { 64 | Object.assign(anyAuthFields, queryFields[protocolName]) 65 | 66 | /** 67 | * Check if the name has already been used (i.e. in the list) 68 | * if so, create a new name and add it to the list 69 | */ 70 | const securityType = data.security[protocolName].def.type 71 | let viewerType: string 72 | 73 | /** 74 | * HTTP is not an authentication protocol 75 | * HTTP covers a number of different authentication type 76 | * change the typeName to match the exact authentication type (e.g. basic 77 | * authentication) 78 | */ 79 | if (securityType === 'http') { 80 | let scheme = data.security[protocolName].def.scheme 81 | switch (scheme) { 82 | case 'basic': 83 | viewerType = 'basicAuth' 84 | break 85 | 86 | default: 87 | handleWarning({ 88 | mitigationType: MitigationTypes.UNSUPPORTED_HTTP_SECURITY_SCHEME, 89 | message: 90 | `Currently unsupported HTTP authentication protocol ` + 91 | `type 'http' and scheme '${scheme}'`, 92 | data, 93 | log: translationLog 94 | }) 95 | 96 | continue 97 | } 98 | } else { 99 | viewerType = securityType 100 | } 101 | 102 | // Create name for the viewer 103 | let viewerName = 104 | operationType === GraphQLOperationType.Query 105 | ? Oas3Tools.sanitize( 106 | `viewer ${viewerType}`, 107 | Oas3Tools.CaseStyle.camelCase 108 | ) 109 | : operationType === GraphQLOperationType.Mutation 110 | ? Oas3Tools.sanitize( 111 | `mutation viewer ${viewerType}`, 112 | Oas3Tools.CaseStyle.camelCase 113 | ) 114 | : Oas3Tools.sanitize( 115 | `subscription viewer ${viewerType}`, 116 | Oas3Tools.CaseStyle.camelCase 117 | ) 118 | 119 | // Ensure unique viewer name 120 | // If name already exists, append a number at the end of the name 121 | if (!(viewerType in viewerNamePostfix)) { 122 | viewerNamePostfix[viewerType] = 1 123 | } else { 124 | viewerName += ++viewerNamePostfix[viewerType] 125 | } 126 | 127 | // Add the viewer object type to the specified root query object type 128 | results[viewerName] = getViewerOT( 129 | viewerName, 130 | protocolName, 131 | securityType, 132 | queryFields[protocolName], 133 | data 134 | ) 135 | } 136 | 137 | // Create name for the AnyAuth viewer 138 | const anyAuthObjectName = 139 | operationType === GraphQLOperationType.Query 140 | ? 'viewerAnyAuth' 141 | : operationType === GraphQLOperationType.Mutation 142 | ? 'mutationViewerAnyAuth' 143 | : 'subscriptionViewerAnyAuth' 144 | 145 | // Add the AnyAuth object type to the specified root query object type 146 | results[anyAuthObjectName] = getViewerAnyAuthOT( 147 | anyAuthObjectName, 148 | anyAuthFields, 149 | data 150 | ) 151 | 152 | return results 153 | } 154 | 155 | /** 156 | * Get the viewer object, resolve function, and arguments 157 | */ 158 | function getViewerOT( 159 | name: string, 160 | protocolName: string, 161 | securityType: string, 162 | queryFields: GraphQLFieldConfigMap, 163 | data: PreprocessingData 164 | ): GraphQLFieldConfig { 165 | const scheme: ProcessedSecurityScheme = data.security[protocolName] 166 | 167 | // Resolve function: 168 | const resolve: GraphQLFieldResolver = ( 169 | source, 170 | args, 171 | context, 172 | info 173 | ) => { 174 | const security = {} 175 | const saneProtocolName = Oas3Tools.sanitize( 176 | protocolName, 177 | Oas3Tools.CaseStyle.camelCase 178 | ) 179 | security[ 180 | Oas3Tools.storeSaneName(saneProtocolName, protocolName, data.saneMap) 181 | ] = args 182 | 183 | /** 184 | * Viewers are always root, so we can instantiate _openAPIToGraphQL here without 185 | * previously checking for its existence 186 | */ 187 | return { 188 | _openAPIToGraphQL: { 189 | security 190 | } 191 | } 192 | } 193 | 194 | // Arguments: 195 | /** 196 | * Do not sort because they are already "sorted" in preprocessing. 197 | * Otherwise, for basic auth, "password" will appear before "username" 198 | */ 199 | const args = {} 200 | if (typeof scheme === 'object') { 201 | for (let parameterName in scheme.parameters) { 202 | // The parameter name should be already sane as it is provided by OpenAPI-to-GraphQL 203 | const saneParameterName = Oas3Tools.sanitize( 204 | parameterName, 205 | Oas3Tools.CaseStyle.camelCase 206 | ) 207 | args[saneParameterName] = { type: new GraphQLNonNull(GraphQLString) } 208 | } 209 | } 210 | 211 | let typeDescription = `A viewer for security scheme '${protocolName}'` 212 | /** 213 | * HTTP authentication uses different schemes. It is not sufficient to name 214 | * only the security type 215 | */ 216 | let description = 217 | securityType === 'http' 218 | ? `A viewer that wraps all operations authenticated via security scheme ` + 219 | `'${protocolName}', which is of type 'http' '${scheme.def.scheme}'` 220 | : `A viewer that wraps all operations authenticated via security scheme ` + 221 | `'${protocolName}', which is of type '${securityType}'` 222 | 223 | if (data.oass.length !== 1) { 224 | typeDescription += ` in OAS '${scheme.oas.info.title}'` 225 | description = `, in OAS '${scheme.oas.info.title}` 226 | } 227 | 228 | return { 229 | type: new GraphQLObjectType({ 230 | name: Oas3Tools.capitalize(name), // Should already be sanitized and in camelCase 231 | description: typeDescription, 232 | fields: () => queryFields 233 | }), 234 | resolve, 235 | args, 236 | description 237 | } 238 | } 239 | 240 | /** 241 | * Create an object containing an AnyAuth viewer, its resolve function, 242 | * and its args. 243 | */ 244 | function getViewerAnyAuthOT( 245 | name: string, 246 | queryFields: GraphQLFieldConfigMap, 247 | data: PreprocessingData 248 | ): GraphQLFieldConfig { 249 | // Resolve function: 250 | const resolve: GraphQLFieldResolver = ( 251 | source, 252 | args, 253 | context, 254 | info 255 | ) => { 256 | return { 257 | _openAPIToGraphQL: { 258 | security: args 259 | } 260 | } 261 | } 262 | 263 | // Arguments: 264 | let args = {} 265 | for (let protocolName in data.security) { 266 | // Create input object types for the viewer arguments 267 | const def = createDataDef( 268 | { fromRef: protocolName }, 269 | data.security[protocolName].schema, 270 | true, 271 | data, 272 | data.security[protocolName].oas 273 | ) 274 | 275 | const type = getGraphQLType({ 276 | def, 277 | data, 278 | isInputObjectType: true 279 | }) 280 | 281 | const saneProtocolName = Oas3Tools.sanitize( 282 | protocolName, 283 | Oas3Tools.CaseStyle.camelCase 284 | ) 285 | args[ 286 | Oas3Tools.storeSaneName(saneProtocolName, protocolName, data.saneMap) 287 | ] = { type } 288 | } 289 | args = sortObject(args) 290 | 291 | return { 292 | type: new GraphQLObjectType({ 293 | name: Oas3Tools.capitalize(name), // Should already be GraphQL safe 294 | description: 'Warning: Not every request will work with this viewer type', 295 | fields: () => queryFields 296 | }), 297 | resolve, 298 | args, 299 | description: 300 | `A viewer that wraps operations for all available ` + 301 | `authentication mechanisms` 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/graphql_tools.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Utilities related to GraphQL. 8 | */ 9 | 10 | import { GraphQLObjectType, GraphQLString } from 'graphql' 11 | 12 | /** 13 | * Returns empty GraphQLObjectType. 14 | */ 15 | export function getEmptyObjectType(name: string): GraphQLObjectType { 16 | return new GraphQLObjectType({ 17 | name: name + 'Placeholder', 18 | description: 'Placeholder object', 19 | fields: { 20 | message: { 21 | type: GraphQLString, 22 | description: 'Placeholder field', 23 | resolve: () => { 24 | return 'This is a placeholder field.' 25 | } 26 | } 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/types/graphql.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Custom type definitions for GraphQL. 8 | */ 9 | 10 | import { 11 | GraphQLObjectType, 12 | GraphQLScalarType, 13 | GraphQLInputObjectType, 14 | GraphQLList, 15 | GraphQLEnumType, 16 | GraphQLUnionType, 17 | GraphQLFieldResolver 18 | } from 'graphql' 19 | 20 | export enum GraphQLOperationType { 21 | Query, 22 | Mutation 23 | } 24 | 25 | export type GraphQLType = 26 | | GraphQLObjectType 27 | | GraphQLInputObjectType 28 | | GraphQLList 29 | | GraphQLUnionType 30 | | GraphQLEnumType 31 | | GraphQLScalarType 32 | 33 | type Arg = { 34 | type: any 35 | description?: string 36 | } 37 | 38 | export type Args = { 39 | [key: string]: Arg 40 | } 41 | 42 | export type Field = { 43 | type: GraphQLType 44 | resolve?: GraphQLFieldResolver 45 | args?: Args 46 | description: string 47 | } 48 | -------------------------------------------------------------------------------- /src/types/oas2.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Type definitions for the OpenAPI specification 2.0 (Swagger). 8 | * 9 | * NOTE: We do not really care about OpenAPI specification 2.0 / Swagger, as we 10 | * translate it to Oas3 immediately anyways. 11 | */ 12 | 13 | export type Oas2 = { 14 | swagger: string 15 | [key: string]: any 16 | } 17 | -------------------------------------------------------------------------------- /src/types/oas3.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Type definitions for the OpenAPI Specification 3. 8 | */ 9 | 10 | type ExternalDocumentationObject = { 11 | description?: string 12 | url: string 13 | } 14 | 15 | export type SchemaObject = { 16 | title?: string 17 | type?: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' 18 | format?: string 19 | nullable?: boolean 20 | description?: string 21 | properties?: { 22 | [key: string]: SchemaObject | ReferenceObject 23 | } 24 | required?: string[] 25 | default?: any 26 | additionalProperties?: SchemaObject | ReferenceObject | boolean 27 | items?: SchemaObject | ReferenceObject // MUST be a single schema object in OAS, see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#properties 28 | additionalItems?: boolean | string[] 29 | enum?: string[] 30 | allOf?: (SchemaObject | ReferenceObject)[] 31 | anyOf?: (SchemaObject | ReferenceObject)[] 32 | oneOf?: (SchemaObject | ReferenceObject)[] 33 | not?: (SchemaObject | ReferenceObject)[] 34 | } 35 | 36 | export type ReferenceObject = { 37 | $ref: string 38 | } 39 | 40 | type ExampleObject = { 41 | summary?: string 42 | description?: string 43 | value?: any 44 | externalValue?: string 45 | } 46 | 47 | type HeaderObject = { 48 | name?: string 49 | in?: 'query' | 'header' | 'path' | 'cookie' 50 | description?: string 51 | required?: boolean 52 | deprecated?: boolean 53 | allowEmptyValue?: boolean 54 | } 55 | 56 | type EncodingObject = { 57 | contentType?: string 58 | headers?: { 59 | [key: string]: HeaderObject | ReferenceObject 60 | } 61 | style?: string 62 | explode?: boolean 63 | allowReserved?: boolean 64 | } 65 | 66 | export type MediaTypeObject = { 67 | schema?: SchemaObject | ReferenceObject 68 | example?: any 69 | examples?: { 70 | [key: string]: ExampleObject | ReferenceObject 71 | } 72 | encoding?: { 73 | [key: string]: EncodingObject 74 | } 75 | } 76 | 77 | export type ParameterObject = { 78 | name: string 79 | in: 'query' | 'header' | 'path' | 'cookie' 80 | description?: string 81 | required?: boolean 82 | deprecated?: boolean 83 | allowEmptyValue?: boolean 84 | style?: 'form' | 'simple' 85 | explode?: boolean 86 | allowReserved?: boolean 87 | schema?: SchemaObject | ReferenceObject 88 | example?: any 89 | examples?: { 90 | [key: string]: ExampleObject | ReferenceObject 91 | } 92 | content?: { 93 | [key: string]: MediaTypeObject 94 | } 95 | } 96 | 97 | export type MediaTypesObject = { 98 | [key: string]: MediaTypeObject 99 | } 100 | 101 | export type ServerObject = { 102 | url: string 103 | description?: string 104 | variables?: object // TODO: extend 105 | } 106 | 107 | export type RequestBodyObject = { 108 | description?: string 109 | content: { 110 | [key: string]: MediaTypeObject 111 | } 112 | required?: boolean 113 | } 114 | 115 | export type LinkObject = { 116 | operationRef?: string 117 | operationId?: string 118 | parameters?: { 119 | [key: string]: any 120 | } 121 | requestBody?: any 122 | description?: string 123 | server?: ServerObject 124 | } 125 | 126 | export type LinksObject = { 127 | [key: string]: LinkObject | ReferenceObject 128 | } 129 | 130 | export type ResponseObject = { 131 | description: string 132 | headers?: { 133 | [key: string]: HeaderObject | ReferenceObject 134 | } 135 | content?: MediaTypesObject 136 | links?: LinksObject 137 | } 138 | 139 | export type ResponsesObject = { 140 | [key: string]: ResponseObject | ReferenceObject 141 | } 142 | 143 | export type SecurityRequirementObject = { 144 | [key: string]: string[] 145 | } 146 | 147 | export type OperationObject = { 148 | tags?: string[] 149 | summary?: string 150 | description?: string 151 | externalDocs?: ExternalDocumentationObject 152 | operationId?: string 153 | parameters?: Array 154 | requestBody?: RequestBodyObject | ReferenceObject 155 | responses?: ResponsesObject 156 | callbacks?: CallbacksObject 157 | deprecated?: boolean 158 | security?: SecurityRequirementObject[] 159 | servers?: ServerObject[] 160 | } 161 | 162 | export type PathItemObject = { 163 | $ref?: string 164 | summary?: string 165 | description?: string 166 | get: OperationObject 167 | put: OperationObject 168 | post: OperationObject 169 | delete: OperationObject 170 | options: OperationObject 171 | head: OperationObject 172 | patch: OperationObject 173 | trace: OperationObject 174 | servers?: ServerObject[] 175 | parameters?: [ParameterObject | ReferenceObject] 176 | } 177 | 178 | type PathsObject = { 179 | [key: string]: PathItemObject 180 | } 181 | 182 | export type CallbackObject = { 183 | [key: string]: PathItemObject 184 | } 185 | 186 | export type CallbacksObject = { 187 | [key: string]: CallbackObject | ReferenceObject 188 | } 189 | 190 | type OAuthFlowObject = { 191 | authorizationUrl?: string // Optional, beacause applies only to certain flows 192 | tokenUrl?: string // Optional, beacause applies only to certain flows 193 | refreshUrl?: string // Optional, beacause applies only to certain flows 194 | scopes?: { 195 | // Optional, beacause applies only to certain flows 196 | [key: string]: string 197 | } 198 | } 199 | 200 | type OAuthFlowsObject = { 201 | implicit?: OAuthFlowObject 202 | password?: OAuthFlowObject 203 | clientCredentials?: OAuthFlowObject 204 | authorizationCode?: OAuthFlowObject 205 | } 206 | 207 | export type SecuritySchemeObject = { 208 | type: 'apiKey' | 'http' | 'oauth2' | 'openIdConnect' 209 | description?: string 210 | name?: string // Optional, because applies only to apiKey 211 | in?: string // Optional, because applies only to apiKey 212 | scheme?: string // Optional, because applies only to http 213 | bearerFormat?: string 214 | flows?: OAuthFlowsObject // Optional, because applies only to oauth2 215 | openIdConnectUrl?: string // // Optional, because applies only to openIdConnect 216 | } 217 | 218 | export type SecuritySchemesObject = { 219 | [key: string]: SecuritySchemeObject | ReferenceObject 220 | } 221 | 222 | type ComponentsObject = { 223 | schemas?: { 224 | [key: string]: SchemaObject | ReferenceObject 225 | } 226 | responses?: ResponsesObject 227 | parameters?: { 228 | [key: string]: ParameterObject | ReferenceObject 229 | } 230 | examples?: { 231 | [key: string]: ExampleObject | ReferenceObject 232 | } 233 | requestBodies?: { 234 | [key: string]: RequestBodyObject | ReferenceObject 235 | } 236 | headers?: { 237 | [key: string]: HeaderObject | ReferenceObject 238 | } 239 | securitySchemes?: SecuritySchemesObject 240 | links?: LinksObject 241 | callbacks?: { 242 | [key: string]: CallbackObject | ReferenceObject 243 | } 244 | } 245 | 246 | type TagObject = { 247 | name: string 248 | description?: string 249 | externalDocs?: ExternalDocumentationObject 250 | } 251 | 252 | export type Oas3 = { 253 | openapi: string 254 | info: { 255 | title: string 256 | description?: string 257 | termsOfService?: string 258 | contact?: { 259 | name?: string 260 | url?: string 261 | email?: string 262 | } 263 | license?: { 264 | name: string 265 | url?: string 266 | } 267 | version: string 268 | } 269 | servers?: ServerObject[] 270 | paths: PathsObject 271 | components?: ComponentsObject 272 | security?: SecurityRequirementObject[] 273 | tags?: TagObject[] 274 | externalDocs?: ExternalDocumentationObject 275 | } 276 | -------------------------------------------------------------------------------- /src/types/operation.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Type definitions for the objects created during preprocessing for every 8 | * operation in the OAS. 9 | */ 10 | 11 | import { 12 | Oas3, 13 | LinkObject, 14 | OperationObject, 15 | ParameterObject, 16 | ServerObject, 17 | SchemaObject 18 | } from './oas3' 19 | 20 | import { GraphQLOperationType } from './graphql' 21 | 22 | import { 23 | GraphQLScalarType, 24 | GraphQLObjectType, 25 | GraphQLInputObjectType, 26 | GraphQLList, 27 | GraphQLEnumType, 28 | GraphQLUnionType 29 | } from 'graphql' 30 | 31 | import { HTTP_METHODS } from '../oas_3_tools' 32 | 33 | export enum TargetGraphQLType { 34 | // scalars 35 | string = 'string', 36 | integer = 'integer', 37 | float = 'float', 38 | boolean = 'boolean', 39 | id = 'id', 40 | 41 | // JSON 42 | json = 'json', 43 | 44 | // non-scalars 45 | object = 'object', 46 | list = 'list', 47 | enum = 'enum', 48 | 49 | anyOfObject = 'anyOfObject', 50 | oneOfUnion = 'oneOfUnion' 51 | } 52 | 53 | export type DataDefinition = { 54 | // OAS-related: 55 | 56 | // Ideal name for the GraphQL type and is used with the schema to identify a specific GraphQL type 57 | preferredName: string 58 | 59 | // The schema of the data type, why may have gone through some resolution, and is used with preferredName to identify a specific GraphQL type 60 | schema: SchemaObject 61 | 62 | /** 63 | * Similar to the required property in object schemas but because of certain 64 | * keywords to combine schemas, e.g. "allOf", this resolves the required 65 | * property in all member schemas 66 | */ 67 | required: string[] 68 | 69 | // The type GraphQL type this dataDefintion will be created into 70 | targetGraphQLType: TargetGraphQLType 71 | 72 | // Collapsed link objects from all operations returning the same response data 73 | links: { [key: string]: LinkObject } 74 | 75 | /** 76 | * Data definitions of subschemas in the schema 77 | * 78 | * I.e. If the dataDef is a list type, the subDefinition is a reference to the 79 | * list item type 80 | * 81 | * Or if the dataDef is an object type, the subDefinitions are references to 82 | * the field types 83 | * 84 | * Or if the dataDef is a union type, the subDefinitions are references to 85 | * the member types 86 | */ 87 | subDefinitions: 88 | | DataDefinition // For GraphQL list type 89 | | { [fieldName: string]: DataDefinition } // For GraphQL (input) object type 90 | | DataDefinition[] // For GraphQL union type 91 | 92 | // GraphQL-related: 93 | 94 | // The potential name of the GraphQL type if it is created 95 | graphQLTypeName: string 96 | 97 | // The potential name of the GraphQL input object type if it is created 98 | graphQLInputObjectTypeName: string 99 | 100 | // The GraphQL type if it is created 101 | graphQLType?: 102 | | GraphQLObjectType 103 | | GraphQLList 104 | | GraphQLUnionType 105 | | GraphQLEnumType 106 | | GraphQLScalarType 107 | 108 | // The GraphQL input object type if it is created 109 | graphQLInputObjectType?: GraphQLInputObjectType | GraphQLList 110 | } 111 | 112 | export type Operation = { 113 | /** 114 | * The raw operation object from the OAS 115 | */ 116 | operation: OperationObject 117 | 118 | /** 119 | * Identifier of the operation - may be created by concatenating method & path 120 | */ 121 | operationId: string 122 | 123 | /** 124 | * A combination of the operation method and path (and the title of the OAS 125 | * where the operation originates from if multiple OASs are provided) in the 126 | * form of: 127 | * 128 | * {title of OAS (if applicable)} {method in ALL_CAPS} {path} 129 | * 130 | * Used for documentation and logging 131 | */ 132 | operationString: string 133 | 134 | /** 135 | * Human-readable description of the operation 136 | */ 137 | description: string 138 | 139 | /** 140 | * Tags of this operation 141 | */ 142 | tags: string[] 143 | 144 | /** 145 | * URL path of this operation 146 | */ 147 | path: string 148 | 149 | /** 150 | * HTTP method for this operation 151 | */ 152 | method: HTTP_METHODS 153 | 154 | /** 155 | * Content-type of the request payload 156 | */ 157 | payloadContentType?: string 158 | 159 | /** 160 | * Information about the request payload (if any) 161 | */ 162 | payloadDefinition?: DataDefinition 163 | 164 | /** 165 | * Determines wheter request payload is required for the request 166 | */ 167 | payloadRequired: boolean 168 | 169 | /** 170 | * Content-type of the request payload 171 | */ 172 | responseContentType?: string 173 | 174 | /** 175 | * Information about the response payload 176 | */ 177 | responseDefinition: DataDefinition 178 | 179 | /** 180 | * List of parameters of the operation 181 | */ 182 | parameters: ParameterObject[] 183 | 184 | /** 185 | * List of keys of security schemes required by this operation 186 | * 187 | * NOTE: Keys are sanitized 188 | * NOTE: Does not contain OAuth 2.0-related security schemes 189 | */ 190 | securityRequirements: string[] 191 | 192 | /** 193 | * (Local) server definitions of the operation. 194 | */ 195 | servers: ServerObject[] 196 | 197 | /** 198 | * Whether this operation should be placed in an authentication viewer 199 | * (cannot be true if "viewer" option passed to OpenAPI-to-GraphQL is false). 200 | */ 201 | inViewer: boolean 202 | 203 | /** 204 | * Type of root operation type, i.e. whether the generated field should be 205 | * added to the Query, Mutation, or Subscription root operation 206 | */ 207 | operationType: GraphQLOperationType 208 | 209 | /** 210 | * The success HTTP code, 200-299, destined to become a GraphQL object type 211 | */ 212 | statusCode: string 213 | 214 | /** 215 | * The OAS which this operation originated from 216 | */ 217 | oas: Oas3 218 | } 219 | -------------------------------------------------------------------------------- /src/types/options.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | // Type imports: 7 | import { GraphQLOperationType } from './graphql' 8 | import { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql' 9 | import { HTTPRequest, RequestOptions } from './request' 10 | 11 | /** 12 | * Type definition of the options that users can pass to OpenAPI-to-GraphQL. 13 | */ 14 | export type Warning = { 15 | type: string 16 | message: string 17 | mitigation: string 18 | path?: string[] 19 | } 20 | 21 | export type Report = { 22 | warnings: Warning[] 23 | numOps: number 24 | numOpsQuery: number 25 | numOpsMutation: number 26 | numQueriesCreated: number 27 | numMutationsCreated: number 28 | } 29 | 30 | export type ConnectOptions = { 31 | [key: string]: boolean | number | string 32 | } 33 | 34 | // A standardized type that can be used to identify a specific operation in an OAS 35 | export type OasTitlePathMethodObject = { 36 | [title: string]: { 37 | [path: string]: { 38 | [method: string]: T 39 | } 40 | } 41 | } 42 | 43 | export type Headers = { [key: string]: string } 44 | 45 | export type Options = Partial< 46 | InternalOptions 47 | > 48 | 49 | export type InternalOptions = { 50 | /* 51 | * Adhere to the OAS as closely as possible. If set to true, any deviation 52 | * from the OAS will lead OpenAPI-to-GraphQL to throw. 53 | */ 54 | strict: boolean 55 | 56 | /** 57 | * Holds information about the GraphQL schema generation process 58 | */ 59 | report: Report 60 | 61 | // Schema options 62 | 63 | /** 64 | * Field names can only be sanitized operationIds 65 | * 66 | * By default, query field names are based on the return type type name and 67 | * mutation field names are based on the operationId, which may be generated 68 | * if it does not exist. 69 | * 70 | * This option forces OpenAPI-to-GraphQL to only create field names based on the 71 | * operationId. 72 | */ 73 | operationIdFieldNames: boolean 74 | 75 | /** 76 | * Under certain circumstances (such as response code 204), some RESTful 77 | * operations should not return any data. However, GraphQL objects must have 78 | * a data structure. Normally, these operations would be ignored but for the 79 | * sake of completeness, the following option will give these operations a 80 | * placeholder data structure. Even though the data structure will not have 81 | * any practical use, at least the operations will show up in the schema. 82 | */ 83 | fillEmptyResponses: boolean 84 | 85 | /** 86 | * Auto-generate a 'limit' argument for all fields that return lists of 87 | * objects, including ones produced by links 88 | * 89 | * Allows to constrain the return size of lists of objects 90 | * 91 | * Returns the first n number of elements in the list 92 | */ 93 | addLimitArgument: boolean 94 | 95 | /** 96 | * If a schema is of type string and has format UUID, it will be translated 97 | * into a GraphQL ID type. To allow for more customzation, this option allows 98 | * users to specify other formats that should be interpreted as ID types. 99 | */ 100 | idFormats?: string[] 101 | 102 | /** 103 | * Allows to define the root operation type (Query or Mutation type) of any 104 | * OAS operation explicitly. 105 | * 106 | * OpenAPI-to-GraphQL will by default make all GET operations Query fields and all other 107 | * operations into Mutation fields. 108 | * 109 | * The field is identifed first by the title of the OAS, then the path of the 110 | * operation, and lastly the method of the operation. 111 | */ 112 | selectQueryOrMutationField?: OasTitlePathMethodObject 113 | 114 | /** 115 | * Sets argument name for the payload of a mutation to 'requestBody' 116 | */ 117 | genericPayloadArgName: boolean 118 | 119 | /** 120 | * By default, field names are sanitized to conform with GraphQL conventions, 121 | * i.e. types should be in PascalCase, fields should be in camelCase, and 122 | * enum values should be in ALL_CAPS. 123 | * 124 | * This option will prevent OpenAPI-to-GraphQL from enforcing camelCase field names and 125 | * PascalCase type names, only removing illegal characters and staying as true 126 | * to the provided names in the OAS as possible. 127 | */ 128 | simpleNames: boolean 129 | 130 | /** 131 | * By default, field names are sanitized to conform with GraphQL conventions, 132 | * i.e. types should be in PascalCase, fields should be in camelCase, and 133 | * enum values should be in ALL_CAPS. 134 | * 135 | * This option will prevent OpenAPI-to-GraphQL from enforcing ALL_CAPS enum 136 | * values, only removing illegal characters and staying as true to the 137 | * provided enum values in the OAS as possible. 138 | */ 139 | simpleEnumValues: boolean 140 | 141 | /** 142 | * Experimental feature that will try to create more meaningful names from 143 | * the operation path than the response object by leveraging common 144 | * conventions. 145 | * 146 | * For example, given the operation GET /users/{userId}/car, OpenAPI-to-GraphQL will 147 | * create a Query field 'userCar'. Note that because 'users' is followed by 148 | * the parameter 'userId', it insinuates that this operation will get the car 149 | * that belongs to a singular user. Hence, the name 'userCar' is more fitting 150 | * than 'usersCar' so the pluralizing 's' is dropped. 151 | * 152 | * This option will also consider irregular plural forms. 153 | */ 154 | singularNames: boolean 155 | 156 | // Resolver options 157 | 158 | /** 159 | * Custom headers to send with every request made by a resolve function. 160 | */ 161 | headers?: { [key: string]: string } 162 | 163 | /** 164 | * Custom query parameters to send with every reqeust by a resolve function. 165 | */ 166 | qs?: { [key: string]: string } 167 | 168 | /** 169 | * Specifies the URL on which all paths will be based on. 170 | * Overrides the server object in the OAS. 171 | */ 172 | baseUrl?: string 173 | 174 | /** 175 | * Allows to define custom resolvers for fields on the Query/Mutation root 176 | * operation type. 177 | * 178 | * In other words, instead of resolving on an operation (REST call) defined in 179 | * the OAS, the field will resolve on the custom resolver. Note that this will 180 | * also affect the behavior of links. 181 | * 182 | * The field is identifed first by the title of the OAS, then the path of the 183 | * operation, and lastly the method of the operation. 184 | * 185 | * Use cases include the resolution of complex relationships between types, 186 | * implementing performance improvements like caching, or dealing with 187 | * non-standard authentication requirements. 188 | */ 189 | customResolvers?: OasTitlePathMethodObject< 190 | GraphQLFieldResolver 191 | > 192 | 193 | // Authentication options 194 | 195 | /** 196 | * Determines whether OpenAPI-to-GraphQL should create viewers that allow users to pass 197 | * basic auth and API key credentials. 198 | */ 199 | viewer: boolean 200 | 201 | /** 202 | * JSON path to OAuth 2 token contained in GraphQL context. Tokens will per 203 | * default be sent in "Authorization" header. 204 | */ 205 | tokenJSONpath?: string 206 | 207 | /** 208 | * Determines whether to send OAuth 2 token as query parameter instead of in 209 | * header. 210 | */ 211 | sendOAuthTokenInQuery: boolean 212 | 213 | // Validation options 214 | 215 | /** 216 | * We use the oas-validator library to validate Swaggers/OASs. 217 | * 218 | * We expose the options so that users can have more control over validation. 219 | * 220 | * Based on: https://github.com/Mermade/oas-kit/blob/master/docs/options.md 221 | */ 222 | oasValidatorOptions: object 223 | 224 | /** 225 | * We use the swagger2graphql library to translate Swaggers to OASs. 226 | * 227 | * We expose the options so that users can have more control over translation. 228 | * 229 | * Based on: https://github.com/Mermade/oas-kit/blob/master/docs/options.md 230 | */ 231 | swagger2OpenAPIOptions: object 232 | 233 | // Logging options 234 | 235 | /** 236 | * The error extensions is part of the GraphQLErrors that will be returned if 237 | * the query cannot be fulfilled. It provides information about the failed 238 | * REST call(e.g. the method, path, status code, response 239 | * headers, and response body). It can be useful for debugging but may 240 | * unintentionally leak information. 241 | * 242 | * This option prevents the extensions from being created. 243 | */ 244 | provideErrorExtensions: boolean 245 | 246 | /** 247 | * Appends a small statement to the end of field description that clarifies 248 | * the operation that the field will trigger. 249 | * 250 | * Will affect query and mutation fields as well as fields created from links 251 | * 252 | * In the form of: 'Equivalent to {title of OAS} {method in ALL_CAPS} {path}' 253 | * Will forgo the title is only one OAS is provided 254 | */ 255 | equivalentToMessages: boolean 256 | 257 | httpRequest: HTTPRequest 258 | requestOptions?: RequestOptions 259 | } 260 | -------------------------------------------------------------------------------- /src/types/preprocessing_data.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | /** 7 | * Type definitions for the data created during preprocessing. 8 | */ 9 | 10 | import { Operation, DataDefinition } from './operation' 11 | import { InternalOptions } from './options' 12 | import { SecuritySchemeObject, SchemaObject, Oas3, LinkObject } from './oas3' 13 | 14 | export type ProcessedSecurityScheme = { 15 | rawName: string 16 | def: SecuritySchemeObject 17 | 18 | /** 19 | * Stores the names of the authentication credentials 20 | * NOTE: Structure depends on the type of the protocol (basic, API key...) 21 | * NOTE: Mainly used for the AnyAuth viewers 22 | * NOTE: Values are sanitized (see getProcessedSecuritySchemes() in preprocessor.ts) 23 | */ 24 | parameters: { [key: string]: string } 25 | 26 | /** 27 | * JSON schema to create the viewer for this security scheme from. 28 | */ 29 | schema: SchemaObject 30 | 31 | /** 32 | * The OAS which this operation originated from 33 | */ 34 | oas: Oas3 35 | } 36 | 37 | export type PreprocessingData = { 38 | /** 39 | * List of operation objects 40 | */ 41 | operations: { [key: string]: Operation } 42 | 43 | /** 44 | * List of all the used object names to avoid collision 45 | */ 46 | usedTypeNames: string[] 47 | 48 | /** 49 | * List of data definitions for JSON schemas already used. 50 | */ 51 | defs: DataDefinition[] 52 | 53 | /** 54 | * The security definitions contained in the OAS. References are resolved. 55 | * 56 | * NOTE: Keys are sanitized 57 | * NOTE: Does not contain OAuth 2.0-related security schemes 58 | */ 59 | security: { [key: string]: ProcessedSecurityScheme } 60 | 61 | /** 62 | * Mapping between sanitized strings and their original ones 63 | */ 64 | saneMap: { [key: string]: string } 65 | 66 | /** 67 | * Options passed to OpenAPI-to-GraphQL by the user 68 | */ 69 | options: InternalOptions 70 | 71 | /** 72 | * All of the provided OASs 73 | */ 74 | oass: Oas3[] 75 | } 76 | -------------------------------------------------------------------------------- /src/types/request.ts: -------------------------------------------------------------------------------- 1 | export type RequestOptions = { 2 | headers?: { [key: string]: any } 3 | body?: any 4 | qs?: any 5 | method?: string 6 | baseUrl?: string 7 | url?: string 8 | } 9 | 10 | export type Response = { 11 | statusCode: number 12 | headers: { [key: string]: any } 13 | body: any // Buffer, string, stream.Readable, or a plain object if `json` was truthy 14 | } 15 | 16 | export interface HTTPRequest { 17 | (options: RequestOptions, context: TContext): Promise 18 | } 19 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | import { PreprocessingData } from './types/preprocessing_data' 7 | import { Warning } from './types/options' 8 | 9 | export enum MitigationTypes { 10 | /** 11 | * Problems with the OAS 12 | * 13 | * Should be caught by the module oas-validator 14 | */ 15 | INVALID_OAS = 'INVALID_OAS', 16 | UNNAMED_PARAMETER = 'UNNAMED_PARAMETER', 17 | 18 | // General problems 19 | AMBIGUOUS_UNION_MEMBERS = 'AMBIGUOUS_UNION_MEMBERS', 20 | CANNOT_GET_FIELD_TYPE = 'CANNOT_GET_FIELD_TYPE', 21 | COMBINE_SCHEMAS = 'COMBINE_SCHEMAS', 22 | DUPLICATE_FIELD_NAME = 'DUPLICATE_FIELD_NAME', 23 | DUPLICATE_LINK_KEY = 'DUPLICATE_LINK_KEY', 24 | INVALID_HTTP_METHOD = 'INVALID_HTTP_METHOD', 25 | INPUT_UNION = 'INPUT_UNION', 26 | MISSING_RESPONSE_SCHEMA = 'MISSING_RESPONSE_SCHEMA', 27 | MISSING_SCHEMA = 'MISSING_SCHEMA', 28 | MULTIPLE_RESPONSES = 'MULTIPLE_RESPONSES', 29 | NON_APPLICATION_JSON_SCHEMA = 'NON_APPLICATION_JSON_SCHEMA', 30 | OBJECT_MISSING_PROPERTIES = 'OBJECT_MISSING_PROPERTIES', 31 | UNKNOWN_TARGET_TYPE = 'UNKNOWN_TARGET_TYPE', 32 | UNRESOLVABLE_SCHEMA = 'UNRESOLVABLE_SCHEMA', 33 | UNSUPPORTED_HTTP_SECURITY_SCHEME = 'UNSUPPORTED_HTTP_SECURITY_SCHEME', 34 | UNSUPPORTED_JSON_SCHEMA_KEYWORD = 'UNSUPPORTED_JSON_SCHEMA_KEYWORD', 35 | CALLBACKS_MULTIPLE_OPERATION_OBJECTS = 'CALLBACKS_MULTIPLE_OPERATION_OBJECTS', 36 | 37 | // Links 38 | AMBIGUOUS_LINK = 'AMBIGUOUS_LINK', 39 | LINK_NAME_COLLISION = 'LINK_NAME_COLLISION', 40 | UNRESOLVABLE_LINK = 'UNRESOLVABLE_LINK', 41 | 42 | // Multiple OAS 43 | DUPLICATE_OPERATIONID = 'DUPLICATE_OPERATIONID', 44 | DUPLICATE_SECURITY_SCHEME = 'DUPLICATE_SECURITY_SCHEME', 45 | MULTIPLE_OAS_SAME_TITLE = 'MULTIPLE_OAS_SAME_TITLE', 46 | 47 | // Options 48 | CUSTOM_RESOLVER_UNKNOWN_OAS = 'CUSTOM_RESOLVER_UNKNOWN_OAS', 49 | CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD = 'CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD', 50 | LIMIT_ARGUMENT_NAME_COLLISION = 'LIMIT_ARGUMENT_NAME_COLLISION', 51 | 52 | // Miscellaneous 53 | OAUTH_SECURITY_SCHEME = 'OAUTH_SECURITY_SCHEME' 54 | } 55 | 56 | export const mitigations: { [mitigationType in MitigationTypes]: string } = { 57 | /** 58 | * Problems with the OAS 59 | * 60 | * Should be caught by the module oas-validator 61 | */ 62 | INVALID_OAS: 'Ignore issue and continue.', 63 | UNNAMED_PARAMETER: 'Ignore parameter.', 64 | 65 | // General problems 66 | AMBIGUOUS_UNION_MEMBERS: 'Ignore issue and continue.', 67 | CANNOT_GET_FIELD_TYPE: 'Ignore field and continue.', 68 | COMBINE_SCHEMAS: 'Ignore combine schema keyword and continue.', 69 | DUPLICATE_FIELD_NAME: 'Ignore field and maintain preexisting field.', 70 | DUPLICATE_LINK_KEY: 'Ignore link and maintain preexisting link.', 71 | INPUT_UNION: 'The data will be stored in an arbitrary JSON type.', 72 | INVALID_HTTP_METHOD: 'Ignore operation and continue.', 73 | MISSING_RESPONSE_SCHEMA: 'Ignore operation.', 74 | MISSING_SCHEMA: 'Use arbitrary JSON type.', 75 | MULTIPLE_RESPONSES: 76 | 'Select first response object with successful status code (200-299).', 77 | NON_APPLICATION_JSON_SCHEMA: 'Ignore schema', 78 | OBJECT_MISSING_PROPERTIES: 79 | 'The (sub-)object will be stored in an arbitrary JSON type.', 80 | UNKNOWN_TARGET_TYPE: 'The data will be stored in an arbitrary JSON type.', 81 | UNRESOLVABLE_SCHEMA: 'Ignore and continue. May lead to unexpected behavior.', 82 | UNSUPPORTED_HTTP_SECURITY_SCHEME: 'Ignore security scheme.', 83 | UNSUPPORTED_JSON_SCHEMA_KEYWORD: 'Ignore keyword and continue.', 84 | CALLBACKS_MULTIPLE_OPERATION_OBJECTS: 'Select arbitrary operation object', 85 | 86 | // Links 87 | AMBIGUOUS_LINK: `Use first occurance of '#/'.`, 88 | LINK_NAME_COLLISION: 'Ignore link and maintain preexisting field.', 89 | UNRESOLVABLE_LINK: 'Ignore link.', 90 | 91 | // Multiple OAS 92 | DUPLICATE_OPERATIONID: 'Ignore operation and maintain preexisting operation.', 93 | DUPLICATE_SECURITY_SCHEME: 94 | 'Ignore security scheme and maintain preexisting scheme.', 95 | MULTIPLE_OAS_SAME_TITLE: 'Ignore issue and continue.', 96 | 97 | // Options 98 | CUSTOM_RESOLVER_UNKNOWN_OAS: 'Ignore this set of custom resolvers.', 99 | CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD: 'Ignore this set of custom resolvers.', 100 | LIMIT_ARGUMENT_NAME_COLLISION: `Do not override existing 'limit' argument.`, 101 | 102 | // Miscellaneous 103 | OAUTH_SECURITY_SCHEME: `Do not create OAuth viewer. OAuth support is provided using the 'tokenJSONpath' option.` 104 | } 105 | 106 | /** 107 | * Utilities that are specific to OpenAPI-to-GraphQL 108 | */ 109 | export function handleWarning({ 110 | mitigationType, 111 | message, 112 | mitigationAddendum, 113 | path, 114 | data, 115 | log 116 | }: { 117 | mitigationType: MitigationTypes 118 | message: string 119 | mitigationAddendum?: string 120 | path?: string[] 121 | data: PreprocessingData 122 | log?: Function 123 | }) { 124 | const mitigation = mitigations[mitigationType] 125 | 126 | const warning: Warning = { 127 | type: mitigationType, 128 | message, 129 | mitigation: mitigationAddendum 130 | ? `${mitigation} ${mitigationAddendum}` 131 | : mitigation 132 | } 133 | 134 | if (path) { 135 | warning['path'] = path 136 | } 137 | 138 | if (data.options.strict) { 139 | throw new Error(`${warning.type} - ${warning.message}`) 140 | } else { 141 | const output = `Warning: ${warning.message} - ${warning.mitigation}` 142 | if (typeof log === 'function') { 143 | log(output) 144 | } else { 145 | console.log(output) 146 | } 147 | data.options.report.warnings.push(warning) 148 | } 149 | } 150 | 151 | // Code provided by codename- from StackOverflow 152 | // Link: https://stackoverflow.com/a/29622653 153 | export function sortObject(o: T): T { 154 | return Object.keys(o) 155 | .sort() 156 | .reduce((r, k) => ((r[k] = o[k]), r), {}) as T 157 | } 158 | 159 | /** 160 | * Finds the common property names between two objects 161 | */ 162 | export function getCommonPropertyNames(object1, object2): string[] { 163 | return Object.keys(object1).filter((propertyName) => { 164 | return propertyName in object2 165 | }) 166 | } 167 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | We have a number of test suites used to verify the behavior of OpenAPI-to-GraphQL. 4 | 5 | ### Tests against real-world APIs 6 | 7 | The following test suites perform a simple wrapping test and do not make call against any of the respective APIs. 8 | 9 | | API | Test file | 10 | |---|---| 11 | | N/A | `cloudfunction.test.ts` | 12 | | [DocuSign](https://www.docusign.com/) | `docusign.test.ts` | 13 | | N/A | `government_social_work.test.ts` | 14 | | [IBM Language Translator](https://www.ibm.com/watson/services/language-translator/) | `ibm_language_translator.test.ts` | 15 | | [Instagram](https://www.instagram.com/) | `instagram.test.ts` | 16 | | [Stripe](https://stripe.com/) | `stripe.test.ts` | 17 | | [Weather Underground](https://www.wunderground.com/) | `weather_underground_test.ts` | 18 | 19 | ### Tests against custom APIs 20 | 21 | We have created a number of example APIs for finer grain testing. Unfortunately, for a number of reasons including difficulty keeping tests on theme and some tests requiring their own specialized APIs, the number of tests have quickly grown and do not have meaningful identifiers. 22 | 23 | The following table summarizes the purposes of these tests. 24 | 25 | | Test file | API(s) | Testing purpose | 26 | |---|---|---| 27 | | `example_api.test.ts` | `Example API` | An assortment of basic functionality and options on a company-themed API | 28 | | `authentication.test.ts` | `Example API` | Basic authentication tests including using the [viewer functionality](https://github.com/IBM/openapi-to-graphql/blob/master/packages/openapi-to-graphql/README.md#authentication) | 29 | | `example_api2.test.ts` | `Example API 2` | The [`operationIdFieldNames` option](https://github.com/IBM/openapi-to-graphql/blob/master/packages/openapi-to-graphql/README.md#options) | 30 | | `example_api3.test.ts` | `Example API` and `Example API 3` | Creating GraphQL wrappers from multiple APIs and [interOAS links](https://github.com/IBM/openapi-to-graphql/blob/master/packages/openapi-to-graphql/README.md#nested-objects) | 31 | | `example_api4.test.ts` | `Example API 4` | JSON schema [combining schema](https://json-schema.org/understanding-json-schema/reference/combining.html) keywords | 32 | | `example_api5.test.ts` | `Example API 5` | The [`simpleNames` option](https://github.com/IBM/openapi-to-graphql/blob/master/packages/openapi-to-graphql/README.md#options) | 33 | | `example_api6.test.ts` | `Example API 6` | An assortment of other functionality and options | 34 | | `example_api7.test.ts` | `Example API 7` | [Subscription support](../docs/subscriptions.md) | 35 | | `extensions.test.ts` | `Extensions`, `Extensions Error 1`, `Extensions Error 2`, `Extensions Error 3`, `Extensions Error 4`, `Extensions Error 5`, `Extensions Error 6`, `Extensions Error 7` | The [`x-graphql-field-name`, `x-graphql-type-name`, and `x-graphql-enum-mapping` extensions](https://github.com/IBM/openapi-to-graphql/tree/master/packages/openapi-to-graphql#custom-type-and-field-names-and-enum-values) -------------------------------------------------------------------------------- /test/authentication.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { graphql } from 'graphql' 9 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 10 | 11 | import * as openAPIToGraphQL from '../lib/index' 12 | import { startServer, stopServer } from './example_api_server' 13 | import { httpRequest } from './httprequest' 14 | 15 | const oas = require('./fixtures/example_oas.json') 16 | const PORT = 3003 17 | // update PORT for this test case: 18 | oas.servers[0].variables.port.default = String(PORT) 19 | 20 | let createdSchema 21 | 22 | /** 23 | * Set up the schema first and run example API server 24 | */ 25 | beforeAll(() => { 26 | return Promise.all([ 27 | openAPIToGraphQL 28 | .createGraphQLSchema(oas, { httpRequest }) 29 | .then(({ schema }) => { 30 | createdSchema = schema 31 | }), 32 | startServer(PORT) 33 | ]) 34 | }) 35 | 36 | /** 37 | * Shut down API server 38 | */ 39 | afterAll(() => { 40 | return stopServer() 41 | }) 42 | 43 | test('Get patent using basic auth', () => { 44 | const query = `{ 45 | viewerBasicAuth (username: "arlene123", password: "password123") { 46 | patentWithId (patentId: "100") { 47 | patentId 48 | } 49 | } 50 | }` 51 | return graphql({ schema: createdSchema, source: query }).then((result) => { 52 | expect(result).toEqual({ 53 | data: { 54 | viewerBasicAuth: { 55 | patentWithId: { 56 | patentId: '100' 57 | } 58 | } 59 | } 60 | }) 61 | }) 62 | }) 63 | 64 | test('Get patent using API key', () => { 65 | const query = `{ 66 | viewerApiKey2 (apiKey: "abcdef") { 67 | patentWithId (patentId: "100") { 68 | patentId 69 | } 70 | } 71 | }` 72 | return graphql({ schema: createdSchema, source: query }).then((result) => { 73 | expect(result).toEqual({ 74 | data: { 75 | viewerApiKey2: { 76 | patentWithId: { 77 | patentId: '100' 78 | } 79 | } 80 | } 81 | }) 82 | }) 83 | }) 84 | 85 | test('Get patent using API key 3', () => { 86 | const query = `{ 87 | viewerApiKey3 (apiKey: "abcdef") { 88 | patentWithId (patentId: "100") { 89 | patentId 90 | } 91 | } 92 | }` 93 | return graphql({ schema: createdSchema, source: query }).then((result) => { 94 | expect(result).toEqual({ 95 | data: { 96 | viewerApiKey3: { 97 | patentWithId: { 98 | patentId: '100' 99 | } 100 | } 101 | } 102 | }) 103 | }) 104 | }) 105 | 106 | test('Get project using API key 1', () => { 107 | const query = `{ 108 | viewerApiKey (apiKey: "abcdef") { 109 | projectWithId (projectId: 1) { 110 | active 111 | projectId 112 | } 113 | } 114 | }` 115 | return graphql({ schema: createdSchema, source: query }).then((result) => { 116 | expect(result).toEqual({ 117 | data: { 118 | viewerApiKey: { 119 | projectWithId: { 120 | active: true, 121 | projectId: 1 122 | } 123 | } 124 | } 125 | }) 126 | }) 127 | }) 128 | 129 | test('Get project using API key passed as option - viewer is disabled', async () => { 130 | const { schema } = await openAPIToGraphQL.createGraphQLSchema(oas, { 131 | viewer: false, 132 | headers: { 133 | access_token: 'abcdef' 134 | }, 135 | httpRequest 136 | }) 137 | const query = `{ 138 | projectWithId (projectId: 1) { 139 | projectId 140 | } 141 | }` 142 | return graphql({ schema: schema, source: query }).then((result) => { 143 | expect(result).toEqual({ 144 | data: { 145 | projectWithId: { 146 | projectId: 1 147 | } 148 | } 149 | }) 150 | }) 151 | }) 152 | 153 | test('Get project using API key passed in the requestOptions - viewer is disabled', async () => { 154 | const { schema } = await openAPIToGraphQL.createGraphQLSchema(oas, { 155 | viewer: false, 156 | requestOptions: { 157 | headers: { 158 | access_token: 'abcdef' 159 | }, 160 | url: undefined // Mandatory for requestOptions type 161 | }, 162 | httpRequest 163 | }) 164 | const query = `{ 165 | projectWithId (projectId: 1) { 166 | projectId 167 | } 168 | }` 169 | return graphql({ schema: schema, source: query }).then((result) => { 170 | expect(result).toEqual({ 171 | data: { 172 | projectWithId: { 173 | projectId: 1 174 | } 175 | } 176 | }) 177 | }) 178 | }) 179 | 180 | test('Get project using API key 2', () => { 181 | const query = `{ 182 | viewerApiKey2 (apiKey: "abcdef") { 183 | projectWithId (projectId: 1) { 184 | projectId 185 | } 186 | } 187 | }` 188 | return graphql({ schema: createdSchema, source: query }).then((result) => { 189 | expect(result).toEqual({ 190 | data: { 191 | viewerApiKey2: { 192 | projectWithId: { 193 | projectId: 1 194 | } 195 | } 196 | } 197 | }) 198 | }) 199 | }) 200 | 201 | test('Post project using API key 1', () => { 202 | const query = `mutation { 203 | mutationViewerApiKey (apiKey: "abcdef") { 204 | postProjectWithId (projectWithIdInput: { 205 | projectId: 123 206 | leadId: "arlene" 207 | }) { 208 | projectLead { 209 | name 210 | } 211 | } 212 | } 213 | }` 214 | return graphql({ schema: createdSchema, source: query }).then((result) => { 215 | expect(result).toEqual({ 216 | data: { 217 | mutationViewerApiKey: { 218 | postProjectWithId: { 219 | projectLead: { 220 | name: 'Arlene L McMahon' 221 | } 222 | } 223 | } 224 | } 225 | }) 226 | }) 227 | }) 228 | 229 | test('Post project using API key 2', () => { 230 | const query = `mutation { 231 | mutationViewerApiKey2 (apiKey: "abcdef") { 232 | postProjectWithId (projectWithIdInput: { 233 | projectId: 123 234 | leadId: "arlene" 235 | }) { 236 | projectLead { 237 | name 238 | } 239 | } 240 | } 241 | }` 242 | return graphql({ schema: createdSchema, source: query }).then((result) => { 243 | expect(result).toEqual({ 244 | data: { 245 | mutationViewerApiKey2: { 246 | postProjectWithId: { 247 | projectLead: { 248 | name: 'Arlene L McMahon' 249 | } 250 | } 251 | } 252 | } 253 | }) 254 | }) 255 | }) 256 | 257 | test('Get project using API key 3', async () => { 258 | const query = `{ 259 | viewerApiKey3 (apiKey: "abcdef") { 260 | projectWithId (projectId: 1) { 261 | projectId 262 | } 263 | } 264 | }` 265 | return graphql({ schema: createdSchema, source: query }).then((result) => { 266 | expect(result).toEqual({ 267 | data: { 268 | viewerApiKey3: { 269 | projectWithId: { 270 | projectId: 1 271 | } 272 | } 273 | } 274 | }) 275 | }) 276 | }) 277 | 278 | test('Get project using API key 3 passed as option - viewer is disabled', async () => { 279 | const { schema } = await openAPIToGraphQL.createGraphQLSchema(oas, { 280 | viewer: false, 281 | headers: { 282 | cookie: 'access_token=abcdef' 283 | }, 284 | httpRequest 285 | }) 286 | const query = `{ 287 | projectWithId (projectId: 1) { 288 | projectId 289 | } 290 | }` 291 | return graphql({ schema: schema, source: query }).then((result) => { 292 | expect(result).toEqual({ 293 | data: { 294 | projectWithId: { 295 | projectId: 1 296 | } 297 | } 298 | }) 299 | }) 300 | }) 301 | 302 | test('Get project using API key 3 passed in the requestOptions - viewer is disabled', async () => { 303 | const { schema } = await openAPIToGraphQL.createGraphQLSchema(oas, { 304 | viewer: false, 305 | requestOptions: { 306 | headers: { 307 | cookie: 'access_token=abcdef' 308 | }, 309 | url: undefined // Mandatory for requestOptions type 310 | }, 311 | httpRequest 312 | }) 313 | const query = `{ 314 | projectWithId (projectId: 1) { 315 | projectId 316 | } 317 | }` 318 | return graphql({ schema: schema, source: query }).then((result) => { 319 | expect(result).toEqual({ 320 | data: { 321 | projectWithId: { 322 | projectId: 1 323 | } 324 | } 325 | }) 326 | }) 327 | }) 328 | 329 | test('Basic AnyAuth usage', () => { 330 | const query = `{ 331 | viewerAnyAuth(exampleApiBasicProtocol: {username: "arlene123", password: "password123"}) { 332 | patentWithId (patentId: "100") { 333 | patentId 334 | } 335 | } 336 | }` 337 | return graphql({ schema: createdSchema, source: query }).then((result) => { 338 | expect(result).toEqual({ 339 | data: { 340 | viewerAnyAuth: { 341 | patentWithId: { 342 | patentId: '100' 343 | } 344 | } 345 | } 346 | }) 347 | }) 348 | }) 349 | 350 | test('Basic AnyAuth usage with extraneous auth data', () => { 351 | const query = `{ 352 | viewerAnyAuth(exampleApiKeyProtocol: {apiKey: "abcdef"}, exampleApiBasicProtocol: {username: "arlene123", password: "password123"}) { 353 | patentWithId (patentId: "100") { 354 | patentId 355 | } 356 | } 357 | }` 358 | return graphql({ schema: createdSchema, source: query }).then((result) => { 359 | expect(result).toEqual({ 360 | data: { 361 | viewerAnyAuth: { 362 | patentWithId: { 363 | patentId: '100' 364 | } 365 | } 366 | } 367 | }) 368 | }) 369 | }) 370 | 371 | test('Basic AnyAuth usage with multiple operations', () => { 372 | const query = `{ 373 | viewerAnyAuth(exampleApiKeyProtocol2: {apiKey: "abcdef"}) { 374 | patentWithId (patentId: "100") { 375 | patentId 376 | } 377 | projectWithId (projectId: 1) { 378 | projectId 379 | } 380 | } 381 | }` 382 | return graphql({ schema: createdSchema, source: query }).then((result) => { 383 | expect(result).toEqual({ 384 | data: { 385 | viewerAnyAuth: { 386 | patentWithId: { 387 | patentId: '100' 388 | }, 389 | projectWithId: { 390 | projectId: 1 391 | } 392 | } 393 | } 394 | }) 395 | }) 396 | }) 397 | 398 | test('AnyAuth with multiple operations with different auth requirements', () => { 399 | const query = `{ 400 | viewerAnyAuth(exampleApiBasicProtocol: {username: "arlene123", password: "password123"}, exampleApiKeyProtocol: {apiKey: "abcdef"}) { 401 | patentWithId (patentId: "100") { 402 | patentId 403 | } 404 | projectWithId (projectId: 1) { 405 | projectId 406 | } 407 | } 408 | }` 409 | return graphql({ schema: createdSchema, source: query }).then((result) => { 410 | expect(result).toEqual({ 411 | data: { 412 | viewerAnyAuth: { 413 | patentWithId: { 414 | patentId: '100' 415 | }, 416 | projectWithId: { 417 | projectId: 1 418 | } 419 | } 420 | } 421 | }) 422 | }) 423 | }) 424 | 425 | // This request can only be fulfilled using AnyAuth 426 | test('AnyAuth with multiple operations with different auth requirements in a link', () => { 427 | const query = `{ 428 | viewerAnyAuth(exampleApiBasicProtocol: {username: "arlene123", password: "password123"}, exampleApiKeyProtocol: {apiKey: "abcdef"}) { 429 | projectWithId (projectId: 3) { 430 | projectId 431 | patentId 432 | patent { 433 | patentId 434 | } 435 | projectLead { 436 | name 437 | } 438 | } 439 | } 440 | }` 441 | return graphql({ schema: createdSchema, source: query }).then((result) => { 442 | expect(result).toEqual({ 443 | data: { 444 | viewerAnyAuth: { 445 | projectWithId: { 446 | projectId: 3, 447 | patentId: '100', 448 | patent: { 449 | patentId: '100' 450 | }, 451 | projectLead: { 452 | name: 'William B Ropp' 453 | } 454 | } 455 | } 456 | } 457 | }) 458 | }) 459 | }) 460 | 461 | test('Extract token from context', () => { 462 | const query = `{ 463 | secure 464 | }` 465 | 466 | return openAPIToGraphQL 467 | .createGraphQLSchema(oas, { 468 | tokenJSONpath: '$.user.token', 469 | viewer: true, 470 | httpRequest 471 | }) 472 | .then(({ schema }) => { 473 | return graphql({ 474 | schema, 475 | source: query, 476 | contextValue: { user: { token: 'abcdef' } } 477 | }).then((result) => { 478 | expect(result).toEqual({ 479 | data: { 480 | secure: 'A secure message.' 481 | } 482 | }) 483 | }) 484 | }) 485 | }) 486 | -------------------------------------------------------------------------------- /test/cloudfunction.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { graphql, parse, validate } from 'graphql' 9 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 10 | 11 | import * as openAPIToGraphQL from '../lib/index' 12 | 13 | const oas = require('./fixtures/cloudfunction.json') 14 | 15 | let createdSchema 16 | 17 | beforeAll(async () => { 18 | const { schema } = await openAPIToGraphQL.createGraphQLSchema(oas) 19 | createdSchema = schema 20 | }) 21 | 22 | test('Get response', async () => { 23 | const query = `mutation { 24 | mutationViewerBasicAuth (username: "test" password: "data") { 25 | postTestAction2 (payloadInput: {age: 27}) { 26 | payload 27 | age 28 | } 29 | } 30 | }` 31 | // validate that 'limit' parameter is covered by options: 32 | const ast = parse(query) 33 | const errors = validate(createdSchema, ast) 34 | expect(errors).toEqual([]) 35 | }) 36 | -------------------------------------------------------------------------------- /test/docusign.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { graphql } from 'graphql' 9 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 10 | 11 | import * as openAPIToGraphQL from '../lib/index' 12 | import { Options } from '../lib/types/options' 13 | 14 | const oas = require('./fixtures/docusign.json') 15 | 16 | test('Generate schema without problems', () => { 17 | const options: Options = { 18 | strict: false 19 | } 20 | return openAPIToGraphQL 21 | .createGraphQLSchema(oas, options) 22 | .then(({ schema }) => { 23 | expect(schema).toBeTruthy() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/evaluation/README.md: -------------------------------------------------------------------------------- 1 | # Evaluation of OpenAPI-to-GraphQL with APIs.guru APIs 2 | 3 | ## Prerequisite: Load data 4 | 5 | Run: 6 | 7 | ``` 8 | node apis_guru_test.js 9 | ``` 10 | 11 | ...to load APIs.guru APIs into `tmp` subfolder in the `test/evaluation` directory. 12 | 13 | ## Run evaluation 14 | 15 | Run: 16 | 17 | ``` 18 | node eval_apis.guru.js 19 | ``` 20 | 21 | `limit` determines the maximum number of Swagger files to evaluate. 22 | -------------------------------------------------------------------------------- /test/evaluation/eval_apis_guru.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | const openapiToGraphql = require('../../lib/index') 9 | const Glob = require('glob') 10 | const fs = require('fs') 11 | const YAML = require('js-yaml') 12 | const ss = require('simple-statistics') 13 | 14 | /** 15 | * Download all OAS from APIs.guru. 16 | * 17 | * @return {Promise} Resolves on array of OAS 18 | */ 19 | async function readOas(limit) { 20 | const OASList = [] 21 | const paths = Glob.sync('tmp/APIs/**/@(*.yaml|*.json)') 22 | let index = -1 23 | 24 | while (OASList.length < limit && index < paths.length) { 25 | index++ 26 | const path = paths[index] 27 | const oas = readFile(path) 28 | if (!oas) continue 29 | if (!isValidOAS(oas)) continue 30 | 31 | // keep track of path for later logging: 32 | oas['x-file-path'] = path 33 | 34 | OASList.push(oas) 35 | } 36 | 37 | return OASList 38 | } 39 | 40 | /** 41 | * Attempts to build schema for every OAS in given list. 42 | */ 43 | async function checkOas(OASList) { 44 | const results = { 45 | overall: OASList.length, 46 | successes: [], 47 | errors: [] 48 | } 49 | for (let oas of OASList) { 50 | const name = oas.info.title 51 | console.log(`Process "${name}" (${oas['x-file-path']})...`) 52 | try { 53 | const { report } = await openapiToGraphql.createGraphQLSchema(oas, { 54 | strict: false 55 | }) 56 | results.successes.push({ name, report }) 57 | } catch (error) { 58 | results.errors.push({ 59 | name, 60 | error: error.message, 61 | path: oas['x-file-path'] 62 | }) 63 | } 64 | } 65 | // console.log(JSON.stringify(results, null, 2)) 66 | 67 | // print results: 68 | printOverallResults(results) 69 | printWarningsBreakdown(results) 70 | // printErrorBreakdown(results) 71 | // printStats(results) 72 | // console.log(JSON.stringify(getWarningsDistribution(results), null, 2)) 73 | // console.log(JSON.stringify(warningsPerApi(results), null, 2)) 74 | } 75 | 76 | function printOverallResults(results) { 77 | const noWarnings = results.successes.filter( 78 | (s) => s.report.warnings.length === 0 79 | ).length 80 | console.log('----------------------') 81 | console.log('Overall results:') 82 | console.log( 83 | `Assessed APIs: ${results.overall}\n` + 84 | `Successes: ${results.successes.length}\n` + 85 | ` with no warnings: ${noWarnings}\n` + 86 | `Errors: ${results.errors.length}` 87 | ) 88 | } 89 | 90 | function printWarningsBreakdown(results) { 91 | let allWarnings = [] 92 | results.successes.forEach((suc) => { 93 | allWarnings = allWarnings.concat(suc.report.warnings) 94 | }) 95 | const warningDict = groupBy(allWarnings, 'type') 96 | for (let key in warningDict) { 97 | warningDict[key] = warningDict[key].length 98 | } 99 | console.log('----------------------') 100 | console.log('Warnings breakdown:') 101 | console.log(JSON.stringify(warningDict, null, 2)) 102 | } 103 | 104 | function printErrorBreakdown(results) { 105 | const errors = { 106 | validationFails: 0, // thrown by: Swagger2Openapi 107 | invalidEnumValue: 0, // thrown by: GraphQL 108 | invalidFields: 0, // thrown by: GraphQL 109 | duplicateNamesInSchema: 0, // thrown by: GraphQL 110 | cannotSanitize: 0, // thrown by: OpenAPI-to-GraphQL 111 | resolveAllOf: 0, // thrown by: OpenAPI-to-GraphQL 112 | itemsPropertyMissing: 0, // thrown by: OpenAPI-to-GraphQL 113 | invalidReference: 0, // thrown by: OpenAPI-to-GraphQL 114 | other: 0 115 | } 116 | results.errors.forEach((err) => { 117 | if (/can not be used as an Enum value/.test(err.error)) { 118 | errors.invalidEnumValue++ 119 | } else if (/^Cannot sanitize /.test(err.error)) { 120 | errors.cannotSanitize++ 121 | } else if (/allOf will overwrite/.test(err.error)) { 122 | errors.resolveAllOf++ 123 | } else if (/(Patchable)/.test(err.error)) { 124 | errors.validationFails++ 125 | } else if (/Items property missing in array/.test(err.error)) { 126 | errors.itemsPropertyMissing++ 127 | } else if (/Schema must contain unique named types/.test(err.error)) { 128 | errors.duplicateNamesInSchema++ 129 | } else if (/must be an object with field names as keys/.test(err.error)) { 130 | errors.invalidFields++ 131 | } else if (/Could not resolve reference/.test(err.error)) { 132 | errors.invalidReference++ 133 | } else { 134 | errors.other++ 135 | } 136 | }) 137 | 138 | console.log('----------------------') 139 | console.log('Errors breakdown:') 140 | console.log(JSON.stringify(errors, null, 2)) 141 | } 142 | 143 | function getWarningsDistribution(results) { 144 | const dist = { 145 | overall: {}, 146 | MissingResponseSchema: {}, 147 | InvalidSchemaType: {}, 148 | MultipleResponses: {}, 149 | InvalidSchemaTypeScalar: {} 150 | } 151 | 152 | results.successes.forEach((suc) => { 153 | const overall = suc.report.warnings.length 154 | if (typeof dist.overall[overall] === 'undefined') dist.overall[overall] = 0 155 | dist.overall[overall]++ 156 | 157 | const missingResponseSchema = suc.report.warnings.filter( 158 | (w) => w.type === 'MissingResponseSchema' 159 | ).length 160 | if ( 161 | typeof dist.MissingResponseSchema[missingResponseSchema] === 'undefined' 162 | ) 163 | dist.MissingResponseSchema[missingResponseSchema] = 0 164 | dist.MissingResponseSchema[missingResponseSchema]++ 165 | 166 | const invalidSchemaType = suc.report.warnings.filter( 167 | (w) => w.type === 'InvalidSchemaType' 168 | ).length 169 | if (typeof dist.InvalidSchemaType[invalidSchemaType] === 'undefined') 170 | dist.InvalidSchemaType[invalidSchemaType] = 0 171 | dist.InvalidSchemaType[invalidSchemaType]++ 172 | 173 | const multipleResponses = suc.report.warnings.filter( 174 | (w) => w.type === 'MultipleResponses' 175 | ).length 176 | if (typeof dist.MultipleResponses[multipleResponses] === 'undefined') 177 | dist.MultipleResponses[multipleResponses] = 0 178 | dist.MultipleResponses[multipleResponses]++ 179 | 180 | const invalidSchemaTypeScalar = suc.report.warnings.filter( 181 | (w) => w.type === 'InvalidSchemaTypeScalar' 182 | ).length 183 | if ( 184 | typeof dist.InvalidSchemaTypeScalar[invalidSchemaTypeScalar] === 185 | 'undefined' 186 | ) 187 | dist.InvalidSchemaTypeScalar[invalidSchemaTypeScalar] = 0 188 | dist.InvalidSchemaTypeScalar[invalidSchemaTypeScalar]++ 189 | }) 190 | 191 | // fill up empty values for easier plotting: 192 | for (let i = 0; i < 550; i++) { 193 | if (typeof dist.overall[i] === 'undefined') dist.overall[i] = 0 194 | if (typeof dist.MissingResponseSchema[i] === 'undefined') 195 | dist.MissingResponseSchema[i] = 0 196 | if (typeof dist.InvalidSchemaType[i] === 'undefined') 197 | dist.InvalidSchemaType[i] = 0 198 | if (typeof dist.MultipleResponses[i] === 'undefined') 199 | dist.MultipleResponses[i] = 0 200 | if (typeof dist.InvalidSchemaTypeScalar[i] === 'undefined') 201 | dist.InvalidSchemaTypeScalar[i] = 0 202 | } 203 | 204 | return dist 205 | } 206 | 207 | function printStats(results) { 208 | const numOps = results.successes.map((succ) => succ.report.numOps) 209 | console.log(`Number of operations:`) 210 | console.log(printSummary(numOps) + '\n') 211 | 212 | const numOpsQuery = results.successes.map((succ) => succ.report.numOpsQuery) 213 | console.log(`Number of query operations:`) 214 | console.log(printSummary(numOpsQuery) + '\n') 215 | 216 | const numOpsMutation = results.successes.map( 217 | (succ) => succ.report.numOpsMutation 218 | ) 219 | console.log(`Number of mutation operations:`) 220 | console.log(printSummary(numOpsMutation) + '\n') 221 | 222 | const numQueries = results.successes.map( 223 | (succ) => succ.report.numQueriesCreated 224 | ) 225 | console.log(`Number of queries created:`) 226 | console.log(printSummary(numQueries) + '\n') 227 | 228 | const numMutations = results.successes.map( 229 | (succ) => succ.report.numMutationsCreated 230 | ) 231 | console.log(`Number of mutations created:`) 232 | console.log(printSummary(numMutations) + '\n') 233 | 234 | const numQueriesSkipped = [] 235 | numOpsQuery.forEach((numOps, index) => { 236 | numQueriesSkipped.push(numOps - numQueries[index]) 237 | }) 238 | console.log(`Number of queries skipped:`) 239 | console.log(printSummary(numQueriesSkipped) + '\n') 240 | 241 | const numMutationsSkipped = [] 242 | numOpsMutation.forEach((numOps, index) => { 243 | numMutationsSkipped.push(numOps - numMutations[index]) 244 | }) 245 | console.log(`Number of mutations skipped:`) 246 | console.log(printSummary(numMutationsSkipped) + '\n') 247 | } 248 | 249 | function printSummary(arr) { 250 | console.log(`mean: ${ss.mean(arr)}`) 251 | console.log(`min: ${ss.min(arr)}`) 252 | console.log(`max: ${ss.max(arr)}`) 253 | console.log(`---`) 254 | console.log(`25%: ${ss.quantile(arr, 0.25)}`) 255 | console.log(`50%: ${ss.quantile(arr, 0.5)}`) 256 | console.log(`75%: ${ss.quantile(arr, 0.75)}`) 257 | console.log(`90%: ${ss.quantile(arr, 0.9)}`) 258 | } 259 | 260 | function warningsPerApi(results) { 261 | const apiDict = {} 262 | results.successes.forEach((suc) => { 263 | let name = suc.name 264 | while (typeof apiDict[name] !== 'undefined') { 265 | name += '_1' 266 | } 267 | apiDict[name] = { 268 | overall: suc.report.warnings.length, 269 | MissingResponseSchema: suc.report.warnings.filter( 270 | (w) => w.type === 'MissingResponseSchema' 271 | ).length, 272 | InvalidSchemaType: suc.report.warnings.filter( 273 | (w) => w.type === 'InvalidSchemaType' 274 | ).length, 275 | MultipleResponses: suc.report.warnings.filter( 276 | (w) => w.type === 'MultipleResponses' 277 | ).length, 278 | InvalidSchemaTypeScalar: suc.report.warnings.filter( 279 | (w) => w.type === 'InvalidSchemaTypeScalar' 280 | ).length, 281 | numOps: suc.report.numOps, 282 | numOpsCreated: 283 | suc.report.numQueriesCreated + suc.report.numMutationsCreated 284 | } 285 | }) 286 | return apiDict 287 | } 288 | 289 | /** 290 | * Helper util to group objects in an array based on a given property. 291 | * 292 | * @param {Array} list 293 | * @param {String} prop Name of property to group by 294 | * @return {Object} 295 | */ 296 | function groupBy(list, prop) { 297 | const groups = {} 298 | list.forEach(function (item) { 299 | const list = groups[item[prop]] 300 | 301 | if (list) { 302 | list.push(item) 303 | } else { 304 | groups[item[prop]] = [item] 305 | } 306 | }) 307 | return groups 308 | } 309 | 310 | /** 311 | * Returns content of read JSON/YAML file. 312 | * 313 | * @param {String} path Path to file to read 314 | * @return {Object} Content of read file 315 | */ 316 | function readFile(path) { 317 | try { 318 | let doc 319 | if (/json$/.test(path)) { 320 | doc = JSON.parse(fs.readFileSync(path, 'utf8')) 321 | } else if (/yaml$|yml$/.test(path)) { 322 | doc = YAML.safeLoad(fs.readFileSync(path, 'utf8')) 323 | } 324 | return doc 325 | } catch (e) { 326 | console.error('Error: failed to parse YAML/JSON: ' + e) 327 | return null 328 | } 329 | } 330 | 331 | /** 332 | * Basic checks to make sure we are dealing with a vaild Swagger / OAS 2.0 333 | * 334 | * @param {Object} oas 335 | * @return {Boolean} 336 | */ 337 | const isValidOAS = (oas) => { 338 | return ( 339 | typeof oas === 'object' && 340 | typeof oas.info === 'object' && 341 | typeof oas.info.title === 'string' && 342 | typeof oas.info.description === 'string' && 343 | typeof oas.swagger === 'string' && 344 | oas.swagger === '2.0' 345 | ) 346 | } 347 | 348 | // determine maximum number of OAS to test: 349 | let limit = 0 350 | try { 351 | limit = Number(process.argv[2]) 352 | if (isNaN(limit)) throw new Error(`Not a number`) 353 | } catch (e) { 354 | console.error( 355 | `Error: Please provide maximum number of APIs to check. ` + 356 | `For example:\n\n npm run guru-test 10\n` 357 | ) 358 | process.exit() 359 | } 360 | 361 | // go go go: 362 | readOas(limit).then(checkOas).catch(console.error) 363 | -------------------------------------------------------------------------------- /test/evaluation/load_apis_guru.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | const git = require('isomorphic-git') 9 | const http = require('isomorphic-git/http/node') 10 | const fs = require('fs') 11 | const rimraf = require('rimraf') 12 | 13 | const REPO_URL = 'https://github.com/APIs-guru/openapi-directory.git' 14 | const FOLDER_PATH = 'tmp' 15 | 16 | /** 17 | * Download all OAS from APIs.guru. 18 | * 19 | * @return {Promise} Resolves on array of OAS 20 | */ 21 | const downloadOas = () => { 22 | return git.clone({ 23 | fs: fs, 24 | http, 25 | dir: FOLDER_PATH, 26 | url: REPO_URL, 27 | singleBranch: true, 28 | depth: 1 29 | }) 30 | } 31 | 32 | /** 33 | * Helpers 34 | */ 35 | const emptyTmp = () => { 36 | rimraf.sync(FOLDER_PATH) 37 | } 38 | 39 | // go go go: 40 | emptyTmp() 41 | downloadOas() 42 | .then(() => { 43 | console.log(`Loaded files from ${REPO_URL} to ${FOLDER_PATH}`) 44 | }) 45 | .catch(console.error) 46 | -------------------------------------------------------------------------------- /test/evaluation/results/errors_breakdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "validationFails": 6, 3 | "invalidEnumValue": 0, 4 | "invalidFields": 0, 5 | "duplicateNamesInSchema": 0, 6 | "cannotSanitize": 16, 7 | "resolveAllOf": 0, 8 | "itemsPropertyMissing": 0, 9 | "invalidReference": 7, 10 | "other": 0 11 | } 12 | -------------------------------------------------------------------------------- /test/example_api2.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { graphql } from 'graphql' 9 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 10 | 11 | import * as openAPIToGraphQL from '../lib/index' 12 | import { startServer, stopServer } from './example_api2_server' 13 | import { httpRequest } from './httprequest' 14 | 15 | const oas = require('./fixtures/example_oas2.json') 16 | const PORT = 3004 17 | // Update PORT for this test case: 18 | oas.servers[0].variables.port.default = String(PORT) 19 | 20 | let createdSchema 21 | 22 | /** 23 | * This test suite is used to verify the behavior of the operationIdFieldNames 24 | * option. 25 | * 26 | * It is necessary to make a separate OAS because we need all of operations to 27 | * have operationIDs. 28 | */ 29 | 30 | /** 31 | * Set up the schema first and run example API server 32 | */ 33 | beforeAll(() => { 34 | return Promise.all([ 35 | openAPIToGraphQL 36 | .createGraphQLSchema(oas, { 37 | operationIdFieldNames: true, 38 | httpRequest 39 | }) 40 | .then(({ schema, report }) => { 41 | createdSchema = schema 42 | }), 43 | startServer(PORT) 44 | ]) 45 | }) 46 | 47 | /** 48 | * Shut down API server 49 | */ 50 | afterAll(() => { 51 | return stopServer() 52 | }) 53 | 54 | /** 55 | * There should be two operations. 56 | * 57 | * One will be given a field name from the operationId, i.e. user, and the other 58 | * one, because it does not have an operationId defined, will have an 59 | * autogenerated field name based on the path, i.e. getUser 60 | */ 61 | test('The option operationIdFieldNames should allow both operations to be present', () => { 62 | let oasGetCount = 0 63 | for (let path in oas.paths) { 64 | for (let method in oas.paths[path]) { 65 | if (method === 'get') oasGetCount++ 66 | } 67 | } 68 | 69 | const gqlTypes = Object.keys(createdSchema._typeMap.Query.getFields()).length 70 | expect(gqlTypes).toEqual(oasGetCount) 71 | }) 72 | 73 | test('Querying the two operations', () => { 74 | const query = `query { 75 | getUser { 76 | name 77 | } 78 | user { 79 | name 80 | } 81 | }` 82 | return graphql({ schema: createdSchema, source: query }).then((result) => { 83 | expect(result).toEqual({ 84 | data: { 85 | getUser: { 86 | name: 'Arlene L McMahon' 87 | }, 88 | user: { 89 | name: 'William B Ropp' 90 | } 91 | } 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /test/example_api2_server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | let server // holds server object for shutdown 9 | 10 | /** 11 | * Starts the server at the given port 12 | */ 13 | function startServer(PORT) { 14 | const express = require('express') 15 | const app = express() 16 | 17 | const bodyParser = require('body-parser') 18 | app.use(bodyParser.json()) 19 | 20 | app.get('/api/user', (req, res) => { 21 | res.send({ 22 | name: 'Arlene L McMahon' 23 | }) 24 | }) 25 | 26 | app.get('/api/user2', (req, res) => { 27 | res.send({ 28 | name: 'William B Ropp' 29 | }) 30 | }) 31 | 32 | return new Promise((resolve) => { 33 | server = app.listen(PORT, () => { 34 | console.log(`Example API accessible on port ${PORT}`) 35 | resolve() 36 | }) 37 | }) 38 | } 39 | 40 | /** 41 | * Stops server. 42 | */ 43 | function stopServer() { 44 | return new Promise((resolve) => { 45 | server.close(() => { 46 | console.log(`Stopped API server`) 47 | resolve() 48 | }) 49 | }) 50 | } 51 | 52 | // If run from command line, start server: 53 | if (require.main === module) { 54 | startServer(3002) 55 | } 56 | 57 | module.exports = { 58 | startServer, 59 | stopServer 60 | } 61 | -------------------------------------------------------------------------------- /test/example_api3.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { graphql, parse, validate } from 'graphql' 9 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 10 | 11 | import * as openAPIToGraphQL from '../lib/index' 12 | import { Options } from '../lib/types/options' 13 | 14 | import { httpRequest } from './httprequest' 15 | 16 | const api = require('./example_api_server') 17 | const api2 = require('./example_api3_server') 18 | 19 | const oas = require('./fixtures/example_oas.json') 20 | const oas3 = require('./fixtures/example_oas3.json') 21 | const PORT = 3005 22 | const PORT2 = 3006 23 | // Update PORT for this test case: 24 | oas.servers[0].variables.port.default = String(PORT) 25 | oas3.servers[0].variables.port.default = String(PORT2) 26 | 27 | /** 28 | * This test suite is used to verify the behavior of interOAS links, i.e. 29 | * links across different OASs 30 | */ 31 | 32 | let createdSchema 33 | 34 | /** 35 | * Set up the schema first and run example API server 36 | */ 37 | beforeAll(() => { 38 | return Promise.all([ 39 | openAPIToGraphQL 40 | .createGraphQLSchema([oas, oas3], { httpRequest }) 41 | .then(({ schema, report }) => { 42 | createdSchema = schema 43 | }), 44 | api.startServer(PORT), 45 | api2.startServer(PORT2) 46 | ]) 47 | }) 48 | 49 | /** 50 | * Shut down API server 51 | */ 52 | afterAll(() => { 53 | return Promise.all([api.stopServer(), api2.stopServer()]) 54 | }) 55 | 56 | test('Basic query on two APIs', () => { 57 | const query = `query { 58 | author(authorId: "arlene"){ 59 | name 60 | }, 61 | book(bookId: "software") { 62 | title 63 | }, 64 | user(username: "arlene") { 65 | name 66 | } 67 | }` 68 | return graphql({ schema: createdSchema, source: query }).then((result) => { 69 | expect(result).toEqual({ 70 | data: { 71 | author: { 72 | name: 'Arlene L McMahon' 73 | }, 74 | book: { 75 | title: 'The OpenAPI-to-GraphQL Cookbook' 76 | }, 77 | user: { 78 | name: 'Arlene L McMahon' 79 | } 80 | } 81 | }) 82 | }) 83 | }) 84 | 85 | test('Two APIs with independent links', () => { 86 | const query = `query { 87 | author(authorId: "arlene") { 88 | name 89 | masterpieceTitle, 90 | masterpiece { 91 | title 92 | } 93 | }, 94 | book(bookId: "software") { 95 | title 96 | authorName 97 | author { 98 | name 99 | masterpiece { 100 | author { 101 | name 102 | } 103 | } 104 | } 105 | }, 106 | user(username: "arlene") { 107 | name 108 | employerCompany { 109 | name 110 | } 111 | } 112 | }` 113 | return graphql({ schema: createdSchema, source: query }).then((result) => { 114 | expect(result).toEqual({ 115 | data: { 116 | author: { 117 | name: 'Arlene L McMahon', 118 | masterpieceTitle: 'software', 119 | masterpiece: { 120 | title: 'The OpenAPI-to-GraphQL Cookbook' 121 | } 122 | }, 123 | book: { 124 | title: 'The OpenAPI-to-GraphQL Cookbook', 125 | authorName: 'arlene', 126 | author: { 127 | name: 'Arlene L McMahon', 128 | masterpiece: { 129 | author: { 130 | name: 'Arlene L McMahon' 131 | } 132 | } 133 | } 134 | }, 135 | user: { 136 | name: 'Arlene L McMahon', 137 | employerCompany: { 138 | name: 'Binary Solutions' 139 | } 140 | } 141 | } 142 | }) 143 | }) 144 | }) 145 | 146 | test('Two APIs with interrelated links', () => { 147 | const query = `query { 148 | author(authorId: "arlene") { 149 | name 150 | employee{ 151 | name 152 | employerCompany{ 153 | name 154 | } 155 | author{ 156 | name 157 | masterpiece{ 158 | title 159 | author{ 160 | name 161 | employee{ 162 | name 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | }` 170 | return graphql({ schema: createdSchema, source: query }).then((result) => { 171 | expect(result).toEqual({ 172 | data: { 173 | author: { 174 | name: 'Arlene L McMahon', 175 | employee: { 176 | name: 'Arlene L McMahon', 177 | employerCompany: { 178 | name: 'Binary Solutions' 179 | }, 180 | author: { 181 | name: 'Arlene L McMahon', 182 | masterpiece: { 183 | title: 'The OpenAPI-to-GraphQL Cookbook', 184 | author: { 185 | name: 'Arlene L McMahon', 186 | employee: { 187 | name: 'Arlene L McMahon' 188 | } 189 | } 190 | } 191 | } 192 | } 193 | } 194 | } 195 | }) 196 | }) 197 | }) 198 | 199 | test('Two APIs with viewers', () => { 200 | const query = `query { 201 | viewerApiKey (apiKey: "abcdef"){ 202 | nextWork(authorId: "arlene") { 203 | title 204 | author { 205 | name 206 | } 207 | } 208 | } 209 | viewerBasicAuth2 (username: "arlene123", password: "password123") { 210 | patentWithId (patentId: "100") { 211 | patentId 212 | } 213 | } 214 | }` 215 | return graphql({ schema: createdSchema, source: query }).then((result) => { 216 | expect(result).toEqual({ 217 | data: { 218 | viewerApiKey: { 219 | nextWork: { 220 | title: 'OpenAPI-to-GraphQL for Power Users', 221 | author: { 222 | name: 'Arlene L McMahon' 223 | } 224 | } 225 | }, 226 | viewerBasicAuth2: { 227 | patentWithId: { 228 | patentId: '100' 229 | } 230 | } 231 | } 232 | }) 233 | }) 234 | }) 235 | 236 | test('Two APIs with AnyAuth viewer', () => { 237 | const query = `{ 238 | viewerAnyAuth(exampleApiKeyProtocol2: {apiKey: "abcdef"}, exampleApi3BasicProtocol: {username: "arlene123", password: "password123"}) { 239 | projectWithId(projectId: 1) { 240 | projectLead{ 241 | name 242 | } 243 | } 244 | nextWork(authorId: "arlene") { 245 | title 246 | } 247 | } 248 | }` 249 | return graphql({ schema: createdSchema, source: query }).then((result) => { 250 | expect(result).toEqual({ 251 | data: { 252 | viewerAnyAuth: { 253 | projectWithId: { 254 | projectLead: { 255 | name: 'Arlene L McMahon' 256 | } 257 | }, 258 | nextWork: { 259 | title: 'OpenAPI-to-GraphQL for Power Users' 260 | } 261 | } 262 | } 263 | }) 264 | }) 265 | }) 266 | 267 | test('Two APIs with AnyAuth viewer and interrelated links', () => { 268 | const query = `{ 269 | viewerAnyAuth(exampleApiKeyProtocol2: {apiKey: "abcdef"}, exampleApi3BasicProtocol: {username: "arlene123", password: "password123"}) { 270 | projectWithId(projectId: 1) { 271 | projectLead{ 272 | name 273 | author { 274 | name 275 | nextWork { 276 | title 277 | } 278 | } 279 | } 280 | } 281 | } 282 | }` 283 | return graphql({ schema: createdSchema, source: query }).then((result) => { 284 | expect(result).toEqual({ 285 | data: { 286 | viewerAnyAuth: { 287 | projectWithId: { 288 | projectLead: { 289 | name: 'Arlene L McMahon', 290 | author: { 291 | name: 'Arlene L McMahon', 292 | nextWork: { 293 | title: 'OpenAPI-to-GraphQL for Power Users' 294 | } 295 | } 296 | } 297 | } 298 | } 299 | } 300 | }) 301 | }) 302 | }) 303 | 304 | test('Option customResolver with two APIs', () => { 305 | const options: Options = { 306 | httpRequest, 307 | customResolvers: { 308 | 'Example API': { 309 | '/users/{username}': { 310 | get: () => { 311 | return { 312 | name: 'Jenifer Aldric' 313 | } 314 | } 315 | } 316 | }, 317 | 'Example API 3': { 318 | '/authors/{authorId}': { 319 | get: () => { 320 | return { 321 | name: 'Jenifer Aldric, the author' 322 | } 323 | } 324 | } 325 | } 326 | } 327 | } 328 | const query = `query { 329 | user(username: "abcdef") { 330 | name 331 | } 332 | author(authorId: "abcdef") { 333 | name 334 | } 335 | }` 336 | return openAPIToGraphQL 337 | .createGraphQLSchema([oas, oas3], options) 338 | .then(({ schema }) => { 339 | const ast = parse(query) 340 | const errors = validate(schema, ast) 341 | expect(errors).toEqual([]) 342 | return graphql({ schema, source: query }).then((result) => { 343 | expect(result).toEqual({ 344 | data: { 345 | user: { 346 | name: 'Jenifer Aldric' 347 | }, 348 | author: { 349 | name: 'Jenifer Aldric, the author' 350 | } 351 | } 352 | }) 353 | }) 354 | }) 355 | }) 356 | 357 | test('Option customResolver with two APIs and interrelated links', () => { 358 | const options: Options = { 359 | httpRequest, 360 | customResolvers: { 361 | 'Example API': { 362 | '/users/{username}': { 363 | get: () => { 364 | return { 365 | name: 'Jenifer Aldric', 366 | employerId: 'binsol' 367 | } 368 | } 369 | } 370 | }, 371 | 'Example API 3': { 372 | '/authors/{authorId}': { 373 | get: () => { 374 | return { 375 | name: 'Jenifer Aldric, the author', 376 | masterpieceTitle: 'A collection of stories' 377 | } 378 | } 379 | }, 380 | '/books/{bookId}': { 381 | get: () => { 382 | return { 383 | title: 'A collection of stories for babies', 384 | authorName: 'Jenifer Aldric, yet another author' 385 | } 386 | } 387 | } 388 | } 389 | } 390 | } 391 | const query = `query { 392 | author(authorId: "abcdef") { 393 | name 394 | employee{ 395 | name 396 | employerCompany{ 397 | name 398 | } 399 | author{ 400 | name 401 | masterpiece{ 402 | title 403 | author{ 404 | name 405 | employee{ 406 | name 407 | } 408 | } 409 | } 410 | } 411 | } 412 | } 413 | }` 414 | return openAPIToGraphQL 415 | .createGraphQLSchema([oas, oas3], options) 416 | .then(({ schema }) => { 417 | const ast = parse(query) 418 | const errors = validate(schema, ast) 419 | expect(errors).toEqual([]) 420 | return graphql({ schema, source: query }).then((result) => { 421 | expect(result).toEqual({ 422 | data: { 423 | author: { 424 | name: 'Jenifer Aldric, the author', 425 | employee: { 426 | name: 'Jenifer Aldric', 427 | employerCompany: { 428 | name: 'Binary Solutions' 429 | }, 430 | author: { 431 | name: 'Jenifer Aldric, the author', 432 | masterpiece: { 433 | title: 'A collection of stories for babies', 434 | author: { 435 | name: 'Jenifer Aldric, the author', 436 | employee: { 437 | name: 'Jenifer Aldric' 438 | } 439 | } 440 | } 441 | } 442 | } 443 | } 444 | } 445 | }) 446 | }) 447 | }) 448 | }) 449 | -------------------------------------------------------------------------------- /test/example_api3_server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | let server // holds server object for shutdown 9 | 10 | /** 11 | * Starts the server at the given port 12 | */ 13 | function startServer(PORT) { 14 | const express = require('express') 15 | const app = express() 16 | 17 | const bodyParser = require('body-parser') 18 | app.use(bodyParser.text()) 19 | app.use(bodyParser.json()) 20 | 21 | const Authors = { 22 | arlene: { 23 | name: 'Arlene L McMahon', 24 | masterpieceTitle: 'software' 25 | }, 26 | will: { 27 | name: 'William B Ropp', 28 | masterpieceTitle: '' 29 | }, 30 | johnny: { 31 | name: 'John C Barnes', 32 | masterpieceTitle: '' 33 | }, 34 | heather: { 35 | name: 'Heather J Tate', 36 | masterpieceTitle: '' 37 | } 38 | } 39 | 40 | const Books = { 41 | software: { 42 | title: 'The OpenAPI-to-GraphQL Cookbook', 43 | authorName: 'arlene' 44 | }, 45 | frog: { 46 | title: 'One Frog, Two Frog, Red Frog, Blue Frog', 47 | authorName: 'will' 48 | }, 49 | history: { 50 | title: 'A history on history', 51 | authorName: 'will' 52 | } 53 | } 54 | 55 | const NextWorks = { 56 | arlene: { 57 | title: 'OpenAPI-to-GraphQL for Power Users', 58 | authorName: 'arlene' 59 | }, 60 | johnny: { 61 | title: 'A one, a two, a one two three four!', 62 | authorName: 'johnny' 63 | }, 64 | heather: { 65 | title: 'What did the baby computer say to the father computer? Data.', 66 | authorName: 'heather' 67 | } 68 | } 69 | 70 | const Auth = { 71 | arlene: { 72 | username: 'arlene123', 73 | password: 'password123', 74 | accessToken: 'abcdef' 75 | }, 76 | will: { 77 | username: 'catloverxoxo', 78 | password: 'IActuallyPreferDogs', 79 | accessToken: '123456' 80 | }, 81 | johnny: { 82 | username: 'johnny', 83 | password: 'password', 84 | accessToken: 'xyz' 85 | }, 86 | heather: { 87 | username: 'cccrulez', 88 | password: 'johnnyisabully', 89 | accessToken: 'ijk' 90 | } 91 | } 92 | 93 | const authMiddleware = (req, res, next) => { 94 | if (req.headers.authorization) { 95 | let encoded = req.headers.authorization.split(' ')[1] 96 | let decoded = Buffer.from(encoded, 'base64').toString('utf8').split(':') 97 | 98 | if (decoded.length === 2) { 99 | let credentials = { 100 | username: decoded[0], 101 | password: decoded[1] 102 | } 103 | for (let user in Auth) { 104 | if ( 105 | Auth[user].username === credentials.username && 106 | Auth[user].password === credentials.password 107 | ) { 108 | return next() 109 | } 110 | } 111 | res.status(401).send({ 112 | message: 'Incorrect credentials' 113 | }) 114 | } else { 115 | res.status(401).send({ 116 | message: 'Basic Auth expects a single username and a single password' 117 | }) 118 | } 119 | } else if ('access_token' in req.headers) { 120 | for (let user in Auth) { 121 | if (Auth[user].accessToken === req.headers.access_token) { 122 | return next() 123 | } 124 | } 125 | res.status(401).send({ 126 | message: 'Incorrect credentials' 127 | }) 128 | return false 129 | } else if ('access_token' in req.query) { 130 | for (let user in Auth) { 131 | if (Auth[user].accessToken === req.query.access_token) { 132 | return next() 133 | } 134 | } 135 | res.status(401).send({ 136 | message: 'Incorrect credentials' 137 | }) 138 | } else { 139 | res.status(401).send({ 140 | message: 'Unknown/missing credentials' 141 | }) 142 | } 143 | } 144 | 145 | app.get('/api/authors/:authorId', (req, res) => { 146 | res.send(Authors[req.params.authorId]) 147 | }) 148 | 149 | app.get('/api/books/:bookId', (req, res) => { 150 | res.send(Books[req.params.bookId]) 151 | }) 152 | 153 | app.get('/api/nextWorks/:authorId', authMiddleware, (req, res) => { 154 | res.send(NextWorks[req.params.authorId]) 155 | }) 156 | 157 | return new Promise((resolve) => { 158 | server = app.listen(PORT, () => { 159 | console.log(`Example API accessible on port ${PORT}`) 160 | resolve() 161 | }) 162 | }) 163 | } 164 | 165 | /** 166 | * Stops server. 167 | */ 168 | function stopServer() { 169 | return new Promise((resolve) => { 170 | server.close(() => { 171 | console.log(`Stopped API server`) 172 | resolve() 173 | }) 174 | }) 175 | } 176 | 177 | // If run from command line, start server: 178 | if (require.main === module) { 179 | startServer(3003) 180 | } 181 | 182 | module.exports = { 183 | startServer, 184 | stopServer 185 | } 186 | -------------------------------------------------------------------------------- /test/example_api5.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { graphql } from 'graphql' 9 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 10 | 11 | import * as openAPIToGraphQL from '../lib/index' 12 | import { startServer, stopServer } from './example_api5_server' 13 | import { httpRequest } from './httprequest' 14 | 15 | const oas = require('./fixtures/example_oas5.json') 16 | const PORT = 3007 17 | // Update PORT for this test case: 18 | oas.servers[0].variables.port.default = String(PORT) 19 | 20 | // Testing the simpleNames option 21 | 22 | let createdSchema 23 | 24 | /** 25 | * Set up the schema first and run example API server 26 | */ 27 | beforeAll(() => { 28 | return Promise.all([ 29 | openAPIToGraphQL 30 | .createGraphQLSchema(oas, { 31 | simpleNames: true, 32 | httpRequest 33 | }) 34 | .then(({ schema, report }) => { 35 | createdSchema = schema 36 | }), 37 | startServer(PORT) 38 | ]) 39 | }) 40 | 41 | /** 42 | * Shut down API server 43 | */ 44 | afterAll(() => { 45 | return stopServer() 46 | }) 47 | 48 | /** 49 | * Because of the simpleNames option, 'o_d_d___n_a_m_e' will not be turned into 50 | * 'oDDNAME'. 51 | */ 52 | test('Basic simpleNames option test', () => { 53 | const query = `{ 54 | o_d_d___n_a_m_e { 55 | data 56 | } 57 | }` 58 | 59 | return graphql({ schema: createdSchema, source: query }).then((result) => { 60 | expect(result).toEqual({ 61 | data: { 62 | o_d_d___n_a_m_e: { 63 | data: 'odd name' 64 | } 65 | } 66 | }) 67 | }) 68 | }) 69 | 70 | /** 71 | * 'w-e-i-r-d___n-a-m-e' contains GraphQL unsafe characters. 72 | * 73 | * Because of the simpleNames option, 'w-e-i-r-d___n-a-m-e' will be turned into 74 | * 'weird___name' and not 'wEIRDNAME'. 75 | */ 76 | test('Basic simpleNames option test with GraphQL unsafe values', () => { 77 | const query = `{ 78 | weird___name { 79 | data 80 | } 81 | }` 82 | 83 | return graphql({ schema: createdSchema, source: query }).then((result) => { 84 | expect(result).toEqual({ 85 | data: { 86 | weird___name: { 87 | data: 'weird name' 88 | } 89 | } 90 | }) 91 | }) 92 | }) 93 | 94 | /** 95 | * 'w-e-i-r-d___n-a-m-e2' contains GraphQL unsafe characters. 96 | * 97 | * Because of the simpleNames option, 'w-e-i-r-d___n-a-m-e2' will be turned into 98 | * 'weird___name2' and not 'wEIRDNAME2'. 99 | */ 100 | test('Basic simpleNames option test with GraphQL unsafe values and a parameter', () => { 101 | const query = `{ 102 | weird___name2 (funky___parameter: "Arnold") { 103 | data 104 | } 105 | }` 106 | 107 | return graphql({ schema: createdSchema, source: query }).then((result) => { 108 | expect(result).toEqual({ 109 | data: { 110 | weird___name2: { 111 | data: 'weird name 2 param: Arnold' 112 | } 113 | } 114 | }) 115 | }) 116 | }) 117 | 118 | /** 119 | * Because of the simpleNames option, 'w-e-i-r-d___n-a-m-e___l-i-n-k' will be 120 | * turned into 'weird___name___link' and not 'wEIRDNAMELINK'. 121 | */ 122 | test('Basic simpleNames option test with a link', () => { 123 | const query = `{ 124 | o_d_d___n_a_m_e { 125 | weird___name___link { 126 | data 127 | } 128 | } 129 | }` 130 | 131 | return graphql({ schema: createdSchema, source: query }).then((result) => { 132 | expect(result).toEqual({ 133 | data: { 134 | o_d_d___n_a_m_e: { 135 | weird___name___link: { 136 | data: 'weird name' 137 | } 138 | } 139 | } 140 | }) 141 | }) 142 | }) 143 | 144 | /** 145 | * Because of the simpleNames option, 'w-e-i-r-d___n-a-m-e2___l-i-n-k' will be 146 | * turned into 'weird___name2___link' and not 'wEIRDNAME2LINK'. 147 | */ 148 | test('Basic simpleNames option test with a link that has parameters', () => { 149 | const query = `{ 150 | o_d_d___n_a_m_e { 151 | weird___name2___link { 152 | data 153 | } 154 | } 155 | }` 156 | 157 | return graphql({ schema: createdSchema, source: query }).then((result) => { 158 | expect(result).toEqual({ 159 | data: { 160 | o_d_d___n_a_m_e: { 161 | weird___name2___link: { 162 | data: 'weird name 2 param: Charles' 163 | } 164 | } 165 | } 166 | }) 167 | }) 168 | }) 169 | 170 | /** 171 | * Because of the simpleNames option, 'w-e-i-r-d___n-a-m-e3___l-i-n-k' will be 172 | * turned into 'weird___name3___link3' and not 'wEIRDNAME3LINK'. 173 | */ 174 | test('Basic simpleNames option test with a link that has exposed parameters', () => { 175 | const query = `{ 176 | o_d_d___n_a_m_e { 177 | weird___name3___link (funky___parameter: "Brittany") { 178 | data 179 | } 180 | } 181 | }` 182 | 183 | return graphql({ schema: createdSchema, source: query }).then((result) => { 184 | expect(result).toEqual({ 185 | data: { 186 | o_d_d___n_a_m_e: { 187 | weird___name3___link: { 188 | data: 'weird name 3 param: Brittany' 189 | } 190 | } 191 | } 192 | }) 193 | }) 194 | }) 195 | 196 | /** 197 | * Because of the simpleEnumValues option, 'a-m-b-e-r' will be sanitized to 198 | * ALL_CAPS 'A_M_B_E_R' when it is not used and sanitized to 'amber' (only 199 | * removing GraphQL illegal characters) when it is used 200 | */ 201 | test('Basic simpleEnumValues option test', () => { 202 | const query = `{ 203 | getEnum { 204 | data 205 | } 206 | }` 207 | 208 | const promise = graphql({ schema: createdSchema, source: query }).then( 209 | (result) => { 210 | expect(result).toEqual({ 211 | data: { 212 | getEnum: { 213 | data: 'A_M_B_E_R' 214 | } 215 | } 216 | }) 217 | } 218 | ) 219 | 220 | const promise2 = openAPIToGraphQL 221 | .createGraphQLSchema(oas, { 222 | simpleEnumValues: true, 223 | httpRequest 224 | }) 225 | .then(({ schema, report }) => { 226 | return graphql({ schema, source: query }).then((result) => { 227 | expect(result).toEqual({ 228 | data: { 229 | getEnum: { 230 | data: 'amber' 231 | } 232 | } 233 | }) 234 | }) 235 | }) 236 | 237 | return Promise.all([promise, promise2]) 238 | }) 239 | 240 | /** 241 | * Regardless of simpleEnumValues, a GraphQL name cannot begin with a number, 242 | * therefore 3 will be sanitized to '_3' 243 | */ 244 | test('Basic simpleEnumValues option test on numerical enum', () => { 245 | const query = `{ 246 | getNumericalEnum { 247 | data 248 | } 249 | }` 250 | 251 | const promise = graphql({ schema: createdSchema, source: query }).then( 252 | (result) => { 253 | expect(result).toEqual({ 254 | data: { 255 | getNumericalEnum: { 256 | data: '_3' 257 | } 258 | } 259 | }) 260 | } 261 | ) 262 | 263 | const promise2 = openAPIToGraphQL 264 | .createGraphQLSchema(oas, { 265 | simpleEnumValues: true, 266 | httpRequest 267 | }) 268 | .then(({ schema, report }) => { 269 | return graphql({ schema, source: query }).then((result) => { 270 | expect(result).toEqual({ 271 | data: { 272 | getNumericalEnum: { 273 | data: '_3' 274 | } 275 | } 276 | }) 277 | }) 278 | }) 279 | 280 | return Promise.all([promise, promise2]) 281 | }) 282 | 283 | /** 284 | * Regardless of simpleEnumValues, OtG will translate an object enum to an 285 | * arbitrary JSON type 286 | */ 287 | test('Basic simpleEnumValues option test on object enum', () => { 288 | const query = `{ 289 | __type(name: "GetObjectEnum") { 290 | name 291 | kind 292 | } 293 | }` 294 | 295 | const promise = graphql({ schema: createdSchema, source: query }).then( 296 | (result) => { 297 | expect(result).toEqual({ 298 | data: { 299 | __type: { 300 | name: 'GetObjectEnum', 301 | kind: 'OBJECT' 302 | } 303 | } 304 | }) 305 | } 306 | ) 307 | 308 | const promise2 = openAPIToGraphQL 309 | .createGraphQLSchema(oas, { 310 | simpleEnumValues: true, 311 | httpRequest 312 | }) 313 | .then(({ schema, report }) => { 314 | return graphql({ schema, source: query }).then((result) => { 315 | expect(result).toEqual({ 316 | data: { 317 | __type: { 318 | name: 'GetObjectEnum', 319 | kind: 'OBJECT' 320 | } 321 | } 322 | }) 323 | }) 324 | }) 325 | 326 | return Promise.all([promise, promise2]) 327 | }) 328 | -------------------------------------------------------------------------------- /test/example_api5_server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | let server // holds server object for shutdown 9 | 10 | /** 11 | * Starts the server at the given port 12 | */ 13 | function startServer(PORT) { 14 | const express = require('express') 15 | const app = express() 16 | 17 | const bodyParser = require('body-parser') 18 | app.use(bodyParser.json()) 19 | 20 | app.get('/api/o_d_d___n_a_m_e', (req, res) => { 21 | res.send({ 22 | data: 'odd name' 23 | }) 24 | }) 25 | 26 | app.get('/api/w-e-i-r-d___n-a-m-e', (req, res) => { 27 | res.send({ 28 | data: 'weird name' 29 | }) 30 | }) 31 | 32 | /** 33 | * Cannot use f-u-n-k-y___p-a-r-a-m-e-t-e-r (like in the OAS) as it is not 34 | * allowed by Express.js routing 35 | * 36 | * "The name of route parameters must be made up of "word characters" 37 | * ([A-Za-z0-9_])." 38 | */ 39 | app.get('/api/w-e-i-r-d___n-a-m-e2/:funky___parameter', (req, res) => { 40 | res.send({ 41 | data: `weird name 2 param: ${req.params['funky___parameter']}` 42 | }) 43 | }) 44 | 45 | app.get('/api/w-e-i-r-d___n-a-m-e3/:funky___parameter', (req, res) => { 46 | res.send({ 47 | data: `weird name 3 param: ${req.params['funky___parameter']}` 48 | }) 49 | }) 50 | 51 | app.get('/api/getEnum', (req, res) => { 52 | res.send({ 53 | data: 'a-m-b-e-r' 54 | }) 55 | }) 56 | 57 | app.get('/api/getNumericalEnum', (req, res) => { 58 | res.send({ 59 | data: 3 60 | }) 61 | }) 62 | 63 | app.get('/api/getObjectEnum', (req, res) => { 64 | res.send({ 65 | data: { 66 | hello: 'world' 67 | } 68 | }) 69 | }) 70 | 71 | return new Promise((resolve) => { 72 | server = app.listen(PORT, () => { 73 | console.log(`Example API accessible on port ${PORT}`) 74 | resolve() 75 | }) 76 | }) 77 | } 78 | 79 | /** 80 | * Stops server. 81 | */ 82 | function stopServer() { 83 | return new Promise((resolve) => { 84 | server.close(() => { 85 | console.log(`Stopped API server`) 86 | resolve() 87 | }) 88 | }) 89 | } 90 | 91 | // If run from command line, start server: 92 | if (require.main === module) { 93 | startServer(3005) 94 | } 95 | 96 | module.exports = { 97 | startServer, 98 | stopServer 99 | } 100 | -------------------------------------------------------------------------------- /test/example_api6.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { graphql, parse, validate } from 'graphql' 9 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 10 | 11 | import * as openAPIToGraphQL from '../lib/index' 12 | import { Options } from '../lib/types/options' 13 | import { startServer, stopServer } from './example_api6_server' 14 | import { httpRequest } from './httprequest' 15 | 16 | const oas = require('./fixtures/example_oas6.json') 17 | const PORT = 3008 18 | // Update PORT for this test case: 19 | oas.servers[0].variables.port.default = String(PORT) 20 | 21 | let createdSchema 22 | 23 | /** 24 | * Set up the schema first and run example API server 25 | */ 26 | beforeAll(() => { 27 | return Promise.all([ 28 | openAPIToGraphQL 29 | .createGraphQLSchema(oas, { httpRequest }) 30 | .then(({ schema, report }) => { 31 | createdSchema = schema 32 | }), 33 | startServer(PORT) 34 | ]) 35 | }) 36 | 37 | /** 38 | * Shut down API server 39 | */ 40 | afterAll(() => { 41 | return stopServer() 42 | }) 43 | 44 | test('Option requestOptions should work with links', () => { 45 | // Verifying the behavior of the link by itself 46 | const query = `{ 47 | object { 48 | object2Link { 49 | data 50 | } 51 | withParameter: object2Link (specialheader: "extra data"){ 52 | data 53 | } 54 | } 55 | }` 56 | 57 | const promise = graphql({ schema: createdSchema, source: query }).then( 58 | (result) => { 59 | expect(result.data).toEqual({ 60 | object: { 61 | object2Link: { 62 | data: 'object2' 63 | }, 64 | withParameter: { 65 | data: "object2 with special header: 'extra data'" 66 | } 67 | } 68 | }) 69 | } 70 | ) 71 | 72 | const options: Options = { 73 | requestOptions: { 74 | url: undefined, 75 | headers: { 76 | specialheader: 'requestOptions' 77 | } 78 | }, 79 | httpRequest 80 | } 81 | 82 | const query2 = `{ 83 | object { 84 | object2Link { 85 | data 86 | } 87 | } 88 | }` 89 | 90 | const promise2 = openAPIToGraphQL 91 | .createGraphQLSchema(oas, options) 92 | .then(({ schema }) => { 93 | const ast = parse(query2) 94 | const errors = validate(schema, ast) 95 | expect(errors).toEqual([]) 96 | return graphql({ schema, source: query2 }).then((result) => { 97 | expect(result).toEqual({ 98 | data: { 99 | object: { 100 | object2Link: { 101 | data: "object2 with special header: 'requestOptions'" // Data from requestOptions in a link 102 | } 103 | } 104 | } 105 | }) 106 | }) 107 | }) 108 | 109 | return Promise.all([promise, promise2]) 110 | }) 111 | 112 | // Simple scalar fields on the request body 113 | test('Simple request body using application/x-www-form-urlencoded', () => { 114 | const query = `mutation { 115 | postFormUrlEncoded (petInput: { 116 | name: "Mittens", 117 | status: "healthy", 118 | weight: 6 119 | }) { 120 | name 121 | status 122 | weight 123 | } 124 | }` 125 | 126 | return graphql({ schema: createdSchema, source: query }).then((result) => { 127 | expect(result.data).toEqual({ 128 | postFormUrlEncoded: { 129 | name: 'Mittens', 130 | status: 'healthy', 131 | weight: 6 132 | } 133 | }) 134 | }) 135 | }) 136 | 137 | /** 138 | * The field 'previousOwner' should be desanitized to 'previous_owner' 139 | * 140 | * Status is a required field so it is also included 141 | */ 142 | test('Request body using application/x-www-form-urlencoded and desanitization of field name', () => { 143 | const query = `mutation { 144 | postFormUrlEncoded (petInput: { 145 | previousOwner: "Martin", 146 | status: "healthy" 147 | }) { 148 | previousOwner 149 | } 150 | }` 151 | 152 | return graphql({ schema: createdSchema, source: query }).then((result) => { 153 | expect(result.data).toEqual({ 154 | postFormUrlEncoded: { 155 | previousOwner: 'Martin' 156 | } 157 | }) 158 | }) 159 | }) 160 | 161 | /** 162 | * The field 'history' is an object 163 | * 164 | * Status is a required field so it is also included 165 | */ 166 | test('Request body using application/x-www-form-urlencoded containing object', () => { 167 | const query = `mutation { 168 | postFormUrlEncoded (petInput: { 169 | history: { 170 | data: "Friendly" 171 | } 172 | status: "healthy" 173 | }) { 174 | history { 175 | data 176 | } 177 | } 178 | }` 179 | 180 | return graphql({ schema: createdSchema, source: query }).then((result) => { 181 | expect(result.data).toEqual({ 182 | postFormUrlEncoded: { 183 | history: { 184 | data: 'Friendly' 185 | } 186 | } 187 | }) 188 | }) 189 | }) 190 | 191 | test('Request body using application/x-www-form-urlencoded containing object with no properties', () => { 192 | const query = `mutation { 193 | postFormUrlEncoded (petInput: { 194 | history2: { 195 | data: "Friendly" 196 | } 197 | status: "healthy" 198 | }) { 199 | history2 200 | } 201 | }` 202 | 203 | return graphql({ schema: createdSchema, source: query }).then((result) => { 204 | expect(result.data).toEqual({ 205 | postFormUrlEncoded: { 206 | history2: { 207 | data: 'Friendly' 208 | } 209 | } 210 | }) 211 | }) 212 | }) 213 | 214 | /** 215 | * GET /cars/{id} should create a 'car' field 216 | * 217 | * Also the path parameter just contains the term 'id' 218 | */ 219 | test('inferResourceNameFromPath() field with simple plural form', () => { 220 | const query = `{ 221 | car (id: "Super Speed") 222 | }` 223 | 224 | return graphql({ schema: createdSchema, source: query }).then((result) => { 225 | expect(result.data).toEqual({ 226 | car: 'Car ID: Super Speed' 227 | }) 228 | }) 229 | }) 230 | 231 | /** 232 | * GET /cacti/{cactusId} should create an 'cactus' field 233 | * 234 | * Also the path parameter is the combination of the singular form and 'id' 235 | */ 236 | test('inferResourceNameFromPath() field with irregular plural form', () => { 237 | const query = `{ 238 | cactus (cactusId: "Spikey") 239 | }` 240 | 241 | return graphql({ schema: createdSchema, source: query }).then((result) => { 242 | expect(result.data).toEqual({ 243 | cactus: 'Cactus ID: Spikey' 244 | }) 245 | }) 246 | }) 247 | 248 | /** 249 | * GET /eateries/{eatery}/breads/{breadName}/dishes/{dishKey} should create an 250 | * 'eateryBreadDish' field 251 | * 252 | * The path parameters are the singular form, some combination with the term 253 | * 'name', and some combination with the term 'key' 254 | */ 255 | test('inferResourceNameFromPath() field with long path', () => { 256 | const query = `{ 257 | eateryBreadDish(eatery: "Mike's", breadName: "challah", dishKey: "bread pudding") 258 | }` 259 | 260 | return graphql({ schema: createdSchema, source: query }).then((result) => { 261 | expect(result.data).toEqual({ 262 | eateryBreadDish: "Parameters combined: Mike's challah bread pudding" 263 | }) 264 | }) 265 | }) 266 | 267 | /** 268 | * '/nestedReferenceInParameter' contains a query parameter 'russianDoll' that 269 | * contains reference to a component schema. 270 | */ 271 | test('Nested reference in parameter schema', () => { 272 | const query = `{ 273 | nestedReferenceInParameter(russianDoll: { 274 | name: "Gertrude", 275 | nestedDoll: { 276 | name: "Tatiana", 277 | nestedDoll: { 278 | name: "Lidia" 279 | } 280 | } 281 | }) 282 | }` 283 | 284 | return graphql({ schema: createdSchema, source: query }).then((result) => { 285 | expect(result.data).toEqual({ 286 | nestedReferenceInParameter: 'Gertrude, Tatiana, Lidia' 287 | }) 288 | }) 289 | }) 290 | 291 | /** 292 | * 'POST inputUnion' has a request body that contains a oneOf. The request body 293 | * will be converted into an input object type while the oneOf will be turned 294 | * into a union type. However, according to the spec, input object types cannot 295 | * be composed of unions. As a fall back, this pattern should default to the 296 | * arbitrary JSON type instead. 297 | */ 298 | test('Input object types composed of union types should default to arbitrary JSON type', () => { 299 | const query = `{ 300 | __type(name: "Mutation") { 301 | fields { 302 | name 303 | args { 304 | name 305 | type { 306 | name 307 | } 308 | } 309 | } 310 | } 311 | }` 312 | 313 | return graphql({ schema: createdSchema, source: query }).then((result) => { 314 | expect( 315 | result.data['__type']['fields'].find( 316 | (field) => field.name === 'postInputUnion' 317 | ) 318 | ).toEqual({ 319 | name: 'postInputUnion', 320 | args: [ 321 | { 322 | name: 'inputUnionInput', 323 | type: { 324 | name: 'JSON' 325 | } 326 | } 327 | ] 328 | }) 329 | }) 330 | }) 331 | 332 | /** 333 | * GET /strictGetOperation should not receive a Content-Type header 334 | */ 335 | test('Get operation should not receive Content-Type', () => { 336 | const query = `{ 337 | strictGetOperation 338 | }` 339 | 340 | return graphql({ schema: createdSchema, source: query }).then((result) => { 341 | expect(result.data).toEqual({ 342 | strictGetOperation: 'Perfect!' 343 | }) 344 | }) 345 | }) 346 | 347 | /** 348 | * GET /noResponseSchema does not have a response schema 349 | */ 350 | test('Handle no response schema', () => { 351 | const query = `{ 352 | noResponseSchema 353 | }` 354 | 355 | return graphql({ schema: createdSchema, source: query }).then((result) => { 356 | expect(result.data).toEqual({ 357 | noResponseSchema: 'Hello world' 358 | }) 359 | }) 360 | }) 361 | -------------------------------------------------------------------------------- /test/example_api6_server.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | let server // holds server object for shutdown 9 | 10 | /** 11 | * Starts the server at the given port 12 | */ 13 | function startServer(PORT) { 14 | const express = require('express') 15 | const app = express() 16 | 17 | const bodyParser = require('body-parser') 18 | app.use(bodyParser.json()) 19 | app.use(bodyParser.urlencoded({ extended: true })) 20 | 21 | app.get('/api/object', (req, res) => { 22 | res.send({ 23 | data: 'object' 24 | }) 25 | }) 26 | 27 | app.get('/api/object2', (req, res) => { 28 | if (typeof req.headers.specialheader === 'string') { 29 | res.send({ 30 | data: `object2 with special header: '${req.headers.specialheader}'` 31 | }) 32 | } else { 33 | res.send({ 34 | data: 'object2' 35 | }) 36 | } 37 | }) 38 | 39 | app.post('/api/formUrlEncoded', (req, res) => { 40 | res.send(req.body) 41 | }) 42 | 43 | app.get('/api/cars/:id', (req, res) => { 44 | res.send(`Car ID: ${req.params.id}`) 45 | }) 46 | 47 | app.get('/api/cacti/:cactusId', (req, res) => { 48 | res.send(`Cactus ID: ${req.params.cactusId}`) 49 | }) 50 | 51 | app.get( 52 | '/api/eateries/:eatery/breads/:breadName/dishes/:dishKey', 53 | (req, res) => { 54 | res.send( 55 | `Parameters combined: ${req.params.eatery} ${req.params.breadName} ${req.params.dishKey}` 56 | ) 57 | } 58 | ) 59 | 60 | function stringifyRussianDolls(russianDoll) { 61 | if (!typeof russianDoll.name === 'string') { 62 | return '' 63 | } 64 | 65 | if (typeof russianDoll.nestedDoll === 'object') { 66 | return `${russianDoll.name}, ${stringifyRussianDolls( 67 | russianDoll.nestedDoll 68 | )}` 69 | } else { 70 | return russianDoll.name 71 | } 72 | } 73 | 74 | app.get('/api/nestedReferenceInParameter', (req, res) => { 75 | res.send(stringifyRussianDolls(req.query.russianDoll)) 76 | }) 77 | 78 | app.get('/api/strictGetOperation', (req, res) => { 79 | if (req.headers['content-type']) { 80 | res 81 | .status(400) 82 | .set('Content-Type', 'text/plain') 83 | .send('Get request should not have Content-Type') 84 | } else { 85 | res.set('Content-Type', 'text/plain').send('Perfect!') 86 | } 87 | }) 88 | 89 | app.get('/api/noResponseSchema', (req, res) => { 90 | res.set('Content-Type', 'text/plain').send('Hello world') 91 | }) 92 | 93 | return new Promise((resolve) => { 94 | server = app.listen(PORT, () => { 95 | console.log(`Example API accessible on port ${PORT}`) 96 | resolve() 97 | }) 98 | }) 99 | } 100 | 101 | /** 102 | * Stops server. 103 | */ 104 | function stopServer() { 105 | return new Promise((resolve) => { 106 | server.close(() => { 107 | console.log(`Stopped API server`) 108 | resolve() 109 | }) 110 | }) 111 | } 112 | 113 | // If run from command line, start server: 114 | if (require.main === module) { 115 | startServer(3006) 116 | } 117 | 118 | module.exports = { 119 | startServer, 120 | stopServer 121 | } 122 | -------------------------------------------------------------------------------- /test/extensions.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { beforeAll, describe, test, expect } from '@jest/globals' 9 | import { 10 | GraphQLEnumType, 11 | GraphQLInputObjectType, 12 | GraphQLObjectType, 13 | GraphQLSchema 14 | } from 'graphql' 15 | 16 | import * as openAPIToGraphQL from '../lib/index' 17 | import { Oas3 } from '../lib/types/oas3' 18 | 19 | /** 20 | * Set up the schema first 21 | */ 22 | 23 | describe('GraphQL Extensions', () => { 24 | describe('Schema output', () => { 25 | let oas: Oas3 26 | let createdSchema: GraphQLSchema 27 | 28 | beforeAll(async () => { 29 | oas = require('./fixtures/extensions.json') 30 | const { schema } = await openAPIToGraphQL.createGraphQLSchema(oas, { 31 | fillEmptyResponses: true 32 | }) 33 | createdSchema = schema 34 | }) 35 | 36 | test('should rename Query with x-graphql-field-name', () => { 37 | const queries = Object.keys(createdSchema.getQueryType().getFields()) 38 | expect(queries).not.toContain('petFindByStatus') 39 | expect(queries).toContain('getPetsByStatus') 40 | }) 41 | 42 | test('should rename Mutation with x-graphql-field-name', () => { 43 | const mutations = Object.keys(createdSchema.getMutationType().getFields()) 44 | expect(mutations).not.toContain('updatePetWithForm') 45 | expect(mutations).toContain('updatePetForm') 46 | }) 47 | 48 | test('should rename Type with x-graphql-type-name', () => { 49 | const renamedType = createdSchema.getType('Response') 50 | expect(renamedType).toBeInstanceOf(GraphQLObjectType) 51 | expect(createdSchema.getType('ApiResponse')).toBeUndefined() 52 | }) 53 | 54 | test('should rename Type fields with x-graphql-field-name', () => { 55 | const response = createdSchema.getType('Response') as GraphQLObjectType 56 | const fields = Object.keys(response.toConfig().fields) 57 | expect(fields).not.toContain('code') 58 | expect(fields).toContain('statusCode') 59 | }) 60 | 61 | test('should rename Enum values with x-graphql-enum-mapping', () => { 62 | const petStatus = createdSchema.getType('PetStatus') as GraphQLEnumType 63 | const values = petStatus.getValues() 64 | const initialValue = values.find(({ value }) => value === 'available') 65 | const pendingValue = values.find(({ value }) => value === 'pending') 66 | const soldValue = values.find(({ value }) => value === 'sold') 67 | expect(values.length).toEqual(3) 68 | expect(initialValue.name).toEqual('INITIAL') 69 | expect(pendingValue.name).toEqual('IN_PROGRESS') 70 | expect(soldValue.name).toEqual('SOLD') 71 | }) 72 | 73 | test('should rename Links with x-graphql-field-name', () => { 74 | const order = createdSchema.getType('Order') as GraphQLObjectType 75 | const fields = Object.keys(order.getFields()) 76 | expect(fields).not.toContain('pet') 77 | expect(fields).toContain('orderPet') 78 | expect(order.getFields().orderPet.type.toString()).toEqual('Pet') 79 | }) 80 | 81 | test('should rename input object type with x-graphql-type-name and append input at the end', () => { 82 | const renamedType = createdSchema.getType('MetaInput') 83 | expect(renamedType).toBeInstanceOf(GraphQLInputObjectType) 84 | expect(createdSchema.getType('AdditionalMetadata')).toBeUndefined() 85 | expect(createdSchema.getType('AdditionalMetadataInput')).toBeUndefined() 86 | }) 87 | }) 88 | 89 | describe('Error handling', () => { 90 | test('should throw when x-graphql-type-name causes naming conflicts', async () => { 91 | const oas = require('./fixtures/extensions_error1.json') 92 | await expect( 93 | openAPIToGraphQL.createGraphQLSchema(oas) 94 | ).rejects.toThrowError( 95 | new Error( 96 | `Cannot create type with name "User".\nYou provided "User" in ` + 97 | `x-graphql-type-name, but it conflicts with another type named ` + 98 | `"User".` 99 | ) 100 | ) 101 | }) 102 | 103 | test('should throw when x-graphql-field-name causes naming conflicts on objects', async () => { 104 | const oas = require('./fixtures/extensions_error2.json') 105 | await expect( 106 | openAPIToGraphQL.createGraphQLSchema(oas) 107 | ).rejects.toThrowError( 108 | new Error( 109 | `Cannot create field with name "name".\nYou provided "name" in ` + 110 | `x-graphql-field-name, but it conflicts with another field named ` + 111 | `"name".` 112 | ) 113 | ) 114 | }) 115 | 116 | test('should throw when x-graphql-field-name causes naming conflicts on queries', async () => { 117 | const oas = require('./fixtures/extensions_error3.json') 118 | await expect( 119 | openAPIToGraphQL.createGraphQLSchema(oas) 120 | ).rejects.toThrowError( 121 | new Error( 122 | `Cannot create query field with name "user".\nYou provided ` + 123 | `"user" in x-graphql-field-name, but it conflicts with another ` + 124 | `field named "user".` 125 | ) 126 | ) 127 | }) 128 | 129 | test('should throw when x-graphql-field-name causes naming conflicts on mutations', async () => { 130 | const oas = require('./fixtures/extensions_error4.json') 131 | await expect( 132 | openAPIToGraphQL.createGraphQLSchema(oas) 133 | ).rejects.toThrowError( 134 | new Error( 135 | `Cannot create mutation field with name "createUser".\nYou ` + 136 | `provided "createUser" in x-graphql-field-name, but it ` + 137 | `conflicts with another field named "createUser".` 138 | ) 139 | ) 140 | }) 141 | 142 | test('should throw when x-graphql-field-name causes naming conflicts on links', async () => { 143 | const oas = require('./fixtures/extensions_error6.json') 144 | await expect( 145 | openAPIToGraphQL.createGraphQLSchema(oas) 146 | ).rejects.toThrowError( 147 | new Error( 148 | `Cannot create link field with name "group".\nYou provided ` + 149 | `"group" in x-graphql-field-name, but it conflicts with ` + 150 | `another field named "group".` 151 | ) 152 | ) 153 | }) 154 | 155 | test('should throw when x-graphql-enum-mapping causes naming conflicts', async () => { 156 | const oas = require('./fixtures/extensions_error7.json') 157 | await expect( 158 | openAPIToGraphQL.createGraphQLSchema(oas) 159 | ).rejects.toThrowError( 160 | new Error( 161 | `Cannot create enum value "CONFLICT".\nYou provided ` + 162 | `"CONFLICT" in x-graphql-enum-mapping, but it conflicts ` + 163 | `with another value "CONFLICT".` 164 | ) 165 | ) 166 | }) 167 | }) 168 | }) 169 | -------------------------------------------------------------------------------- /test/fixtures/cloudfunction.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "test-action-2", 5 | "version": "1.0.0" 6 | }, 7 | "paths": { 8 | "/test-action-2": { 9 | "post": { 10 | "description": "Description of the action", 11 | "responses": { 12 | "200": { 13 | "description": "Success response for test-action-2", 14 | "content": { 15 | "application/json": { 16 | "schema": { 17 | "$ref": "#/components/schemas/Response" 18 | } 19 | } 20 | } 21 | }, 22 | "default": { 23 | "description": "Error response for test-action-2", 24 | "content": { 25 | "application/json": { 26 | "schema": { 27 | "$ref": "#/components/schemas/Error" 28 | } 29 | } 30 | } 31 | } 32 | }, 33 | "parameters": [ 34 | { 35 | "name": "blocking", 36 | "in": "query", 37 | "schema": { 38 | "type": "boolean", 39 | "default": true 40 | } 41 | }, 42 | { 43 | "name": "result", 44 | "in": "query", 45 | "schema": { 46 | "type": "boolean", 47 | "default": true 48 | } 49 | } 50 | ], 51 | "security": [ 52 | { 53 | "basic_protocol": [] 54 | } 55 | ], 56 | "requestBody": { 57 | "content": { 58 | "application/json": { 59 | "schema": { 60 | "$ref": "#/components/schemas/Payload" 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | }, 68 | "servers": [ 69 | { 70 | "url": "https://openwhisk.ng.bluemix.net/api/v1/namespaces/_/actions" 71 | } 72 | ], 73 | "components": { 74 | "schemas": { 75 | "Response": { 76 | "type": "object", 77 | "properties": { 78 | "payload": { 79 | "type": "string" 80 | }, 81 | "age": { 82 | "type": "number" 83 | }, 84 | "valid": { 85 | "type": "boolean" 86 | } 87 | }, 88 | "required": ["payload", "age", "valid"] 89 | }, 90 | "Error": {}, 91 | "Payload": { 92 | "type": "object", 93 | "properties": { 94 | "age": { 95 | "type": "number" 96 | } 97 | }, 98 | "required": ["age"] 99 | } 100 | }, 101 | "securitySchemes": { 102 | "basic_protocol": { 103 | "type": "http", 104 | "scheme": "basic" 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/fixtures/example_oas2.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Example API 2", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Erik Wittern", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3002" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/user": { 43 | "get": { 44 | "description": "Return a user.", 45 | "responses": { 46 | "202": { 47 | "description": "A user.", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "$ref": "#/components/schemas/user" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "/user2": { 60 | "get": { 61 | "operationId": "User", 62 | "description": "Return a user.", 63 | "responses": { 64 | "202": { 65 | "description": "A user.", 66 | "content": { 67 | "application/json": { 68 | "schema": { 69 | "$ref": "#/components/schemas/user" 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | }, 78 | "components": { 79 | "schemas": { 80 | "user": { 81 | "type": "object", 82 | "description": "A user represents a natural person", 83 | "properties": { 84 | "name": { 85 | "type": "string", 86 | "description": "The legal name of a user" 87 | } 88 | } 89 | } 90 | } 91 | }, 92 | "security": [] 93 | } 94 | -------------------------------------------------------------------------------- /test/fixtures/example_oas3.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Example API 3", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Erik Wittern", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3003" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/authors/{authorId}": { 43 | "get": { 44 | "operationId": "author", 45 | "description": "Return an author.", 46 | "parameters": [ 47 | { 48 | "name": "authorId", 49 | "in": "path", 50 | "required": true, 51 | "schema": { 52 | "type": "string" 53 | } 54 | } 55 | ], 56 | "responses": { 57 | "202": { 58 | "description": "A author.", 59 | "content": { 60 | "application/json": { 61 | "schema": { 62 | "$ref": "#/components/schemas/author" 63 | } 64 | } 65 | }, 66 | "links": { 67 | "masterpiece": { 68 | "operationId": "book", 69 | "parameters": { 70 | "bookId": "$response.body#/masterpieceTitle" 71 | }, 72 | "description": "Fetches the masterpiece" 73 | }, 74 | "nextWork": { 75 | "operationId": "nextWork", 76 | "parameters": { 77 | "authorId": "$request.path.authorId" 78 | }, 79 | "description": "Fetches the author's next work" 80 | }, 81 | "employee": { 82 | "$ref": "#/components/links/Employee" 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }, 89 | "/books/{bookId}": { 90 | "get": { 91 | "operationId": "book", 92 | "description": "Return a book.", 93 | "parameters": [ 94 | { 95 | "name": "bookId", 96 | "in": "path", 97 | "required": true, 98 | "schema": { 99 | "type": "string" 100 | } 101 | } 102 | ], 103 | "responses": { 104 | "202": { 105 | "description": "A book.", 106 | "content": { 107 | "application/json": { 108 | "schema": { 109 | "$ref": "#/components/schemas/book" 110 | } 111 | } 112 | }, 113 | "links": { 114 | "author": { 115 | "operationId": "author", 116 | "parameters": { 117 | "authorId": "$response.body#/authorName" 118 | }, 119 | "description": "Fetches the author" 120 | } 121 | } 122 | } 123 | } 124 | } 125 | }, 126 | "/nextWorks/{authorId}": { 127 | "get": { 128 | "operationId": "nextWork", 129 | "description": "Return the author's next work.", 130 | "parameters": [ 131 | { 132 | "name": "authorId", 133 | "in": "path", 134 | "required": true, 135 | "schema": { 136 | "type": "string" 137 | } 138 | } 139 | ], 140 | "responses": { 141 | "202": { 142 | "description": "An upcoming book.", 143 | "content": { 144 | "application/json": { 145 | "schema": { 146 | "$ref": "#/components/schemas/nextWork" 147 | } 148 | } 149 | }, 150 | "links": { 151 | "author": { 152 | "operationId": "author", 153 | "parameters": { 154 | "authorId": "$request.path.authorId" 155 | }, 156 | "description": "Fetches the author" 157 | } 158 | } 159 | } 160 | }, 161 | "security": [ 162 | { 163 | "example_api3_key_protocol": [] 164 | }, 165 | { 166 | "example_api3_basic_protocol": [] 167 | } 168 | ] 169 | } 170 | } 171 | }, 172 | "components": { 173 | "schemas": { 174 | "book": { 175 | "type": "object", 176 | "description": "A book", 177 | "properties": { 178 | "title": { 179 | "type": "string", 180 | "description": "The title of the book" 181 | }, 182 | "authorName": { 183 | "type": "string", 184 | "description": "The author of the book" 185 | } 186 | } 187 | }, 188 | "nextWork": { 189 | "type": "object", 190 | "description": "A book", 191 | "properties": { 192 | "title": { 193 | "type": "string", 194 | "description": "The title of the book" 195 | }, 196 | "authorName": { 197 | "type": "string", 198 | "description": "The author of the book" 199 | } 200 | } 201 | }, 202 | "author": { 203 | "type": "object", 204 | "description": "An author", 205 | "properties": { 206 | "name": { 207 | "type": "string", 208 | "description": "The name of the author" 209 | }, 210 | "masterpieceTitle": { 211 | "type": "string", 212 | "description": "The artist's bestseller" 213 | } 214 | } 215 | } 216 | }, 217 | "links": { 218 | "Employee": { 219 | "operationRef": "Example API#/paths/~1users~1{username}/get", 220 | "parameters": { 221 | "username": "$request.path.authorId" 222 | }, 223 | "description": "Link between two different APIs" 224 | } 225 | }, 226 | "securitySchemes": { 227 | "example_api3_key_protocol": { 228 | "in": "header", 229 | "name": "access_token", 230 | "type": "apiKey" 231 | }, 232 | "example_api3_key_protocol_2": { 233 | "in": "query", 234 | "name": "access_token", 235 | "type": "apiKey" 236 | }, 237 | "example_api3_basic_protocol": { 238 | "type": "http", 239 | "scheme": "basic" 240 | } 241 | } 242 | }, 243 | "security": [] 244 | } 245 | -------------------------------------------------------------------------------- /test/fixtures/example_oas5.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Example API 5", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Erik Wittern", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3005" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/o_d_d___n_a_m_e": { 43 | "get": { 44 | "description": "Basic simpleNames option test", 45 | "responses": { 46 | "200": { 47 | "description": "Success", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "type": "object", 52 | "properties": { 53 | "data": { 54 | "type": "string" 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | "links": { 61 | "w-e-i-r-d___n-a-m-e___l-i-n-k": { 62 | "operationRef": "#/paths/~1w-e-i-r-d___n-a-m-e/get", 63 | "description": "Basic link" 64 | }, 65 | "w-e-i-r-d___n-a-m-e2___l-i-n-k": { 66 | "operationRef": "#/paths/~1w-e-i-r-d___n-a-m-e2~1{f-u-n-k-y___p-a-r-a-m-e-t-e-r}/get", 67 | "description": "Hardcoded link parameter", 68 | "parameters": { 69 | "f-u-n-k-y___p-a-r-a-m-e-t-e-r": "Charles" 70 | } 71 | }, 72 | "w-e-i-r-d___n-a-m-e3___l-i-n-k": { 73 | "operationRef": "#/paths/~1w-e-i-r-d___n-a-m-e3~1{f-u-n-k-y___p-a-r-a-m-e-t-e-r}/get", 74 | "description": "Exposed link parameter" 75 | } 76 | } 77 | } 78 | } 79 | } 80 | }, 81 | "/w-e-i-r-d___n-a-m-e": { 82 | "get": { 83 | "description": "Basic simpleNames option test with GraphQL unsafe values", 84 | "responses": { 85 | "200": { 86 | "description": "Success", 87 | "content": { 88 | "application/json": { 89 | "schema": { 90 | "type": "object", 91 | "properties": { 92 | "data": { 93 | "type": "string" 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | }, 103 | "/w-e-i-r-d___n-a-m-e2/{f-u-n-k-y___p-a-r-a-m-e-t-e-r}": { 104 | "get": { 105 | "description": "Basic simpleNames option test with links with hard-coded parameters", 106 | "parameters": [ 107 | { 108 | "name": "f-u-n-k-y___p-a-r-a-m-e-t-e-r", 109 | "in": "path", 110 | "required": true, 111 | "schema": { 112 | "type": "string" 113 | } 114 | } 115 | ], 116 | "responses": { 117 | "200": { 118 | "description": "Success", 119 | "content": { 120 | "application/json": { 121 | "schema": { 122 | "type": "object", 123 | "properties": { 124 | "data": { 125 | "type": "string" 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | }, 135 | "/w-e-i-r-d___n-a-m-e3/{f-u-n-k-y___p-a-r-a-m-e-t-e-r}": { 136 | "get": { 137 | "description": "Basic simpleNames option test with links with exposed parameters", 138 | "parameters": [ 139 | { 140 | "name": "f-u-n-k-y___p-a-r-a-m-e-t-e-r", 141 | "in": "path", 142 | "required": true, 143 | "schema": { 144 | "type": "string" 145 | } 146 | } 147 | ], 148 | "responses": { 149 | "200": { 150 | "description": "Success", 151 | "content": { 152 | "application/json": { 153 | "schema": { 154 | "type": "object", 155 | "properties": { 156 | "data": { 157 | "type": "string" 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | }, 167 | "/getEnum": { 168 | "get": { 169 | "description": "Basic simpleEnumValues option test", 170 | "responses": { 171 | "200": { 172 | "description": "Success", 173 | "content": { 174 | "application/json": { 175 | "schema": { 176 | "type": "object", 177 | "properties": { 178 | "data": { 179 | "type": "string", 180 | "enum": ["r-e-d", "a-m-b-e-r", "g-r-e-e-n"] 181 | } 182 | } 183 | } 184 | } 185 | } 186 | } 187 | } 188 | } 189 | }, 190 | "/getNumericalEnum": { 191 | "get": { 192 | "description": "Basic simpleEnumValues option test on numerical enum", 193 | "responses": { 194 | "200": { 195 | "description": "Success", 196 | "content": { 197 | "application/json": { 198 | "schema": { 199 | "type": "object", 200 | "properties": { 201 | "data": { 202 | "type": "number", 203 | "enum": [1, 2, 3, 4, 5] 204 | } 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | } 212 | }, 213 | "/getObjectEnum": { 214 | "get": { 215 | "description": "Basic simpleEnumValues option test on object enum", 216 | "responses": { 217 | "200": { 218 | "description": "Success", 219 | "content": { 220 | "application/json": { 221 | "schema": { 222 | "type": "object", 223 | "properties": { 224 | "data": { 225 | "type": "object", 226 | "enum": [ 227 | { 228 | "hello": "world" 229 | }, 230 | { 231 | "goodbye": "world" 232 | } 233 | ] 234 | } 235 | } 236 | } 237 | } 238 | } 239 | } 240 | } 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /test/fixtures/example_oas6.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Example API 6", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Erik Wittern", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3006" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/object": { 43 | "get": { 44 | "description": "An arbitrary object", 45 | "responses": { 46 | "200": { 47 | "description": "Success", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "type": "object", 52 | "properties": { 53 | "data": { 54 | "type": "string" 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | "links": { 61 | "object2Link": { 62 | "operationId": "getObject2", 63 | "description": "Link with exposed parameter" 64 | } 65 | } 66 | } 67 | } 68 | } 69 | }, 70 | "/object2": { 71 | "get": { 72 | "operationId": "getObject2", 73 | "description": "Serves as a link of GET /object", 74 | "parameters": [ 75 | { 76 | "name": "specialheader", 77 | "description": "HTTP headers are case-insensitive", 78 | "in": "header", 79 | "schema": { 80 | "type": "string" 81 | } 82 | } 83 | ], 84 | "responses": { 85 | "200": { 86 | "description": "Success", 87 | "content": { 88 | "application/json": { 89 | "schema": { 90 | "type": "object", 91 | "properties": { 92 | "data": { 93 | "type": "string" 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | }, 103 | "/formUrlEncoded": { 104 | "post": { 105 | "description": "Basic application/x-www-form-urlencoded test", 106 | "requestBody": { 107 | "content": { 108 | "application/x-www-form-urlencoded": { 109 | "schema": { 110 | "$ref": "#/components/schemas/pet" 111 | } 112 | } 113 | } 114 | }, 115 | "responses": { 116 | "200": { 117 | "description": "A pet", 118 | "content": { 119 | "application/json": { 120 | "schema": { 121 | "$ref": "#/components/schemas/pet" 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | }, 129 | "/cars/{id}": { 130 | "get": { 131 | "description": "A particular car", 132 | "parameters": [ 133 | { 134 | "name": "id", 135 | "in": "path", 136 | "required": true, 137 | "schema": { 138 | "type": "string" 139 | } 140 | } 141 | ], 142 | "responses": { 143 | "200": { 144 | "description": "Success", 145 | "content": { 146 | "text/html": { 147 | "schema": { 148 | "type": "string" 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | }, 156 | "/cacti/{cactusId}": { 157 | "get": { 158 | "description": "A particular cactus", 159 | "parameters": [ 160 | { 161 | "name": "cactusId", 162 | "in": "path", 163 | "required": true, 164 | "schema": { 165 | "type": "string" 166 | } 167 | } 168 | ], 169 | "responses": { 170 | "200": { 171 | "description": "Success", 172 | "content": { 173 | "text/html": { 174 | "schema": { 175 | "type": "string" 176 | } 177 | } 178 | } 179 | } 180 | } 181 | } 182 | }, 183 | "/eateries/{eatery}/breads/{breadName}/dishes/{dishKey}": { 184 | "get": { 185 | "parameters": [ 186 | { 187 | "name": "eatery", 188 | "in": "path", 189 | "required": true, 190 | "schema": { 191 | "type": "string" 192 | } 193 | }, 194 | { 195 | "name": "breadName", 196 | "in": "path", 197 | "required": true, 198 | "schema": { 199 | "type": "string" 200 | } 201 | }, 202 | { 203 | "name": "dishKey", 204 | "in": "path", 205 | "required": true, 206 | "schema": { 207 | "type": "string" 208 | } 209 | } 210 | ], 211 | "responses": { 212 | "200": { 213 | "description": "Success", 214 | "content": { 215 | "text/html": { 216 | "schema": { 217 | "type": "string" 218 | } 219 | } 220 | } 221 | } 222 | } 223 | } 224 | }, 225 | "/nestedReferenceInParameter": { 226 | "get": { 227 | "description": "Resolve a nested reference in the parameter schema", 228 | "parameters": [ 229 | { 230 | "name": "russianDoll", 231 | "in": "query", 232 | "content": { 233 | "application/json": { 234 | "schema": { 235 | "$ref": "#/components/schemas/russianDoll" 236 | } 237 | } 238 | }, 239 | "description": "Arbitrary query parameter object" 240 | } 241 | ], 242 | "responses": { 243 | "200": { 244 | "description": "Names of all the russian dolls", 245 | "content": { 246 | "text/html": { 247 | "schema": { 248 | "type": "string" 249 | } 250 | } 251 | } 252 | } 253 | } 254 | } 255 | }, 256 | "/inputUnion": { 257 | "post": { 258 | "requestBody": { 259 | "content": { 260 | "application/json": { 261 | "schema": { 262 | "oneOf": [ 263 | { 264 | "type": "object", 265 | "properties": { 266 | "dogBreed": { 267 | "type": "string" 268 | } 269 | } 270 | }, 271 | { 272 | "type": "object", 273 | "properties": { 274 | "catBreed": { 275 | "type": "string" 276 | } 277 | } 278 | } 279 | ] 280 | } 281 | } 282 | } 283 | }, 284 | "responses": { 285 | "200": { 286 | "description": "Success", 287 | "content": { 288 | "text/html": { 289 | "schema": { 290 | "type": "string" 291 | } 292 | } 293 | } 294 | } 295 | } 296 | } 297 | }, 298 | "/strictGetOperation": { 299 | "get": { 300 | "description": "An arbitrary object", 301 | "responses": { 302 | "200": { 303 | "description": "Mandatory field", 304 | "content": { 305 | "text/plain": { 306 | "schema": { 307 | "type": "string" 308 | } 309 | } 310 | } 311 | } 312 | } 313 | } 314 | }, 315 | "/noResponseSchema": { 316 | "get": { 317 | "description": "No provided response schema test", 318 | "responses": { 319 | "200": { 320 | "description": "Success", 321 | "content": { 322 | "text/plain": { 323 | "example": "Hello world" 324 | } 325 | } 326 | } 327 | } 328 | } 329 | } 330 | }, 331 | "components": { 332 | "schemas": { 333 | "pet": { 334 | "type": "object", 335 | "properties": { 336 | "name": { 337 | "description": "Name of the pet", 338 | "type": "string" 339 | }, 340 | "status": { 341 | "description": "Status of the pet", 342 | "type": "string" 343 | }, 344 | "weight": { 345 | "description": "Weight of the pet", 346 | "type": "number" 347 | }, 348 | "previous_owner": { 349 | "description": "Previouw owner of the pet", 350 | "type": "string" 351 | }, 352 | "history": { 353 | "description": "History of the pet", 354 | "type": "object", 355 | "properties": { 356 | "data": { 357 | "type": "string" 358 | } 359 | } 360 | }, 361 | "history2": { 362 | "description": "History of the pet", 363 | "type": "object" 364 | } 365 | }, 366 | "required": ["status"] 367 | }, 368 | "russianDoll": { 369 | "type": "object", 370 | "properties": { 371 | "name": { 372 | "type": "string" 373 | }, 374 | "nestedDoll": { 375 | "$ref": "#/components/schemas/russianDoll" 376 | } 377 | } 378 | } 379 | } 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /test/fixtures/example_oas7.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Example API 7", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "license": { 9 | "name": "Apache 2.0", 10 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 11 | } 12 | }, 13 | "externalDocs": { 14 | "url": "http://example.com/docs", 15 | "description": "Some more natural language description." 16 | }, 17 | "tags": [ 18 | { 19 | "name": "test", 20 | "description": "Indicates this API is for testing" 21 | } 22 | ], 23 | "servers": [ 24 | { 25 | "url": "http://localhost:{port}/{basePath}", 26 | "description": "The location of the local test server.", 27 | "variables": { 28 | "port": { 29 | "default": "3007" 30 | }, 31 | "basePath": { 32 | "default": "api" 33 | } 34 | } 35 | }, 36 | { 37 | "url": "mqtt://localhost:{port}/{basePath}", 38 | "description": "The location of the local MQTT test broker.", 39 | "variables": { 40 | "port": { 41 | "default": "1885" 42 | }, 43 | "basePath": { 44 | "default": "api" 45 | } 46 | } 47 | } 48 | ], 49 | "paths": { 50 | "/user": { 51 | "get": { 52 | "description": "Return a user.", 53 | "responses": { 54 | "202": { 55 | "description": "A user.", 56 | "content": { 57 | "application/json": { 58 | "schema": { 59 | "$ref": "#/components/schemas/User" 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | "/devices": { 68 | "get": { 69 | "operationId": "findDevices", 70 | "description": "Return a device collection.", 71 | "responses": { 72 | "200": { 73 | "description": "A device collection", 74 | "content": { 75 | "application/json": { 76 | "schema": { 77 | "type": "array", 78 | "description": "The device collection", 79 | "items": { 80 | "$ref": "#/components/schemas/Device" 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | }, 88 | "post": { 89 | "operationId": "createDevice", 90 | "description": "Create and return a device.", 91 | "requestBody": { 92 | "$ref": "#/components/requestBodies/DeviceBody" 93 | }, 94 | "responses": { 95 | "200": { 96 | "$ref": "#/components/responses/DeviceInstance" 97 | }, 98 | "default": { 99 | "$ref": "#/components/responses/GeneralError" 100 | } 101 | }, 102 | "callbacks": { 103 | "deviceCreated": { 104 | "$ref": "#/components/callbacks/DevicesEvent" 105 | } 106 | } 107 | } 108 | }, 109 | "/devices/{deviceName}": { 110 | "get": { 111 | "operationId": "findDeviceByName", 112 | "description": "Find a device by name.", 113 | "parameters": [ 114 | { 115 | "name": "deviceName", 116 | "in": "path", 117 | "required": true, 118 | "schema": { 119 | "type": "string" 120 | } 121 | } 122 | ], 123 | "responses": { 124 | "200": { 125 | "$ref": "#/components/responses/DeviceInstance" 126 | }, 127 | "default": { 128 | "$ref": "#/components/responses/GeneralError" 129 | } 130 | } 131 | }, 132 | "put": { 133 | "operationId": "replaceDeviceByName", 134 | "description": "Replace a device by name.", 135 | "parameters": [ 136 | { 137 | "name": "deviceName", 138 | "in": "path", 139 | "required": true, 140 | "schema": { 141 | "type": "string" 142 | } 143 | } 144 | ], 145 | "requestBody": { 146 | "$ref": "#/components/requestBodies/DeviceBody" 147 | }, 148 | "responses": { 149 | "200": { 150 | "$ref": "#/components/responses/DeviceInstance" 151 | }, 152 | "default": { 153 | "$ref": "#/components/responses/GeneralError" 154 | } 155 | }, 156 | "callbacks": { 157 | "deviceUpdated": { 158 | "$ref": "#/components/callbacks/DevicesEvent" 159 | } 160 | } 161 | } 162 | } 163 | }, 164 | "components": { 165 | "schemas": { 166 | "User": { 167 | "type": "object", 168 | "description": "A user represents a natural person", 169 | "properties": { 170 | "name": { 171 | "type": "string", 172 | "description": "The legal name of a user" 173 | } 174 | } 175 | }, 176 | "Device": { 177 | "type": "object", 178 | "description": "A device is an object connected to the network", 179 | "properties": { 180 | "name": { 181 | "type": "string", 182 | "description": "The device name in the network" 183 | }, 184 | "userName": { 185 | "type": "string", 186 | "description": "The device owner Name" 187 | }, 188 | "status": { 189 | "type": "boolean" 190 | } 191 | }, 192 | "required": ["name", "userName"] 193 | }, 194 | "Topic": { 195 | "type": "object", 196 | "description": "A topic is used to listen events", 197 | "properties": { 198 | "userName": { 199 | "type": "string", 200 | "description": "The device owner" 201 | }, 202 | "deviceName": { 203 | "type": "string", 204 | "description": "The device name" 205 | }, 206 | "method": { 207 | "type": "string", 208 | "description": "The device method to watch", 209 | "example": "Equivalent to HTTP methods" 210 | } 211 | }, 212 | "required": ["userName", "method"] 213 | }, 214 | "Error": { 215 | "type": "object", 216 | "description": "A topic is used to listen an event", 217 | "properties": { 218 | "code": { 219 | "type": "string", 220 | "description": "Error code" 221 | }, 222 | "message": { 223 | "type": "string", 224 | "description": "Error message" 225 | } 226 | }, 227 | "required": ["code", "message"] 228 | } 229 | }, 230 | "requestBodies": { 231 | "EventsBody": { 232 | "description": "Properties to generate the event path", 233 | "required": true, 234 | "content": { 235 | "application/json": { 236 | "schema": { 237 | "$ref": "#/components/schemas/Topic" 238 | } 239 | } 240 | } 241 | }, 242 | "DeviceBody": { 243 | "description": "Device instance to update / create", 244 | "required": true, 245 | "content": { 246 | "application/json": { 247 | "schema": { 248 | "$ref": "#/components/schemas/Device" 249 | } 250 | } 251 | } 252 | } 253 | }, 254 | "responses": { 255 | "DeviceInstance": { 256 | "description": "Device instance", 257 | "content": { 258 | "application/json": { 259 | "schema": { 260 | "$ref": "#/components/schemas/Device" 261 | } 262 | } 263 | } 264 | }, 265 | "GeneralError": { 266 | "description": "Error reponse", 267 | "content": { 268 | "application/json": { 269 | "schema": { 270 | "$ref": "#/components/schemas/Error" 271 | } 272 | } 273 | } 274 | } 275 | }, 276 | "callbacks": { 277 | "DevicesEvent": { 278 | "/api/{$request.body#/userName}/devices/{$request.body#/method}/+": { 279 | "post": { 280 | "operationId": "devicesEventListener", 281 | "description": "Listen all devices events owned by userName", 282 | "requestBody": { 283 | "$ref": "#/components/requestBodies/EventsBody" 284 | }, 285 | "responses": { 286 | "200": { 287 | "$ref": "#/components/responses/DeviceInstance" 288 | } 289 | } 290 | } 291 | } 292 | } 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /test/fixtures/extensions_error1.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Extensions Error 1", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Elias Meire", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3002" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/user": { 43 | "get": { 44 | "description": "Return a user.", 45 | "responses": { 46 | "202": { 47 | "description": "A user.", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "$ref": "#/components/schemas/user" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "/user2": { 60 | "get": { 61 | "description": "Return a user2.", 62 | "responses": { 63 | "202": { 64 | "description": "A user2.", 65 | "content": { 66 | "application/json": { 67 | "schema": { 68 | "$ref": "#/components/schemas/user2" 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | "components": { 78 | "schemas": { 79 | "user": { 80 | "type": "object", 81 | "description": "A user represents a natural person", 82 | "properties": { 83 | "name": { 84 | "type": "string", 85 | "description": "The legal name of a user" 86 | } 87 | } 88 | }, 89 | "user2": { 90 | "type": "object", 91 | "description": "A user2 represents a natural person", 92 | "x-graphql-type-name": "User", 93 | "properties": { 94 | "name": { 95 | "type": "string", 96 | "description": "The legal name of a user" 97 | } 98 | } 99 | } 100 | } 101 | }, 102 | "security": [] 103 | } 104 | -------------------------------------------------------------------------------- /test/fixtures/extensions_error2.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Extensions Error 2", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Elias Meire", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3002" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/user": { 43 | "get": { 44 | "description": "Return a user.", 45 | "responses": { 46 | "202": { 47 | "description": "A user.", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "$ref": "#/components/schemas/user" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | "components": { 61 | "schemas": { 62 | "user": { 63 | "type": "object", 64 | "description": "A user represents a natural person", 65 | "properties": { 66 | "name": { 67 | "type": "string", 68 | "description": "The legal name of a user" 69 | }, 70 | "name2": { 71 | "type": "string", 72 | "x-graphql-field-name": "name", 73 | "description": "The legal name of a user" 74 | } 75 | } 76 | } 77 | } 78 | }, 79 | "security": [] 80 | } 81 | -------------------------------------------------------------------------------- /test/fixtures/extensions_error3.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Extensions Error 3", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Elias Meire", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3002" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/user": { 43 | "get": { 44 | "description": "Return a user.", 45 | "responses": { 46 | "202": { 47 | "description": "A user.", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "$ref": "#/components/schemas/user" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "/user2": { 60 | "get": { 61 | "description": "Return a user.", 62 | "x-graphql-field-name": "user", 63 | "responses": { 64 | "202": { 65 | "description": "A user.", 66 | "content": { 67 | "application/json": { 68 | "schema": { 69 | "$ref": "#/components/schemas/user" 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | }, 78 | "components": { 79 | "schemas": { 80 | "user": { 81 | "type": "object", 82 | "description": "A user represents a natural person", 83 | "properties": { 84 | "name": { 85 | "type": "string", 86 | "description": "The legal name of a user" 87 | }, 88 | "name2": { 89 | "type": "string", 90 | "description": "The legal name of a user" 91 | } 92 | } 93 | } 94 | } 95 | }, 96 | "security": [] 97 | } 98 | -------------------------------------------------------------------------------- /test/fixtures/extensions_error4.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Extensions Error 4", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Elias Meire", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3002" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/user": { 43 | "post": { 44 | "tags": ["user"], 45 | "summary": "Creates a user", 46 | "description": "Creates a user", 47 | "operationId": "createUser", 48 | "requestBody": { 49 | "content": { 50 | "application/json": { 51 | "schema": { "$ref": "#/components/schemas/user" } 52 | } 53 | } 54 | }, 55 | "responses": { 56 | "200": { 57 | "description": "Successful operation", 58 | "content": { 59 | "application/json": { 60 | "schema": { "$ref": "#/components/schemas/user" } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | "/user2": { 68 | "post": { 69 | "tags": ["user"], 70 | "summary": "Creates a user", 71 | "description": "Creates a user", 72 | "operationId": "createUser2", 73 | "x-graphql-field-name": "createUser", 74 | "requestBody": { 75 | "content": { 76 | "application/json": { 77 | "schema": { "$ref": "#/components/schemas/user" } 78 | } 79 | } 80 | }, 81 | "responses": { 82 | "200": { 83 | "description": "Successful operation", 84 | "content": { 85 | "application/json": { 86 | "schema": { "$ref": "#/components/schemas/user" } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | }, 94 | "components": { 95 | "schemas": { 96 | "user": { 97 | "type": "object", 98 | "description": "A user represents a natural person", 99 | "properties": { 100 | "name": { 101 | "type": "string", 102 | "description": "The legal name of a user" 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "security": [] 109 | } 110 | -------------------------------------------------------------------------------- /test/fixtures/extensions_error5.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Extensions Error 5", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Elias Meire", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3002" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/user": { 43 | "post": { 44 | "tags": ["user"], 45 | "summary": "Creates a user", 46 | "description": "Creates a user", 47 | "operationId": "createUser", 48 | "requestBody": { 49 | "content": { 50 | "application/json": { 51 | "schema": { "$ref": "#/components/schemas/user" } 52 | } 53 | } 54 | }, 55 | "responses": { 56 | "200": { 57 | "description": "Successful operation", 58 | "content": { 59 | "application/json": { 60 | "schema": { "$ref": "#/components/schemas/user" } 61 | } 62 | } 63 | } 64 | }, 65 | "callbacks": { 66 | "userCreated": { 67 | "$ref": "#/components/callbacks/UserEvent" 68 | }, 69 | "userCreated2": { 70 | "$ref": "#/components/callbacks/UserEvent2" 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | "components": { 77 | "schemas": { 78 | "user": { 79 | "type": "object", 80 | "description": "A user represents a natural person", 81 | "properties": { 82 | "name": { 83 | "type": "string", 84 | "description": "The legal name of a user" 85 | } 86 | } 87 | } 88 | }, 89 | "callbacks": { 90 | "UserEvent": { 91 | "/users/{$request.body#/id}/events/+": { 92 | "post": { 93 | "operationId": "userEventListener", 94 | "description": "Listen all user events owned by userName", 95 | "responses": { 96 | "204": { 97 | "description": "user event response" 98 | } 99 | } 100 | } 101 | } 102 | }, 103 | "UserEvent2": { 104 | "/users/{$request.body#/id}/events/+": { 105 | "post": { 106 | "operationId": "userEventListener2", 107 | "x-graphql-field-name": "userEventListener", 108 | "description": "Listen all user events owned by userName", 109 | "responses": { 110 | "204": { 111 | "description": "user event response" 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | }, 119 | "security": [] 120 | } 121 | -------------------------------------------------------------------------------- /test/fixtures/extensions_error6.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Extensions Error 6", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Elias Meire", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3002" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/user": { 43 | "get": { 44 | "description": "Return a user.", 45 | "responses": { 46 | "202": { 47 | "description": "A user.", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "$ref": "#/components/schemas/user" 52 | } 53 | } 54 | }, 55 | "links": { 56 | "group": { 57 | "$ref": "#/components/links/Group" 58 | }, 59 | "group2": { 60 | "$ref": "#/components/links/Group2" 61 | } 62 | } 63 | } 64 | } 65 | } 66 | }, 67 | "/group/{groupId}": { 68 | "get": { 69 | "description": "Return a group.", 70 | "operationId": "getGroupById", 71 | "parameters": [ 72 | { 73 | "name": "groupId", 74 | "in": "path", 75 | "schema": { 76 | "type": "string" 77 | }, 78 | "required": true 79 | } 80 | ], 81 | "responses": { 82 | "202": { 83 | "description": "A group.", 84 | "content": { 85 | "application/json": { 86 | "schema": { 87 | "$ref": "#/components/schemas/group" 88 | } 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | }, 96 | "components": { 97 | "schemas": { 98 | "user": { 99 | "type": "object", 100 | "description": "A user represents a natural person", 101 | "properties": { 102 | "name": { 103 | "type": "string", 104 | "description": "The legal name of a user" 105 | }, 106 | "groupId": { 107 | "type": "string" 108 | } 109 | } 110 | }, 111 | "group": { 112 | "type": "object", 113 | "description": "A group represents a group of people", 114 | "properties": { 115 | "id": { 116 | "type": "string" 117 | }, 118 | "name": { 119 | "type": "string", 120 | "description": "The name of a group" 121 | } 122 | } 123 | } 124 | }, 125 | "links": { 126 | "Group": { 127 | "operationId": "getGroupById", 128 | "parameters": { 129 | "groupId": "$response.body.groupId" 130 | }, 131 | "description": "Link from User to Group" 132 | }, 133 | "Group2": { 134 | "operationId": "getGroupById", 135 | "x-graphql-field-name": "group", 136 | "parameters": { 137 | "groupId": "$response.body.groupId" 138 | }, 139 | "description": "Link from User to Group2" 140 | } 141 | } 142 | }, 143 | "security": [] 144 | } 145 | -------------------------------------------------------------------------------- /test/fixtures/extensions_error7.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Extensions Error 7", 5 | "description": "An API to test converting Open API Specs 3.0 to GraphQL", 6 | "version": "1.0.0", 7 | "termsOfService": "http://example.com/terms/", 8 | "contact": { 9 | "name": "Elias Meire", 10 | "url": "http://www.example.com/support" 11 | }, 12 | "license": { 13 | "name": "Apache 2.0", 14 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 15 | } 16 | }, 17 | "externalDocs": { 18 | "url": "http://example.com/docs", 19 | "description": "Some more natural language description." 20 | }, 21 | "tags": [ 22 | { 23 | "name": "test", 24 | "description": "Indicates this API is for testing" 25 | } 26 | ], 27 | "servers": [ 28 | { 29 | "url": "http://localhost:{port}/{basePath}", 30 | "description": "The location of the local test server.", 31 | "variables": { 32 | "port": { 33 | "default": "3002" 34 | }, 35 | "basePath": { 36 | "default": "api" 37 | } 38 | } 39 | } 40 | ], 41 | "paths": { 42 | "/user": { 43 | "get": { 44 | "description": "Return a user.", 45 | "responses": { 46 | "202": { 47 | "description": "A user.", 48 | "content": { 49 | "application/json": { 50 | "schema": { 51 | "$ref": "#/components/schemas/user" 52 | } 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | "components": { 61 | "schemas": { 62 | "user": { 63 | "type": "object", 64 | "description": "A user represents a natural person", 65 | "properties": { 66 | "name": { 67 | "type": "string", 68 | "description": "The legal name of a user" 69 | }, 70 | "status": { 71 | "type": "string", 72 | "enum": ["pending", "active"], 73 | "x-graphql-enum-mapping": { 74 | "pending": "CONFLICT", 75 | "active": "CONFLICT" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | }, 82 | "security": [] 83 | } 84 | -------------------------------------------------------------------------------- /test/government_social_work.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { graphql, parse, validate } from 'graphql' 9 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 10 | 11 | const openAPIToGraphQL = require('../lib/index') 12 | const Oas3Tools = require('../lib/oas_3_tools') 13 | 14 | /** 15 | * Set up the schema first 16 | */ 17 | const oas = require('./fixtures/government_social_work.json') 18 | 19 | let createdSchema 20 | beforeAll(() => { 21 | return openAPIToGraphQL 22 | .createGraphQLSchema(oas) 23 | .then(({ schema, report }) => { 24 | createdSchema = schema 25 | }) 26 | }) 27 | 28 | test('All query endpoints present', () => { 29 | let oasGetCount = 0 30 | for (let path in oas.paths) { 31 | for (let method in oas.paths[path]) { 32 | if (method === 'get') oasGetCount++ 33 | } 34 | } 35 | const gqlTypes = Object.keys(createdSchema._typeMap.Query.getFields()).length 36 | expect(gqlTypes).toEqual(oasGetCount) 37 | }) 38 | 39 | test('All mutation endpoints present', () => { 40 | let oasMutCount = 0 41 | for (let path in oas.paths) { 42 | for (let method in oas.paths[path]) { 43 | if (Oas3Tools.isHttpMethod(method) && method !== 'get') oasMutCount++ 44 | } 45 | } 46 | const gqlTypes = Object.keys( 47 | createdSchema._typeMap.Mutation.getFields() 48 | ).length 49 | expect(gqlTypes).toEqual(oasMutCount) 50 | }) 51 | 52 | test('Get resource', () => { 53 | const query = `{ 54 | assessmentTypes ( 55 | contentType: "" 56 | acceptLanguage: "" 57 | userAgent:"" 58 | apiVersion:"1.1.0" 59 | offset: "40" 60 | limit: "test" 61 | ) { 62 | data { 63 | assessmentTypeId 64 | } 65 | } 66 | }` 67 | const ast = parse(query) 68 | const errors = validate(createdSchema, ast) 69 | expect(errors).toEqual([]) 70 | }) 71 | -------------------------------------------------------------------------------- /test/httprequest.ts: -------------------------------------------------------------------------------- 1 | import { request, Agent, setGlobalDispatcher } from 'undici' 2 | import querystring from 'qs' 3 | 4 | setGlobalDispatcher( 5 | new Agent({ 6 | keepAliveMaxTimeout: 10 7 | }) 8 | ) 9 | 10 | async function httpRequest(opts, context) { 11 | try { 12 | let url = opts.url 13 | const qs = querystring.stringify(opts.qs) 14 | if (qs) { 15 | url += '?' + qs 16 | } 17 | const res = await request(url, { 18 | method: opts.method.toUpperCase(), 19 | headers: opts.headers, 20 | body: opts.body 21 | }) 22 | 23 | res.body.setEncoding('utf8') 24 | let body = '' 25 | for await (let chunk of res.body) { 26 | body += chunk 27 | } 28 | return { 29 | statusCode: res.statusCode, 30 | headers: res.headers, 31 | body 32 | } 33 | } catch (err) { 34 | console.log(err) 35 | throw err 36 | } 37 | } 38 | 39 | export { httpRequest } 40 | -------------------------------------------------------------------------------- /test/ibm_language_translator.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 9 | 10 | import * as openAPIToGraphQL from '../lib/index' 11 | 12 | /** 13 | * Set up the schema first 14 | */ 15 | const oas = require('./fixtures/ibm_language_translator.json') 16 | 17 | let createdSchema 18 | beforeAll(() => { 19 | return openAPIToGraphQL 20 | .createGraphQLSchema(oas) 21 | .then(({ schema, report }) => { 22 | createdSchema = schema 23 | }) 24 | }) 25 | 26 | test('All IBM Language Translator query endpoints present', () => { 27 | let oasGetCount = 0 28 | for (let path in oas.paths) { 29 | for (let method in oas.paths[path]) { 30 | if (method === 'get') oasGetCount++ 31 | } 32 | } 33 | const gqlTypes = Object.keys( 34 | createdSchema._typeMap.Query.getFields().viewerAnyAuth.type.getFields() 35 | ).length 36 | 37 | expect(gqlTypes).toEqual(oasGetCount) 38 | }) 39 | -------------------------------------------------------------------------------- /test/instagram.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 9 | 10 | import * as openAPIToGraphQL from '../lib/index' 11 | 12 | /** 13 | * Set up the schema first 14 | */ 15 | const oas = require('./fixtures/instagram.json') 16 | 17 | let createdSchema 18 | beforeAll(() => { 19 | return openAPIToGraphQL 20 | .createGraphQLSchema(oas) 21 | .then(({ schema, report }) => { 22 | createdSchema = schema 23 | }) 24 | }) 25 | 26 | test('All Instagram query endpoints present', () => { 27 | let oasGetCount = 0 28 | for (let path in oas.paths) { 29 | for (let method in oas.paths[path]) { 30 | if (method === 'get') oasGetCount++ 31 | } 32 | } 33 | const gqlTypes = Object.keys( 34 | createdSchema._typeMap.Query.getFields().viewerAnyAuth.type.getFields() 35 | ).length 36 | expect(gqlTypes).toEqual(oasGetCount) 37 | }) 38 | -------------------------------------------------------------------------------- /test/oas_3_tools.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 9 | const { 10 | GraphQLSchema, 11 | GraphQLObjectType, 12 | GraphQLString, 13 | graphql 14 | } = require('graphql') 15 | 16 | import * as Oas3Tools from '../lib/oas_3_tools' 17 | import { PathItemObject } from '../lib/types/oas3' 18 | 19 | test('Applying sanitize multiple times does not change outcome', () => { 20 | const str = 'this Super*annoying-string()' 21 | const once = Oas3Tools.sanitize(str, Oas3Tools.CaseStyle.PascalCase) 22 | const twice = Oas3Tools.sanitize(once, Oas3Tools.CaseStyle.PascalCase) 23 | expect(twice).toEqual(once) 24 | }) 25 | 26 | test('Sanitize object keys', () => { 27 | const obj = { 28 | a_key: { 29 | 'b&**key': 'test !!' 30 | } 31 | } 32 | const clean = Oas3Tools.sanitizeObjectKeys(obj) 33 | expect(clean).toEqual({ 34 | aKey: { 35 | bKey: 'test !!' 36 | } 37 | }) 38 | }) 39 | 40 | test('Sanitize object keys including array', () => { 41 | const obj = { 42 | a_key: { 43 | 'b&**key': 'test !!', 44 | 'asf blah': [{ 'a)(a': 'test2' }] 45 | } 46 | } 47 | const clean = Oas3Tools.sanitizeObjectKeys(obj) 48 | expect(clean).toEqual({ 49 | aKey: { 50 | bKey: 'test !!', 51 | asfBlah: [ 52 | { 53 | aA: 'test2' 54 | } 55 | ] 56 | } 57 | }) 58 | }) 59 | 60 | test('Sanitize object keys when given an array', () => { 61 | const obj = [ 62 | { 63 | 'a)(a': { 64 | b_2: 'test' 65 | } 66 | } 67 | ] 68 | const clean = Oas3Tools.sanitizeObjectKeys(obj) 69 | expect(clean).toEqual([ 70 | { 71 | aA: { 72 | b2: 'test' 73 | } 74 | } 75 | ]) 76 | }) 77 | 78 | const mapping = { 79 | productId: 'product-id', 80 | productName: 'product-name', 81 | productTag: 'product-tag' 82 | } 83 | 84 | test('Desanitize object keys', () => { 85 | const obj = { 86 | productId: '123', 87 | info: { 88 | productName: 'Soccer' 89 | } 90 | } 91 | const raw = Oas3Tools.desanitizeObjectKeys(obj, mapping) 92 | expect(raw).toEqual({ 93 | 'product-id': '123', 94 | info: { 95 | 'product-name': 'Soccer' 96 | } 97 | }) 98 | }) 99 | 100 | test('Desanitize object keys including array', () => { 101 | const obj = { 102 | productId: { 103 | info: [{ productName: 'test1' }, { productTag: 'test2' }] 104 | } 105 | } 106 | const clean = Oas3Tools.desanitizeObjectKeys(obj, mapping) 107 | expect(clean).toEqual({ 108 | 'product-id': { 109 | info: [{ 'product-name': 'test1' }, { 'product-tag': 'test2' }] 110 | } 111 | }) 112 | }) 113 | 114 | test('Desanitize object keys when given an array', () => { 115 | const obj = [ 116 | { 117 | productName: { 118 | productTag: 'test' 119 | } 120 | } 121 | ] 122 | const clean = Oas3Tools.desanitizeObjectKeys(obj, mapping) 123 | expect(clean).toEqual([ 124 | { 125 | 'product-name': { 126 | 'product-tag': 'test' 127 | } 128 | } 129 | ]) 130 | }) 131 | 132 | test('Desanitize object keys with null value', () => { 133 | const obj = { 134 | productId: null 135 | } 136 | const raw = Oas3Tools.desanitizeObjectKeys(obj, mapping) 137 | expect(raw).toEqual({ 138 | 'product-id': null 139 | }) 140 | }) 141 | 142 | test('Properly treat null values during sanitization', () => { 143 | const schema = new GraphQLSchema({ 144 | query: new GraphQLObjectType({ 145 | name: 'Query', 146 | fields: { 147 | User: { 148 | name: 'name', 149 | type: new GraphQLObjectType({ 150 | name: 'user', 151 | fields: { 152 | name: { 153 | type: GraphQLString 154 | } 155 | } 156 | }), 157 | resolve: (root, args, context) => { 158 | const data = { 159 | name: null 160 | } 161 | return Oas3Tools.sanitizeObjectKeys(data) 162 | } 163 | } 164 | } 165 | }) 166 | }) 167 | 168 | const query = `{ 169 | User { 170 | name 171 | } 172 | }` 173 | 174 | graphql({ schema, source: query }).then((result) => { 175 | expect(result).toEqual({ 176 | data: { 177 | User: { 178 | name: null 179 | } 180 | } 181 | }) 182 | }) 183 | }) 184 | 185 | test('Handle encoded JSON pointer references', () => { 186 | const oas = { 187 | openapi: '3.0.0', 188 | info: { 189 | title: 'test', 190 | version: '0.0.1' 191 | }, 192 | paths: { 193 | '/users': getPathItemObject('all'), 194 | '/users/{id}': getPathItemObject('one') 195 | } 196 | } 197 | 198 | expect(Oas3Tools.resolveRef('/openapi', oas)).toBe('3.0.0') 199 | expect(Oas3Tools.resolveRef('/paths/~1users/description', oas)).toBe('all') 200 | expect(Oas3Tools.resolveRef('#/paths/~1users/description', oas)).toBe('all') 201 | expect( 202 | Oas3Tools.resolveRef('#/paths/~1users~1%7bid%7d/description', oas) 203 | ).toBe('one') 204 | 205 | function getPathItemObject(description): PathItemObject { 206 | return { 207 | description, 208 | get: {}, 209 | put: {}, 210 | post: {}, 211 | delete: {}, 212 | options: {}, 213 | head: {}, 214 | patch: {}, 215 | trace: {} 216 | } 217 | } 218 | }) 219 | -------------------------------------------------------------------------------- /test/stripe.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 9 | 10 | import * as openAPIToGraphQL from '../lib/index' 11 | 12 | /** 13 | * Set up the schema first 14 | */ 15 | const oas = require('./fixtures/stripe.json') 16 | 17 | let createdSchema 18 | beforeAll(() => { 19 | return openAPIToGraphQL 20 | .createGraphQLSchema(oas) 21 | .then(({ schema, report }) => { 22 | createdSchema = schema 23 | }) 24 | }) 25 | 26 | test('All Stripe query endpoints present', () => { 27 | let oasGetCount = 0 28 | for (let path in oas.paths) { 29 | for (let method in oas.paths[path]) { 30 | if (method === 'get') oasGetCount++ 31 | } 32 | } 33 | const gqlTypes = Object.keys( 34 | createdSchema._typeMap.Query.getFields().viewerAnyAuth.type.getFields() 35 | ).length 36 | 37 | expect(gqlTypes).toEqual(oasGetCount) 38 | }) 39 | -------------------------------------------------------------------------------- /test/weather_underground.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017,2018. All Rights Reserved. 2 | // Node module: openapi-to-graphql 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict' 7 | 8 | import { afterAll, beforeAll, expect, test } from '@jest/globals' 9 | 10 | import * as openAPIToGraphQL from '../lib/index' 11 | 12 | /** 13 | * Set up the schema first 14 | */ 15 | const oas = require('./fixtures/weather_underground.json') 16 | 17 | let createdSchema 18 | beforeAll(() => { 19 | return openAPIToGraphQL 20 | .createGraphQLSchema(oas) 21 | .then(({ schema, report }) => { 22 | createdSchema = schema 23 | }) 24 | }) 25 | 26 | test('All Weather Underground query endpoints present', () => { 27 | let oasGetCount = 0 28 | for (let path in oas.paths) { 29 | for (let method in oas.paths[path]) { 30 | if (method === 'get') oasGetCount++ 31 | } 32 | } 33 | const gqlTypes = Object.keys(createdSchema._typeMap.Query.getFields()).length 34 | expect(gqlTypes).toEqual(oasGetCount) 35 | }) 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "lib", 6 | "sourceMap": true, 7 | "skipLibCheck": true, 8 | "lib": ["es2017", "esnext.asynciterable"], 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "types": ["jest"] 12 | }, 13 | "files": ["./node_modules/@types/node/index.d.ts"], 14 | "include": ["src/**/*.ts"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | --------------------------------------------------------------------------------