feat(shared-ts): federation shared utilities for apollo subgraphs

- RouterAuthGuard: validate Router-Authorization header (ADR-036)

- DataLoader factory: request-scoped batching (ADR-035)

- ScopeTokenService: Redis-backed scope token (ADR-041)

- GraphqlContext: build context from HTTP headers

- FederationExceptionFilter: HTTP-to-GraphQL error mapping
This commit is contained in:
SpecialX
2026-07-14 23:46:51 +08:00
parent 3c2ea50c7f
commit 5fcb831a18
9 changed files with 1074 additions and 4 deletions

View File

@@ -0,0 +1,60 @@
/**
* GraphQL Context - Apollo Router 下传的请求上下文
*
* Apollo Router 通过 headers 传递以下信息到子图:
* - authorization: JWT token透传
* - x-user-id: 已认证用户 ID
* - x-user-role: 用户角色
* - x-dataScope: 数据范围ALL / SCHOOL / GRADE / CLASS / STUDENT
* - x-request-id: 请求追踪 ID
*
* 子图从 HTTP headers 提取后构造 GraphqlContext注入 Resolver。
*/
export type DataScope = "ALL" | "SCHOOL" | "GRADE" | "CLASS" | "STUDENT";
export interface GraphqlContextInput {
userId?: string;
userRole?: string;
dataScope?: DataScope;
requestId?: string;
jwt?: string;
}
export class GraphqlContext {
readonly userId?: string;
readonly userRole?: string;
readonly dataScope: DataScope;
readonly requestId: string;
readonly jwt?: string;
constructor(input: GraphqlContextInput) {
this.userId = input.userId;
this.userRole = input.userRole;
this.dataScope = input.dataScope ?? "ALL";
this.requestId = input.requestId ?? "unknown";
this.jwt = input.jwt;
}
/** 是否管理员(全量可见) */
get isAdmin(): boolean {
return this.dataScope === "ALL" || this.userRole === "admin";
}
/** 是否已认证 */
get isAuthenticated(): boolean {
return !!this.userId;
}
/** 从 HTTP headers 构造NestJS @Context 提取) */
static fromHeaders(
headers: Record<string, string | undefined>,
): GraphqlContext {
return new GraphqlContext({
userId: headers["x-user-id"],
userRole: headers["x-user-role"],
dataScope: headers["x-datapscope"] as DataScope | undefined,
requestId: headers["x-request-id"],
jwt: headers["authorization"]?.replace(/^Bearer\s+/i, ""),
});
}
}