OpenAPI & Service Generator
Optionales Node-Skript, das zusätzlich zum Core HTTP-Client bereitgestellt wird. Wird nur verwendet, um TypeScript-Typen und Dienstdateien aus OpenAPI/Swagger JSON zu generieren.
Optionale Funktion: Nicht erforderlich für die Verwendung des Cores
Allgemeine Benutzer von @byeolnaerim/typed-rx-http, /next oder /rsocket müssen dieses Skript nicht ausführen und müssen openapi-typescript nicht im Projekt installieren.
import {
connectToSwaggerEventStream,
generateSwaggerFromHttp,
generateSwaggerFromFile,
} from "@byeolnaerim/typed-rx-http/auto-node-script/openapi";Isolierung der openapi-typescript-Abhängigkeit
Für die Erstellung von OpenAPI-Typen ist die openapi-typescript CLI erforderlich, aber das Paket wird nicht in die allgemeinen Abhängigkeiten aufgenommen. Die devDependencies.typescript von typed-rx-http halten sich an den Standard von TypeScript 6.0.3 und verwenden nur eine separate npx-Temporärumgebung im OpenAPI-Auto-Node-Skript.
Im grundlegenden OpenAPI-Generierungsprozess werden openapi-typescript und [email protected] zusammen ausgeführt, sodass die in Ihrem Projekt installierte TypeScript/openapi-typescript-Version nicht geändert oder nicht verwendet wird.
npx -y -p openapi-typescript@latest -p [email protected] openapi-typescript swagger.json --output ./src/handler/service/@types/ApiTypes.d.tsWenn nötig, kann der gesamte Befehl auf openApiTypescriptCommand festgelegt werden.
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",
});Alternativ können Sie nur die Paketversion ändern, die den Standardbefehl konfiguriert.
generateSwaggerFromFile({
inputFile: "./swagger.json",
openApiTypescriptPackage: "[email protected]",
openApiTypescriptTypescriptPackage: "[email protected]",
});Zuerst: Bereiten Sie die Projektdatei vor, auf die commonServiceFile verweist.
Bevor commonServiceFile im Generatorbeispiel erscheint, müssen Sie diese Datei verstehen. commonServiceFile ist der Pfad zur Projektbesitzdatei, die die gemeinsamen HTTP-Funktionen des generierten Dienstes importiert, nicht eine Option, die eine Datei erstellt. Der Dateiname kann frei gewählt werden, z. B. rxjsHttpService.ts, commonService.ts.
Minimale vollständige Codeversion
Die folgende Datei erstellt einmal den createHttpClient im Projekt und exportiert callApi und callApiStream zur Wiederverwendung durch den generierten Dienst. Wenn kein Cache oder keine Sitzungsauthentifizierung erforderlich ist, können Sie mit dieser Struktur beginnen.
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;Vollständiger Code einschließlich Session/CSR/SSR-Cache
Wenn der generierte Dienst callApiClientCache oder callApiServerCache verwenden soll oder wenn Next.js die header und Sitzungsauthentifizierung pro Anfrage gemeinsam verarbeiten soll, erweitern Sie es in dieser vollständigen Form.
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;
};
});
};Ausführungsweise
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",
});Generierte Dateien
Die Standardeinstellungen erzeugen die folgenden Dateien.
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.tserzeugt nicht nur OpenAPI-Schema-Enums, sondern auch readonly-Konstantenarrays für die Enums von Abfrage-, Pfad-, Header- und Cookie-Parametern und verarbeitet auch die items.enum von Abfrageparametern.
Optional: Verbindung zwischen Backend-Swagger und Projekt-commonService
Hier beginnt ein Beispiel, wie der Generator mit einem echten Projekt verbunden wird. WebFlux/webflux-fe-dev-assistant ist nur eine Möglichkeit, Swagger bereitzustellen, und keine zwingende Abhängigkeit.
Vorbereitung der Swagger-Dokumentation
Swagger kann von Springdoc, anderen OpenAPI-Tools oder von Ihnen selbst erstellten Dateien stammen. Wenn Sie WebFlux-Funktionsendpunkte verwenden, können Sie webflux-fe-dev-assistantEs kann auch eine Methode verwendet werden, um Swagger-Dokumentation bereitzustellen.
Beispiel für die Generierung mit expliziten Optionen im Projekt
Sie können den Backend-Endpunkt, den zu speichernden swagger.json, den Typ-/Service-Ausgabepfad und commonServiceFile entsprechend der Projektstruktur angeben.
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",
});In Umgebungen, in denen Dokumente nicht über HTTP empfangen werden können, können Sie die gleichen Ausgabeoptionen verwenden. Swagger aus Datei generierenkann verwendet werden.
commonServiceFile ist der Importzielpfad
commonServiceFileist der Pfad zur Projektbesitzdatei, die die generierte Dienstleistung callApi, callApiStream und den Cache-Wrapper importiert. Der Generator hat keine Option, diese Datei zu erstellen oder zu überschreiben.
ApiBusinessService.ts
tsimport { callApi } from "../rxjsHttpService";
export const workplacesSearch = ({ params }) => {
return callApi<SearchWorkplacesResponse>({
url: "/api/business/workplaces/search",
method: "get",
queryString: params,
});
};Beispiel für die Platzierung der Generierungsergebnisse im Projekt
src/handler/service/
├─ rxjsHttpService.ts # 사용자 소유, 생성하지 않음
├─ apiUnionArrays.ts
├─ auto/
│ ├─ ApiBusinessService.ts
│ ├─ ApiOrdersService.ts
│ └─ Oauth2LoginService.ts
└─ @types/
├─ ApiTypes.d.ts
└─ auto/
├─ ApiBusinessTypes.ts
├─ ApiOrdersTypes.ts
└─ Oauth2LoginTypes.tsEine gemeinsame Datei, die als commonServiceFile wie rxjsHttpService.ts angegeben ist, wird direkt vom Projekt verwaltet. Die Ergebnisse unter auto und @types/auto werden neu generiert, wenn sich das Dokument ändert.
Dateinamen- und Funktionsnamensregeln
Der Name der Dienstdatei wird aus den ersten beiden Segmenten der URL gebildet, der Funktionsname aus dem dritten und den folgenden Segmenten. Pfadvariablen werden in der Form By + PascalCase in den Funktionsnamen aufgenommen.
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 })
Argumente für den Aufruf der Generierungsfunktion
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 },
});Der Schlüssel für die Abfrageparameter des generierten Dienstes ist params, die Pfadvariable ist path, der Anfragekörper ist body. Innerhalb der Generierungsfunktion werden sie jeweils in die ServiceArguments von queryString, pathVariable und body umgewandelt.