Open douglasg14b opened 2 weeks ago
Honestly, just giving library consumers the ability to pass in an env or client config themselves to define the baseUrl would be enough to get rolling. Is this possible without a custom client?
Similar to what we see here: https://github.com/ferdikoomen/openapi-typescript-codegen/wiki/Client-instances
Or if there is a straight forward way to wrap the client and set it's global config at runtime?
@douglasg14b are you able to show roughly what kind of API you're hoping to use?
We also make heavy use in my team of the "name" option, now with asClass, to generate client classes for each of our APIs.
It's very useful because we can new up and inject client instances into our BFF, and also pass around services polymorphically.
It hasn't been entirely clear to me what the deprecation notice for "name" is about, since at face value that's the only way to generate a instantiable client for injection or connect to 2 instances of the same API within one application - mutating the global OpenApi object to configure the API is a step backwards in that regard
@Nick-Lucas We will want to handle those use cases for sure. The main issue with name is it's not obvious when to use it/what it does. It's a completely different way of generating clients with its own set of problems. And if you have two clients, they're 99% identical. There will be a cleaner approach to this, it just hasn't been prioritised
@Nick-Lucas We will want to handle those use cases for sure. The main issue with name is it's not obvious when to use it/what it does. It's a completely different way of generating clients with its own set of problems. And if you have two clients, they're 99% identical. There will be a cleaner approach to this, it just hasn't been prioritised
Understood! Well if I can help to figure this out I'll be happy to
Right now setting name: 'MyClient', services: { asClass: true }
is working fine as before, though if you don't enable asClass it's bugged right now and imports a non-existent DefaultService for the class. That probably deserves its own bug ticket though
Pretty sure someone reported it already, there might be an issue open. This illustrates perfectly the challenge of maintaining such a large API surface. I wanted to bring that feature in, but adding it to every configuration would take way longer and I know I want to rework "named" clients, so I didn't bother spending way more time on adding it to them
are you able to show roughly what kind of API you're hoping to use?
yeah!
Essentially my goal is an API client that's namespaced, the user can pass in options once, and can reference an instantiated version of.
// Somewhere
export const someApiClient = createSomeApiClient({ baseUrl: '' });
// elsewear:
import someApiClient from '@/something/client';`
...
await someApiClient.feature.endpoint();
And it by type safe throughout.
import * as services from './services.gen';
import type { CamelCase } from 'type-fest';
type RemoveServiceSuffix<S extends string | number | symbol> = S extends `${infer Prefix}Service` ? Prefix : S;
type ApiClient<T extends object> = {
[Service in keyof T as CamelCase<RemoveServiceSuffix<Service>>]: T[Service];
};
type ApiClientOptions = {
baseUrl: string;
};
/** This creates a client SDK from the generated services, adding them as properties to the client and automatically filling in the provided baseUrl */
const createApiClient = <T extends object>(services: T, options: ApiClientOptions): ApiClient<T> => {
const apiClient = {};
for (const serviceName in services) {
const serviceClass = (services as any)[serviceName];
const serviceClient: { [methodName: string]: any } = {};
for (const methodName of Object.getOwnPropertyNames(serviceClass)) {
if (methodName !== 'constructor' && typeof (serviceClass as any)[methodName] === 'function') {
serviceClient[methodName] = (args) => {
return (serviceClass as any)[methodName]({ path: args?.path, body: args?.body, ...options });
};
}
}
// Convert PascalCase to camelCase for the service name, remove "Service" from name
const camelCaseServiceName = serviceName.charAt(0).toLowerCase() + serviceName.slice(1).replace(/Service$/, '');
apiClient[camelCaseServiceName] = serviceClient;
}
return apiClient as ApiClient<T>;
};
export type CreateClientOptions = {
baseUrl: string;
};
export function createClient({ baseUrl }: CreateClientOptions) {
return createApiClient(services, { baseUrl });
}
This is of course, a bit of a hack given all the any
casting, however it works since we separate the type definition from the actual runtime client object. anyways.
Now that I've done this, there are a few things this lib could do to make such extensibility easier:
index.ts
(Which is generated so I can't export this client from it)A bundled up "client" that can be instantiated would do essentially what this does
Bonus:
@douglasg14b Using the new fetch client, I was able to create a sort of my own client that uses either "Got" or "Ky"
import {createClient as createFetchClient} from '@hey-api/client-fetch'
export async function createClient(
options: Parameters<typeof createFetchClient>[0] & {httpClient: Got | KyInstance},
) {
const {httpClient} = options
const client = createFetchClient({
...options,
fetch: createFetch(httpClient),
})
return client
}
export function createFetch(client: Got | KyInstance) {
return async (request: Request) => {
const result = await client[request.method?.toLowerCase() as 'get'](request.url.toString(), {
throwHttpErrors: false,
headers: normalizeHeaders(request.headers) as unknown as Headers,
...(request.body && {json: await request.json()}),
}).json()
const response = new Response(JSON.stringify(result), {})
return response
}
}
this is just a POC but it seems to be working, there are probably some edge case, but the idea is that you can use the new fetch client and just provide your own fetch implementation
I can of course pass a custom base URL to my fetch implementation
That's pretty cool @omridevk. Still think we need a separate client for those?
nope, this solution seems to be good enough at least for our needs.
also created this PR: https://github.com/7nohe/openapi-react-query-codegen/pull/125/files so we can use this version with react-query
I'm actually unclear how what's being described here is different from setting name
and asClass
. You already get a named client class with all the services attached as class instances, and can pass all your options in to the constructor.
Description
This is similar to https://github.com/hey-api/openapi-ts/issues/425
We need to be able to pass envs to our custom client, and even wrap the API services within the client itself so they are accessible as properties.
Are there docs or examples outlining the best ways to go about this? The deprecation docs are unclear on how
clients
replaces or supplements those options. or how one goes about constructing their own custom client 🤔The surface area is awkward in that the API calls are not pure, they leak the abstraction back up to the caller who has to specify a
body
property on theiroptions
. It would be nice to expose a more pure client SDK.Thanks!