OpenAPI & مولد الخدمة
برنامج نصي اختياري يتم توفيره بشكل منفصل عن Core HTTP client. يستخدم فقط عند إنشاء أنواع TypeScript وملفات الخدمة من OpenAPI/Swagger JSON.
وظيفة اختيارية: غير ضرورية لاستخدام 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، لكن الحزمة لا تضيفها إلى الاعتمادات العامة. تحافظ devDependencies.typescript الخاصة بـ typed-rx-http على معيار TypeScript 6.0.3، وتستخدم بيئة npx مؤقتة منفصلة فقط في برنامج نصي auto node الخاص بـ OpenAPI.
في مرحلة إنشاء 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",
});أو يمكنك تغيير إصدار الحزمة فقط الذي يشكل الأمر الأساسي.
generateSwaggerFromFile({
inputFile: "./swagger.json",
openApiTypescriptPackage: "[email protected]",
openApiTypescriptTypescriptPackage: "[email protected]",
});أولاً: إعداد ملف المشروع الذي سيشير إليه commonServiceFile
يجب فهم هذا الملف قبل ظهور commonServiceFile في مثال المولد. commonServiceFile هو مسار الملف الذي يمتلكه المشروع والذي تستورد منه الخدمة المولدة الوظائف HTTP المشتركة. يمكن اختيار اسم الملف بحرية مثل rxjsHttpService.ts أو commonService.ts.
الكود الكامل للحد الأدنى من الشكل المكتمل
الملف أدناه ينشئ createHttpClient مرة واحدة في المشروع ويصدر callApi و callApiStream لإعادة استخدامهما بواسطة الخدمة المولدة. إذا لم تكن بحاجة إلى ذاكرة التخزين المؤقت أو مصادقة الجلسة، يمكنك البدء من هذا الهيكل.
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;الكود الكامل بما في ذلك ذاكرة التخزين المؤقت للجلسة/CSR/SSR
لتوسيع الخدمة المولدة لاستخدام callApiClientCache أو callApiServerCache أو لمعالجة رأس الطلب ومصادقة الجلسة بشكل مشترك في Next.js، قم بتوسيع الشكل الكامل التالي.
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ينشئ مصفوفة ثوابت readonly ليس فقط لـ OpenAPI schema enum ولكن أيضًا لـ query و path و header و cookie parameter enum، ويعالج أيضًا items.enum لمتغيرات المصفوفة.
اختياري: ربط Swagger الخلفي بـ commonService للمشروع
من هنا فصاعدًا، هذا مثال على ربط المولد بمشروع حقيقي. WebFlux/webflux-fe-dev-assistant هو مجرد طريقة واحدة لتوفير Swagger وليس اعتمادًا ضروريًا.
إعداد وثيقة Swagger
يمكن أن تكون Swagger من Springdoc، أو أدوات OpenAPI أخرى، أو ملف كتبته بنفسك. إذا كنت تستخدم نقطة النهاية الوظيفية WebFlux، webflux-fe-dev-assistantيمكن أيضًا استخدام طريقة تقديم وثيقة Swagger.
مثال على إنشاء خيارات محددة في المشروع
يمكنك تحديد نقطة النهاية الخلفية، و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، يمكنك استخدام نفس خيارات الإخراج مع توليدSwaggerمنملف.
commonServiceFile هو مسار الهدف للاستيراد
commonServiceFileهو مسار الملف الذي يمتلكه المشروع والذي تستورد منه الخدمة المولدة callApi و callApiStream و cache wrapper. ليس خيارًا لإنشاء أو الكتابة فوق هذا الملف.
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الملف العام المحدد بـ commonServiceFile مثل rxjsHttpService.ts يتم إدارته مباشرة من قبل المشروع. يتم إعادة إنشاء النتائج تحت auto و @types/auto عند تغيير الوثيقة.
قواعد أسماء الملفات والأسماء الدالة
يتم تحديد اسم ملف الخدمة من أول مقطعين من URL، ويتم تحديد اسم الدالة من المقطع الثالث وما بعده. يتم تضمين المتغيرات في اسم الدالة بشكل 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 },
});مفتاح معلمات الاستعلام للخدمة المولدة هو params، والمتغيرات في المسار هي path، وجسم الطلب هو body. يتم تحويل كل من queryString و pathVariable و body إلى ServiceArguments داخل دالة الإنشاء.