Open SenseiMarv opened 1 month ago
Hi @SenseiMarv, let me put a vote label on this feature. As for your use case, what are you currently doing/using? I'm also curious why are you requesting this feature vs using another tool that presumably handles HATEOAS already? Thanks!
Hey there, chipping in with an extra add-on to this request 😄 Adding on to that, supporting that those URLs are "partially" filled out, yet still a template would be amazing too.
E.g. having an OpenAPI like this (sticking loosely with the bank account example):
/accounts/{accountId}/transactions:
parameters:
- name: accountId
in: path
required: true
schema:
type: string
get:
parameters:
- name: limit
in: query
schema:
type: integer
- name: offset
in: query
schema:
type: integer
- name: sender
in: query
schema:
type: string
description: Filters transactions by sender including this string
we could get a response from our backend linking to this endpoint that looks like this:
{
"_links": {
"next": {
"href": "https://myhost/accounts/123456/transactions?limit=10&offset=30{&sender}"
"templated": true
},
...
},
...
}
The idea here would then to still take the sender
for the query from the options
, but ignore the path.accountId
, query.limit
and query.offset
as they are already set in the provided link/URL.
Probably would require making all of those options of the request optional though as one can never know what parameters are already pre-filled (at least not at code-generation-time).
Thanks for the lightning fast response! We are currently trying out Hey API to see if we can use API client generation for our OpenAPI files. We use our OpenAPI files as a single source of truth and do not want to write/generate them using contracts with tools like ts-rest or Effect.
Hey API is particularly interesting as it provides native TanStack Query integration and soon Zod integration. As we may build our production software on TanStack Query in the future and already use Zod heavily for form validation, this library brings a lot to the table. At the moment we write our frontend client code ourselves, but it might be interesting to move to Codegen.
We are currently trying out Hey API with a small prototype and have run into this hurdle. It is a bit of a blocker for us as we do not want to parse our valid URLs to extract the path parameters. The only alternative would be to copy/edit the generated code to take our links - which would defeat the purpose of Codegen. We also tried another project first: openapi-ts, but didn't like the output from that. The output here is very logical, readable and easy to work with. If there are other tools you have in mind that we should use instead, we are open to recommendations :) We looked at all the solutions linked here and at openapi-zod-client, but none of them looked as promising as Hey API.
@SenseiMarv this seems like a quite large feature to implement so it might take a while unless there's an enormous interest in it. If resolving #452 would unblock you, please let me know and I can prioritise that one
@mrlubos Absolutely understandable if this takes longer to support if it is complex to implement. Luckily, the minimal workaround to enable us to still use our links is very simple :) Changing the Codegen to instead of generating this
/**
* Find pet by ID
* Returns a single pet
*/
export const getPetById = <ThrowOnError extends boolean = false>(
options: Options<GetPetByIdData, ThrowOnError>,
) => {
return (options?.client ?? client).get<
ThrowOnError,
GetPetByIdResponse,
GetPetByIdError
>({
...options,
url: '/pet/{petId}',
});
};
To allow overriding the url by placing the spread options below
/**
* Find pet by ID
* Returns a single pet
*/
export const getPetById = <ThrowOnError extends boolean = false>(
options: Options<GetPetByIdData, ThrowOnError>,
) => {
return (options?.client ?? client).get<
ThrowOnError,
GetPetByIdResponse,
GetPetByIdError
>({
url: '/pet/{petId}',
...options,
});
};
And then changing the OptionsBase
type
type OptionsBase<ThrowOnError extends boolean> = Omit<RequestOptionsBase<ThrowOnError>, 'url'> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
};
to no longer omit url
(PS: I think url should actually be set to optional here and not only the omit dropped. It will be given by default, but it could be overridden, so it should be optionally assignable):
type OptionsBase<ThrowOnError extends boolean> = RequestOptionsBase<ThrowOnError> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
};
Is enough so we can define our own url in case we have a HATEOAS link:
const { data: pet } = await getPetById({
url: '/pet/{petId}',
path: {
// random id 1-10
petId: Math.floor(Math.random() * (10 - 1 + 1) + 1),
},
});
(I've used the linked StackBlitz demo from the homepage for my example here)
This all looks pretty straightforward and no objection to these changes. So no need for #452 at all?
And can you just explain for me, how will you know which function to call with your custom URL?
@mrlubos Good to hear! Upon a second look, it seems like https://github.com/hey-api/openapi-ts/issues/452 is going into a slightly different direction of wanting access to the URLs by having Hey API export them. This is, as we already clarified, more about "overriding" them.
To stay with the Petstore example from the demo on the homepage:
Let's imagine that the response body of getPetById
changes slightly. It now follows HATEOAS and includes a link to delete the fetched pet. The response body would then look something like this:
{
// ...
"_links": {
"delete": {
"href": "https://petstore3.swagger.io/api/v3/pet/5"
}
}
}
The link provided can now be used for the delete endpoint and will delete the pet we've just fetched:
const [pet, setPet] = useState<Pet>();
const onFetchPet = async () => {
const { data: pet } = await getPetById({
path: {
// random id 1-10
petId: Math.floor(Math.random() * (10 - 1 + 1) + 1),
},
});
setPet(pet);
};
const onDeletePet = async () => {
await deletePet({
url: pet._links.delete.href,
});
setPet(undefined);
};
So, in general, you know where to use the provided links by semantics or as defined in the API docs. I hope this answers your question?
Two notes about this:
OptionsBase
type needs to be adapted for this. url
needs to be made optional as we know it will be set in the generated code, it just COULD be overridden if neccessary. This could be done either by appending Partial<Pick<RequestOptionsBase<ThrowOnError>, 'url'>>
or by changing RequestOptionsBase
to take a second generic that controls whether url
should be required or not depending on the generic "flag" and then using that optional flag in OptionsBase
. If the second example is confusing in text form, here is an example in code (using an unrelated topic) of how this could be done:type OptionalFlag = 'optional';
export type Embedded<
TItem,
Optional extends OptionalFlag | undefined = undefined,
> = Optional extends OptionalFlag
? { _embedded?: { items: TItem[] } }
: { _embedded: { items: TItem[] } };
// And then the optional flag can be used like this: Embedded<{ name: string }, 'optional'>;
path
is still required. I think this should remain so as not to break the default workflow where it is indeed required. However, the problem with this is that the delete example above cannot fully work this way, as it would still need to provide path
. I'm not sure yet what would be the best solution for this. One option would be to just provide any dummy data to satisfy the required prop, since the path data shouldn't go anywhere with the overridden url
. But this is not the nicest solution. Perhaps you have another idea. Theoretically, though, the dummy data would suffice.After thinking about it, one comment about my note 2 above: path
is actually still necessary, even when technically not using it's values. But when using the TanStack Query integration, it becomes relevant since it is being used for the queryKey
generation. The full url isn't used right now and if this stays like that, the created keys wouldn't be unique. If we pass the url
for the path though, we will still get unique queries.
Description
Our API uses Spring HATEOAS (short explanation: https://en.wikipedia.org/wiki/HATEOAS). So a typical API response body might look like this:
Note the added
_links
at the end of the response body. This contains several valid links (URLs) to other endpoints. These endpoints normally use Path Parameters, but are already fully resolved here.If we now want to use Hey API, we run into the problem that we already have a fully valid link, but there is no way to use it in the generated code. We would have to somehow parse our valid URL, extract the path parameters and then pass them to the generated functions with the
path
props. This is really cumbersome and involves introducing parsing that could fall apart with any change to the API.Is there any way to add support for HATEOAS so that we can use our links directly? A cheap solution would be to support passing of a URL to the generated functions, which as far as I can see has already been requested: https://github.com/hey-api/openapi-ts/issues/452.