feat(portal-shell): add lib/api skeleton with errors and types

M1 Task 1: 创建 4 层数据抽象层的骨架文件

- errors.ts: ApiError + GraphQLErrorCode 枚举

- types.ts: Pagination / PaginatedResult / UseQueryResult / UseMutationResult

- internal.ts: normalizeError 把 ApolloError 转为 ApiError

- index.ts: barrel 出口
This commit is contained in:
SpecialX
2026-07-17 12:06:01 +08:00
parent 117c89396d
commit 989603e318
4 changed files with 86 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
/**
* API 层统一错误类型
*
* 所有 lib/api/ 函数抛出的错误都用 ApiError 包装,
* 便于 widget 层按 code 分类处理401 跳登录、403 显示无权限等)。
*
* 关联spec §6.1
*/
export type GraphQLErrorCode =
| "UNAUTHORIZED"
| "FORBIDDEN"
| "NOT_FOUND"
| "VALIDATION_ERROR"
| "QUERY_DEPTH_EXCEEDED"
| "QUERY_COMPLEXITY_EXCEEDED"
| "PERSISTED_QUERY_NOT_FOUND"
| "INTERNAL_ERROR";
export class ApiError extends Error {
constructor(
message: string,
public readonly code: GraphQLErrorCode,
public readonly statusCode: number = 500,
public readonly fields?: string[],
) {
super(message);
this.name = "ApiError";
}
}

View File

@@ -0,0 +1,10 @@
/**
* API 层统一出口
*
* widget 通过 `import { useParentChildren } from "@/lib/api"` 调用。
* 各 domain 文件在 M2 阶段逐步加入。
*
* 关联spec §2.2
*/
export * from "./errors";
export * from "./types";

View File

@@ -0,0 +1,18 @@
/**
* API 层共享工具
*
* 错误归一化:把 ApolloError 转为 ApiError
* 让 widget 层可以用 instanceof ApiError 判断。
*
* 关联spec §6.2
*/
import type { ApolloError } from "@apollo/client";
import { ApiError, type GraphQLErrorCode } from "./errors";
export function normalizeError(error: ApolloError): ApiError {
const gqlError = error.graphQLErrors?.[0];
const code = (gqlError?.extensions?.code ??
"INTERNAL_ERROR") as GraphQLErrorCode;
const statusCode = (gqlError?.extensions?.statusCode ?? 500) as number;
return new ApiError(gqlError?.message ?? error.message, code, statusCode);
}

View File

@@ -0,0 +1,29 @@
/**
* API 层共享类型
*
* 跨 domain 复用的分页、筛选等通用类型。
* 领域专属类型(如 ChildSummary放在各自的 domain 文件。
*
* 关联spec §2.2
*/
export interface Pagination {
limit: number;
offset: number;
}
export interface PaginatedResult<T> {
items: T[];
total: number;
}
export interface UseQueryResult<TData> {
data: TData | undefined;
loading: boolean;
error: unknown;
refetch: () => Promise<unknown>;
}
export interface UseMutationResult {
loading: boolean;
error: unknown;
}