feat(content): graphql subgraph with dataloader for textbook/chapter/kp/question

- GraphQLModule: Apollo Federation 2 at /graphql

- Textbook/Chapter/KnowledgePoint/Question @key with @ResolveReference

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

- batchFind methods added to 4 repositories

- Domain modules export repositories for GraphqlModule injection
This commit is contained in:
SpecialX
2026-07-15 00:18:57 +08:00
parent 9ff7a61ee2
commit d59c4e585f
18 changed files with 1073 additions and 12 deletions

View File

@@ -0,0 +1,84 @@
/**
* content Textbook Resolverv2.1 M1
*
* Apollo Federation 子图Textbook Entity
* - @key(fields: "id") 支持跨子图引用
* - @ResolveReference 使用 DataLoader 批量加载ADR-035
* - Query 入口供 Apollo Router 直接查询
*/
import {
Resolver,
Query,
Args,
ID,
ResolveReference,
ObjectType,
Field,
Directive,
} from "@nestjs/graphql";
import {
DataLoaderService,
type TextbookEntity,
} from "../dataloader.service.js";
/**
* Textbook ObjectTypeFederation @key
* 对应 content_textbooks 表
*/
@ObjectType()
@Directive(`@key(fields: "id")`)
export class Textbook {
@Field(() => ID)
id!: string;
@Field()
title!: string;
@Field()
subjectId!: string;
@Field()
gradeId!: string;
@Field()
version!: string;
@Field()
status!: string;
@Field({ nullable: true })
tenantId: string | null = null;
@Field()
createdAt!: Date;
@Field()
updatedAt!: Date;
}
@Resolver(() => Textbook)
export class TextbookResolver {
constructor(private readonly loader: DataLoaderService) {}
/**
* Federation Reference Resolver
*
* 当其他子图通过 @key 引用 Textbook 时Router 调用此方法解析。
* 使用 DataLoader 批量加载,消除 N+1 查询ADR-035
*/
@ResolveReference()
async resolveReference(ref: { id: string }): Promise<TextbookEntity | null> {
return this.loader.textbookLoader.load(ref.id);
}
/**
* Query: textbook(id) → Textbook
* 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝ADR-036
*/
@Query(() => Textbook, { nullable: true })
async textbook(
@Args("id", { type: () => ID }) id: string,
): Promise<TextbookEntity | null> {
return this.loader.textbookLoader.load(id);
}
}