@yarch/contract 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/package.json +17 -0
- package/src/api-error.ts +21 -0
- package/src/error-codes.ts +20 -0
- package/src/http.ts +54 -0
- package/src/index.ts +6 -0
- package/src/navigator.ts +18 -0
- package/src/rest-response.ts +35 -0
- package/src/trace-id.ts +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @yarch/contract
|
|
2
|
+
|
|
3
|
+
yarch 前端契约 SDK——契约唯一权威为 [yarch 仓 contract/](https://github.com/yuandonghao/yarch/tree/main/contract),本包是其 TypeScript 方言实现(React/Vue 通吃,零框架依赖)。
|
|
4
|
+
|
|
5
|
+
- `rest-response`:`RestResponse<T>` 信封类型 + 解包(失败抛 `ApiError`)
|
|
6
|
+
- `error-codes`:13 码常量表(0 成功 · 1xxx 通用 · 2xxx 认证 · 3xxx+ 业务注册段)
|
|
7
|
+
- `api-error`:traceId 报障凭证 · `shouldRedirectToLogin`
|
|
8
|
+
- `trace-id`:`X-Trace-Id` 生成/透传(W3C traceparent 对齐)
|
|
9
|
+
- `http`:fetch 封装 + 401 映射
|
|
10
|
+
- `navigator`:导航端口(依赖倒置——框架适配包 `@yarch/react` / `@yarch/vue` 注入实现)
|
|
11
|
+
|
|
12
|
+
用法:`createClient({ baseUrl, getHeaders })` → `api.get<PageData<T>>("/items?page=1")`。
|
|
13
|
+
|
|
14
|
+
配套:工程脚手架 `npm create @yarch/admin@latest <name>`([@yarch/create-admin](https://www.npmjs.com/package/@yarch/create-admin))。
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yarch/contract",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "yarch 前端契约 SDK(跨框架,React/Vue 通吃):RestResponse/PageData 类型与解包、错误码常量表、ApiError、traceId、fetch 封装",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": { "test": "vitest run" },
|
|
13
|
+
"publishConfig": { "access": "public" },
|
|
14
|
+
"repository": { "type": "git", "url": "git+https://github.com/yuandonghao/yarch.git", "directory": "stacks/web/packages/contract" },
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"devDependencies": { "vitest": "^3.0.0", "typescript": "^5.6.0" }
|
|
17
|
+
}
|
package/src/api-error.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** ApiError:业务错误的统一形态 —— code 对应错误码表,traceId 是报障凭证 */
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
readonly code: number;
|
|
4
|
+
readonly traceId: string;
|
|
5
|
+
|
|
6
|
+
constructor(code: number, message: string, traceId = "") {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ApiError";
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.traceId = traceId;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** 契约:token 过期引导重登录,禁无脑重试(rest-conventions.md 认证段) */
|
|
14
|
+
get shouldRedirectToLogin(): boolean {
|
|
15
|
+
return this.code === 2001 || this.code === 2002;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get isForbidden(): boolean {
|
|
19
|
+
return this.code === 2003 || this.code === 2004;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** 错误码常量表 —— 与 contract/api/error-codes.md v1.0 同源(0/1xxx/2xxx 归 yarch;3xxx+ 业务侧扩展) */
|
|
2
|
+
export const CODE_SUCCESS = 0;
|
|
3
|
+
|
|
4
|
+
export const errorCodes = {
|
|
5
|
+
INTERNAL_ERROR: 1000,
|
|
6
|
+
INVALID_ARGUMENT: 1001,
|
|
7
|
+
MALFORMED_BODY: 1002,
|
|
8
|
+
NOT_FOUND: 1004,
|
|
9
|
+
CONFLICT: 1005,
|
|
10
|
+
RATE_LIMITED: 1006,
|
|
11
|
+
IDEMPOTENCY_CONFLICT: 1007,
|
|
12
|
+
UPSTREAM_TIMEOUT: 1008,
|
|
13
|
+
UNAVAILABLE: 1009,
|
|
14
|
+
UNAUTHORIZED: 2001,
|
|
15
|
+
CREDENTIALS_EXPIRED: 2002,
|
|
16
|
+
FORBIDDEN: 2003,
|
|
17
|
+
ACCOUNT_DISABLED: 2004,
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type YarchErrorCode = (typeof errorCodes)[keyof typeof errorCodes];
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** 框架无关 fetch 封装:traceId 透传 + 信封解包 + 错误映射(401/402→重登录端口) */
|
|
2
|
+
import { ApiError } from "./api-error";
|
|
3
|
+
import { RestResponse } from "./rest-response";
|
|
4
|
+
import { newTraceId, TRACE_ID_HEADER } from "./trace-id";
|
|
5
|
+
import { navigateToLogin } from "./navigator";
|
|
6
|
+
|
|
7
|
+
export interface HttpOptions {
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
getHeaders?: () => Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createClient(options: HttpOptions = {}) {
|
|
13
|
+
const baseUrl = options.baseUrl ?? "";
|
|
14
|
+
|
|
15
|
+
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
16
|
+
const headers: Record<string, string> = {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
[TRACE_ID_HEADER]: newTraceId(),
|
|
19
|
+
...(options.getHeaders?.() ?? {}),
|
|
20
|
+
};
|
|
21
|
+
let response: Response;
|
|
22
|
+
try {
|
|
23
|
+
response = await fetch(baseUrl + path, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
27
|
+
});
|
|
28
|
+
} catch (cause) {
|
|
29
|
+
throw new ApiError(-1, `网络错误:${String(cause)}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (response.status === 401) {
|
|
33
|
+
navigateToLogin("unauthorized");
|
|
34
|
+
throw new ApiError(2001, "未认证", response.headers.get(TRACE_ID_HEADER) ?? "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const envelope = (await response.json()) as RestResponse<T>;
|
|
38
|
+
if (envelope.code !== 0) {
|
|
39
|
+
const apiError = new ApiError(envelope.code, envelope.message, envelope.traceId);
|
|
40
|
+
if (apiError.shouldRedirectToLogin) {
|
|
41
|
+
navigateToLogin(`code ${envelope.code}`);
|
|
42
|
+
}
|
|
43
|
+
throw apiError;
|
|
44
|
+
}
|
|
45
|
+
return envelope.data as T;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
get: <T>(path: string) => request<T>("GET", path),
|
|
50
|
+
post: <T>(path: string, body: unknown) => request<T>("POST", path, body),
|
|
51
|
+
put: <T>(path: string, body: unknown) => request<T>("PUT", path, body),
|
|
52
|
+
delete: <T>(path: string) => request<T>("DELETE", path),
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/index.ts
ADDED
package/src/navigator.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导航端口(依赖倒置):共享包零框架依赖,401→重登录 的路由跳转由各框架适配层注入实现
|
|
3
|
+
* (对应 java 侧 ProductCache 端口的同一手法)。
|
|
4
|
+
*/
|
|
5
|
+
export interface YarchNavigator {
|
|
6
|
+
/** 401/2002 时引导到登录页(记录回跳地址) */
|
|
7
|
+
redirectToLogin(reason: string): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let navigator: YarchNavigator | null = null;
|
|
11
|
+
|
|
12
|
+
export function setNavigator(impl: YarchNavigator): void {
|
|
13
|
+
navigator = impl;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function navigateToLogin(reason: string): void {
|
|
17
|
+
navigator?.redirectToLogin(reason);
|
|
18
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** 信封与分页负载(rest-response.md v1.0):code/message/data/traceId 四字段,前端只认这个形状 */
|
|
2
|
+
import { ApiError } from "./api-error";
|
|
3
|
+
|
|
4
|
+
export interface RestResponse<T> {
|
|
5
|
+
code: number;
|
|
6
|
+
message: string;
|
|
7
|
+
data: T | null;
|
|
8
|
+
traceId: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface PageData<T> {
|
|
12
|
+
list: T[];
|
|
13
|
+
total: number;
|
|
14
|
+
page: number;
|
|
15
|
+
pageSize: number;
|
|
16
|
+
nextCursor?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 解包:code != 0 抛 ApiError(含 traceId 报障凭证);data 为 null 抛 ApiError */
|
|
20
|
+
export function unwrap<T>(envelope: RestResponse<T>): T {
|
|
21
|
+
if (envelope.code !== 0) {
|
|
22
|
+
throw new ApiError(envelope.code, envelope.message, envelope.traceId);
|
|
23
|
+
}
|
|
24
|
+
if (envelope.data === null) {
|
|
25
|
+
throw new ApiError(-1, "信封 data 为 null(成功无负载场景请用 unwrapAllowNull)", envelope.traceId);
|
|
26
|
+
}
|
|
27
|
+
return envelope.data as T;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function unwrapAllowNull<T>(envelope: RestResponse<T>): T | null {
|
|
31
|
+
if (envelope.code !== 0) {
|
|
32
|
+
throw new ApiError(envelope.code, envelope.message, envelope.traceId);
|
|
33
|
+
}
|
|
34
|
+
return envelope.data;
|
|
35
|
+
}
|
package/src/trace-id.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** traceId(logging-trace.md 前端行):生成/透传 X-Trace-Id,错误对象暴露 traceId 供报障 */
|
|
2
|
+
const TRACE_HEADER = "X-Trace-Id";
|
|
3
|
+
|
|
4
|
+
export function newTraceId(): string {
|
|
5
|
+
const bytes = new Uint8Array(16);
|
|
6
|
+
crypto.getRandomValues(bytes);
|
|
7
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function traceHeader(): Record<string, string> {
|
|
11
|
+
return { [TRACE_HEADER]: newTraceId() };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const TRACE_ID_HEADER = TRACE_HEADER;
|