diff --git a/Gemfile b/Gemfile index 6c1575e98..b01e364df 100644 --- a/Gemfile +++ b/Gemfile @@ -33,10 +33,13 @@ gem 'puma', '~> 6' gem 'rack-cors' gem 'rails', '~> 7.1' gem 'roo' +gem 'rswag-api' +gem 'rswag-ui' gem 'scout_apm' gem 'sentry-rails' group :development, :test do + gem 'appmap' gem 'bullet' gem 'dotenv-rails' gem 'factory_bot_rails' @@ -46,6 +49,7 @@ group :development, :test do gem 'rspec' gem 'rspec_junit_formatter' gem 'rspec-rails' + gem 'rswag-specs' gem 'rubocop', require: false gem 'rubocop-graphql', require: false gem 'rubocop-rails', require: false diff --git a/Gemfile.lock b/Gemfile.lock index e8a9d5f40..b271abded 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -98,6 +98,11 @@ GEM administrate-field-active_storage (1.0.1) administrate (>= 0.2.2) rails (>= 7.0) + appmap (1.1.0) + activesupport + method_source + rack + reverse_markdown ast (2.4.2) aws-eventstream (1.2.0) aws-partitions (1.718.0) @@ -218,6 +223,8 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (2.6.3) + json-schema (4.3.0) + addressable (>= 2.8) jwt (2.2.3) kaminari (1.2.2) activesupport (>= 4.1.0) @@ -358,6 +365,8 @@ GEM regexp_parser (2.7.0) reline (0.5.9) io-console (~> 0.5) + reverse_markdown (2.1.1) + nokogiri rexml (3.3.0) strscan roo (2.10.1) @@ -386,6 +395,17 @@ GEM rspec-support (3.12.0) rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) + rswag-api (2.13.0) + activesupport (>= 3.1, < 7.2) + railties (>= 3.1, < 7.2) + rswag-specs (2.13.0) + activesupport (>= 3.1, < 7.2) + json-schema (>= 2.2, < 5.0) + railties (>= 3.1, < 7.2) + rspec-core (>= 2.14) + rswag-ui (2.13.0) + actionpack (>= 3.1, < 7.2) + railties (>= 3.1, < 7.2) rubocop (1.47.0) json (~> 2.3) parallel (~> 1.10) @@ -503,6 +523,7 @@ PLATFORMS DEPENDENCIES administrate (~> 0.20.1) administrate-field-active_storage + appmap aws-sdk-s3 bootsnap bullet @@ -539,6 +560,9 @@ DEPENDENCIES rspec rspec-rails rspec_junit_formatter + rswag-api + rswag-specs + rswag-ui rubocop rubocop-graphql rubocop-rails diff --git a/appmap.yml b/appmap.yml new file mode 100644 index 000000000..5a15fa833 --- /dev/null +++ b/appmap.yml @@ -0,0 +1,11 @@ +--- +name: editor-api +language: ruby +appmap_dir: tmp/appmap +packages: +- path: app +- path: lib + exclude: + - controllers/admin + - dashboard + - graphql diff --git a/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb new file mode 100644 index 000000000..c4462b277 --- /dev/null +++ b/config/initializers/rswag_api.rb @@ -0,0 +1,14 @@ +Rswag::Api.configure do |c| + + # Specify a root folder where Swagger JSON files are located + # This is used by the Swagger middleware to serve requests for API descriptions + # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure + # that it's configured to generate files in the same folder + c.openapi_root = Rails.root.to_s + '/swagger' + + # Inject a lambda function to alter the returned Swagger prior to serialization + # The function will have access to the rack env for the current request + # For example, you could leverage this to dynamically assign the "host" property + # + #c.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] } +end diff --git a/config/initializers/rswag_ui.rb b/config/initializers/rswag_ui.rb new file mode 100644 index 000000000..1d6151b6e --- /dev/null +++ b/config/initializers/rswag_ui.rb @@ -0,0 +1,16 @@ +Rswag::Ui.configure do |c| + + # List the Swagger endpoints that you want to be documented through the + # swagger-ui. The first parameter is the path (absolute or relative to the UI + # host) to the corresponding endpoint and the second is a title that will be + # displayed in the document selector. + # NOTE: If you're using rspec-api to expose Swagger files + # (under openapi_root) as JSON or YAML endpoints, then the list below should + # correspond to the relative paths for those endpoints. + + c.swagger_endpoint '/api-docs/v1/swagger.yaml', 'API V1 Docs' + + # Add Basic Auth in case your API is private + # c.basic_auth_enabled = true + # c.basic_auth_credentials 'username', 'password' +end diff --git a/config/routes.rb b/config/routes.rb index 33829a18d..8d3c2b7f1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,8 @@ # rubocop:disable Metrics/BlockLength Rails.application.routes.draw do + mount Rswag::Api::Engine => '/api-docs' + mount Rswag::Ui::Engine => '/api-docs' namespace :admin do mount GoodJob::Engine => 'good_job' resources :components diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb new file mode 100644 index 000000000..aee34ff40 --- /dev/null +++ b/spec/requests/api/projects_spec.rb @@ -0,0 +1,100 @@ +require 'swagger_helper' + +RSpec.describe 'api/projects', type: :request do + + path '/api/projects' do + + get('list projects') do + response(200, 'successful') do + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + post('create project') do + response(200, 'successful') do + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end + + path '/api/projects/{id}' do + # You'll want to customize the parameter types... + parameter name: 'id', in: :path, type: :string, description: 'id' + + get('show project') do + response(200, 'successful') do + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + patch('update project') do + response(200, 'successful') do + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + put('update project') do + response(200, 'successful') do + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + + delete('delete project') do + response(200, 'successful') do + let(:id) { '123' } + + after do |example| + example.metadata[:response][:content] = { + 'application/json' => { + example: JSON.parse(response.body, symbolize_names: true) + } + } + end + run_test! + end + end + end +end diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb new file mode 100644 index 000000000..14c121497 --- /dev/null +++ b/spec/swagger_helper.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.configure do |config| + # Specify a root folder where Swagger JSON files are generated + # NOTE: If you're using the rswag-api to serve API descriptions, you'll need + # to ensure that it's configured to serve Swagger from the same folder + config.openapi_root = Rails.root.join('swagger').to_s + + # Define one or more Swagger documents and provide global metadata for each one + # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will + # be generated at the provided relative path under openapi_root + # By default, the operations defined in spec files are added to the first + # document below. You can override this behavior by adding a openapi_spec tag to the + # the root example_group in your specs, e.g. describe '...', openapi_spec: 'v2/swagger.json' + config.openapi_specs = { + 'v1/swagger.yaml' => { + openapi: '3.0.1', + info: { + title: 'API V1', + version: 'v1' + }, + paths: {}, + servers: [ + { + url: 'https://{defaulthost}/', + variables: { + defaultHost: { + default: 'www.example.com' + } + } + } + ] + } + } + + # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'. + # The openapi_specs configuration option has the filename including format in + # the key, this may want to be changed to avoid putting yaml in json files. + # Defaults to json. Accepts ':json' and ':yaml'. + config.openapi_format = :yaml +end diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml new file mode 100644 index 000000000..21c07e256 --- /dev/null +++ b/swagger/v1/swagger.yaml @@ -0,0 +1,1251 @@ +# This document can be generated with the following command: +# appmap openapi +# +# Some helpful options: +# --output-file output file name +# --openapi-title title field of the OpenAPI document +# --openapi-version version field of the OpenAPI document +# +# For more info, run: +# appmap openapi --help +# +# Visit our docs: https://appmap.io/docs/openapi.html +# +openapi: 3.0.1 +info: + title: My project + version: v1 +paths: + /: + get: + responses: + '200': + content: + text/html: {} + description: OK + /admin: + get: + responses: + '200': + content: + text/html: {} + description: OK + '302': + content: + text/html: {} + description: Found + /admin/schools: + get: + responses: + '200': + content: + text/html: {} + description: OK + /api-docs/favicon-32x32.png: + get: + responses: + '304': + content: {} + description: Not Modified + /api-docs/index.html: + get: + responses: + '304': + content: {} + description: Not Modified + /api-docs/swagger-ui-bundle.js: + get: + responses: + '304': + content: {} + description: Not Modified + /api-docs/swagger-ui-standalone-preset.js: + get: + responses: + '304': + content: {} + description: Not Modified + /api-docs/swagger-ui.css: + get: + responses: + '304': + content: {} + description: Not Modified + /api-docs/v1/swagger.yaml: + get: + responses: + '200': + content: + text/yaml: {} + description: OK + /api/lessons: + get: + responses: + '200': + content: + application/json: {} + description: OK + security: + - api_key: [] + parameters: + - name: include_archived + in: query + schema: + type: string + - name: school_class_id + in: query + schema: + type: string + post: + responses: + '201': + content: + application/json: {} + description: Created + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + text/html: {} + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + lesson: + type: object + properties: + name: + type: string + /api/lessons/{id}: + delete: + responses: + '204': + content: {} + description: No Content + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + parameters: + - name: undo + in: query + schema: + type: string + get: + responses: + '200': + content: + application/json: {} + description: OK + '403': + content: + text/html: {} + description: Forbidden + '404': + content: + text/html: {} + description: Not Found + security: + - api_key: [] + put: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + lesson: + type: object + properties: + name: + type: string + /api/lessons/{id}/copy: + post: + responses: + '201': + content: + application/json: {} + description: Created + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + lesson: + type: object + properties: + name: + type: string + visibility: + type: string + /api/projects: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + security: + - api_key: [] + parameters: + - name: page + in: query + schema: + type: string + post: + responses: + '201': + content: + application/json: {} + description: Created + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + text/html: {} + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + project: + type: object + properties: + components: + type: array + items: + type: object + properties: + content: + type: string + extension: + type: string + name: + type: string + name: + type: string + /api/projects/{id}: + delete: + responses: + '200': + content: + text/html: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + get: + responses: + '200': + content: + application/json: {} + description: OK + '403': + content: + text/html: {} + description: Forbidden + '404': + content: + text/html: {} + description: Not Found + security: + - api_key: [] + parameters: + - name: locale + in: query + schema: + type: string + put: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + project: + type: object + properties: + components: + type: array + items: + type: string + /api/projects/{project_id}/images: + post: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '404': + content: + text/html: {} + description: Not Found + security: + - api_key: [] + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + images: + type: array + items: + type: object + /api/projects/{project_id}/remix: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '404': + content: + text/html: {} + description: Not Found + security: + - api_key: [] + post: + responses: + '200': + content: + application/json: {} + description: OK + '400': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Bad Request + '401': + content: + text/html: {} + description: Unauthorized + '404': + content: + text/html: {} + description: Not Found + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + project: + type: object + properties: + components: + type: array + items: + type: string + identifier: + type: string + name: + type: string + /api/school: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '404': + content: + text/html: {} + description: Not Found + security: + - api_key: [] + /api/schools: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + security: + - api_key: [] + post: + responses: + '201': + content: + application/json: {} + description: Created + '400': + content: + text/html: {} + description: Bad Request + '401': + content: + text/html: {} + description: Unauthorized + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: object + properties: + address_line_1: + type: array + items: + type: string + country_code: + type: array + items: + type: string + creator_agree_authority: + type: array + items: + type: string + creator_agree_terms_and_conditions: + type: array + items: + type: string + municipality: + type: array + items: + type: string + name: + type: array + items: + type: string + website: + type: array + items: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + school: + type: object + properties: + address_line_1: + type: string + country_code: + type: string + creator_agree_authority: + type: string + creator_agree_terms_and_conditions: + type: string + municipality: + type: string + name: + type: string + website: + type: string + /api/schools/{id}: + delete: + responses: + '204': + content: {} + description: No Content + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '404': + content: + text/html: {} + description: Not Found + security: + - api_key: [] + put: + responses: + '200': + content: + application/json: {} + description: OK + '400': + content: + text/html: {} + description: Bad Request + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '404': + content: + text/html: {} + description: Not Found + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + school: + type: object + properties: + name: + type: string + /api/schools/{school_id}/classes: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + security: + - api_key: [] + post: + responses: + '201': + content: + application/json: {} + description: Created + '400': + content: + text/html: {} + description: Bad Request + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + school_class: + type: object + properties: + description: + type: string + name: + type: string + /api/schools/{school_id}/classes/{class_id}/members: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + post: + responses: + '201': + content: + application/json: {} + description: Created + '400': + content: + text/html: {} + description: Bad Request + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + class_member: + type: object + properties: + student_id: + type: string + /api/schools/{school_id}/classes/{class_id}/members/{id}: + delete: + responses: + '204': + content: {} + description: No Content + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + /api/schools/{school_id}/classes/{id}: + delete: + responses: + '204': + content: {} + description: No Content + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '404': + content: + text/html: {} + description: Not Found + security: + - api_key: [] + put: + responses: + '200': + content: + application/json: {} + description: OK + '400': + content: + text/html: {} + description: Bad Request + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + school_class: + type: object + properties: + name: + type: string + /api/schools/{school_id}/owners: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + post: + responses: + '201': + content: + text/html: {} + description: Created + '400': + content: + text/html: {} + description: Bad Request + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + school_owner: + type: object + properties: + email_address: + type: string + /api/schools/{school_id}/owners/{id}: + delete: + responses: + '204': + content: {} + description: No Content + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + /api/schools/{school_id}/students: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + post: + responses: + '204': + content: {} + description: No Content + '400': + content: + text/html: {} + description: Bad Request + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + school_student: + type: object + properties: + name: + type: string + password: + type: string + username: + type: string + /api/schools/{school_id}/students/batch: + post: + responses: + '204': + content: {} + description: No Content + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: object + /api/schools/{school_id}/students/{id}: + delete: + responses: + '204': + content: {} + description: No Content + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + put: + responses: + '204': + content: {} + description: No Content + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + school_student: + type: object + properties: + name: + type: string + password: + type: string + username: + type: string + /api/schools/{school_id}/teachers: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + post: + responses: + '201': + content: + text/html: {} + description: Created + '400': + content: + text/html: {} + description: Bad Request + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: string + description: Unprocessable Entity + security: + - api_key: [] + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + school_teacher: + type: object + properties: + email_address: + type: string + /api/schools/{school_id}/teachers/{id}: + delete: + responses: + '204': + content: {} + description: No Content + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + text/html: {} + description: Forbidden + security: + - api_key: [] + /api/teacher_invitations/{token}: + get: + responses: + '200': + content: + application/json: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + application/json: + schema: + type: object + properties: + error: + type: string + text/html: {} + description: Forbidden + '404': + content: + text/html: {} + description: Not Found + security: + - api_key: [] + /api/teacher_invitations/{token}/accept: + put: + responses: + '200': + content: + text/html: {} + description: OK + '401': + content: + text/html: {} + description: Unauthorized + '403': + content: + application/json: + schema: + type: object + properties: + error: + type: string + text/html: {} + description: Forbidden + '404': + content: + text/html: {} + description: Not Found + '422': + content: + application/json: + schema: + type: object + properties: + error: + type: object + properties: + base: + type: array + items: + type: string + description: Unprocessable Entity + security: + - api_key: [] + /graphql: + post: + responses: + '200': + content: + application/json: + schema: + type: object + description: OK + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + operationName: + type: string + query: + type: string + variables: + type: object + properties: + key: + type: string +components: {} +