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
|
// @ts-nocheck
|
||||||
// This file is auto-generated by @hey-api/openapi-ts
|
// This file is auto-generated by @hey-api/openapi-ts
|
||||||
|
|
||||||
import { type ClientOptions, type Config, createClient, createConfig } from "./client";
|
import { type ClientOptions, type Config, createClient, createConfig } from "./client";
|
||||||
import type { ClientOptions as ClientOptions2 } from "./types.gen";
|
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> = (
|
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (
|
||||||
override?: Config<ClientOptions & T>,
|
override?: Config<ClientOptions & T>,
|
||||||
) => Config<Required<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 { createMiddleware } from "@tanstack/react-start";
|
||||||
import { client } from "~/client/api-client/client.gen";
|
|
||||||
import { getRequestHeaders } from "@tanstack/react-start/server";
|
import { getRequestHeaders } from "@tanstack/react-start/server";
|
||||||
|
import {
|
||||||
|
createRequestClient,
|
||||||
|
runWithRequestClient,
|
||||||
|
} from "~/lib/request-client";
|
||||||
|
|
||||||
export const apiClientMiddleware = createMiddleware().server(async ({ next, request }) => {
|
export const apiClientMiddleware = createMiddleware().server(async ({ next, request }) => {
|
||||||
client.setConfig({
|
const client = createRequestClient({
|
||||||
baseUrl: `${new URL(request.url).origin}`,
|
baseUrl: new URL(request.url).origin,
|
||||||
headers: {
|
headers: {
|
||||||
cookie: getRequestHeaders().get("cookie") ?? "",
|
cookie: getRequestHeaders().get("cookie") ?? "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return next();
|
return runWithRequestClient(client, () => next());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ export default defineConfig({
|
||||||
"// @ts-nocheck",
|
"// @ts-nocheck",
|
||||||
"// This file is auto-generated by @hey-api/openapi-ts",
|
"// 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"],
|
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