feat(msg): graphql subgraph with dataloader for notification/template

- GraphQLModule: Apollo Federation 2 at /graphql

- Notification/NotificationTemplate @key with @ResolveReference

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

- batchFindByIds added to notifications and templates repositories
This commit is contained in:
SpecialX
2026-07-15 00:19:26 +08:00
parent d59c4e585f
commit d3b126ed9d
11 changed files with 704 additions and 2 deletions

51
pnpm-lock.yaml generated
View File

@@ -775,6 +775,12 @@ importers:
services/content:
dependencies:
'@apollo/subgraph':
specifier: ^2.2.3
version: 2.14.2(graphql@16.14.2)
'@edu/shared-ts':
specifier: workspace:*
version: link:../../packages/shared-ts
'@elastic/elasticsearch':
specifier: ^8.13.0
version: 8.19.2
@@ -784,12 +790,18 @@ importers:
'@grpc/proto-loader':
specifier: ^0.7.13
version: 0.7.13
'@nestjs/apollo':
specifier: ^12.2.0
version: 12.2.0(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.14.2))(@apollo/subgraph@2.14.2(graphql@16.14.2))(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.0(@apollo/subgraph@2.14.2(graphql@16.14.2))(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(graphql@16.14.2)(reflect-metadata@0.2.2))(graphql@16.14.2)
'@nestjs/common':
specifier: ^10.4.0
version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core':
specifier: ^10.4.0
version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@10.4.22)(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/graphql':
specifier: ^12.2.0
version: 12.2.0(@apollo/subgraph@2.14.2(graphql@16.14.2))(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(graphql@16.14.2)(reflect-metadata@0.2.2)
'@nestjs/microservices':
specifier: ^10.4.0
version: 10.4.22(@grpc/grpc-js@1.14.4)(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(ioredis@5.11.1)(kafkajs@2.2.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -811,9 +823,15 @@ importers:
'@paralleldrive/cuid2':
specifier: ^2.2.2
version: 2.2.2
dataloader:
specifier: ^2.2.2
version: 2.2.3
drizzle-orm:
specifier: ^0.31.0
version: 0.31.0(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@types/react@18.3.31)(better-sqlite3@11.3.0)(mysql2@3.22.6(@types/node@22.20.1))(react@18.3.0)
graphql:
specifier: ^16.9.0
version: 16.14.2
kafkajs:
specifier: ^2.2.4
version: 2.2.4
@@ -860,18 +878,30 @@ importers:
services/core-edu:
dependencies:
'@apollo/subgraph':
specifier: ^2.2.3
version: 2.14.2(graphql@16.14.2)
'@edu/shared-ts':
specifier: workspace:*
version: link:../../packages/shared-ts
'@grpc/grpc-js':
specifier: ^1.12.0
version: 1.14.4
'@grpc/proto-loader':
specifier: ^0.7.13
version: 0.7.13
'@nestjs/apollo':
specifier: ^12.2.0
version: 12.2.0(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.14.2))(@apollo/subgraph@2.14.2(graphql@16.14.2))(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.0(@apollo/subgraph@2.14.2(graphql@16.14.2))(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(graphql@16.14.2)(reflect-metadata@0.2.2))(graphql@16.14.2)
'@nestjs/common':
specifier: ^10.4.0
version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core':
specifier: ^10.4.0
version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@10.4.22)(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/graphql':
specifier: ^12.2.0
version: 12.2.0(@apollo/subgraph@2.14.2(graphql@16.14.2))(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(graphql@16.14.2)(reflect-metadata@0.2.2)
'@nestjs/platform-express':
specifier: ^10.4.0
version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)
@@ -887,9 +917,15 @@ importers:
'@opentelemetry/sdk-node':
specifier: ^0.53.0
version: 0.53.0(@opentelemetry/api@1.9.1)
dataloader:
specifier: ^2.2.2
version: 2.2.3
drizzle-orm:
specifier: ^0.31.0
version: 0.31.0(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@types/react@18.3.31)(better-sqlite3@11.3.0)(mysql2@3.22.6(@types/node@22.20.1))(react@18.3.0)
graphql:
specifier: ^16.9.0
version: 16.14.2
kafkajs:
specifier: ^2.2.4
version: 2.2.4
@@ -1063,6 +1099,9 @@ importers:
services/msg:
dependencies:
'@apollo/subgraph':
specifier: ^2.2.3
version: 2.14.2(graphql@16.14.2)
'@edu/shared-ts':
specifier: workspace:*
version: link:../../packages/shared-ts
@@ -1075,12 +1114,18 @@ importers:
'@grpc/proto-loader':
specifier: ^0.7.13
version: 0.7.13
'@nestjs/apollo':
specifier: ^12.2.0
version: 12.2.0(@apollo/server@4.13.0(encoding@0.1.13)(graphql@16.14.2))(@apollo/subgraph@2.14.2(graphql@16.14.2))(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(@nestjs/graphql@12.2.0(@apollo/subgraph@2.14.2(graphql@16.14.2))(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(graphql@16.14.2)(reflect-metadata@0.2.2))(graphql@16.14.2)
'@nestjs/common':
specifier: ^10.4.0
version: 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/core':
specifier: ^10.4.0
version: 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@10.4.22)(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)
'@nestjs/graphql':
specifier: ^12.2.0
version: 12.2.0(@apollo/subgraph@2.14.2(graphql@16.14.2))(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(graphql@16.14.2)(reflect-metadata@0.2.2)
'@nestjs/microservices':
specifier: ^10.4.0
version: 10.4.22(@grpc/grpc-js@1.14.4)(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)(ioredis@5.11.1)(kafkajs@2.2.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -1102,9 +1147,15 @@ importers:
'@paralleldrive/cuid2':
specifier: ^2.2.2
version: 2.2.2
dataloader:
specifier: ^2.2.2
version: 2.2.3
drizzle-orm:
specifier: ^0.31.0
version: 0.31.0(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(@types/react@18.3.31)(better-sqlite3@11.3.0)(mysql2@3.22.6(@types/node@22.20.1))(react@18.3.0)
graphql:
specifier: ^16.9.0
version: 16.14.2
grpc-reflection-js:
specifier: ^0.3.0
version: 0.3.0(@grpc/grpc-js@1.14.4)

View File

@@ -34,7 +34,12 @@
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/sdk-node": "^0.55.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.55.0",
"@opentelemetry/auto-instrumentations-node": "^0.55.0"
"@opentelemetry/auto-instrumentations-node": "^0.55.0",
"@apollo/subgraph": "^2.2.3",
"@nestjs/apollo": "^12.2.0",
"@nestjs/graphql": "^12.2.0",
"dataloader": "^2.2.2",
"graphql": "^16.9.0"
},
"devDependencies": {
"@nestjs/cli": "^10.4.0",

View File

@@ -9,6 +9,8 @@ import { HealthModule } from "./shared/health/health.module.js";
import { PermissionGuard } from "./middleware/permission.guard.js";
import { LifecycleService } from "./shared/lifecycle/lifecycle.service.js";
import { KafkaConsumerService } from "./shared/kafka/kafka.consumer.js";
import { GraphqlModule } from "./graphql/graphql.module.js";
import { RouterAuthGuard } from "./graphql/router-auth.guard.js";
/**
* AppModule —— msg 服务根模块。
@@ -37,9 +39,11 @@ import { KafkaConsumerService } from "./shared/kafka/kafka.consumer.js";
AnnouncementsModule,
GrpcModule,
HealthModule,
GraphqlModule,
],
providers: [
{ provide: APP_GUARD, useClass: PermissionGuard },
{ provide: APP_GUARD, useClass: RouterAuthGuard },
LifecycleService,
KafkaConsumerService,
],

View File

@@ -0,0 +1,103 @@
/**
* msg DataLoader 服务v2.1 M1 / ADR-035
*
* 强制约束:@key Reference Resolver 必须使用 DataLoader 请求合并。
*
* 每个 GraphQL 请求独立 DataLoader 实例(请求级缓存)。
* 通过 REQUEST scope 注入,确保不同请求不共享缓存。
*
* 注msg 仓库为函数式非类DataLoader 直接 import batch 函数调用,
* 这是函数式仓库对 iam 类仓库 DI 注入的等价适配。
*/
import { Injectable, Scope } from "@nestjs/common";
import DataLoader from "dataloader";
import { batchFindByIds as batchFindNotifications } from "../notifications/notifications.repository.js";
import { batchFindByIds as batchFindTemplates } from "../templates/templates.repository.js";
/**
* Notification EntityGraphQL 输出类型)
* 对应 msg.msg_notifications 表
*/
export interface NotificationEntity {
id: string;
userId: string;
type: string;
title: string;
content: string;
channel: string;
isRead: boolean;
status: string;
createdAt: Date;
updatedAt: Date;
relatedEntityType: string | null;
relatedEntityId: string | null;
groupId: string | null;
senderId: string | null;
templateId: string | null;
eventId: string | null;
readAt: Date | null;
}
/**
* NotificationTemplate EntityGraphQL 输出类型)
* 对应 msg.msg_notification_templates 表
*/
export interface TemplateEntity {
id: string;
code: string;
type: string;
titleTemplate: string;
contentTemplate: string;
defaultChannels: string[];
variables: string[];
locale: string;
status: string;
createdAt: Date;
updatedAt: Date;
}
@Injectable({ scope: Scope.REQUEST })
export class DataLoaderService {
private notificationLoaderInstance: DataLoader<
string,
NotificationEntity | null
> | null = null;
private templateLoaderInstance: DataLoader<
string,
TemplateEntity | null
> | null = null;
/** Notification @key 解析器 DataLoader */
get notificationLoader(): DataLoader<string, NotificationEntity | null> {
if (!this.notificationLoaderInstance) {
this.notificationLoaderInstance = new DataLoader<
string,
NotificationEntity | null
>(async (ids) => {
const notifications = await batchFindNotifications([...ids]);
const map = new Map(
notifications.map((n) => [n.id, n satisfies NotificationEntity]),
);
return ids.map((id) => map.get(id) ?? null);
});
}
return this.notificationLoaderInstance;
}
/** Template @key 解析器 DataLoader */
get templateLoader(): DataLoader<string, TemplateEntity | null> {
if (!this.templateLoaderInstance) {
this.templateLoaderInstance = new DataLoader<
string,
TemplateEntity | null
>(async (ids) => {
const templates = await batchFindTemplates([...ids]);
const map = new Map(
templates.map((t) => [t.id, t satisfies TemplateEntity]),
);
return ids.map((id) => map.get(id) ?? null);
});
}
return this.templateLoaderInstance;
}
}

View File

@@ -0,0 +1,222 @@
# 自动生成的 GraphQL Federation 子图v2.1 M0
# 源文件msg.proto
# 请勿手动修改;如需调整,改 proto 后重新运行 pnpm run proto:gen-graphql
extend type Query
type Notification @key(fields: "id") {
id: String
user_id: String
type: String
title: String
content: String
channel: String
is_read: Boolean
created_at: String
status: String
related_entity_type: String
related_entity_id: String
group_id: String
sender_id: String
template_id: String
event_id: String
read_at: String
updated_at: String
}
type MarkAsReadRequest @key(fields: "id") {
id: String
user_id: String
}
type NotificationPreference @key(fields: "id") {
id: String
user_id: String
type: String
channels: String
frequency_limit: Int
quiet_hours_start: String
quiet_hours_end: String
quiet_hours_timezone: String
enabled: Boolean
created_at: String
updated_at: String
}
type NotificationTemplate @key(fields: "id") {
id: String
code: String
type: String
title_template: String
content_template: String
default_channels: String
variables: String
locale: String
status: String
created_at: String
updated_at: String
}
type GetTemplateRequest @key(fields: "id") {
id: String
}
type UpdateTemplateRequest @key(fields: "id") {
id: String
title_template: String
content_template: String
default_channels: String
variables: String
status: String
}
type DeleteTemplateRequest @key(fields: "id") {
id: String
}
input SendNotificationRequestInput {
user_id: String
type: String
title: String
content: String
channel: String
related_entity_type: String
related_entity_id: String
group_id: String
sender_id: String
template_id: String
event_id: String
}
input BatchSendNotificationRequestInput {
items: SendNotificationRequest
group_id: String
}
input BatchSendNotificationResponseInput {
ids: String
failed: BatchSendFailure
}
type BatchSendFailure {
user_id: String
error: String
}
input ListNotificationsRequestInput {
user_id: String
only_unread: Boolean
type: String
page: Int
page_size: Int
}
input ListNotificationsResponseInput {
notifications: Notification
total: Int
}
input GetUnreadCountRequestInput {
user_id: String
}
input GetUnreadCountResponseInput {
count: Int
}
input BatchMarkAsReadRequestInput {
ids: String
user_id: String
}
input MarkAllAsReadRequestInput {
user_id: String
before: String
}
input SearchNotificationsRequestInput {
user_id: String
query: String
type: String
page: Int
page_size: Int
}
input SearchNotificationsResponseInput {
notifications: Notification
total: Int
}
input RecallNotificationRequestInput {
group_id: String
reason: String
}
input RecallNotificationResponseInput {
recalled_count: Int
}
input GetPreferencesRequestInput {
user_id: String
}
input GetPreferencesResponseInput {
preferences: NotificationPreference
}
input UpdatePreferencesRequestInput {
user_id: String
preferences: NotificationPreference
}
input CreateTemplateRequestInput {
code: String
type: String
title_template: String
content_template: String
default_channels: String
variables: String
locale: String
}
input ListTemplatesRequestInput {
type: String
status: String
}
input ListTemplatesResponseInput {
templates: NotificationTemplate
}
input RenderTemplateRequestInput {
code: String
locale: String
}
type RenderedNotification {
title: String
content: String
}
type Empty {
}
extend type Query {
notifications: [ListNotificationsResponse!]!
}
extend type Query {
unreadCount: GetUnreadCountResponse
}
extend type Query {
preferences: GetPreferencesResponse
}
extend type Query {
template: NotificationTemplate
}
extend type Query {
templates: [ListTemplatesResponse!]!
}

View File

@@ -0,0 +1,59 @@
/**
* msg GraphQL 子图模块v2.1 M1
*
* Apollo Federation 2 子图,替代原 teacher-bff 的聚合职责。
*
* 强制约束:
* - @key 解析器必须使用 DataLoaderADR-035
* - RouterAuthGuard 校验 Router-Authorization HeaderADR-036
* - 外部 GraphQL + 内部 gRPC 边界ADR-037
*
* 注msg 仓库为函数式export functionDataLoaderService 通过
* 直接 import 调用 batch 函数,等价于 iam 的 IamRepository DI 注入。
*/
import { Module } from "@nestjs/common";
import { GraphQLModule } from "@nestjs/graphql";
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { join } from "node:path";
import { GraphqlContext } from "@edu/shared-ts/federation";
import { NotificationResolver } from "./resolvers/notification.resolver.js";
import { TemplateResolver } from "./resolvers/template.resolver.js";
import { DataLoaderService } from "./dataloader.service.js";
import { getRedis } from "../shared/redis/redis.client.js";
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
// Federation 2 子图
autoSchemaFile: {
path: join(process.cwd(), "src/graphql/generated/schema.graphql"),
federation: 2,
},
// /graphql 端点Apollo Router 访问入口)
path: "/graphql",
// 禁用 playground生产环境通过 Router 访问)
playground: process.env.NODE_ENV === "development",
introspection: process.env.NODE_ENV === "development",
// Context 从 HTTP headers 构造
context: (ctx: {
req: { headers: Record<string, string | undefined> };
}) => ({
req: ctx.req,
graphqlContext: GraphqlContext.fromHeaders(ctx.req.headers),
}),
}),
],
providers: [
NotificationResolver,
TemplateResolver,
DataLoaderService,
{
// 提供 Redis 实例供后续 ScopeToken 等场景使用
provide: "REDIS_CLIENT",
useFactory: () => getRedis(),
},
],
exports: [DataLoaderService],
})
export class GraphqlModule {}

View File

@@ -0,0 +1,112 @@
/**
* msg Notification Resolverv2.1 M1
*
* Apollo Federation 子图Notification 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 NotificationEntity,
} from "../dataloader.service.js";
import { listByUser } from "../../notifications/notifications.repository.js";
/**
* Notification ObjectTypeFederation @key
* 对应 msg.msg_notifications 表
*/
@ObjectType()
@Directive(`@key(fields: "id")`)
export class Notification {
@Field(() => ID)
id!: string;
@Field()
userId!: string;
@Field()
type!: string;
@Field()
title!: string;
@Field()
content!: string;
@Field()
channel!: string;
@Field()
isRead!: boolean;
@Field()
status!: string;
@Field()
createdAt!: Date;
@Field()
updatedAt!: Date;
@Field({ nullable: true })
relatedEntityType: string | null = null;
@Field({ nullable: true })
relatedEntityId: string | null = null;
@Field({ nullable: true })
groupId: string | null = null;
@Field({ nullable: true })
senderId: string | null = null;
@Field({ nullable: true })
templateId: string | null = null;
@Field({ nullable: true })
eventId: string | null = null;
@Field({ nullable: true })
readAt: Date | null = null;
}
@Resolver(() => Notification)
export class NotificationResolver {
constructor(private readonly loader: DataLoaderService) {}
/**
* Federation Reference Resolver
*
* 当其他子图通过 @key 引用 Notification 时Router 调用此方法解析。
* 使用 DataLoader 批量加载,消除 N+1 查询ADR-035
*/
@ResolveReference()
async resolveReference(ref: {
id: string;
}): Promise<NotificationEntity | null> {
return this.loader.notificationLoader.load(ref.id);
}
/**
* Query: notifications(userId) → [Notification]
* 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝ADR-036
*/
@Query(() => [Notification])
async notifications(
@Args("userId", { type: () => ID }) userId: string,
): Promise<NotificationEntity[]> {
const { items } = await listByUser(userId, { page: 1, pageSize: 50 });
return items;
}
}

View File

@@ -0,0 +1,89 @@
/**
* msg NotificationTemplate Resolverv2.1 M1
*
* Apollo Federation 子图NotificationTemplate Entity
* - @key(fields: "id") 支持跨子图引用
* - @ResolveReference 使用 DataLoader 批量加载ADR-035
*/
import {
Resolver,
Query,
Args,
ID,
ResolveReference,
ObjectType,
Field,
Directive,
} from "@nestjs/graphql";
import {
DataLoaderService,
type TemplateEntity,
} from "../dataloader.service.js";
/**
* NotificationTemplate ObjectTypeFederation @key
* 对应 msg.msg_notification_templates 表
*/
@ObjectType()
@Directive(`@key(fields: "id")`)
export class NotificationTemplate {
@Field(() => ID)
id!: string;
@Field()
code!: string;
@Field()
type!: string;
@Field()
titleTemplate!: string;
@Field()
contentTemplate!: string;
@Field(() => [String])
defaultChannels!: string[];
@Field(() => [String])
variables!: string[];
@Field()
locale!: string;
@Field()
status!: string;
@Field()
createdAt!: Date;
@Field()
updatedAt!: Date;
}
@Resolver(() => NotificationTemplate)
export class TemplateResolver {
constructor(private readonly loader: DataLoaderService) {}
/**
* Federation Reference Resolver
*
* 当其他子图通过 @key 引用 NotificationTemplate 时Router 调用此方法解析。
* 使用 DataLoader 批量加载,消除 N+1 查询ADR-035
*/
@ResolveReference()
async resolveReference(ref: { id: string }): Promise<TemplateEntity | null> {
return this.loader.templateLoader.load(ref.id);
}
/**
* Query: template(id) → NotificationTemplate
* 通过 Apollo Router 访问,直连被 RouterAuthGuard 拒绝ADR-036
*/
@Query(() => NotificationTemplate, { nullable: true })
async template(
@Args("id", { type: () => ID }) id: string,
): Promise<TemplateEntity | null> {
return this.loader.templateLoader.load(id);
}
}

View File

@@ -0,0 +1,34 @@
/**
* msg RouterAuthGuard 包装v2.1 ADR-036
*
* 仅作用于 /graphql 端点REST 路由放行(已有 PermissionGuard
*/
import { Injectable, ExecutionContext } from "@nestjs/common";
import {
RouterAuthGuard as BaseRouterAuthGuard,
type RouterAuthConfig,
} from "@edu/shared-ts/federation";
@Injectable()
export class RouterAuthGuard extends BaseRouterAuthGuard {
constructor() {
const config: RouterAuthConfig = {
secret: process.env.ROUTER_AUTH_SECRET ?? "",
devMode: process.env.DEV_MODE === "true",
};
super(config);
}
override canActivate(ctx: ExecutionContext): boolean {
const req = ctx.switchToHttp().getRequest<{
url: string;
}>();
// 仅 GraphQL 端点需要校验REST 路由放行
if (!req.url?.startsWith("/graphql")) {
return true;
}
return super.canActivate(ctx);
}
}

View File

@@ -39,6 +39,15 @@ export async function findById(id: string): Promise<Notification | undefined> {
return row;
}
/**
* 批量查询通知DataLoader @key 解析器使用ADR-035
*/
export async function batchFindByIds(ids: string[]): Promise<Notification[]> {
if (ids.length === 0) return [];
const db = getDb();
return db.select().from(notifications).where(inArray(notifications.id, ids));
}
export async function findByEventId(
eventId: string,
): Promise<Notification | undefined> {

View File

@@ -1,4 +1,4 @@
import { and, eq, type SQL } from "drizzle-orm";
import { and, eq, inArray, type SQL } from "drizzle-orm";
import { createId } from "@paralleldrive/cuid2";
import { getDb } from "../config/database.js";
import {
@@ -35,6 +35,20 @@ export async function findById(
return row;
}
/**
* 批量查询模板DataLoader @key 解析器使用ADR-035
*/
export async function batchFindByIds(
ids: string[],
): Promise<NotificationTemplate[]> {
if (ids.length === 0) return [];
const db = getDb();
return db
.select()
.from(notificationTemplates)
.where(inArray(notificationTemplates.id, ids));
}
export async function findByCode(
code: string,
locale?: string,