OpenAPI & 服務生成器
與Core HTTP client分開提供的可選Node腳本。僅在從OpenAPI/Swagger JSON生成TypeScript類型和服務文件時使用。
可選功能:不需要Core使用
一般的@byeolnaerim/typed-rx-http、/next或/rsocket用戶無需執行此腳本,也不需要在專案中安裝openapi-typescript。
import {
connectToSwaggerEventStream,
generateSwaggerFromHttp,
generateSwaggerFromFile,
} from "@byeolnaerim/typed-rx-http/auto-node-script/openapi";openapi-typescript依賴隔離
生成OpenAPI類型需要openapi-typescript CLI,但該包不會放入一般依賴中。typed-rx-http本身的devDependencies.typescript保持在TypeScript 6.0.3基準,並且僅在OpenAPI自動Node腳本中使用單獨的npx臨時環境。
在基本OpenAPI生成階段,openapi-typescript和[email protected]會一起執行,因此不會更改或使用用戶專案中安裝的TypeScript/openapi-typescript版本。
npx -y -p openapi-typescript@latest -p [email protected] openapi-typescript swagger.json --output ./src/handler/service/@types/ApiTypes.d.ts如有需要,可以將整個命令固定為openApiTypescriptCommand。
generateSwaggerFromFile({
inputFile: "./swagger.json",
openApiTypescriptCommand:
"npx -y -p [email protected] -p [email protected] openapi-typescript ./swagger.json --output ./src/handler/service/@types/ApiTypes.d.ts",
});或者僅更改構成基本命令的package版本。
generateSwaggerFromFile({
inputFile: "./swagger.json",
openApiTypescriptPackage: "[email protected]",
openApiTypescriptTypescriptPackage: "[email protected]",
});首先:準備commonServiceFile所指向的專案文件
在generator範例中commonServiceFile出現之前,必須先理解此文件。commonServiceFile不是創建文件的選項,而是生成的服務導入公共HTTP函數的專案擁有文件的路徑。文件名可以自由選擇,如rxjsHttpService.ts、commonService.ts等。
最小完成型完整代碼
以下文件在專案中創建一次createHttpClient,並export以便生成的服務重用callApi和callApiStream。如果不需要快取或session auth,可以從此結構開始。
rxjsHttpService.ts
tsimport {
createHeaderStore,
createHttpClient,
type ServiceArguments,
} from "@byeolnaerim/typed-rx-http";
import type { Observable } from "rxjs";
import type { paths } from "./@types/ApiTypes";
export const headerStore = createHeaderStore({
"Content-Type": "application/json",
});
export const service = createHttpClient<paths>({
baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
headerStore,
});
export const callApi = <
R,
TPath extends keyof paths = keyof paths,
TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
return service.callApi<R, TPath, TMethod>(serviceArguments);
};
export const callApiStream = <
R,
TPath extends keyof paths = keyof paths,
TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
return service.callApiStream<R, TPath, TMethod>(serviceArguments);
};
export const uploadFile = service.uploadFile;
export const createSSEObservable = service.createSSEObservable;包含Session/CSR/SSR快取的完整代碼
如果生成的服務使用callApiClientCache或callApiServerCache,或者在Next.js中共用處理請求的header和session認證,則擴展為以下完整形式。
rxjsHttpService.ts
tsimport {
createCsrCache,
createHeaderStore,
createHttpClient,
createSessionAuth,
} from "@byeolnaerim/typed-rx-http";
import type {
CacheForService,
ServiceArguments,
} from "@byeolnaerim/typed-rx-http";
import { defer, Observable } from "rxjs";
import { setLogin } from "@ui/handler/hooks/useAccounts";
import type { paths } from "./@types/ApiTypes";
import type { CacheNames } from "./@types/CacheNames";
export const redirectToUnauthorizedOnServer401 = async () => {
const { headers } = await import("next/headers");
const { redirect } = await import("next/navigation");
const h = await headers();
const pageUrl = h.get("x-page-url") || "/";
const redirectUri = encodeURIComponent(pageUrl);
redirect(`/unauthorized?redirect_uri=${redirectUri}&logout=true`);
};
export const headerStore = createHeaderStore({
"Content-Type": "application/json",
});
const headersProvider = async () => {
if (typeof window !== "undefined") {
/**
* 라이브러리쪽 코드가
* if (headersProvider) return headersProvider();
* if (headerStore) return headerStore.get();
* 이렇게 되어있음 같이 못씀, 방향자체는 맞음. 라이브러리쪽에서 marge해버리면 csr/ssr 구분못함
*/
return headerStore.get();
}
const { cookies } = await import("next/headers");
const cookieStore = await cookies();
const cookieHeader = cookieStore
.getAll()
.map((c) => `${c.name}=${c.value}`)
.join("; ");
const authorization = cookieStore.get("Authorization")?.value;
const accessToken = cookieStore.get("accessToken")?.value;
return {
...(cookieHeader ? { Cookie: cookieHeader } : {}),
...(authorization
? {
Authorization: authorization.startsWith("Bearer ")
? authorization
: `Bearer ${authorization}`,
}
: {}),
...(!authorization && accessToken
? {
Authorization: accessToken.startsWith("Bearer ")
? accessToken
: `Bearer ${accessToken}`,
}
: {}),
};
};
export const service = createHttpClient<paths>({
baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
onServer401: redirectToUnauthorizedOnServer401,
defaultErrorMessage: () => {
return "알 수 없는 오류가 발생했습니다.";
},
headerStore,
headersProvider,
dropAuthWhenCacheControl: true,
});
const sessionAuth = createSessionAuth({
headerStore,
tokenUrl: "/api/auth/token",
refreshUrl: "/api/auth/token/refresh",
logoutUrl: "/api/auth/logout",
onLoginChange: (loggedIn) => {
if (typeof window !== "undefined") {
setLogin(loggedIn);
}
},
});
export const callApi = <
R,
TPath extends keyof paths = keyof paths,
TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
return defer(() =>
service.callApi<R, TPath, TMethod>(serviceArguments),
).pipe(sessionAuth.withSessionAuth());
};
export const callApiStream = <
R,
TPath extends keyof paths = keyof paths,
TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
): Observable<R> => {
return defer(() =>
service.callApiStream<R, TPath, TMethod>(serviceArguments),
).pipe(sessionAuth.withSessionAuth());
};
// CSR cache
const csrCache = createCsrCache<CacheNames>();
export const callApiClientCache = <
R,
TPath extends keyof paths = keyof paths,
TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
cacheForService: CacheForService<CacheNames>,
): Observable<R> => {
return csrCache.callApiCsrCache(callApi, serviceArguments, cacheForService);
};
export const removeCsrCache = csrCache.removeCsrCache;
export const callApiServerCache = <
R,
TPath extends keyof paths = keyof paths,
TMethod extends keyof paths[TPath] & string = keyof paths[TPath] & string,
>(
serviceArguments: ServiceArguments<paths, TPath, TMethod, R>,
cacheForService: CacheForService<CacheNames>,
): Observable<R> => {
if (typeof window !== "undefined") {
return callApiClientCache(serviceArguments, cacheForService);
}
return new Observable<R>((subscriber) => {
let closed = false;
import("@byeolnaerim/typed-rx-http/next")
.then(({ callApiSsrCache }) => {
if (closed) {
return;
}
const subscription = callApiSsrCache<paths, R, CacheNames>({
baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
serviceArguments: serviceArguments as ServiceArguments<
paths,
keyof paths,
keyof paths[keyof paths] & string,
R
>,
cacheForService,
headersProvider,
onServer401: redirectToUnauthorizedOnServer401,
}).subscribe(subscriber);
subscriber.add(() => {
closed = true;
subscription.unsubscribe();
});
})
.catch((error) => {
if (!closed) {
subscriber.error(error);
}
});
return () => {
closed = true;
};
});
};執行方式
connectToSwaggerEventStream({
hostname: "localhost",
port: 8788,
path: "/oauth2/for-local/get-swagger",
serviceDir: "./src/handler/service",
typesDir: "./src/handler/service/@types",
commonServiceFile: "./src/handler/service/rxjsHttpService.ts",
});生成的檔案
預設會生成以下檔案。
swagger.json
src/handler/service/@types/ApiTypes.d.ts
src/handler/service/auto/*Service.ts
src/handler/service/@types/auto/*Types.ts
src/handler/service/apiUnionArrays.tsapiUnionArrays.ts不僅生成OpenAPI schema enum,還生成query、path、header、cookie參數enum的readonly常數數組,並處理數組query參數的items.enum。
可選:將後端Swagger與專案commonService連接
從這裡開始是將generator實際連接到專案的範例。WebFlux/webflux-fe-dev-assistant僅是提供Swagger的一種方法,並不是必需的依賴。
準備Swagger文檔
Swagger 可以是 Springdoc、其他 OpenAPI 工具或手動編寫的文件。如果使用 WebFlux 功能端點,則可以 webflux-fe-dev-assistant也可以使用提供Swagger文檔的方式。
在專案中明確選項以生成的範例
可以根據專案結構明確後端endpoint、要保存的swagger.json、類型/服務輸出路徑和commonServiceFile。
generateSwagger.cjs
jsconst {
generateSwaggerFromHttp,
} = require("@byeolnaerim/typed-rx-http/auto-node-script/openapi");
generateSwaggerFromHttp({
hostname: "localhost",
port: 8123,
path: "/for-local/get-swagger",
swaggerFile: "./swagger.json",
serviceDir: "./src/handler/service",
typesDir: "./src/handler/service/@types",
typesFile: "./src/handler/service/@types/ApiTypes.d.ts",
unionArraysFile: "./src/handler/service/apiUnionArrays.ts",
commonServiceFile:
"./src/handler/service/rxjsHttpService.ts",
});在無法通過HTTP接收文檔的環境中,可以使用相同的輸出選項。 generateSwaggerFromFile可以使用。
commonServiceFile是導入目標路徑
commonServiceFile是生成的服務導入callApi、callApiStream和快取包裝器的專案擁有文件的路徑。generator不是生成或覆蓋此文件的選項。
ApiBusinessService.ts
tsimport { callApi } from "../rxjsHttpService";
export const workplacesSearch = ({ params }) => {
return callApi<SearchWorkplacesResponse>({
url: "/api/business/workplaces/search",
method: "get",
queryString: params,
});
};專案中生成結果的佈局範例
src/handler/service/
├─ rxjsHttpService.ts # 사용자 소유, 생성하지 않음
├─ apiUnionArrays.ts
├─ auto/
│ ├─ ApiBusinessService.ts
│ ├─ ApiOrdersService.ts
│ └─ Oauth2LoginService.ts
└─ @types/
├─ ApiTypes.d.ts
└─ auto/
├─ ApiBusinessTypes.ts
├─ ApiOrdersTypes.ts
└─ Oauth2LoginTypes.ts像rxjsHttpService.ts這樣指定為commonServiceFile的公共文件由專案直接管理。auto和@types/auto下的結果在文檔變更時會重新生成。
文件名和函數名規則
服務文件名由URL的前兩個段決定,函數名由第三個及以後的段決定。path變數以By + PascalCase形式包含在函數名中。
GET /api/business/workplaces/search
ApiBusinessService.ts → workplacesSearch({ params })
GET /api/business/workplaces/{id}
ApiBusinessService.ts → workplacesById({ path })
GET /api/orders/history/search
ApiOrdersService.ts → historySearch({ params })
POST /oauth2/login
Oauth2LoginService.ts → post({ body })
生成函數的調用參數
GeneratedServiceUsage.tsx
tsx// path variable
workplacesById({
path: { id: "workplace-1" },
});
// query parameter
workplacesSearch({
params: { q: "auction", size: 20 },
});
// JSON request body
post({
body: { username, password },
});生成的服務的query參數key為params,path變數為path,請求主體為body。在生成函數內部分別轉換為queryString、pathVariable、body的ServiceArguments。