Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[client-fetch] Strongly typed Error response ? #1762

Open
warrenbuckley opened this issue Feb 28, 2025 · 3 comments
Open

[client-fetch] Strongly typed Error response ? #1762

warrenbuckley opened this issue Feb 28, 2025 · 3 comments
Labels
bug 🔥 Something isn't working

Comments

@warrenbuckley
Copy link

Description

When using an OpenAPI spec that describes the shape of the error response for a 400 bad request, I am unable to use that as the type to get completions and make the TS compiler happy when trying to use the properties from that error response model from my API.

Hopefully I have left enough info below to help diagnose or reproduce the issue.

Cheers,
Warren 😄

Reproducible example or configuration

My usage with unable to get error back as a nice type

// TODO: error will come back as this shape of JSON
/*
{
  "type": "Error",
  "title": "Unauthorized",
  "status": 400,
  "detail": "Only the original user who locked this content can unlock it or a super user with the unlocking permission"
}
*/
const { data, error } = await ContentLockService.lockOverview() as { data: LockOverviewResponse, error: LockOverviewError };;
if (error) {
  this._notificationCtx?.peek('danger', {
    data: {
      headline: error.title, // error still seems to be `unknown`
      message: error.detail
    }
  })
...
}

Generated Client Files from Schema

schemas.gen.ts

// This file is auto-generated by @hey-api/openapi-ts

export const ContentLockOverviewSchema = {
    required: ['items', 'totalResults'],
    type: 'object',
    properties: {
        totalResults: {
            type: 'integer',
            format: 'int32'
        },
        items: {
            type: 'array',
            items: {
                oneOf: [
                    {
                        '$ref': '#/components/schemas/ContentLockOverviewItem'
                    }
                ]
            }
        }
    },
    additionalProperties: false
} as const;

export const ContentLockOverviewItemSchema = {
    required: ['checkedOutBy', 'checkedOutByKey', 'contentType', 'key', 'lastEdited', 'nodeName'],
    type: 'object',
    properties: {
        key: {
            type: 'string',
            format: 'uuid'
        },
        nodeName: {
            type: 'string'
        },
        contentType: {
            type: 'string'
        },
        checkedOutBy: {
            type: 'string'
        },
        checkedOutByKey: {
            type: 'string',
            format: 'uuid'
        },
        lastEdited: {
            type: 'string',
            format: 'date-time'
        }
    },
    additionalProperties: false
} as const;

export const ContentLockStatusSchema = {
    required: ['isLocked', 'lockedByKey', 'lockedBySelf'],
    type: 'object',
    properties: {
        isLocked: {
            type: 'boolean'
        },
        lockedByKey: {
            type: 'string',
            format: 'uuid'
        },
        lockedByName: {
            type: 'string',
            nullable: true
        },
        lockedBySelf: {
            type: 'boolean'
        }
    },
    additionalProperties: false
} as const;

export const EventMessageTypeModelSchema = {
    enum: ['Default', 'Info', 'Error', 'Success', 'Warning'],
    type: 'string'
} as const;

export const NotificationHeaderModelSchema = {
    required: ['category', 'message', 'type'],
    type: 'object',
    properties: {
        message: {
            type: 'string'
        },
        category: {
            type: 'string'
        },
        type: {
            '$ref': '#/components/schemas/EventMessageTypeModel'
        }
    },
    additionalProperties: false
} as const;

export const ProblemDetailsSchema = {
    type: 'object',
    properties: {
        type: {
            type: 'string',
            nullable: true
        },
        title: {
            type: 'string',
            nullable: true
        },
        status: {
            type: 'integer',
            format: 'int32',
            nullable: true
        },
        detail: {
            type: 'string',
            nullable: true
        },
        instance: {
            type: 'string',
            nullable: true
        }
    },
    additionalProperties: {}
} as const;

services.gen.ts

// This file is auto-generated by @hey-api/openapi-ts

import { createClient, createConfig, type Options } from '@hey-api/client-fetch';
import type { BulkUnlockData, LockContentData, LockContentError, LockContentResponse, LockOverviewError, LockOverviewResponse, StatusData, StatusError, StatusResponse, UnlockContentData, UnlockContentError, UnlockContentResponse } from './types.gen';

export const client = createClient(createConfig());

export class ContentLockService {
    public static bulkUnlock<ThrowOnError extends boolean = false>(options?: Options<BulkUnlockData, ThrowOnError>) {
        return (options?.client ?? client).post<void, unknown, ThrowOnError>({
            ...options,
            url: '/umbraco/contentlock/api/v1/ContentGuard/BulkUnlock'
        });
    }
    
