graphql-engine/rfcs/openapi-to-hasura-single-action.md
Daniele Cammareri 16561d2c9a feat(console): add import single endpoint from openAPI
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6970
GitOrigin-RevId: 14bbca9efbda6fa71f6b00ab5d45062d71abd794
2022-11-29 19:09:53 +00:00

11 KiB

Introduction

We want to translate OpenAPI specifications into GraqhQL to facilitate the creation of actions. The translation process presents several issues; you can explore them in this paper: https://arxiv.org/abs/1809.08319.

I find it interesting to point out the following aspects of the translation process:

  • deduplication of input type names: in GraphQL, it is convenient to have shared input types across operations, but in an OpenAPI specification, you could define the same type multiple times across operations. The proposed approach is to deduplicate input types by making a deep comparison between them.
  • sanitization of names: In GraphQL, names must follow the following regular expression n /[_A-Za-z][_0-9A-Za-z]*/, whereas in OpenAPI this is not the case. The proposed approach uses a name sanitization function, which is important to consider when implementing the call translation process. In our case, the configuration of the actions

The authors of the paper also tested their implementation (the one we used) against many OpenAPI specifications, and they found those results:

  • 97% of the OpenAPI specifications can be translated into GraphQL
  • 27.1% of the OpenAPI specifications can be translated into GraphQL when strict mode is enabled (without any warning)

