OpenAPI & Générateur de Service
Script Node optionnel fourni séparément du client HTTP de base. Utilisé uniquement lors de la génération de types TypeScript et de fichiers de service à partir de JSON OpenAPI/Swagger.
Fonctionnalité optionnelle : non nécessaire pour l'utilisation de Core
Les utilisateurs de @byeolnaerim/typed-rx-http, /next ou /rsocket n'ont pas besoin d'exécuter ce script et n'ont pas besoin d'installer openapi-typescript dans leur projet.
import {
connectToSwaggerEventStream,
generateSwaggerFromHttp,
generateSwaggerFromFile,
} from "@byeolnaerim/typed-rx-http/auto-node-script/openapi";Isolation de la dépendance openapi-typescript
La génération de types OpenAPI nécessite le CLI openapi-typescript, mais le package ne l'inclut pas dans les dépendances générales. Les devDependencies.typescript de typed-rx-http maintiennent la version TypeScript 6.0.3, et n'utilisent un environnement npx temporaire que dans le script auto node OpenAPI.
À l'étape de génération OpenAPI de base, openapi-typescript et [email protected] sont exécutés ensemble, donc cela ne modifie ni n'utilise la version TypeScript/openapi-typescript installée dans le projet de l'utilisateur.
npx -y -p openapi-typescript@latest -p [email protected] openapi-typescript swagger.json --output ./src/handler/service/@types/ApiTypes.d.tsSi nécessaire, vous pouvez fixer la commande complète à 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",
});Ou vous pouvez simplement changer la version du package qui compose la commande de base.
generateSwaggerFromFile({
inputFile: "./swagger.json",
openApiTypescriptPackage: "[email protected]",
openApiTypescriptTypescriptPackage: "[email protected]",
});D'abord : préparez le fichier projet auquel commonServiceFile fera référence.
Avant que commonServiceFile n'apparaisse dans l'exemple de générateur, vous devez comprendre ce fichier. commonServiceFile n'est pas une option pour créer un fichier, mais le chemin du fichier possédé par le projet où le service généré importera des fonctions HTTP communes. Le nom du fichier peut être librement choisi, comme rxjsHttpService.ts, commonService.ts, etc.
Code complet de la forme minimale
Le fichier ci-dessous crée une fois createHttpClient dans le projet et exporte callApi et callApiStream pour réutilisation par le service généré. Si le cache ou l'authentification de session n'est pas nécessaire, vous pouvez commencer avec cette structure.
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;Code complet incluant le cache de session/CSR/SSR
Pour que le service généré utilise callApiClientCache ou callApiServerCache, ou pour gérer communément les en-têtes par requête et l'authentification de session dans Next.js, étendez-le à la forme complète suivante.
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;
};
});
};Mode d'exécution
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",
});Fichiers générés
Les paramètres par défaut génèrent les fichiers ci-dessous.
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.tsgénère non seulement l'énumération du schéma OpenAPI, mais aussi un tableau constant en lecture seule pour les énumérations de paramètres de requête, de chemin, d'en-tête et de cookie, et traite également les items.enum des paramètres de requête en tableau.
Option : connecter Swagger backend et commonService du projet
À partir d'ici, voici un exemple de connexion du générateur à un projet réel. WebFlux/webflux-fe-dev-assistant est une méthode pour fournir Swagger, mais ce n'est pas une dépendance obligatoire.
Préparation de la documentation Swagger
Swagger peut être Springdoc, un autre outil OpenAPI ou un fichier écrit manuellement. Si vous utilisez un point de terminaison fonctionnel WebFlux, webflux-fe-dev-assistantVous pouvez également utiliser cette méthode pour fournir la documentation Swagger.
Exemple de génération en spécifiant des options dans le projet
Vous pouvez spécifier l'endpoint backend, le swagger.json à enregistrer, le chemin de sortie des types/services et commonServiceFile selon la structure de votre projet.
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",
});Dans un environnement où la documentation ne peut pas être reçue par HTTP, vous pouvez utiliser generateSwaggerFromFileavec les mêmes options de sortie.
commonServiceFile est le chemin d'importation cible
commonServiceFileest le chemin du fichier possédé par le projet où le service généré importera callApi, callApiStream et le wrapper de cache. Ce n'est pas une option pour que le générateur crée ou écrase ce fichier.
ApiBusinessService.ts
tsimport { callApi } from "../rxjsHttpService";
export const workplacesSearch = ({ params }) => {
return callApi<SearchWorkplacesResponse>({
url: "/api/business/workplaces/search",
method: "get",
queryString: params,
});
};Exemple de résultats de génération dans le projet
src/handler/service/
├─ rxjsHttpService.ts # 사용자 소유, 생성하지 않음
├─ apiUnionArrays.ts
├─ auto/
│ ├─ ApiBusinessService.ts
│ ├─ ApiOrdersService.ts
│ └─ Oauth2LoginService.ts
└─ @types/
├─ ApiTypes.d.ts
└─ auto/
├─ ApiBusinessTypes.ts
├─ ApiOrdersTypes.ts
└─ Oauth2LoginTypes.tsLe fichier public désigné par commonServiceFile, comme rxjsHttpService.ts, est géré directement par le projet. Les résultats sous auto et @types/auto sont régénérés lorsque la documentation change.
Règles de nom de fichier et de fonction
Le nom du fichier de service est déterminé par les deux premiers segments de l'URL, et le nom de la fonction est déterminé par les segments suivants à partir du troisième. Les variables de chemin sont incluses dans le nom de la fonction sous la forme 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 })
Arguments d'appel de la fonction de génération
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 },
});La clé du paramètre de requête du service généré est params, la variable de chemin est path, et le corps de la requête est body. À l'intérieur de la fonction de génération, ils sont convertis respectivement en ServiceArguments pour queryString, pathVariable et body.