    public static lockContent<ThrowOnError extends boolean = false>(options: Options<LockContentData, ThrowOnError>) {
        return (options?.client ?? client).get<LockContentResponse, LockContentError, ThrowOnError>({
            ...options,
            url: '/umbraco/contentlock/api/v1/ContentGuard/Lock/{key}'
        });
    }
    
    public static lockOverview<ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) {
        return (options?.client ?? client).get<LockOverviewResponse, LockOverviewError, ThrowOnError>({
            ...options,
            url: '/umbraco/contentlock/api/v1/ContentGuard/LockOverview'
        });
    }
    
    public static status<ThrowOnError extends boolean = false>(options: Options<StatusData, ThrowOnError>) {
        return (options?.client ?? client).get<StatusResponse, StatusError, ThrowOnError>({
            ...options,
            url: '/umbraco/contentlock/api/v1/ContentGuard/Status/{key}'
        });
    }
    
    public static unlockContent<ThrowOnError extends boolean = false>(options: Options<UnlockContentData, ThrowOnError>) {
        return (options?.client ?? client).get<UnlockContentResponse, UnlockContentError, ThrowOnError>({
            ...options,
            url: '/umbraco/contentlock/api/v1/ContentGuard/Unlock/{key}'
        });
    }
    
}

types.gen.ts

// This file is auto-generated by @hey-api/openapi-ts

export type ContentLockOverview = {
    totalResults: number;
    items: Array<(ContentLockOverviewItem)>;
};

export type ContentLockOverviewItem = {
    key: string;
    nodeName: string;
    contentType: string;
    checkedOutBy: string;
    checkedOutByKey: string;
    lastEdited: string;
};

export type ContentLockStatus = {
    isLocked: boolean;
    lockedByKey: string;
    lockedByName?: (string) | null;
    lockedBySelf: boolean;
};

export type EventMessageTypeModel = 'Default' | 'Info' | 'Error' | 'Success' | 'Warning';

export type NotificationHeaderModel = {
    message: string;
    category: string;
    type: EventMessageTypeModel;
};

export type ProblemDetails = {
    type?: (string) | null;
    title?: (string) | null;
    status?: (number) | null;
    detail?: (string) | null;
    instance?: (string) | null;
    [key: string]: (unknown | string | number) | undefined;
};

export type BulkUnlockData = {
    body?: Array<(string)>;
};

export type LockContentData = {
    path: {
        key: string;
    };
};

export type LockContentResponse = (unknown);

export type LockContentError = (unknown);

export type LockOverviewResponse = ((ContentLockOverview));

export type LockOverviewError = ((ProblemDetails) | unknown);

export type StatusData = {
    path: {
        key: string;
    };
};

export type StatusResponse = ((ContentLockStatus));

export type StatusError = (unknown);

export type UnlockContentData = {
    path: {
        key: string;
    };
};

export type UnlockContentResponse = (unknown);

export type UnlockContentError = (unknown);

OpenAPI specification (optional)