The errors/warings they found are the following:

  • Name sanitization errors: mostly due to the translation of boolean enum values (true and false are not valid GraphQL enum values)
  • Invalid OAS: the input OpenAPI specification could not be successfully validated.`
  • Missing ref: the input OpenAPI specification contains a reference to a schema that could not be found.

In addition, I will report the most common problems that we found when translating OpenAPI operations to Hasura Actions:

  • Wrong response type: the response type in the OpenAPI specification is missing or is wrong. An additional problem is when the success response is defined as default instead of 200.
  • Missing operations: operations that cannot be translated are missing in the translation output.
  • Failing OpenAPI specification: a few number of OpenAPI specifications are not valid, and they cannot be translated at all.
  • Forms: some operations require the input to be passed as a form, which is not supported by Hasura Actions.
  • OpenAPI 2.0 support: the OpenAPI 2.0 specifications must be translated to OpenAPI 3.0 before being translated to GraphQL. This sometimes leads to errors in the translation process.
  • Array query Params: when array inputs must be serialized as query parameters, there isn't a kriti function that let you to do that

In this document, we explore our translation proposal directly into the console. We want to create a tool for importing a single action. The input is the GraphQL specification and the operation's name to translate, and the result is the complete pre-compilation of the action creation form.

Tools selection

We need several operations to generate Hasura actions from OpenAPI specifications.

  • parsing of the OpenAPI specification
  • translating the OpenAPI specification into GraphQL
  • manipulating the GraphQL schema.

For the first two points, the choice seems inevitable to fall on the openapi-to-graphql library https://github.com/IBM/openapi-to-graphql, created by the authors of the article cited above.

An alternative might be swagger-to-graphql https://github.com/yarax/swagger-to-graphql. However, this project is much less widely used https://npmtrends.com/openapi-to-graphql-vs-swagger-to-graphql and is less robust than the other https://npmcompare.com/compare/openapi-to-graphql,swagger-to-graphql

For the third point, the choice fell on microfiber (https://github.com/anvilco/graphql-introspection-tools), a tool for manipulating GraphQL schemas. There does not seem to be any other tool that performs this task https://graphql.org/code/#javascript-tools

The bundle size (minified + gzipped) for the two tools is 6.2kb for microfiber https://bundlephobia.com/package/microfiber@1.3.1 and 331kb for openapi-to-graphql https://bundlephobia.com/package/openapi-to-graphql@2.6.3. We could consider checking if tree-shaking is available or maintaining a lighter version ourselves for the latter.

Openapi to Hasura action

To achieve complete action, we need to:

  • Generate the GraphQL types:
    • for the action
    • for the types used by the action
  • Generate the action configuration:
    • the payload of the request
    • the http method
    • the path parameters of the URL
    • the parameters to pass in the query string\
    • the response transformation

As an example, we will simulate the generation of the updatePet action of the https://petstore3.swagger.io/api/v3/openapi.json specification.

Generate Graphql types

The process of generating Graphql types is the following:

  1. Translate the OpenAPI spec into a GraphQL schema using openapi-to-graphql
  2. Removing from the schema all the operations but the selected one using microfiber print the resulting schema
  3. dividing action definition (everything withing type Query {} or type Mutation {} ) from type definition (anything else)
  4. We should note that we cannot translate all operations in the openapi specification to GraphQL. In this case, the openapi-to-graphql librate will exclude these operations, which will not be among those selectable for translation.

The schema generated by openapi-to-graphql, after point 2, is the following:

scalar BigInt

type Category {
  id: BigInt
  name: String
}

input CategoryInput {
  id: BigInt
  name: String
}

type Mutation {
  updatePet(petInput: PetInput!): Pet
}

type Pet {
  category: Category
  id: BigInt
  name: String!
  photoUrls: [String]!
  status: Status
  tags: [Tag]
}

input PetInput {
  category: CategoryInput
  id: BigInt
  name: String!
  photoUrls: [String]!
  status: Status
  tags: [TagInput]
}

type Query

enum Status {
  available
  pending
  sold
}

type Tag {
  id: BigInt
  name: String
}

input TagInput {
  id: BigInt
  name: String
}

then, since updatePet is a POST operation, the action is defined in the Mutation type. We can extract the action definition of the updatePet action by removing all the types except Mutation.

type Mutation {
  updatePet(petInput: PetInput!): Pet
}

the remaining are the types used by the action. We can extract them by removing all the types except Query and Mutation.

Deal with translation errors

While openapi-to-graphql sometimes fails, we could try a best-effort approach to fix those errors in the original specification and then translate it again. The proposal approach is to make some midifications to the OpenAPI specification before translating it to GraphQL. We can have two possible outcomes

  • The action is modified in such a way that it can be translated to GraphQL. In this case, we can generate the action.
  • The action is discarded so the other actions can be translated.

The errors type and corresponding solutions are the following:

Boolean enum types

GraphQL does not support boolean enum types. Since this is most likely done to restrict the values to only true or false (e.g., in some API the deleted response can be only true), we can replace the enum type with a boolean type, and translate the action successfully.

Empty types

GraphQL does not support empty types, which sometimes are present in the OpenAPI specification. We can change the empty response to another type (e.g. string) or create a fake non-empty object type with a nullable fake field and translate the action successfully.

Generate Hasura action configuration

We can derive all the Hasura action configurations by the openapi-to-graphql metadata for the selected operation in a straightforward way.

  • the request and response transformations are discussed in the section below.
  • the http method is got directly from the openapi-to-graphql metadata
  • the base URL of the API is got from the server section of the openapi specification
  • the operation URL is the path of the operation in the metadata
  • the path parameters of the URL is the list of arguments that are marked as path parameters in the metadata
  • the parameters to pass in the query string: is the list of arguments that are marked as query parameters in the metadata

Request and response transormation

If there were a one-to-one relationship between REST and GraphQL types, there would be no need for any request or response transformation. But, as is stated in the IBM article, to generate GrahpQL types, some names could be sanitized and hence be different from the REST ones. This could lead to broken Hasura action calls.

To solve this problem, a layer of request and response transformation is needed to perform the translation of types between the REST and GraphQL worlds.

While in the article this is is done in the generated resolvers, in Hasura action kriti templates must be generated by recursively traversing the GraphQL schema and the OpenAPI specification and used as request and response transformation.

This is an example of PetInput request and response kriti transformation. We artificially renamed the name field to in OpenAPI spefication to $name to simulate the incompatibility.

{
  "id": {{$body.input.petInput?.id}},
  "$name": {{$body.input.petInput?.name}},
  "category": {
    "id": {{$body.input.petInput?.category?.id}},
    "name": {{$body.input.petInput?.category?.name}}
  },
  "photoUrls": {{$body.input.petInput?.photoUrls}},
  "tags": {{if inverse(empty($body.input.petInput?.tags))}} {{ range _, tags := $body.input.petInput?.tags}} {
    "id": {{tags?.id}},
    "name": {{tags?.name}}
    } {{end}} {{else}} null {{end}},
  "status": {{$body.input.petInput?.status}}
}

and the opposite:

{
  "id": {{$body?.id}},
  "name": {{$body?.$name}},
  "category": {
    "id": {{$body?.category?.id}},
    "name": {{$body?.category?.name}}
  },
  "photoUrls": {{$body?.photoUrls}},
  "tags": {{if inverse(empty($body?.tags))}} {{ range _, tags := $body?.tags}} {
    "id": {{tags?.id}},
    "name": {{tags?.name}}
    } {{end}} {{else}} null {{end}},
  "status": {{$body?.status}}
}

note how objects and arrays are handled.

Authentication

OpenAPI specification allows to specify authentication methods in the security section and should be managed case by case. In the paper, IBM folks manage authentication through GraphQL Viewers. In our case, for the first release, we will enable the flag Forward client headers to webhook, which will probably be enough for most cases (e.g., users can pass the JWT token in the headers).