OpenAPI & Generator Layanan
Skrip Node opsional yang disediakan terpisah dari klien HTTP inti. Hanya digunakan saat menghasilkan tipe TypeScript dan file layanan dari JSON OpenAPI/Swagger.
Fitur opsional: Tidak diperlukan untuk penggunaan inti
Pengguna umum @byeolnaerim/typed-rx-http, /next, atau /rsocket tidak perlu menjalankan skrip ini dan tidak perlu menginstal openapi-typescript di proyek.
import {
connectToSwaggerEventStream,
generateSwaggerFromHttp,
generateSwaggerFromFile,
} from "@byeolnaerim/typed-rx-http/auto-node-script/openapi";Isolasi ketergantungan openapi-typescript
Pembuatan tipe OpenAPI memerlukan openapi-typescript CLI, tetapi paket tidak dimasukkan ke dalam dependensi umum. devDependencies.typescript dari typed-rx-http mempertahankan standar TypeScript 6.0.3, dan hanya menggunakan lingkungan npx sementara di skrip node auto OpenAPI.
Pada tahap pembuatan OpenAPI dasar, openapi-typescript dan [email protected] dijalankan bersamaan, sehingga tidak mengubah atau menggunakan versi TypeScript/openapi-typescript yang diinstal di proyek pengguna.
npx -y -p openapi-typescript@latest -p [email protected] openapi-typescript swagger.json --output ./src/handler/service/@types/ApiTypes.d.tsJika perlu, seluruh perintah dapat dikunci dengan 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",
});Atau Anda juga dapat mengubah hanya versi paket yang membentuk perintah dasar.
generateSwaggerFromFile({
inputFile: "./swagger.json",
openApiTypescriptPackage: "[email protected]",
openApiTypescriptTypescriptPackage: "[email protected]",
});Pertama: Siapkan file proyek yang akan ditunjuk oleh commonServiceFile
Sebelum commonServiceFile muncul dalam contoh generator, Anda harus memahami file ini terlebih dahulu. commonServiceFile adalah jalur file yang dimiliki proyek yang mengimpor fungsi HTTP umum dari layanan yang dihasilkan, bukan opsi untuk membuat file. Nama file dapat ditentukan secara bebas seperti rxjsHttpService.ts, commonService.ts, dll.
Kode lengkap minimum
File di bawah ini membuat createHttpClient sekali di proyek dan mengekspor callApi dan callApiStream agar layanan yang dihasilkan dapat digunakan kembali. Jika tidak memerlukan cache atau otentikasi sesi, Anda dapat memulai dari struktur ini.
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;Kode lengkap termasuk cache Session/CSR/SSR
Jika layanan yang dihasilkan menggunakan callApiClientCache atau callApiServerCache, atau jika Anda ingin menangani header dan otentikasi sesi secara umum di Next.js, perluasan dilakukan dengan bentuk lengkap berikut.
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;
};
});
};Metode eksekusi
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",
});File yang dihasilkan
Pengaturan default menghasilkan file-file di bawah ini.
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.tsmembuat array konstanta readonly untuk enum parameter query, path, header, dan cookie, serta menangani items.enum dari parameter query array.
Opsional: Menghubungkan Swagger backend dengan commonService proyek
Mulai dari sini adalah contoh menghubungkan generator ke proyek nyata. WebFlux/webflux-fe-dev-assistant hanyalah salah satu cara untuk menyediakan Swagger dan bukan ketergantungan yang wajib.
Siapkan dokumen Swagger
Swagger bisa berasal dari Springdoc, alat OpenAPI lainnya, atau file yang ditulis secara manual. Jika menggunakan endpoint fungsional WebFlux, webflux-fe-dev-assistantAnda juga dapat menggunakan cara untuk menyediakan dokumen Swagger.
Contoh pembuatan dengan menyatakan opsi di proyek
Anda dapat menyatakan endpoint backend, swagger.json yang akan disimpan, jalur output tipe/layanan, dan commonServiceFile sesuai dengan struktur proyek.
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",
});Dalam lingkungan di mana dokumen tidak dapat diterima melalui HTTP, Anda dapat menggunakan generateSwaggerFromFiledengan opsi output yang sama.
commonServiceFile adalah jalur target import
commonServiceFileadalah jalur file yang dimiliki proyek yang mengimpor callApi, callApiStream, dan pembungkus cache dari layanan yang dihasilkan. Ini bukan opsi untuk membuat atau menimpa file ini.
ApiBusinessService.ts
tsimport { callApi } from "../rxjsHttpService";
export const workplacesSearch = ({ params }) => {
return callApi<SearchWorkplacesResponse>({
url: "/api/business/workplaces/search",
method: "get",
queryString: params,
});
};Contoh di mana hasil pembuatan ditempatkan di proyek
src/handler/service/
├─ rxjsHttpService.ts # 사용자 소유, 생성하지 않음
├─ apiUnionArrays.ts
├─ auto/
│ ├─ ApiBusinessService.ts
│ ├─ ApiOrdersService.ts
│ └─ Oauth2LoginService.ts
└─ @types/
├─ ApiTypes.d.ts
└─ auto/
├─ ApiBusinessTypes.ts
├─ ApiOrdersTypes.ts
└─ Oauth2LoginTypes.tsFile umum yang ditentukan dengan commonServiceFile seperti rxjsHttpService.ts dikelola langsung oleh proyek. Hasil di bawah auto dan @types/auto akan dibuat ulang jika dokumen berubah.
Aturan nama file dan fungsi
Nama file layanan ditentukan oleh dua segmen pertama dari URL, dan nama fungsi ditentukan oleh segmen ketiga dan seterusnya. Variabel path disertakan dalam nama fungsi dalam bentuk 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 })
Argumen pemanggilan fungsi pembuatan
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 },
});Kunci parameter query dari layanan yang dihasilkan adalah params, variabel path adalah path, dan body permintaan adalah body. Di dalam fungsi pembuatan, masing-masing akan diubah menjadi ServiceArguments dari queryString, pathVariable, dan body.