refactor: patch api client to have a global client per call
This commit is contained in:
parent
9422815843
commit
0da4b45d67
6 changed files with 136 additions and 24 deletions
|
|
@ -1,19 +1,27 @@
|
|||
// @ts-nocheck
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
import { type ClientOptions, type Config, createClient, createConfig } from "./client";
|
||||
import type { ClientOptions as ClientOptions2 } from "./types.gen";
|
||||
import { getRequestClient } from "../../lib/request-client";
|
||||
|
||||
/**
|
||||
* The `createClientConfig()` function will be called on client initialization
|
||||
* and the returned object will become the client's initial configuration.
|
||||
*
|
||||
* You may want to initialize your client this way instead of calling
|
||||
* `setConfig()`. This is useful for example if you're using Next.js
|
||||
* to ensure your client always has the correct values.
|
||||
*/
|
||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
|
||||
override?: Config<ClientOptions & T>,
|
||||
) => Config<Required<ClientOptions> & T>;
|
||||
|
||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }));
|
||||
const fallbackClient = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }));
|
||||
|
||||
/**
|
||||
* Proxy client that automatically uses the per-request client from request-scoped server storage.
|
||||
* Falls back to a default client if no request context is available.
|
||||
*/
|
||||
export const client = new Proxy(fallbackClient, {
|
||||
get(target, prop, receiver) {
|
||||
try {
|
||||
const requestClient = getRequestClient();
|
||||
return Reflect.get(requestClient, prop, receiver);
|
||||
} catch {
|
||||
// No request context available, use fallback
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
57
app/lib/request-client.ts
Normal file
57
app/lib/request-client.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { Config } from "~/client/api-client/client";
|
||||
import { createClient, createConfig } from "~/client/api-client/client";
|
||||
|
||||
export type RequestClient = ReturnType<typeof createClient>;
|
||||
|
||||
type RequestClientStore = {
|
||||
getStore: () => RequestClient | undefined;
|
||||
run: <T>(client: RequestClient, fn: () => T) => T;
|
||||
};
|
||||
|
||||
type AsyncLocalStorageConstructor = new <T>() => {
|
||||
getStore: () => T | undefined;
|
||||
run: <R>(store: T, callback: () => R) => R;
|
||||
};
|
||||
|
||||
let requestClientStore: RequestClientStore | undefined;
|
||||
|
||||
const ASYNC_HOOKS_MODULE = "node:async_hooks";
|
||||
|
||||
const loadRequestClientStore = async (): Promise<RequestClientStore | undefined> => {
|
||||
if (typeof window !== "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!requestClientStore) {
|
||||
const asyncHooksModule = (await import(/* @vite-ignore */ ASYNC_HOOKS_MODULE)) as {
|
||||
AsyncLocalStorage: AsyncLocalStorageConstructor;
|
||||
};
|
||||
requestClientStore = new asyncHooksModule.AsyncLocalStorage<RequestClient>();
|
||||
}
|
||||
|
||||
return requestClientStore;
|
||||
};
|
||||
|
||||
export function getRequestClient(): RequestClient {
|
||||
const client = requestClientStore?.getStore();
|
||||
|
||||
if (!client) {
|
||||
throw new Error("No request client available");
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
export async function runWithRequestClient<T>(client: RequestClient, fn: () => T): Promise<T> {
|
||||
const store = await loadRequestClientStore();
|
||||
|
||||
if (!store) {
|
||||
return fn();
|
||||
}
|
||||
|
||||
return store.run(client, fn);
|
||||
}
|
||||
|
||||
export function createRequestClient(config: Config): RequestClient {
|
||||
return createClient(createConfig(config));
|
||||
}
|
||||
|
|
@ -1,14 +1,17 @@
|
|||
import { createMiddleware } from "@tanstack/react-start";
|
||||
import { client } from "~/client/api-client/client.gen";
|
||||
import { getRequestHeaders } from "@tanstack/react-start/server";
|
||||
import {
|
||||
createRequestClient,
|
||||
runWithRequestClient,
|
||||
} from "~/lib/request-client";
|
||||
|
||||
export const apiClientMiddleware = createMiddleware().server(async ({ next, request }) => {
|
||||
client.setConfig({
|
||||
baseUrl: `${new URL(request.url).origin}`,
|
||||
const client = createRequestClient({
|
||||
baseUrl: new URL(request.url).origin,
|
||||
headers: {
|
||||
cookie: getRequestHeaders().get("cookie") ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
return next();
|
||||
return runWithRequestClient(client, () => next());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ export default defineConfig({
|
|||
"// @ts-nocheck",
|
||||
"// This file is auto-generated by @hey-api/openapi-ts",
|
||||
],
|
||||
postProcess: ["oxfmt"],
|
||||
postProcess: [
|
||||
"oxfmt",
|
||||
{ command: "bun", args: ["scripts/patch-api-client.ts"] },
|
||||
],
|
||||
},
|
||||
plugins: [...defaultPlugins, "@tanstack/react-query", "@hey-api/client-fetch"],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
export default {
|
||||
buildDirectory: "dist",
|
||||
ssr: true,
|
||||
future: {
|
||||
v8_middleware: true,
|
||||
},
|
||||
} satisfies Config;
|
||||
50
scripts/patch-api-client.ts
Executable file
50
scripts/patch-api-client.ts
Executable file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Post-process script for openapi-ts generation.
|
||||
* Patches client.gen.ts to use request-scoped clients from server storage,
|
||||
* preventing cross-request cookie/origin leakage during SSR.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const CLIENT_GEN_PATH = resolve(process.cwd(), "app/client/api-client/client.gen.ts");
|
||||
|
||||
const PATCHED_CONTENT = `// @ts-nocheck
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
import { type ClientOptions, type Config, createClient, createConfig } from "./client";
|
||||
import type { ClientOptions as ClientOptions2 } from "./types.gen";
|
||||
import { getRequestClient } from "../../lib/request-client";
|
||||
|
||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
|
||||
override?: Config<ClientOptions & T>,
|
||||
) => Config<Required<ClientOptions> & T>;
|
||||
|
||||
const fallbackClient = createClient(createConfig<ClientOptions2>({ baseUrl: "http://localhost:4096" }));
|
||||
|
||||
/**
|
||||
* Proxy client that automatically uses the per-request client from request-scoped server storage.
|
||||
* Falls back to a default client if no request context is available.
|
||||
*/
|
||||
export const client = new Proxy(fallbackClient, {
|
||||
get(target, prop, receiver) {
|
||||
try {
|
||||
const requestClient = getRequestClient();
|
||||
return Reflect.get(requestClient, prop, receiver);
|
||||
} catch {
|
||||
// No request context available, use fallback
|
||||
return Reflect.get(target, prop, receiver);
|
||||
}
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
try {
|
||||
writeFileSync(CLIENT_GEN_PATH, PATCHED_CONTENT);
|
||||
console.log("✓ Patched client.gen.ts with request-scoped client support");
|
||||
} catch (error) {
|
||||
console.error("✗ Failed to patch client.gen.ts:", error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
Loading…
Reference in a new issue