ardatan / graphql-mesh

🕸️ GraphQL Federation Framework for any API services such as REST, OpenAPI, Swagger, SOAP, gRPC and more...
https://the-guild.dev/graphql/mesh
MIT License
3.27k stars 339 forks source link

[Feature request] Better discriminator handling in union types #5201

Open tsirlucas opened 1 year ago

tsirlucas commented 1 year ago

Is your feature request related to a problem? Please describe.

Typescript codegen does not apply open api discriminator as string constant in related objects.

For example, in the following open api file

openapi: 3.0.0
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
paths:
  /pets/{id}:
    get:
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: string
      responses:
        200:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'

components:
  schemas:
    Pet:
      oneOf:
        - $ref: '#/components/schemas/Cat'
        - $ref: '#/components/schemas/Dog'
      discriminator:
        propertyName: petType
        mapping:
          Dog: '#/components/schemas/Dog'
          Cat: '#/components/schemas/Cat'
    Cat:
      type: object
      properties:
        petType:
          type: string
        cat_exclusive:
          type: string
    Dog:
      type: object
      properties:
        petType:
          type: string
        dog_exclusive:
          type: string

Output is

schema {
  query: Query
}

directive @oneOf on OBJECT | INTERFACE

directive @discriminator(field: String) on INTERFACE | UNION

directive @globalOptions(sourceName: String, endpoint: String, operationHeaders: ObjMap, queryStringOptions: ObjMap, queryParams: ObjMap) on OBJECT

directive @httpOperation(path: String, operationSpecificHeaders: ObjMap, httpMethod: HTTPMethod, isBinary: Boolean, requestBaseBody: ObjMap, queryParamArgMap: ObjMap, queryStringOptionsByParam: ObjMap) on FIELD_DEFINITION

type Query @globalOptions(sourceName: "Pet") {
  pets_by_id(id: String!): Pet @httpOperation(path: "/pets/{args.id}", operationSpecificHeaders: "{\\"accept\\":\\"application/json\\"}", httpMethod: GET)
}

union Pet @discriminator(field: "petType") = Cat | Dog

type Cat {
  petType: String
  cat_exclusive: String
}

type Dog {
  petType: String
  dog_exclusive: String
}

scalar ObjMap

enum HTTPMethod {
  GET
  HEAD
  POST
  PUT
  DELETE
  CONNECT
  OPTIONS
  TRACE
  PATCH
}

Which is correct, but then, when generating types, the discriminator is completely ignored

export type Pet = Cat | Dog;

export type Cat = {
  petType?: Maybe<Scalars['String']>;
  cat_exclusive?: Maybe<Scalars['String']>;
};

export type Dog = {
  petType?: Maybe<Scalars['String']>;
  dog_exclusive?: Maybe<Scalars['String']>;
};

This way we lost the ability to narrow Pet down to Cat or Dog using its petType property.

One alternative that fixes the issue is to declare the properties as constants like the following:

openapi: 3.0.0
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
paths:
  /pets/{id}:
    get:
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: string
      responses:
        200:
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'

components:
  schemas:
    Pet:
      oneOf:
        - $ref: '#/components/schemas/Cat'
        - $ref: '#/components/schemas/Dog'
      discriminator:
        propertyName: petType
        mapping:
          Dog: '#/components/schemas/Dog'
          Cat: '#/components/schemas/Cat'
    Cat:
      type: object
      properties:
        petType:
          type: string
          enum: ['Cat']
        cat_exclusive:
          type: string
    Dog:
      type: object
      properties:
        petType:
          type: string
          enum: ['Dog']
        dog_exclusive:
          type: string

Then, we get something like the following in schema

type Cat {
  petType: Cat_const
  cat_exclusive: String
}

enum Cat_const @typescript(type: "\"Cat\"") @example(value: "\"Cat\"") {
  Cat @enum(value: "\"Cat\"")
}

And types are correct

export type Pet = Cat | Dog;

export type Cat = {
  petType?: Maybe<Cat_const>;
  cat_exclusive?: Maybe<Scalars['String']>;
};

export type Cat_const =
  | 'Cat';

The issue with that approach is that we will now have conflicting constants in our unions in graphql.

Describe the solution you'd like

I would like one of the following solutions

Describe alternatives you've considered

We currently use the enum solution, but then we do some post-processing in the schemas and replace all constants with strings so we dont face the problem with conflicting types. Thats not a nice solution tho as we are manipulating generated code and if we want to use constants at some point, we wont be able to.

vanbujm commented 1 week ago

I'm facing the same problem. Happy to submit a fix if someone could point me to where codgen happens. 😅