feat(iam): graphql subgraph with dataloader and router auth guard

- GraphQLModule: Apollo Federation 2 subgraph at /graphql

- UserResolver/RoleResolver: @key with @ResolveReference using DataLoader (ADR-035)

- DataScopeResolver: ScopeToken for visible class/student IDs (ADR-041)

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

- DataLoaderService: REQUEST-scoped userLoader + roleLoader

- IamRepository.batchFindRoles: batch query for DataLoader

- IamService.getVisibleClassIds/getVisibleStudentIds: dataScope calculation

- app.module.ts: register PermissionGuard + RouterAuthGuard as APP_GUARD
This commit is contained in:
SpecialX
2026-07-14 23:48:16 +08:00
parent 5fcb831a18
commit 6bed673d9f
12 changed files with 694 additions and 6 deletions

View File

@@ -0,0 +1,54 @@
/**
* iam Role Resolverv2.1 M1
*
* Apollo Federation 子图Role Entity
* - @key(fields: "roleId") 支持跨子图引用
* - @ResolveReference 使用 DataLoader 批量加载ADR-035
*/
import {
Resolver,
Query,
Args,
ID,
ResolveReference,
ObjectType,
Field,
Directive,
} from "@nestjs/graphql";
import { DataLoaderService, type RoleEntity } from "../dataloader.service.js";
@ObjectType()
@Directive(`@key(fields: "roleId")`)
export class Role {
@Field(() => ID)
roleId!: string;
@Field()
name!: string;
@Field({ nullable: true })
description: string | null = null;
@Field()
roleType!: string;
@Field()
level!: number;
}
@Resolver(() => Role)
export class RoleResolver {
constructor(private readonly loader: DataLoaderService) {}
@ResolveReference()
async resolveReference(ref: { roleId: string }): Promise<RoleEntity | null> {
return this.loader.roleLoader.load(ref.roleId);
}
@Query(() => Role, { nullable: true })
async role(
@Args("roleId", { type: () => ID }) roleId: string,
): Promise<RoleEntity | null> {
return this.loader.roleLoader.load(roleId);
}
}