{
  "openapi": "3.0.1",
  "info": {
    "title": "Content Lock Backoffice API",
    "contact": {
      "name": "Warren Buckley",
      "url": "https://hackmakedo.com",
      "email": "[email protected]"
    },
    "version": "1.0"
  },
  "paths": {
    "/umbraco/contentlock/api/v1/ContentGuard/BulkUnlock": {
      "post": {
        "tags": [
          "Content Lock"
        ],
        "operationId": "BulkUnlock",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "text/json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            },
            "application/*+json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "uuid"
                }
              }
            }
          }
        },
        "responses": {
          "400": {
            "description": "Bad Request",
            "headers": {
              "Umb-Notifications": {
                "description": "The list of notifications produced during the request.",
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NotificationHeaderModel"
                  },
                  "nullable": true
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ProblemDetails"
                    }
                  ]
                }
              },
              "text/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ProblemDetails"
                    }
                  ]
                }
              },
              "text/plain": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ProblemDetails"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The resource is protected and requires an authentication token"
          }
        },
        "security": [
          {
            "Backoffice User": [ ]
          }
        ]
      }
    },
    "/umbraco/contentlock/api/v1/ContentGuard/Lock/{key}": {
      "get": {
        "tags": [
          "Content Lock"
        ],
        "operationId": "LockContent",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The resource is protected and requires an authentication token"
          }
        },
        "security": [
          {
            "Backoffice User": [ ]
          }
        ]
      }
    },
    "/umbraco/contentlock/api/v1/ContentGuard/LockOverview": {
      "get": {
        "tags": [
          "Content Lock"
        ],
        "operationId": "LockOverview",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ContentLockOverview"
                    }
                  ]
                }
              },
              "text/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ContentLockOverview"
                    }
                  ]
                }
              },
              "text/plain": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ContentLockOverview"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad Request",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ProblemDetails"
                    }
                  ]
                }
              },
              "text/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ProblemDetails"
                    }
                  ]
                }
              },
              "text/plain": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ProblemDetails"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The resource is protected and requires an authentication token"
          }
        },
        "security": [
          {
            "Backoffice User": [ ]
          }
        ]
      }
    },
    "/umbraco/contentlock/api/v1/ContentGuard/Status/{key}": {
      "get": {
        "tags": [
          "Content Lock"
        ],
        "operationId": "Status",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ContentLockStatus"
                    }
                  ]
                }
              },
              "text/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ContentLockStatus"
                    }
                  ]
                }
              },
              "text/plain": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ContentLockStatus"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "The resource is protected and requires an authentication token"
          }
        },
        "security": [
          {
            "Backoffice User": [ ]
          }
        ]
      }
    },
    "/umbraco/contentlock/api/v1/ContentGuard/Unlock/{key}": {
      "get": {
        "tags": [
          "Content Lock"
        ],
        "operationId": "UnlockContent",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "The resource is protected and requires an authentication token"
          }
        },
        "security": [
          {
            "Backoffice User": [ ]
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "ContentLockOverview": {
        "required": [
          "items",
          "totalResults"
        ],
        "type": "object",
        "properties": {
          "totalResults": {
            "type": "integer",
            "format": "int32"
          },
          "items": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/ContentLockOverviewItem"
                }
              ]
            }
          }
        },
        "additionalProperties": false
      },
      "ContentLockOverviewItem": {
        "required": [
          "checkedOutBy",
          "checkedOutByKey",
          "contentType",
          "key",
          "lastEdited",
          "nodeName"
        ],
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "format": "uuid"
          },
          "nodeName": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "checkedOutBy": {
            "type": "string"
          },
          "checkedOutByKey": {
            "type": "string",
            "format": "uuid"
          },
          "lastEdited": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false
      },
      "ContentLockStatus": {
        "required": [
          "isLocked",
          "lockedByKey",
          "lockedBySelf"
        ],
        "type": "object",
        "properties": {
          "isLocked": {
            "type": "boolean"
          },
          "lockedByKey": {
            "type": "string",
            "format": "uuid"
          },
          "lockedByName": {
            "type": "string",
            "nullable": true
          },
          "lockedBySelf": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "EventMessageTypeModel": {
        "enum": [
          "Default",
          "Info",
          "Error",
          "Success",
          "Warning"
        ],
        "type": "string"
      },
      "NotificationHeaderModel": {
        "required": [
          "category",
          "message",
          "type"
        ],
        "type": "object",
        "properties": {
          "message": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "type": {
            "$ref": "#/components/schemas/EventMessageTypeModel"
          }
        },
        "additionalProperties": false
      },
      "ProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "nullable": true
          },
          "title": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": { }
      }
    },
    "securitySchemes": {
      "Backoffice User": {
        "type": "oauth2",
        "description": "Umbraco Authentication",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "/umbraco/management/api/v1/security/back-office/authorize",
            "tokenUrl": "/umbraco/management/api/v1/security/back-office/token",
            "scopes": { }
          }
        }
      }
    }
  }
}

System information (optional)

General

  • Windows 11
  • Node: v20.12.1
  • NPM: 10.5.0

package.json Versions

"@hey-api/client-fetch": "^0.4.2",
"@hey-api/openapi-ts": "^0.53.11",
@warrenbuckley warrenbuckley added the bug 🔥 Something isn't working label Feb 28, 2025
@mrlubos
Copy link
Member

mrlubos commented Feb 28, 2025

Heyo @warrenbuckley, nice to see you again! I'll get back to this package hopefully next week once the platform is live as I see a lot of issues piling up 😔

@warrenbuckley
Copy link
Author

Heya 👋
OK I will keep an eye on this issue for updates. Not sure if my C# code that outputs the OpenAPI Schema is generated correctly or if its something with Hey-API.

Thanks again.
Warren 😄

@warrenbuckley
Copy link
Author

I see I am quite behind on versions when looking at npm outdated

Package                Current   Wanted  Latest  Location                            Depended by
@hey-api/client-fetch    0.4.4    0.4.4   0.8.2  node_modules/@hey-api/client-fetch  Client
@hey-api/openapi-ts    0.53.12  0.53.12  0.64.7  node_modules/@hey-api/openapi-ts    Client
vite                    5.4.14   5.4.14   6.2.0  node_modules/vite                   Client

Will update to latest and try again and see how it goes...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🔥 Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants