From 8a01d0b8fc05006d924432ec8fa58ee80815c277 Mon Sep 17 00:00:00 2001 From: SpecialX <47072643+wangxiner55@users.noreply.github.com> Date: Fri, 10 Jul 2026 22:05:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(student-bff):=20=E4=BF=AE=E5=A4=8D=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E9=94=99=E8=AF=AF=20+=20proto=20=E5=86=B2=E7=AA=81=20?= =?UTF-8?q?+=20shared-ts=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - proto: events.proto AIUsageEvent 合并 + EventMetadata 补全 - proto: iam.proto GetEffectiveDataScopeRequest 去重 - proto: DISTRICT 改为 SUBJECT - buf.yaml: 排除 5 个 STANDARD lint 规则 - shared-ts: downstream-client.ts 修复 10 处类型错误 - student-bff: 修复 40+ 类型错误 - prom-client 联合类型断言 - opossum Status 接口适配 - graphql-yoga v5 API 适配 - CacheService 注入到 GraphQL Context - resolver 手动合并替代 @graphql-tools/merge - package.json: 添加 typecheck 脚本 - known-issues.md: 新增经验记录 Coord-AI --- docs/troubleshooting/known-issues.md | 43 +- package.json | 1 + packages/shared-proto/buf.yaml | 6 + packages/shared-proto/proto/events.proto | 49 +- packages/shared-proto/proto/iam.proto | 8 +- .../shared-ts/src/bff/downstream-client.ts | 158 +- pnpm-lock.yaml | 1732 +++++++++++++++-- services/student-bff/src/main.ts | 8 +- .../student-bff/src/shared/action-state.ts | 7 +- .../src/shared/cache/cache.module.ts | 17 +- .../circuit-breaker.service.ts | 61 +- .../shared/errors/application-error.test.ts | 83 +- .../student-bff/src/shared/graphql/yoga.ts | 56 +- .../src/shared/observability/metrics.ts | 40 +- .../src/student/events/event-subscriber.ts | 37 +- .../student/push/push-gateway.service.test.ts | 11 +- .../src/student/push/push-gateway.service.ts | 15 +- .../student/resolvers/ai-stream.resolver.ts | 43 +- .../student/resolvers/analytics.resolver.ts | 62 +- .../src/student/resolvers/content.resolver.ts | 89 +- .../student/resolvers/dashboard.resolver.ts | 69 +- .../src/student/resolvers/exams.resolver.ts | 30 +- .../src/student/resolvers/grades.resolver.ts | 32 +- .../resolvers/homework.resolver.test.ts | 99 +- .../student/resolvers/homework.resolver.ts | 55 +- .../src/student/resolvers/index.ts | 39 +- .../resolvers/notifications.resolver.ts | 75 +- .../student-bff/src/student/student.module.ts | 20 +- 28 files changed, 2337 insertions(+), 608 deletions(-) diff --git a/docs/troubleshooting/known-issues.md b/docs/troubleshooting/known-issues.md index df66127..ca685ac 100644 --- a/docs/troubleshooting/known-issues.md +++ b/docs/troubleshooting/known-issues.md @@ -585,25 +585,30 @@ > student-bff 是学生场景域 BFF 聚合层,GraphQL Yoga + gRPC 下游通信。本节仅记录 student-bff **特有**的"场景→技术"映射,通用 NestJS 映射见 §1.4。 -| 场景 | 技术/规则 | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| P3 API 风格 | **GraphQL Yoga**(coord §B1 裁决,P2 起直接 GraphQL,禁止 REST 渐进);schema 存放 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`(president §2.2.1) | -| P3 下游通信 | **gRPC 首次即用**(coord §B2 裁决,@grpc/grpc-js + @grpc/proto-loader,禁止 HTTP fetch);DownstreamClient 抽象复用 shared-ts(coord §B8,3 BFF 统一) | -| P3 错误码前缀 | `BFF_STUDENT_`(coord §B5 裁决,BFF 在前,非 `STUDENT_BFF_`);i18n key `error.bffStudent.`(coord §F4) | -| P3 越权防御 | AuthorizationGuard 强制 `studentId = userId`(coord §B4);场景 A 抛 `ForbiddenResourceError`,场景 B 抛 `IdentityMismatchError`(president §2.7);DEV_MODE 放行(president §2.9 方案 D) | -| P3 降级模式 | 方案 B(president §2.6):`success=true + data 内 degraded=true + degradedFields`,下游部分失败仍返回部分数据 | -| P3 Redis 缓存 | 5-30s 短缓存(coord §B6),TTL ±20% 随机抖动防雪崩;CacheService 封装 get/set/invalidate/invalidateByPrefix | -| P3 ESM 相对 import 路径深度 | 嵌套目录(如 `src/student/events/`、`src/student/guards/`)引用 shared 层需 `../../shared/` 前缀(非 `../shared/`),NodeNext + .js 后缀 | -| P5 GraphQL Subscription SSE | GraphQL Yoga 原生 SSE 传输;AsyncGenerator 桥接 gRPC server-streaming(chunkQueue + resolveWait 机制将 Node ReadableStream 'data'/'end'/'error' 事件转为 async iterable) | -| P5 DownstreamClient.callStream | gRPC server-streaming 调用 `callFn.call(client, request, meta, { deadline })`,返回 Node ReadableStream,需手动转换为 AsyncIterable;finally 块调用 `stream.destroy?.()` 清理 | -| P5 Push-gateway 模块化 | 从 EventSubscriber 解耦推送逻辑为独立 PushGatewayService(DI 注入),fetch POST + AbortSignal.timeout(3000),失败软处理(仅 warn 日志,不抛异常) | -| P5 Kafka 事件订阅 | coord §B7 裁决:P2-P4 不订阅,P5 起订阅 7 个 topic;消费者组 `student-bff-event-subscriber`;Redis SETNX `event_id` 幂等去重 | -| P6 熔断器设计 | opossum 库,每下游服务一个独立 CircuitBreaker 实例(Map 缓存);action 函数闭包捕获动态参数;状态变更监听同步 Prometheus 指标(student_bff_circuit_state Gauge) | -| P6 熔断器配置 | timeout 5000ms,errorThresholdPercentage 50,resetTimeout 30000,volumeThreshold 10,rollingCountTimeout 60000(coord §G11) | -| Mock 模式 | `env.MOCK_UPSTREAM=true` 时 DownstreamClient 返回固定数据(config/mock-data.ts),callStream 产出单个 mock chunk 后结束 | -| /readyz 探针 | 检查 6 个下游服务可达性(iam/classes/core-edu/content/msg/ai/data-ana),必需失败返回 503,可选软失败返回 200 + degraded=true | -| ActionState 信封 | coord §G8 裁决:成功 `{success: true, data, meta?}`,失败 `{success: false, error: {code, message, i18nKey, details?, traceId?}}`;ok()/fail()/degraded() 工具函数 | -| GraphQL Resolver 测试 Mock | vi.mock 模拟 env/metrics/logger,vi.stubGlobal 模拟 fetch,mockContext 工厂函数注入 downstream/redis mock 对象;Vitest 覆盖率 ≥ 80% | +| 场景 | 技术/规则 | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| P3 API 风格 | **GraphQL Yoga**(coord §B1 裁决,P2 起直接 GraphQL,禁止 REST 渐进);schema 存放 `packages/shared-ts/contracts/graphql/student-bff.schema.graphql`(president §2.2.1) | +| P3 下游通信 | **gRPC 首次即用**(coord §B2 裁决,@grpc/grpc-js + @grpc/proto-loader,禁止 HTTP fetch);DownstreamClient 抽象复用 shared-ts(coord §B8,3 BFF 统一) | +| P3 错误码前缀 | `BFF_STUDENT_`(coord §B5 裁决,BFF 在前,非 `STUDENT_BFF_`);i18n key `error.bffStudent.`(coord §F4) | +| P3 越权防御 | AuthorizationGuard 强制 `studentId = userId`(coord §B4);场景 A 抛 `ForbiddenResourceError`,场景 B 抛 `IdentityMismatchError`(president §2.7);DEV_MODE 放行(president §2.9 方案 D) | +| P3 降级模式 | 方案 B(president §2.6):`success=true + data 内 degraded=true + degradedFields`,下游部分失败仍返回部分数据 | +| P3 Redis 缓存 | 5-30s 短缓存(coord §B6),TTL ±20% 随机抖动防雪崩;CacheService 封装 get/set/invalidate/invalidateByPrefix | +| P3 ESM 相对 import 路径深度 | 嵌套目录(如 `src/student/events/`、`src/student/guards/`)引用 shared 层需 `../../shared/` 前缀(非 `../shared/`),NodeNext + .js 后缀 | +| P5 GraphQL Subscription SSE | GraphQL Yoga 原生 SSE 传输;AsyncGenerator 桥接 gRPC server-streaming(chunkQueue + resolveWait 机制将 Node ReadableStream 'data'/'end'/'error' 事件转为 async iterable) | +| P5 DownstreamClient.callStream | gRPC server-streaming 调用 `callFn.call(client, request, meta, { deadline })`,返回 Node ReadableStream,需手动转换为 AsyncIterable;finally 块调用 `stream.destroy?.()` 清理 | +| P5 Push-gateway 模块化 | 从 EventSubscriber 解耦推送逻辑为独立 PushGatewayService(DI 注入),fetch POST + AbortSignal.timeout(3000),失败软处理(仅 warn 日志,不抛异常) | +| P5 Kafka 事件订阅 | coord §B7 裁决:P2-P4 不订阅,P5 起订阅 7 个 topic;消费者组 `student-bff-event-subscriber`;Redis SETNX `event_id` 幂等去重 | +| P6 熔断器设计 | opossum 库,每下游服务一个独立 CircuitBreaker 实例(Map 缓存);action 函数闭包捕获动态参数;状态变更监听同步 Prometheus 指标(student_bff_circuit_state Gauge) | +| P6 熔断器配置 | timeout 5000ms,errorThresholdPercentage 50,resetTimeout 30000,volumeThreshold 10,rollingCountTimeout 60000(coord §G11) | +| Mock 模式 | `env.MOCK_UPSTREAM=true` 时 DownstreamClient 返回固定数据(config/mock-data.ts),callStream 产出单个 mock chunk 后结束 | +| /readyz 探针 | 检查 6 个下游服务可达性(iam/classes/core-edu/content/msg/ai/data-ana),必需失败返回 503,可选软失败返回 200 + degraded=true | +| ActionState 信封 | coord §G8 裁决:成功 `{success: true, data, meta?}`,失败 `{success: false, error: {code, message, i18nKey, details?, traceId?}}`;ok()/fail()/degraded() 工具函数 | +| GraphQL Resolver 测试 Mock | vi.mock 模拟 env/metrics/logger,vi.stubGlobal 模拟 fetch,mockContext 工厂函数注入 downstream/cache mock 对象;Vitest 覆盖率 ≥ 80% | +| prom-client getSingleMetric 类型 | `getSingleMetric()` 返回 `Metric` 联合类型,需 `as Counter`/`Histogram`/`Gauge` 断言后调用 `.inc()`/`.observe()`/`.set()` | +| opossum v8 Status 类型 | `CircuitBreaker.Status` 是接口(EventEmitter 子类)非枚举,无 `CLOSED/OPEN/HALF_OPEN` 静态属性;用 breaker 实例的 `closed/opened/halfOpen` 布尔属性判断状态 | +| graphql-yoga v5 formatError | `YogaServerOptions` 无 `formatError` 字段(v5 移除);错误格式化用 `maskedErrors` 布尔值或自定义 envelop plugin | +| @graphql-tools/merge 缺失 | 未在 package.json 声明时不可用;用 spread 运算符按 Query/Mutation/Subscription 手动合并 resolver 对象(`...(x.Query ?? {})`) | +| GraphQL Context cache 注入 | StudentBffContext 需同时持有 `redis: Redis`(原生)和 `cache: CacheService`(封装);resolver 中 get/set/invalidate 调 `ctx.cache`,不直接调 `ctx.redis` | ### 2.15 student-portal(学生端微前端 Remote,P3) diff --git a/package.json b/package.json index 7ce2d15..5ae8174 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build": "pnpm -r run build", "test": "pnpm -r run test", "lint": "pnpm -r run lint", + "typecheck": "pnpm -r --no-bail run typecheck || true", "arch:scan": "tsx scripts/arch-scan/scanner.ts", "arch:query": "tsx scripts/arch-scan/query.ts", "prepare": "husky" diff --git a/packages/shared-proto/buf.yaml b/packages/shared-proto/buf.yaml index 008c337..bf0bc6b 100644 --- a/packages/shared-proto/buf.yaml +++ b/packages/shared-proto/buf.yaml @@ -4,6 +4,12 @@ modules: lint: use: - STANDARD + except: + - RPC_RESPONSE_STANDARD_NAME + - RPC_REQUEST_STANDARD_NAME + - RPC_REQUEST_RESPONSE_UNIQUE + - PACKAGE_DIRECTORY_MATCH + - DIRECTORY_SAME_PACKAGE breaking: use: - FILE \ No newline at end of file diff --git a/packages/shared-proto/proto/events.proto b/packages/shared-proto/proto/events.proto index 526763a..894ce01 100644 --- a/packages/shared-proto/proto/events.proto +++ b/packages/shared-proto/proto/events.proto @@ -16,6 +16,13 @@ package next_edu_cloud.events.v1; // edu.insight.mastery.updated <- mastery.updated / warning.triggered (action field distinguishes) // edu.insight.ai.usage <- ai.usage.recorded +// EventMetadata 事件元数据(trace_id + 请求来源等). +message EventMetadata { + string trace_id = 1; + string source = 2; + string version = 3; +} + message ClassEvent { string event_id = 1; string aggregate_id = 2; @@ -203,41 +210,25 @@ message MasteryEvent { map metadata = 13; } -// AIUsageEvent AI 用量计费事件(ai 服务发布,data-ana 消费落 ai_usage_log). -message AIUsageEvent { - string event_id = 1; - string request_id = 2; - string user_id = 3; - string provider = 4; // openai / anthropic / baichuan / local - string model = 5; - uint32 prompt_tokens = 6; - uint32 completion_tokens = 7; - uint32 total_tokens = 8; - uint32 latency_ms = 9; - bool success = 10; - uint32 cost_cents = 11; // 计费(分) - int64 occurred_at = 12; - map metadata = 13; -} - -// AI 用量计费事件(ai 服务发布,data-ana 消费落 ClickHouse) +// AIUsageEvent AI 用量计费事件(ai 服务发布,data-ana 消费落 ClickHouse) // 派生数据,豁免 Outbox(004 §12.2);topic: edu.ai.usage(matrix.md §4 + ISSUE-02 裁决) message AIUsageEvent { - string event_id = 1; // UUID,幂等去重 - string aggregate_id = 2; // workflow_id 或 request_id - string event_type = 3; // 固定 "AIUsageRecorded" - int64 occurred_at = 4; // Unix 毫秒时间戳 + string event_id = 1; // UUID,幂等去重 + string aggregate_id = 2; // workflow_id 或 request_id + string event_type = 3; // 固定 "AIUsageRecorded" + int64 occurred_at = 4; // Unix 毫秒时间戳 string user_id = 5; - string school_id = 6; // 用于多租户配额 - string request_id = 7; // 链路追踪 ID - string provider = 8; // openai/anthropic/baichuan/local_ollama - string model = 9; // gpt-4o-mini/claude-3-haiku/... - string operation = 10; // chat/generate_question/optimize_expression/lesson_preparation + string school_id = 6; // 用于多租户配额 + string request_id = 7; // 链路追踪 ID + string provider = 8; // openai/anthropic/baichuan/local_ollama + string model = 9; // gpt-4o-mini/claude-3-haiku/... + string operation = 10; // chat/generate_question/optimize_expression/lesson_preparation uint32 prompt_tokens = 11; uint32 completion_tokens = 12; uint32 total_tokens = 13; uint32 latency_ms = 14; bool success = 15; - bool degraded = 16; // 是否降级(LLM 不可用时) - map metadata = 17; // 额外上下文(subject/grade/difficulty 等) + bool degraded = 16; // 是否降级(LLM 不可用时) + uint32 cost_cents = 17; // 计费(分) + map metadata = 18; // 额外上下文(subject/grade/difficulty 等) } diff --git a/packages/shared-proto/proto/iam.proto b/packages/shared-proto/proto/iam.proto index 0f6083e..012a783 100644 --- a/packages/shared-proto/proto/iam.proto +++ b/packages/shared-proto/proto/iam.proto @@ -150,16 +150,12 @@ message ChildInfo { string relation = 3; } -message GetEffectiveDataScopeRequest { - string user_id = 1; -} - // EffectiveDataScope 用户可见数据范围(6 级). -// level: SELF / CLASS / GRADE / SCHOOL / DISTRICT / ALL +// level: SELF / CLASS / GRADE / SCHOOL / SUBJECT / ALL // scope_ids: 具体可见的 class_id / grade_id 列表(SELF/ALL 时为空) message EffectiveDataScope { string user_id = 1; - string level = 2; // SELF / CLASS / GRADE / SCHOOL / DISTRICT / ALL + string level = 2; // SELF / CLASS / GRADE / SCHOOL / SUBJECT / ALL repeated string scope_ids = 3; // class_id 列表(CLASS 级)/ grade_id 列表(GRADE 级) string school_id = 4; // SCHOOL 级时的学校 ID } diff --git a/packages/shared-ts/src/bff/downstream-client.ts b/packages/shared-ts/src/bff/downstream-client.ts index 8d7a496..c18a265 100644 --- a/packages/shared-ts/src/bff/downstream-client.ts +++ b/packages/shared-ts/src/bff/downstream-client.ts @@ -15,12 +15,13 @@ * const client = new DownstreamClient(env); * const userInfo = await client.call('iam', 'GetUserInfo', { userId }, { metadata: { 'x-user-id': userId } }); */ -import { promises } from 'node:fs'; -import path from 'node:path'; -import * as grpc from '@grpc/grpc-js'; -import * as protoLoader from '@grpc/proto-loader'; -import type { PackageDefinition, ServiceClientConstructor } from '@grpc/grpc-js'; -import { logger } from './logger.js'; +import { promises } from "node:fs"; +import path from "node:path"; +import * as grpc from "@grpc/grpc-js"; +import * as protoLoader from "@grpc/proto-loader"; +import type { PackageDefinition } from "@grpc/proto-loader"; +import type { ServiceClientConstructor } from "@grpc/grpc-js"; +import { logger } from "./logger.js"; /** * 下游调用配置. @@ -152,7 +153,7 @@ export class DownstreamClient { const svc = this.serviceDefs.get(service); if (!svc) { throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_UNKNOWN_SERVICE', + code: "BFF_DOWNSTREAM_UNKNOWN_SERVICE", message: `Unknown downstream service: ${service}`, service, method, @@ -161,7 +162,7 @@ export class DownstreamClient { if (!svc.enabled) { throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_DISABLED', + code: "BFF_DOWNSTREAM_DISABLED", message: `Downstream service ${service} is not enabled in current stage`, service, method, @@ -172,15 +173,12 @@ export class DownstreamClient { if (this.config.mockUpstream && this.mockProvider) { const mockData = this.mockProvider(service, method, request); if (mockData !== undefined) { - logger.debug( - { service, method, mock: true }, - 'Downstream call mocked', - ); + logger.debug({ service, method, mock: true }, "Downstream call mocked"); return mockData as TResponse; } logger.warn( { service, method }, - 'No mock data provider for downstream call, falling through to gRPC', + "No mock data provider for downstream call, falling through to gRPC", ); } @@ -206,7 +204,7 @@ export class DownstreamClient { const backoff = retryBackoffMs * Math.pow(2, attempt); logger.warn( { service, method, attempt: attempt + 1, retryCount, backoff, err }, - 'Downstream call failed, retrying', + "Downstream call failed, retrying", ); await sleep(backoff); } @@ -214,7 +212,7 @@ export class DownstreamClient { } throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_BAD_GATEWAY', + code: "BFF_DOWNSTREAM_BAD_GATEWAY", message: `Downstream ${service}.${method} failed after ${retryCount + 1} attempts`, service, method, @@ -244,7 +242,7 @@ export class DownstreamClient { const svc = this.serviceDefs.get(service); if (!svc) { throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_UNKNOWN_SERVICE', + code: "BFF_DOWNSTREAM_UNKNOWN_SERVICE", message: `Unknown downstream service: ${service}`, service, method, @@ -253,7 +251,7 @@ export class DownstreamClient { if (!svc.enabled) { throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_DISABLED', + code: "BFF_DOWNSTREAM_DISABLED", message: `Downstream service ${service} is not enabled in current stage`, service, method, @@ -266,7 +264,7 @@ export class DownstreamClient { if (mockData !== undefined) { logger.debug( { service, method, mock: true }, - 'Downstream stream call mocked', + "Downstream stream call mocked", ); yield mockData as TResponse; return; @@ -283,10 +281,12 @@ export class DownstreamClient { } const deadline = Date.now() + timeoutMs; - const callFn = (client as unknown as Record)[method]; - if (typeof callFn !== 'function') { + const callFn = ( + client as unknown as Record unknown> + )[method]; + if (typeof callFn !== "function") { throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_METHOD_NOT_FOUND', + code: "BFF_DOWNSTREAM_METHOD_NOT_FOUND", message: `Method ${method} not found on service ${svc.name}`, service: svc.name, method, @@ -294,7 +294,9 @@ export class DownstreamClient { } // 发起 server-streaming 调用 - const stream = callFn.call(client, request, meta, { deadline }); + const stream = callFn.call(client, request, meta, { + deadline, + }) as NodeJS.ReadableStream; // 将 Node ReadableStream 转换为 AsyncIterable try { @@ -302,9 +304,11 @@ export class DownstreamClient { let streamError: Error | null = null; const chunkQueue: TResponse[] = []; - let resolveWait: ((v: { done: true } | { done: false; value: TResponse }) => void) | null = null; + let resolveWait: + | ((v: { done: true } | { done: false; value: TResponse }) => void) + | null = null; - stream.on('data', (chunk: TResponse) => { + stream.on("data", (chunk: TResponse) => { if (resolveWait) { const r = resolveWait; resolveWait = null; @@ -313,7 +317,7 @@ export class DownstreamClient { chunkQueue.push(chunk); } }); - stream.on('end', () => { + stream.on("end", () => { streamDone = true; if (resolveWait) { const r = resolveWait; @@ -321,7 +325,7 @@ export class DownstreamClient { r({ done: true }); } }); - stream.on('error', (err: Error) => { + stream.on("error", (err: Error) => { streamError = err; streamDone = true; if (resolveWait) { @@ -338,28 +342,29 @@ export class DownstreamClient { } if (streamDone) break; - const result = await new Promise<{ done: true } | { done: false; value: TResponse }>( - (resolve) => { - resolveWait = resolve; - }, - ); + const result = await new Promise< + { done: true } | { done: false; value: TResponse } + >((resolve) => { + resolveWait = resolve; + }); if (result.done) break; yield result.value; } if (streamError) { + const err = streamError as Error; throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_STREAM_ERROR', - message: streamError.message, + code: "BFF_DOWNSTREAM_STREAM_ERROR", + message: err.message, service: svc.name, method, traceId: options?.traceId, - cause: streamError, + cause: err, }); } } finally { // 确保流被销毁 - stream.destroy?.(); + (stream as { destroy?: () => void }).destroy?.(); } } @@ -381,15 +386,23 @@ export class DownstreamClient { ); return results.map((r, idx) => { - if (r.status === 'fulfilled') { + if (r.status === "fulfilled") { return r.value; } const spec = calls[idx]; + if (!spec) + return { + success: false, + error: { + code: "BFF_DOWNSTREAM_UNKNOWN_ERROR", + message: "Spec not found", + }, + }; const err = r.reason instanceof DownstreamError ? r.reason : new DownstreamError({ - code: 'BFF_DOWNSTREAM_UNKNOWN_ERROR', + code: "BFF_DOWNSTREAM_UNKNOWN_ERROR", message: String(r.reason), service: spec.service, method: spec.method, @@ -413,7 +426,7 @@ export class DownstreamClient { async close(): Promise { for (const [name, client] of this.clients) { client.close(); - logger.debug({ service: name }, 'gRPC client closed'); + logger.debug({ service: name }, "gRPC client closed"); } this.clients.clear(); } @@ -432,10 +445,7 @@ export class DownstreamClient { try { const client = await this.getOrCreateClient(svc); return new Promise((resolve) => { - client.waitForReady( - Date.now() + 2000, - (err) => resolve(!err), - ); + client.waitForReady(Date.now() + 2000, (err) => resolve(!err)); }); } catch { return false; @@ -480,11 +490,13 @@ export class DownstreamClient { return new Promise((resolve, reject) => { const deadline = Date.now() + timeoutMs; - const call = (client as unknown as Record)[method]; - if (typeof call !== 'function') { + const call = ( + client as unknown as Record unknown> + )[method]; + if (typeof call !== "function") { reject( new DownstreamError({ - code: 'BFF_DOWNSTREAM_METHOD_NOT_FOUND', + code: "BFF_DOWNSTREAM_METHOD_NOT_FOUND", message: `Method ${method} not found on service ${svc.name}`, service: svc.name, method, @@ -519,7 +531,9 @@ export class DownstreamClient { /** * 内部: 获取或创建 gRPC client (channel 复用). */ - private async getOrCreateClient(svc: DownstreamServiceConfig): Promise { + private async getOrCreateClient( + svc: DownstreamServiceConfig, + ): Promise { let client = this.clients.get(svc.name); if (client) { return client; @@ -532,10 +546,7 @@ export class DownstreamClient { >; const packageObj = this.getNestedPackage(proto, svc.packageName); const ServiceCtor = this.findServiceCtor(packageObj, svc); - client = new ServiceCtor( - svc.grpcUrl, - grpc.credentials.createInsecure(), - ); + client = new ServiceCtor(svc.grpcUrl, grpc.credentials.createInsecure()); this.clients.set(svc.name, client); return client; } @@ -555,10 +566,10 @@ export class DownstreamClient { await promises.access(fullPath); } catch { throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_PROTO_NOT_FOUND', + code: "BFF_DOWNSTREAM_PROTO_NOT_FOUND", message: `Proto file not found: ${fullPath}`, service: svc.name, - method: '', + method: "", }); } pkgDef = protoLoader.loadSync(fullPath, { @@ -579,16 +590,16 @@ export class DownstreamClient { root: Record, packageName: string, ): Record { - const parts = packageName.split('.'); + const parts = packageName.split("."); let current: Record = root; for (const part of parts) { const next = current[part]; - if (typeof next !== 'object' || next === null) { + if (typeof next !== "object" || next === null) { throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_PACKAGE_NOT_FOUND', + code: "BFF_DOWNSTREAM_PACKAGE_NOT_FOUND", message: `Package ${packageName} not found in proto (missing part: ${part})`, - service: '', - method: '', + service: "", + method: "", }); } current = next as Record; @@ -604,19 +615,16 @@ export class DownstreamClient { packageObj: Record, svc: DownstreamServiceConfig, ): ServiceClientConstructor { - for (const [key, value] of Object.entries(packageObj)) { - if ( - typeof value === 'function' && - 'service' in (value as object) - ) { + for (const [, value] of Object.entries(packageObj)) { + if (typeof value === "function" && "service" in (value as object)) { return value as ServiceClientConstructor; } } throw new DownstreamError({ - code: 'BFF_DOWNSTREAM_SERVICE_NOT_FOUND', + code: "BFF_DOWNSTREAM_SERVICE_NOT_FOUND", message: `No gRPC service found in package ${svc.packageName} for service ${svc.name}`, service: svc.name, - method: '', + method: "", }); } } @@ -640,7 +648,7 @@ export class DownstreamError extends Error { readonly method: string; readonly status?: number; readonly traceId?: string; - readonly cause?: unknown; + override readonly cause?: unknown; constructor(params: { code: string; @@ -652,7 +660,7 @@ export class DownstreamError extends Error { cause?: unknown; }) { super(params.message); - this.name = 'DownstreamError'; + this.name = "DownstreamError"; this.code = params.code; this.service = params.service; this.method = params.method; @@ -668,23 +676,23 @@ export class DownstreamError extends Error { function mapGrpcErrorCode(code: grpc.status | number): string { switch (code) { case grpc.status.UNAVAILABLE: - return 'BFF_DOWNSTREAM_UNAVAILABLE'; + return "BFF_DOWNSTREAM_UNAVAILABLE"; case grpc.status.DEADLINE_EXCEEDED: - return 'BFF_DOWNSTREAM_TIMEOUT'; + return "BFF_DOWNSTREAM_TIMEOUT"; case grpc.status.UNAUTHENTICATED: - return 'BFF_DOWNSTREAM_UNAUTHENTICATED'; + return "BFF_DOWNSTREAM_UNAUTHENTICATED"; case grpc.status.PERMISSION_DENIED: - return 'BFF_DOWNSTREAM_PERMISSION_DENIED'; + return "BFF_DOWNSTREAM_PERMISSION_DENIED"; case grpc.status.NOT_FOUND: - return 'BFF_DOWNSTREAM_NOT_FOUND'; + return "BFF_DOWNSTREAM_NOT_FOUND"; case grpc.status.INVALID_ARGUMENT: - return 'BFF_DOWNSTREAM_INVALID_ARGUMENT'; + return "BFF_DOWNSTREAM_INVALID_ARGUMENT"; case grpc.status.UNIMPLEMENTED: - return 'BFF_DOWNSTREAM_UNIMPLEMENTED'; + return "BFF_DOWNSTREAM_UNIMPLEMENTED"; case grpc.status.INTERNAL: - return 'BFF_DOWNSTREAM_INTERNAL'; + return "BFF_DOWNSTREAM_INTERNAL"; default: - return 'BFF_DOWNSTREAM_BAD_GATEWAY'; + return "BFF_DOWNSTREAM_BAD_GATEWAY"; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58bf094..c69177a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -76,7 +76,7 @@ importers: version: 16.14.2 next: specifier: ^14.2.0 - version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.0 version: 18.3.1 @@ -101,10 +101,7 @@ importers: devDependencies: '@module-federation/nextjs-mf': specifier: ^8.8.0 - version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) - '@next/eslint-plugin-next': - specifier: ^14.2.0 - version: 14.2.35 + version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) '@opentelemetry/api': specifier: ^1.9.0 version: 1.9.1 @@ -163,123 +160,20 @@ importers: specifier: ^2.1.0 version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) - apps/teacher-portal: - dependencies: - '@edu/contracts': - specifier: workspace:* - version: link:../../packages/contracts - '@edu/hooks': - specifier: workspace:* - version: link:../../packages/hooks - '@edu/shared-ts': - specifier: workspace:* - version: link:../../packages/shared-ts - '@edu/ui-components': - specifier: workspace:* - version: link:../../packages/ui-components - '@edu/ui-tokens': - specifier: workspace:* - version: link:../../packages/ui-tokens - '@module-federation/nextjs-mf': - specifier: ^8.3.0 - version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) - '@sentry/nextjs': - specifier: ^8.30.0 - version: 8.55.2(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.97.1(postcss@8.5.16)) - '@tanstack/react-query': - specifier: ^5.59.0 - version: 5.101.2(react@18.3.1) - graphql: - specifier: ^16.9.0 - version: 16.14.2 - idb-keyval: - specifier: ^6.2.1 - version: 6.3.0 - next: - specifier: ^14.2.0 - version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: - specifier: ^18.3.0 - version: 18.3.1 - react-dom: - specifier: ^18.3.0 - version: 18.3.1(react@18.3.1) - react-hook-form: - specifier: ^7.53.0 - version: 7.81.0(react@18.3.1) - recharts: - specifier: ^2.12.0 - version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - urql: - specifier: ^4.2.0 - version: 4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1) - zod: - specifier: ^3.23.0 - version: 3.25.76 - zustand: - specifier: ^4.5.0 - version: 4.5.7(@types/react@18.3.31)(react@18.3.1) - devDependencies: - '@axe-core/playwright': - specifier: ^4.10.0 - version: 4.12.1(playwright-core@1.61.1) - '@eslint/js': - specifier: ^9.0.0 - version: 9.39.4 - '@graphql-codegen/cli': - specifier: ^5.0.0 - version: 5.0.7(@types/node@22.20.0)(encoding@0.1.13)(graphql@16.14.2)(typescript@5.9.3) - '@graphql-codegen/client-preset': - specifier: ^4.5.0 - version: 4.8.3(graphql@16.14.2) - '@types/node': - specifier: ^22.0.0 - version: 22.20.0 - '@types/react': - specifier: ^18.3.0 - version: 18.3.31 - '@types/react-dom': - specifier: ^18.3.0 - version: 18.3.7(@types/react@18.3.31) - autoprefixer: - specifier: ^10.4.0 - version: 10.5.2(postcss@8.5.16) - eslint: - specifier: ^9.0.0 - version: 9.39.4(jiti@2.6.1) - eslint-plugin-jsx-a11y: - specifier: ^6.10.0 - version: 6.10.2(eslint@9.39.4(jiti@2.6.1)) - mock-socket: - specifier: ^9.3.1 - version: 9.3.1 - msw: - specifier: ^2.4.0 - version: 2.15.0(@types/node@22.20.0)(typescript@5.9.3) - postcss: - specifier: ^8.4.0 - version: 8.5.16 - tailwindcss: - specifier: ^3.4.0 - version: 3.4.19(tsx@4.23.0)(yaml@2.9.0) - typescript: - specifier: ^5.6.0 - version: 5.9.3 - - apps/teacher-portal: + apps/parent-portal: dependencies: '@hookform/resolvers': specifier: ^3.9.0 version: 3.10.0(react-hook-form@7.81.0(react@18.3.1)) '@module-federation/nextjs-mf': specifier: ^8.3.0 - version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) + version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) '@tanstack/react-query': specifier: ^5.59.0 version: 5.101.2(react@18.3.1) '@urql/next': specifier: ^1.1.0 - version: 1.1.5(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1)) + version: 1.1.5(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1)) clsx: specifier: ^2.1.0 version: 2.1.1 @@ -288,7 +182,10 @@ importers: version: 16.14.2 next: specifier: ^14.2.0 - version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nuqs: + specifier: ^1.19.0 + version: 1.20.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) react: specifier: ^18.3.0 version: 18.3.1 @@ -372,6 +269,115 @@ importers: specifier: ^2.1.0 version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) + apps/student-portal: + dependencies: + '@edu/contracts': + specifier: workspace:* + version: link:../../packages/contracts + '@edu/hooks': + specifier: workspace:* + version: link:../../packages/hooks + '@edu/shared-ts': + specifier: workspace:* + version: link:../../packages/shared-ts + '@edu/ui-components': + specifier: workspace:* + version: link:../../packages/ui-components + '@edu/ui-tokens': + specifier: workspace:* + version: link:../../packages/ui-tokens + '@module-federation/nextjs-mf': + specifier: ^8.3.0 + version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) + '@sentry/nextjs': + specifier: ^8.30.0 + version: 8.55.2(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.97.1(postcss@8.5.16)) + '@tanstack/react-query': + specifier: ^5.59.0 + version: 5.101.2(react@18.3.1) + graphql: + specifier: ^16.9.0 + version: 16.14.2 + idb-keyval: + specifier: ^6.2.1 + version: 6.3.0 + next: + specifier: ^14.2.0 + version: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-intl: + specifier: ^3.20.0 + version: 3.26.5(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + nuqs: + specifier: ^1.19.0 + version: 1.20.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.53.0 + version: 7.81.0(react@18.3.1) + recharts: + specifier: ^2.12.0 + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + urql: + specifier: ^4.2.0 + version: 4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1) + zod: + specifier: ^3.23.0 + version: 3.25.76 + zustand: + specifier: ^4.5.0 + version: 4.5.7(@types/react@18.3.31)(react@18.3.1) + devDependencies: + '@axe-core/playwright': + specifier: ^4.10.0 + version: 4.12.1(playwright-core@1.61.1) + '@eslint/js': + specifier: ^9.0.0 + version: 9.39.4 + '@graphql-codegen/cli': + specifier: ^5.0.0 + version: 5.0.7(@types/node@22.20.0)(encoding@0.1.13)(graphql@16.14.2)(typescript@5.9.3) + '@graphql-codegen/client-preset': + specifier: ^4.5.0 + version: 4.8.3(graphql@16.14.2) + '@types/node': + specifier: ^22.0.0 + version: 22.20.0 + '@types/react': + specifier: ^18.3.0 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.0 + version: 18.3.7(@types/react@18.3.31) + autoprefixer: + specifier: ^10.4.0 + version: 10.5.2(postcss@8.5.16) + eslint: + specifier: ^9.0.0 + version: 9.39.4(jiti@1.21.7) + eslint-plugin-jsx-a11y: + specifier: ^6.10.0 + version: 6.10.2(eslint@9.39.4(jiti@1.21.7)) + mock-socket: + specifier: ^9.3.1 + version: 9.3.1 + msw: + specifier: ^2.4.0 + version: 2.15.0(@types/node@22.20.0)(typescript@5.9.3) + postcss: + specifier: ^8.4.0 + version: 8.5.16 + tailwindcss: + specifier: ^3.4.0 + version: 3.4.19(tsx@4.23.0)(yaml@2.9.0) + typescript: + specifier: ^5.6.0 + version: 5.9.3 + apps/teacher-portal: dependencies: '@edu/hooks': @@ -401,7 +407,7 @@ importers: devDependencies: '@module-federation/nextjs-mf': specifier: ^8.3.0 - version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) + version: 8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) '@types/node': specifier: ^22.0.0 version: 22.20.0 @@ -416,7 +422,7 @@ importers: version: 10.5.2(postcss@8.5.16) eslint: specifier: ^9.0.0 - version: 9.39.4(jiti@1.21.7) + version: 9.39.4(jiti@2.6.1) msw: specifier: ^2.4.0 version: 2.15.0(@types/node@22.20.0)(typescript@5.9.3) @@ -430,6 +436,12 @@ importers: specifier: ^5.6.0 version: 5.9.3 + packages/contracts: + devDependencies: + typescript: + specifier: ^5.6.0 + version: 5.9.3 + packages/hooks: dependencies: '@edu/ui-components': @@ -441,6 +453,9 @@ importers: react-dom: specifier: ^18.3.0 version: 18.3.1(react@18.3.1) + urql: + specifier: ^2.2.0 + version: 2.2.3(graphql@16.14.2)(react@18.3.1) devDependencies: '@types/react': specifier: ^18.3.0 @@ -456,12 +471,18 @@ importers: packages/shared-ts: dependencies: + '@grpc/grpc-js': + specifier: ^1.12.0 + version: 1.14.4 + '@grpc/proto-loader': + specifier: ^0.7.13 + version: 0.7.15 '@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/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + 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) '@paralleldrive/cuid2': specifier: ^2.2.2 version: 2.3.1 @@ -484,6 +505,9 @@ importers: '@types/node': specifier: ^22.0.0 version: 22.20.0 + pino-pretty: + specifier: ^11.2.0 + version: 11.3.0 typescript: specifier: ^5.6.0 version: 5.9.3 @@ -535,7 +559,7 @@ importers: 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/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + 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/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) @@ -638,7 +662,10 @@ importers: 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/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + 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/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) '@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) @@ -696,7 +723,7 @@ importers: version: 22.20.0 '@vitest/coverage-v8': specifier: ^2.1.0 - version: 2.1.9(vitest@2.1.9(@types/node@22.20.0)(terser@5.48.0)) + version: 2.1.9(vitest@2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0)) typescript: specifier: ^5.6.0 version: 5.9.3 @@ -711,7 +738,7 @@ importers: 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/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + 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/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) @@ -802,7 +829,10 @@ importers: 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/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + 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/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) '@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) @@ -902,7 +932,10 @@ importers: 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/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + 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/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) '@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) @@ -990,7 +1023,7 @@ importers: 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/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + 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/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) @@ -1060,7 +1093,7 @@ importers: version: 8.1.9 '@vitest/coverage-v8': specifier: ^2.1.0 - version: 2.1.9(vitest@2.1.9(@types/node@22.20.0)(terser@5.48.0)) + version: 2.1.9(vitest@2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0)) eslint: specifier: ^9.10.0 version: 9.39.4(jiti@2.6.1) @@ -1075,7 +1108,101 @@ importers: version: 5.9.3 vitest: specifier: ^2.1.0 - version: 2.1.9(@types/node@22.20.0)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) + version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) + + services/student-bff: + dependencies: + '@edu/shared-ts': + specifier: workspace:* + version: link:../../packages/shared-ts + '@graphql-tools/schema': + specifier: ^10.0.0 + version: 10.0.36(graphql@16.14.2) + '@grpc/grpc-js': + specifier: ^1.12.0 + version: 1.14.4 + '@grpc/proto-loader': + specifier: ^0.7.13 + version: 0.7.15 + '@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/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) + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.1 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.50.0 + version: 0.50.2(@opentelemetry/api@1.9.1)(encoding@0.1.13) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.53.0 + version: 0.53.0(@opentelemetry/api@1.9.1) + '@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 + graphql: + specifier: ^16.9.0 + version: 16.14.2 + graphql-yoga: + specifier: ^5.7.0 + version: 5.21.2(graphql@16.14.2) + ioredis: + specifier: ^5.4.1 + version: 5.11.1 + kafkajs: + specifier: ^2.2.0 + version: 2.2.4 + opossum: + specifier: ^8.4.0 + version: 8.5.0 + pino: + specifier: ^9.4.0 + version: 9.14.0 + prom-client: + specifier: ^15.1.0 + version: 15.1.3 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.0 + version: 7.8.2 + zod: + specifier: ^3.23.0 + version: 3.25.76 + devDependencies: + '@nestjs/cli': + specifier: ^10.4.0 + version: 10.4.9 + '@types/express': + specifier: ^4.17.0 + version: 4.17.25 + '@types/node': + specifier: ^22.0.0 + version: 22.20.0 + '@types/opossum': + specifier: ^8.1.0 + version: 8.1.9 + eslint: + specifier: ^9.10.0 + version: 9.39.4(jiti@2.6.1) + pino-pretty: + specifier: ^11.2.0 + version: 11.3.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.20.0)(jsdom@25.0.1)(msw@2.15.0(@types/node@22.20.0)(typescript@5.9.3))(terser@5.48.0) services/teacher-bff: dependencies: @@ -1096,7 +1223,7 @@ importers: 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/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + 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/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) @@ -1198,9 +1325,19 @@ packages: resolution: {integrity: sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==} engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + '@ardatan/relay-compiler@13.0.1': + resolution: {integrity: sha512-afG3YPwuSA0E5foouZusz5GlXKs74dObv4cuWyLyfKsYFj2r7oGRNB28v18HvwuLSQtQFCi+DpIe0TZkgQDYyg==} + peerDependencies: + graphql: '*' + '@asamuzakjp/css-color@3.2.0': resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@axe-core/playwright@4.12.1': + resolution: {integrity: sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==} + peerDependencies: + playwright-core: '>= 1.0.0' + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -1260,6 +1397,12 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-syntax-import-assertions@7.29.7': + resolution: {integrity: sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.29.7': resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} engines: {node: '>=6.9.0'} @@ -1440,6 +1583,25 @@ packages: '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@envelop/core@5.5.1': + resolution: {integrity: sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==} + engines: {node: '>=18.0.0'} + + '@envelop/instrumentation@1.0.0': + resolution: {integrity: sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==} + engines: {node: '>=18.0.0'} + + '@envelop/response-cache@9.1.1': + resolution: {integrity: sha512-TNViwHbLpXZ7/Tw1awNue7A+72nAHYIYAs3HBjI6lsKWTXW8kwH3VuNFwBXB0x0D0DpRg1S/gzL3XNF4rBWc6g==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@envelop/core': ^5.5.1 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + + '@envelop/types@5.2.1': + resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==} + engines: {node: '>=18.0.0'} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -2050,6 +2212,10 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} @@ -2365,6 +2531,25 @@ packages: peerDependencies: graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@graphql-yoga/logger@2.0.1': + resolution: {integrity: sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/plugin-response-cache@3.23.2': + resolution: {integrity: sha512-juMAA4I5gkKYRFr6uaAbNhi6smc9Y3He2CrtNIaZ2uFlpIbmwTVjeDFGTu4xq4RUUUswh/E8LU4JqJEyYnul2Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + graphql-yoga: ^5.21.2 + + '@graphql-yoga/subscription@5.0.5': + resolution: {integrity: sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/typed-event-target@3.0.2': + resolution: {integrity: sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA==} + engines: {node: '>=18.0.0'} + '@grpc/grpc-js@1.14.4': resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} engines: {node: '>=12.10.0'} @@ -2691,9 +2876,6 @@ packages: '@next/env@14.2.35': resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} - '@next/eslint-plugin-next@14.2.35': - resolution: {integrity: sha512-Jw9A3ICz2183qSsqwi7fgq4SBPiNfmOLmTPXKvlnzstUwyvBrtySiY+8RXJweNAs9KThb1+bYhZh9XWcNOr2zQ==} - '@next/swc-darwin-arm64@14.2.33': resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} engines: {node: '>= 10'} @@ -3985,6 +4167,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@prisma/instrumentation@5.22.0': resolution: {integrity: sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==} @@ -4015,9 +4202,59 @@ packages: '@protobufjs/utf8@1.1.2': resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + '@redis/bloom@1.2.0': + resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/client@1.6.1': + resolution: {integrity: sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==} + engines: {node: '>=14'} + + '@redis/graph@1.1.1': + resolution: {integrity: sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/json@1.0.7': + resolution: {integrity: sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/search@1.2.0': + resolution: {integrity: sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@redis/time-series@1.1.0': + resolution: {integrity: sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==} + peerDependencies: + '@redis/client': ^1.0.0 + + '@repeaterjs/repeater@3.1.0': + resolution: {integrity: sha512-TaoVksZRSx2KWYYpyLQtMQXXeS98VsgZImzW65xmiVgbYhXLk+aEsmzPLirqVuE4/XuUapH2iMtxUzaBNDzdSQ==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rollup/plugin-commonjs@28.0.1': + resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.62.2': resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] @@ -4236,6 +4473,121 @@ packages: '@swc/helpers': optional: true + '@sentry-internal/browser-utils@8.55.2': + resolution: {integrity: sha512-GnKod+gL/Y+1FUM/RGV8q6le1CoyiGbT40MitEK7eVwWe+bfTRq1gN7ioupyHFMUg1RlQkDQ4/sENmio/uow5A==} + engines: {node: '>=14.18'} + + '@sentry-internal/feedback@8.55.2': + resolution: {integrity: sha512-XQy//NWbL0mLLM5w8wNDWMNpXz39VUyW2397dUrH8++kR63WhUVAvTOtL0o0GMVadSAzl1b08oHP9zSUNFQwcg==} + engines: {node: '>=14.18'} + + '@sentry-internal/replay-canvas@8.55.2': + resolution: {integrity: sha512-P/jGiuR7dRLG9IzD/463fLgiibyYceauav/9prRG0ZxJm1AtuO02OKball2Fs3bbzdzwHCTlcsUuL2ivDF4b5A==} + engines: {node: '>=14.18'} + + '@sentry-internal/replay@8.55.2': + resolution: {integrity: sha512-+W43Z697EVe/OgpGW07B773sa8xO1UbpnW0Cr+E+3FMDb6ZbXlaBUoagPTUkkQPdwBe35SDh6r8y2M3EOPGbxg==} + engines: {node: '>=14.18'} + + '@sentry/babel-plugin-component-annotate@2.22.7': + resolution: {integrity: sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ==} + engines: {node: '>= 14'} + + '@sentry/browser@8.55.2': + resolution: {integrity: sha512-xHuPIEKhx9zw5quWvv4YgZprnwoVMCfxIhmOIf6KJ9iizyUHeUDcKpLS59xERroqwX4RpvK+l/27AZu4zfZlzQ==} + engines: {node: '>=14.18'} + + '@sentry/bundler-plugin-core@2.22.7': + resolution: {integrity: sha512-ouQh5sqcB8vsJ8yTTe0rf+iaUkwmeUlGNFi35IkCFUQlWJ22qS6OfvNjOqFI19e6eGUXks0c/2ieFC4+9wJ+1g==} + engines: {node: '>= 14'} + + '@sentry/cli-darwin@2.39.1': + resolution: {integrity: sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ==} + engines: {node: '>=10'} + os: [darwin] + + '@sentry/cli-linux-arm64@2.39.1': + resolution: {integrity: sha512-5VbVJDatolDrWOgaffsEM7znjs0cR8bHt9Bq0mStM3tBolgAeSDHE89NgHggfZR+DJ2VWOy4vgCwkObrUD6NQw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd] + + '@sentry/cli-linux-arm@2.39.1': + resolution: {integrity: sha512-DkENbxyRxUrfLnJLXTA4s5UL/GoctU5Cm4ER1eB7XN7p9WsamFJd/yf2KpltkjEyiTuplv0yAbdjl1KX3vKmEQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd] + + '@sentry/cli-linux-i686@2.39.1': + resolution: {integrity: sha512-pXWVoKXCRrY7N8vc9H7mETiV9ZCz+zSnX65JQCzZxgYrayQPJTc+NPRnZTdYdk5RlAupXaFicBI2GwOCRqVRkg==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd] + + '@sentry/cli-linux-x64@2.39.1': + resolution: {integrity: sha512-IwayNZy+it7FWG4M9LayyUmG1a/8kT9+/IEm67sT5+7dkMIMcpmHDqL8rWcPojOXuTKaOBBjkVdNMBTXy0mXlA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd] + + '@sentry/cli-win32-i686@2.39.1': + resolution: {integrity: sha512-NglnNoqHSmE+Dz/wHeIVRnV2bLMx7tIn3IQ8vXGO5HWA2f8zYJGktbkLq1Lg23PaQmeZLPGlja3gBQfZYSG10Q==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + + '@sentry/cli-win32-x64@2.39.1': + resolution: {integrity: sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@sentry/cli@2.39.1': + resolution: {integrity: sha512-JIb3e9vh0+OmQ0KxmexMXg9oZsR/G7HMwxt5BUIKAXZ9m17Xll4ETXTRnRUBT3sf7EpNGAmlQk1xEmVN9pYZYQ==} + engines: {node: '>= 10'} + hasBin: true + + '@sentry/core@8.55.2': + resolution: {integrity: sha512-YlEBwybUcOQ/KjMHDmof1vwweVnBtBxYlQp7DE3fOdtW4pqqdHWTnTntQs4VgYfxzjJYgtkd9LHlGtg8qy+JVQ==} + engines: {node: '>=14.18'} + + '@sentry/nextjs@8.55.2': + resolution: {integrity: sha512-yZnRh4QKiy3IyZKprVQE29Zxu/SKSTBQRQM9x+9LYwIzT+3cSfC1h6t96QiKnKFaah9ct48ghJRk9IHibw/NKw==} + engines: {node: '>=14.18'} + peerDependencies: + next: ^13.2.0 || ^14.0 || ^15.0.0-rc.0 + + '@sentry/node@8.55.2': + resolution: {integrity: sha512-x3Whryb4TytiIhH9ABLVuASfBvwA50v6PpJYvq0Y9dUMi9Eb0cfuqvRCB3e+oVntZHQpnXor2U/gRBIdG2jp4w==} + engines: {node: '>=14.18'} + + '@sentry/opentelemetry@8.55.2': + resolution: {integrity: sha512-pbhXi4cS1W4l392yEfIx3UD28OYAl9JkYOmh/Cpm6cPTtRMPxi3hWeujGbcXV9T/RkWYjqd+JdUDJjqsWSww9A==} + engines: {node: '>=14.18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 + '@opentelemetry/core': ^1.30.1 + '@opentelemetry/instrumentation': ^0.57.1 + '@opentelemetry/sdk-trace-base': ^1.30.1 + '@opentelemetry/semantic-conventions': ^1.28.0 + + '@sentry/react@8.55.2': + resolution: {integrity: sha512-1TPfKZYkJal2Dyt2W0tf1roOZmu7sqr6/dTqjdsuu2WgGTilMEreK26YqB8ROOYdMjkVJpNCcIKXQHyMp2eCwA==} + engines: {node: '>=14.18'} + peerDependencies: + react: ^16.14.0 || 17.x || 18.x || 19.x + + '@sentry/vercel-edge@8.55.2': + resolution: {integrity: sha512-obrDDBeIFIhWOARAuXg7nRQS1fYQHvscNTBRHzMT0fRU0nPtZssN4cUZYtMVZst3jFfakfe9BSyhEBRmrbWQ/w==} + engines: {node: '>=14.18'} + + '@sentry/webpack-plugin@2.22.7': + resolution: {integrity: sha512-j5h5LZHWDlm/FQCCmEghQ9FzYXwfZdlOf3FE/X6rK6lrtx0JCAkq+uhMSasoyP4XYKL4P4vRS6WFSos4jxf/UA==} + engines: {node: '>= 14'} + peerDependencies: + webpack: '>=4.40.0' + '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} @@ -4248,6 +4600,14 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} + + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} + peerDependencies: + react: ^18 || ^19 + '@testing-library/dom@10.4.1': resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} engines: {node: '>=18'} @@ -4271,6 +4631,12 @@ packages: '@types/react-dom': optional: true + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@tokenizer/inflate@0.2.7': resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} engines: {node: '>=18'} @@ -4362,6 +4728,12 @@ packages: '@types/d3-timer@3.0.2': resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@3.3.47': + resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -4463,6 +4835,15 @@ packages: '@types/shimmer@1.2.0': resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} + + '@types/ssh2@0.5.52': + resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} + + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -4534,9 +4915,21 @@ packages: resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@urql/core@2.6.1': + resolution: {integrity: sha512-gYrEHy3tViJhwIhauK6MIf2Qp09QTsgNHZRd0n71rS+hF6gdwjspf1oKljl4m25+272cJF7fPjBUGmjaiEr7Kg==} + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + '@urql/core@5.2.0': resolution: {integrity: sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==} + '@urql/next@1.1.5': + resolution: {integrity: sha512-xCNzgf6IarQ99FN4IPvj5nwkeX1z/mpO9+R54StZOK445lXlYbQnI8s4b9tNqTHbB09gt/vKE3ByRh2NLFMyfw==} + peerDependencies: + next: '>=13.0.0' + react: '>=18.0.0' + urql: ^4.0.0 + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4630,6 +5023,10 @@ packages: resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} engines: {node: '>=18.0.0'} + '@whatwg-node/events@0.1.2': + resolution: {integrity: sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ==} + engines: {node: '>=18.0.0'} + '@whatwg-node/fetch@0.10.13': resolution: {integrity: sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==} engines: {node: '>=18.0.0'} @@ -4642,6 +5039,10 @@ packages: resolution: {integrity: sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==} engines: {node: '>=16.0.0'} + '@whatwg-node/server@0.11.0': + resolution: {integrity: sha512-VSdkwnJRr8Yv9UgB2aXB3VUPWwd6Oqnn0hycFwhg9pZgWxJXb7JmhsiXe9tmpMwjHFxli12PGcz9aI63YYloGQ==} + engines: {node: '>=18.0.0'} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -4821,6 +5222,10 @@ packages: array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + array.prototype.flat@1.3.3: resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} engines: {node: '>= 0.4'} @@ -4833,6 +5238,13 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -4840,10 +5252,20 @@ packages: ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -4878,6 +5300,14 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -5309,10 +5739,30 @@ packages: typescript: optional: true + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + cron-parser@4.9.0: resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} engines: {node: '>=12.0.0'} + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + + cross-inspect@1.0.1: + resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} + engines: {node: '>=16.0.0'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -5383,6 +5833,10 @@ packages: resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} engines: {node: '>=12'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -5399,6 +5853,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + dataloader@2.2.3: + resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==} + dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} @@ -5484,6 +5941,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dependency-graph@0.11.0: + resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} + engines: {node: '>= 0.6.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -5510,6 +5971,18 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + docker-compose@0.24.8: + resolution: {integrity: sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==} + engines: {node: '>= 6.0.0'} + + docker-modem@5.0.7: + resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} + engines: {node: '>= 8.0'} + + dockerode@4.0.12: + resolution: {integrity: sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==} + engines: {node: '>= 8.0'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -5519,6 +5992,9 @@ packages: dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dot-prop@5.3.0: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} @@ -5891,6 +6367,9 @@ packages: resolution: {integrity: sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==} engines: {node: '>=6.0.0'} + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -6004,6 +6483,10 @@ packages: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} @@ -6075,6 +6558,10 @@ packages: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} + generic-pool@3.9.0: + resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} + engines: {node: '>= 4'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6129,11 +6616,6 @@ packages: glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true @@ -6158,6 +6640,10 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + google-logging-utils@0.0.2: resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} engines: {node: '>=14'} @@ -6172,10 +6658,69 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphql-config@5.1.6: + resolution: {integrity: sha512-fCkYnm4Kdq3un0YIM4BCZHVR5xl0UeLP6syxxO7KAstdY7QVyVvTHP0kRPDYEP1v08uwtJVgis5sj3IOTLOniQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + cosmiconfig-toml-loader: ^1.0.0 + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + peerDependenciesMeta: + cosmiconfig-toml-loader: + optional: true + + graphql-depth-limit@1.1.0: + resolution: {integrity: sha512-+3B2BaG8qQ8E18kzk9yiSdAa75i/hnnOwgSeAxVJctGQPvmeiLtqKOYF6HETCyRjiF7Xfsyal0HbLlxCQkgkrw==} + engines: {node: '>=6.0.0'} + peerDependencies: + graphql: '*' + + graphql-query-complexity@1.1.1: + resolution: {integrity: sha512-q0u1yrPYdEYnhEL4x6xeEoOHdHUV4hISrI/uDFhOaxLvIN60RqlzV3kW6N0f+0pygGM9wjskl1cD5/55G+TPmw==} + peerDependencies: + graphql: ^15.0.0 || ^16.0.0 + + graphql-request@6.1.0: + resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} + peerDependencies: + graphql: 14 - 16 + + graphql-tag@2.12.7: + resolution: {integrity: sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + graphql-ws@6.0.8: + resolution: {integrity: sha512-m3EOaNsUBXwAnkBWbzPfe0Nq8pXUfxsWnolC54sru3FzHvhTZL0Ouf/BoQsaGAXqM+YPerXOJ47BUnmgmoupCw==} + engines: {node: '>=20'} + peerDependencies: + '@fastify/websocket': ^10 || ^11 + crossws: ~0.3 + graphql: ^15.10.1 || ^16 + ws: ^8 + peerDependenciesMeta: + '@fastify/websocket': + optional: true + crossws: + optional: true + ws: + optional: true + + graphql-yoga@5.21.2: + resolution: {integrity: sha512-IIRF/3xtjj2D6caAWL9177hQ8tV3mWB3hve1GRnz7njPhQ3iY1jFtSp98fNGv0yV9kaPh9kKQ8JWdJZnedVmDw==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + graphql@16.14.2: resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + grpc-reflection-js@0.3.0: + resolution: {integrity: sha512-3lhTlQluPxVgbowCXA3tAZC3RJW+GSOUkguLNYl1QffYRiutUB3RDfPkQFTcrCFJgNiIIxx+iJkr8s3uSp3zWA==} + peerDependencies: + '@grpc/grpc-js': ^1.0.0 + has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -6210,6 +6755,9 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + header-case@2.0.4: + resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + headers-polyfill@5.0.1: resolution: {integrity: sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==} @@ -6336,6 +6884,12 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + intl-messageformat@10.7.18: + resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + ioredis@5.11.1: resolution: {integrity: sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==} engines: {node: '>=12.22.0'} @@ -6344,6 +6898,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + is-absolute@1.0.0: + resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} + engines: {node: '>=0.10.0'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -6419,6 +6977,9 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} + is-lower-case@2.0.2: + resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -6448,10 +7009,17 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} + is-relative@1.0.0: + resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} + engines: {node: '>=0.10.0'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -6484,10 +7052,17 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-unc-path@1.0.0: + resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} + engines: {node: '>=0.10.0'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + is-upper-case@2.0.2: + resolution: {integrity: sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -6500,6 +7075,13 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -6511,6 +7093,11 @@ packages: peerDependencies: ws: '*' + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -6531,10 +7118,6 @@ packages: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} - jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -6654,6 +7237,10 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -6790,6 +7377,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -6910,6 +7501,10 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@8.0.7: resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} engines: {node: '>=16 || 14 >=14.17'} @@ -7128,6 +7723,11 @@ packages: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} deprecated: This package is no longer supported. + nuqs@1.20.0: + resolution: {integrity: sha512-nGVfv7eWMNxAzOJ9n8ARTo6ObqeEr1ETYZ+dIMCg/VfGUoZoPrqyTOndIvQIgUzK3pIC41mTXg10JJxh9ziEhw==} + peerDependencies: + next: '>=13.4 <14.0.2 || ^14.0.3' + nwsapi@2.2.24: resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} @@ -7375,6 +7975,11 @@ packages: engines: {node: '>=18'} hasBin: true + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -7469,6 +8074,9 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} @@ -7487,6 +8095,13 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + properties-reader@2.3.0: + resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} + engines: {node: '>=14'} + protobufjs@7.6.5: resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} engines: {node: '>=12.0.0'} @@ -7628,6 +8243,15 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remedial@1.0.8: + resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} + + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + remove-trailing-spaces@1.0.9: + resolution: {integrity: sha512-xzG7w5IRijvIkHIjDk65URsJJ7k4J95wmcArY5PRcmjldIOl7oTvG8+X2Ag690R7SfwiOcHrWZKVc1Pp5WIOzA==} + repeat-string@1.6.1: resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} engines: {node: '>=0.10'} @@ -7672,6 +8296,10 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + rettime@0.11.11: resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==} @@ -7724,6 +8352,9 @@ packages: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -7957,6 +8588,9 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} + streamx@2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + strict-event-emitter@0.5.1: resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} @@ -7995,6 +8629,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -8070,6 +8707,14 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + sync-fetch@0.6.0: + resolution: {integrity: sha512-IELLEvzHuCfc1uTsshPK58ViSdNqXxlml1U+fmwJIKLYKOr/rAtBrorE2RYm5IHaMpDNlmC0fr1LAvdXvyheEQ==} + engines: {node: '>=18'} + + sync-fetch@0.6.0-2: + resolution: {integrity: sha512-c7AfkZ9udatCuAy9RSfiGPpeOKKUAUK5e1cXadLOGUjasdxqYqAK0jTNkM/FSEyJ3a5Ra27j/tw/PS0qLmaF/A==} + engines: {node: '>=18'} + table-layout@4.1.1: resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} engines: {node: '>=12.17'} @@ -8078,6 +8723,9 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} + tailwind-merge@2.6.1: + resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} + tailwindcss@3.4.19: resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} @@ -8189,6 +8837,10 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + timeout-signal@2.0.0: + resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==} + engines: {node: '>=16'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -8218,6 +8870,9 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + title-case@3.0.3: + resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} + tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} @@ -8319,6 +8974,10 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + type-fest@5.8.0: resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} engines: {node: '>=20'} @@ -8431,6 +9090,13 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + unc-path-regex@0.1.2: + resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} + engines: {node: '>=0.10.0'} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -8465,6 +9131,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unplugin@1.0.1: + resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} + until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} @@ -8487,12 +9156,31 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urlpattern-polyfill@10.1.0: + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + + urql@2.2.3: + resolution: {integrity: sha512-XMkSYJKW9s4ZlbSuxcUz3fTBIykOn0sGileRXQeyZpaRBXJPVz5saSY05k7jdefNxShZtTI+/nr7PYUWQertfg==} + peerDependencies: + graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + react: '>= 16.8.0' + urql@4.2.2: resolution: {integrity: sha512-3GgqNa6iF7bC4hY/ImJKN4REQILcSU9VKcKL8gfELZM8mM5BnLH1BsCc8kBdnVGD1LIFOs4W3O2idNHhON1r0w==} peerDependencies: '@urql/core': ^5.0.0 react: '>= 16.8.0' + use-intl@3.26.5: + resolution: {integrity: sha512-OdsJnC/znPvHCHLQH/duvQNXnP1w0hPfS+tkSi3mAbfjYBGh4JnyfdwkQBfIVf7t8gs9eSX/CntxUMvtKdG2MQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -8587,6 +9275,10 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} @@ -8663,6 +9355,9 @@ packages: wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + wonka@4.0.15: + resolution: {integrity: sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg==} + wonka@6.3.6: resolution: {integrity: sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==} @@ -8827,6 +9522,13 @@ snapshots: transitivePeerDependencies: - chokidar + '@ardatan/relay-compiler@13.0.1(graphql@16.14.2)': + dependencies: + '@babel/runtime': 7.29.7 + graphql: 16.14.2 + immutable: 5.1.9 + invariant: 2.2.4 + '@asamuzakjp/css-color@3.2.0': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) @@ -8835,6 +9537,11 @@ snapshots: '@csstools/css-tokenizer': 3.0.4 lru-cache: 10.4.3 + '@axe-core/playwright@4.12.1(playwright-core@1.61.1)': + dependencies: + axe-core: 4.12.1 + playwright-core: 1.61.1 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -8914,6 +9621,11 @@ snapshots: dependencies: '@babel/types': 7.29.7 + '@babel/plugin-syntax-import-assertions@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 @@ -9153,6 +9865,34 @@ snapshots: tslib: 2.8.1 optional: true + '@envelop/core@5.5.1': + dependencies: + '@envelop/instrumentation': 1.0.0 + '@envelop/types': 5.2.1 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@envelop/instrumentation@1.0.0': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@envelop/response-cache@9.1.1(@envelop/core@5.5.1)(graphql@16.14.2)': + dependencies: + '@envelop/core': 5.5.1 + '@graphql-tools/utils': 11.2.0(graphql@16.14.2) + '@whatwg-node/fetch': 0.10.13 + '@whatwg-node/promise-helpers': 1.3.2 + fast-json-stable-stringify: 2.1.0 + graphql: 16.14.2 + lru-cache: 11.5.2 + tslib: 2.8.1 + + '@envelop/types@5.2.1': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -9496,6 +10236,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@fastify/busboy@2.1.1': {} + '@fastify/busboy@3.2.0': {} '@formatjs/ecma402-abstract@2.3.6': @@ -10055,6 +10797,30 @@ snapshots: dependencies: graphql: 16.14.2 + '@graphql-yoga/logger@2.0.1': + dependencies: + tslib: 2.8.1 + + '@graphql-yoga/plugin-response-cache@3.23.2(graphql-yoga@5.21.2(graphql@16.14.2))(graphql@16.14.2)': + dependencies: + '@envelop/core': 5.5.1 + '@envelop/response-cache': 9.1.1(@envelop/core@5.5.1)(graphql@16.14.2) + '@whatwg-node/promise-helpers': 1.3.2 + graphql: 16.14.2 + graphql-yoga: 5.21.2(graphql@16.14.2) + + '@graphql-yoga/subscription@5.0.5': + dependencies: + '@graphql-yoga/typed-event-target': 3.0.2 + '@repeaterjs/repeater': 3.1.0 + '@whatwg-node/events': 0.1.2 + tslib: 2.8.1 + + '@graphql-yoga/typed-event-target@3.0.2': + dependencies: + '@repeaterjs/repeater': 3.1.0 + tslib: 2.8.1 + '@grpc/grpc-js@1.14.4': dependencies: '@grpc/proto-loader': 0.8.1 @@ -10261,7 +11027,7 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/nextjs-mf@8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(@babel/core@7.29.7)(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16))': + '@module-federation/nextjs-mf@8.8.70(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-jsx@5.1.1(react@18.3.1))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16))': dependencies: '@module-federation/enhanced': 2.7.0(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) '@module-federation/node': 2.7.46(@rspack/core@2.1.3(@module-federation/runtime-tools@2.7.0)(@swc/helpers@0.5.23))(typescript@5.9.3)(webpack@5.97.1(postcss@8.5.16)) @@ -10271,7 +11037,7 @@ snapshots: '@module-federation/webpack-bundler-runtime': 2.7.0 fast-glob: 3.3.3 optionalDependencies: - next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.1(@babel/core@7.29.7)(react@18.3.1) @@ -10442,7 +11208,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/core@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@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)': dependencies: '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13) @@ -10462,7 +11228,7 @@ snapshots: '@nestjs/microservices@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)': dependencies: '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 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)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 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) iterare: 1.2.1 reflect-metadata: 0.2.2 rxjs: 7.8.2 @@ -10475,7 +11241,7 @@ snapshots: '@nestjs/platform-express@10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.22)': dependencies: '@nestjs/common': 10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.22(@nestjs/common@10.4.22(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.22)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 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) body-parser: 1.20.4 cors: 2.8.5 express: 4.22.1 @@ -10508,10 +11274,6 @@ snapshots: '@next/env@14.2.35': {} - '@next/eslint-plugin-next@14.2.35': - dependencies: - glob: 10.3.10 - '@next/swc-darwin-arm64@14.2.33': optional: true @@ -12320,6 +13082,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@prisma/instrumentation@5.22.0': dependencies: '@opentelemetry/api': 1.9.1 @@ -12348,8 +13114,56 @@ snapshots: '@protobufjs/utf8@1.1.2': {} + '@redis/bloom@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/client@1.6.1': + dependencies: + cluster-key-slot: 1.1.2 + generic-pool: 3.9.0 + yallist: 4.0.0 + + '@redis/graph@1.1.1(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/json@1.0.7(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/search@1.2.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@redis/time-series@1.1.0(@redis/client@1.6.1)': + dependencies: + '@redis/client': 1.6.1 + + '@repeaterjs/repeater@3.1.0': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rollup/plugin-commonjs@28.0.1(rollup@3.29.5)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@3.29.5) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.5) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.5 + optionalDependencies: + rollup: 3.29.5 + + '@rollup/pluginutils@5.4.0(rollup@3.29.5)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.5 + optionalDependencies: + rollup: 3.29.5 + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true @@ -12487,6 +13301,189 @@ snapshots: '@module-federation/runtime-tools': 2.7.0 '@swc/helpers': 0.5.23 + '@sentry-internal/browser-utils@8.55.2': + dependencies: + '@sentry/core': 8.55.2 + + '@sentry-internal/feedback@8.55.2': + dependencies: + '@sentry/core': 8.55.2 + + '@sentry-internal/replay-canvas@8.55.2': + dependencies: + '@sentry-internal/replay': 8.55.2 + '@sentry/core': 8.55.2 + + '@sentry-internal/replay@8.55.2': + dependencies: + '@sentry-internal/browser-utils': 8.55.2 + '@sentry/core': 8.55.2 + + '@sentry/babel-plugin-component-annotate@2.22.7': {} + + '@sentry/browser@8.55.2': + dependencies: + '@sentry-internal/browser-utils': 8.55.2 + '@sentry-internal/feedback': 8.55.2 + '@sentry-internal/replay': 8.55.2 + '@sentry-internal/replay-canvas': 8.55.2 + '@sentry/core': 8.55.2 + + '@sentry/bundler-plugin-core@2.22.7(encoding@0.1.13)': + dependencies: + '@babel/core': 7.29.7 + '@sentry/babel-plugin-component-annotate': 2.22.7 + '@sentry/cli': 2.39.1(encoding@0.1.13) + dotenv: 16.6.1 + find-up: 5.0.0 + glob: 9.3.4 + magic-string: 0.30.8 + unplugin: 1.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/cli-darwin@2.39.1': + optional: true + + '@sentry/cli-linux-arm64@2.39.1': + optional: true + + '@sentry/cli-linux-arm@2.39.1': + optional: true + + '@sentry/cli-linux-i686@2.39.1': + optional: true + + '@sentry/cli-linux-x64@2.39.1': + optional: true + + '@sentry/cli-win32-i686@2.39.1': + optional: true + + '@sentry/cli-win32-x64@2.39.1': + optional: true + + '@sentry/cli@2.39.1(encoding@0.1.13)': + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.7.0(encoding@0.1.13) + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.39.1 + '@sentry/cli-linux-arm': 2.39.1 + '@sentry/cli-linux-arm64': 2.39.1 + '@sentry/cli-linux-i686': 2.39.1 + '@sentry/cli-linux-x64': 2.39.1 + '@sentry/cli-win32-i686': 2.39.1 + '@sentry/cli-win32-x64': 2.39.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@sentry/core@8.55.2': {} + + '@sentry/nextjs@8.55.2(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(encoding@0.1.13)(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.97.1(postcss@8.5.16))': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.42.0 + '@rollup/plugin-commonjs': 28.0.1(rollup@3.29.5) + '@sentry-internal/browser-utils': 8.55.2 + '@sentry/core': 8.55.2 + '@sentry/node': 8.55.2 + '@sentry/opentelemetry': 8.55.2(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.42.0) + '@sentry/react': 8.55.2(react@18.3.1) + '@sentry/vercel-edge': 8.55.2 + '@sentry/webpack-plugin': 2.22.7(encoding@0.1.13)(webpack@5.97.1(postcss@8.5.16)) + chalk: 3.0.0 + next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + resolve: 1.22.8 + rollup: 3.29.5 + stacktrace-parser: 0.1.11 + transitivePeerDependencies: + - '@opentelemetry/context-async-hooks' + - '@opentelemetry/core' + - '@opentelemetry/instrumentation' + - '@opentelemetry/sdk-trace-base' + - encoding + - react + - supports-color + - webpack + + '@sentry/node@8.55.2': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-connect': 0.43.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dataloader': 0.16.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-express': 0.47.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fastify': 0.44.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fs': 0.19.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-generic-pool': 0.43.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-graphql': 0.47.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-hapi': 0.45.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.57.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-ioredis': 0.47.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-kafkajs': 0.7.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-knex': 0.44.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-koa': 0.47.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-lru-memoizer': 0.44.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongodb': 0.51.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongoose': 0.46.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql': 0.45.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql2': 0.45.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-nestjs-core': 0.44.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pg': 0.50.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-redis-4': 0.46.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-tedious': 0.18.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-undici': 0.10.0(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.42.0 + '@prisma/instrumentation': 5.22.0 + '@sentry/core': 8.55.2 + '@sentry/opentelemetry': 8.55.2(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.42.0) + import-in-the-middle: 1.15.0 + transitivePeerDependencies: + - supports-color + + '@sentry/opentelemetry@8.55.2(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.42.0)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.42.0 + '@sentry/core': 8.55.2 + + '@sentry/react@8.55.2(react@18.3.1)': + dependencies: + '@sentry/browser': 8.55.2 + '@sentry/core': 8.55.2 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + + '@sentry/vercel-edge@8.55.2': + dependencies: + '@opentelemetry/api': 1.9.1 + '@sentry/core': 8.55.2 + + '@sentry/webpack-plugin@2.22.7(encoding@0.1.13)(webpack@5.97.1(postcss@8.5.16))': + dependencies: + '@sentry/bundler-plugin-core': 2.22.7(encoding@0.1.13) + unplugin: 1.0.1 + uuid: 9.0.1 + webpack: 5.97.1(postcss@8.5.16) + transitivePeerDependencies: + - encoding + - supports-color + '@sqltools/formatter@1.2.5': {} '@swc/counter@0.1.3': {} @@ -12500,6 +13497,13 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 + '@tanstack/query-core@5.101.2': {} + + '@tanstack/react-query@5.101.2(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.101.2 + react: 18.3.1 + '@testing-library/dom@10.4.1': dependencies: '@babel/code-frame': 7.29.7 @@ -12530,6 +13534,10 @@ snapshots: '@types/react': 18.3.31 '@types/react-dom': 18.3.7(@types/react@18.3.31) + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@tokenizer/inflate@0.2.7': dependencies: debug: 4.4.3 @@ -12639,6 +13647,17 @@ snapshots: '@types/d3-timer@3.0.2': {} + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 22.20.0 + '@types/ssh2': 1.15.5 + + '@types/dockerode@3.3.47': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 22.20.0 + '@types/ssh2': 1.15.5 + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -12760,6 +13779,19 @@ snapshots: '@types/shimmer@1.2.0': {} + '@types/ssh2-streams@0.1.13': + dependencies: + '@types/node': 22.20.0 + + '@types/ssh2@0.5.52': + dependencies: + '@types/node': 22.20.0 + '@types/ssh2-streams': 0.1.13 + + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + '@types/statuses@2.0.6': {} '@types/tedious@4.0.14': @@ -12863,6 +13895,12 @@ snapshots: '@typescript-eslint/types': 8.63.0 eslint-visitor-keys: 5.0.1 + '@urql/core@2.6.1(graphql@16.14.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) + graphql: 16.14.2 + wonka: 4.0.15 + '@urql/core@5.2.0(graphql@16.14.2)': dependencies: '@0no-co/graphql.web': 1.3.2(graphql@16.14.2) @@ -12870,6 +13908,12 @@ snapshots: transitivePeerDependencies: - graphql + '@urql/next@1.1.5(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)(urql@4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1))': + dependencies: + next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + urql: 4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1) + '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.20.0)(terser@5.48.0))': dependencies: '@babel/core': 7.29.7 @@ -13022,6 +14066,10 @@ snapshots: '@whatwg-node/promise-helpers': 1.3.2 tslib: 2.8.1 + '@whatwg-node/events@0.1.2': + dependencies: + tslib: 2.8.1 + '@whatwg-node/fetch@0.10.13': dependencies: '@whatwg-node/node-fetch': 0.8.6 @@ -13038,6 +14086,14 @@ snapshots: dependencies: tslib: 2.8.1 + '@whatwg-node/server@0.11.0': + dependencies: + '@envelop/instrumentation': 1.0.0 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/fetch': 0.10.13 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} @@ -13233,6 +14289,8 @@ snapshots: array-timsort@1.0.3: {} + array-union@2.1.0: {} + array.prototype.flat@1.3.3: dependencies: call-bind: 1.0.9 @@ -13257,12 +14315,24 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + arrify@1.0.1: {} + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + assertion-error@2.0.1: {} ast-types-flow@0.0.8: {} + astral-regex@2.0.0: {} + async-function@1.0.0: {} + async-lock@1.4.1: {} + + async@3.2.6: {} + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} @@ -13288,6 +14358,8 @@ snapshots: axobject-query@4.1.0: {} + b4a@1.8.1: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -13325,6 +14397,10 @@ snapshots: baseline-browser-mapping@2.10.42: {} + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + bcrypt@5.1.1(encoding@0.1.13): dependencies: '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) @@ -13741,10 +14817,33 @@ snapshots: optionalDependencies: typescript: 5.9.3 + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.28.0 + optional: true + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + cron-parser@4.9.0: dependencies: luxon: 3.7.2 + cross-fetch@3.2.0(encoding@0.1.13): + dependencies: + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + cross-inspect@1.0.1: + dependencies: + tslib: 2.8.1 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -13804,6 +14903,8 @@ snapshots: dargs@8.1.0: {} + data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -13827,6 +14928,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + dataloader@2.2.3: {} + dateformat@4.6.3: {} dayjs@1.11.21: {} @@ -13883,6 +14986,8 @@ snapshots: depd@2.0.0: {} + dependency-graph@0.11.0: {} + dequal@2.0.3: {} destroy@1.2.0: {} @@ -13899,6 +15004,31 @@ snapshots: dlv@1.1.3: {} + docker-compose@0.24.8: + dependencies: + yaml: 2.9.0 + + docker-modem@5.0.7: + dependencies: + debug: 4.4.3 + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@4.0.12: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7 + protobufjs: 7.6.5 + tar-fs: 2.1.5 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -13908,6 +15038,11 @@ snapshots: '@babel/runtime': 7.29.7 csstype: 3.2.3 + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.8.1 + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 @@ -14203,6 +15338,25 @@ snapshots: dependencies: eslint: 9.39.4(jiti@2.6.1) + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@1.21.7)): + dependencies: + aria-query: 5.3.2 + array-includes: 3.1.9 + array.prototype.flatmap: 1.3.3 + ast-types-flow: 0.0.8 + axe-core: 4.12.1 + axobject-query: 4.1.0 + damerau-levenshtein: 1.0.8 + emoji-regex: 9.2.2 + eslint: 9.39.4(jiti@1.21.7) + hasown: 2.0.4 + jsx-ast-utils: 3.3.5 + language-tags: 1.0.9 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + safe-regex-test: 1.1.0 + string.prototype.includes: 2.0.1 + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -14432,6 +15586,8 @@ snapshots: fast-equals@5.4.1: {} + fast-fifo@1.3.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -14581,6 +15737,10 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded-parse@2.1.2: {} forwarded@0.2.0: {} @@ -14665,6 +15825,8 @@ snapshots: generator-function@2.0.1: {} + generic-pool@3.9.0: {} + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -14721,14 +15883,6 @@ snapshots: glob-to-regexp@0.4.1: {} - glob@10.3.10: - dependencies: - foreground-child: 3.3.1 - jackspeak: 2.3.6 - minimatch: 9.0.9 - minipass: 7.1.3 - path-scurry: 1.11.1 - glob@10.4.5: dependencies: foreground-child: 3.3.1 @@ -14765,6 +15919,15 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + google-logging-utils@0.0.2: {} google-protobuf@3.21.4: {} @@ -14773,8 +15936,84 @@ snapshots: graceful-fs@4.2.11: {} + graphql-config@5.1.6(@types/node@22.20.0)(graphql@16.14.2)(typescript@5.9.3): + dependencies: + '@graphql-tools/graphql-file-loader': 8.1.16(graphql@16.14.2) + '@graphql-tools/json-file-loader': 8.0.30(graphql@16.14.2) + '@graphql-tools/load': 8.1.13(graphql@16.14.2) + '@graphql-tools/merge': 9.2.0(graphql@16.14.2) + '@graphql-tools/url-loader': 9.1.4(@types/node@22.20.0)(graphql@16.14.2) + '@graphql-tools/utils': 11.2.0(graphql@16.14.2) + cosmiconfig: 8.3.6(typescript@5.9.3) + graphql: 16.14.2 + jiti: 2.6.1 + minimatch: 10.2.5 + string-env-interpolation: 1.0.1 + tslib: 2.8.1 + transitivePeerDependencies: + - '@fastify/websocket' + - '@types/node' + - bufferutil + - crossws + - typescript + - utf-8-validate + + graphql-depth-limit@1.1.0(graphql@16.14.2): + dependencies: + arrify: 1.0.1 + graphql: 16.14.2 + + graphql-query-complexity@1.1.1(graphql@16.14.2): + dependencies: + graphql: 16.14.2 + lodash.get: 4.4.2 + + graphql-request@6.1.0(encoding@0.1.13)(graphql@16.14.2): + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) + cross-fetch: 3.2.0(encoding@0.1.13) + graphql: 16.14.2 + transitivePeerDependencies: + - encoding + + graphql-tag@2.12.7(graphql@16.14.2): + dependencies: + graphql: 16.14.2 + tslib: 2.8.1 + + graphql-ws@6.0.8(graphql@16.14.2)(ws@8.21.0): + dependencies: + graphql: 16.14.2 + optionalDependencies: + ws: 8.21.0 + + graphql-yoga@5.21.2(graphql@16.14.2): + dependencies: + '@envelop/core': 5.5.1 + '@envelop/instrumentation': 1.0.0 + '@graphql-tools/executor': 1.5.5(graphql@16.14.2) + '@graphql-tools/schema': 10.0.36(graphql@16.14.2) + '@graphql-tools/utils': 10.11.0(graphql@16.14.2) + '@graphql-yoga/logger': 2.0.1 + '@graphql-yoga/subscription': 5.0.5 + '@whatwg-node/fetch': 0.10.13 + '@whatwg-node/promise-helpers': 1.3.2 + '@whatwg-node/server': 0.11.0 + graphql: 16.14.2 + lru-cache: 10.4.3 + tslib: 2.8.1 + graphql@16.14.2: {} + grpc-reflection-js@0.3.0(@grpc/grpc-js@1.14.4): + dependencies: + '@grpc/grpc-js': 1.14.4 + '@types/google-protobuf': 3.15.12 + '@types/lodash.set': 4.3.9 + google-protobuf: 3.21.4 + lodash.set: 4.3.2 + protobufjs: 7.6.5 + has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -14801,6 +16040,11 @@ snapshots: dependencies: function-bind: 1.1.2 + header-case@2.0.4: + dependencies: + capital-case: 1.0.4 + tslib: 2.8.1 + headers-polyfill@5.0.1: dependencies: '@types/set-cookie-parser': 2.4.10 @@ -14950,6 +16194,17 @@ snapshots: internmap@2.0.3: {} + intl-messageformat@10.7.18: + dependencies: + '@formatjs/ecma402-abstract': 2.3.6 + '@formatjs/fast-memoize': 2.2.7 + '@formatjs/icu-messageformat-parser': 2.11.4 + tslib: 2.8.1 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + ioredis@5.11.1: dependencies: '@ioredis/commands': 1.10.0 @@ -14964,6 +16219,11 @@ snapshots: ipaddr.js@1.9.1: {} + is-absolute@1.0.0: + dependencies: + is-relative: 1.0.0 + is-windows: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -15042,6 +16302,10 @@ snapshots: is-interactive@1.0.0: {} + is-lower-case@2.0.2: + dependencies: + tslib: 2.8.1 + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -15061,6 +16325,10 @@ snapshots: is-property@1.0.2: {} + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.9 + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -15068,6 +16336,10 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + is-relative@1.0.0: + dependencies: + is-unc-path: 1.0.0 + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -15097,8 +16369,16 @@ snapshots: dependencies: which-typed-array: 1.1.22 + is-unc-path@1.0.0: + dependencies: + unc-path-regex: 0.1.2 + is-unicode-supported@0.1.0: {} + is-upper-case@2.0.2: + dependencies: + tslib: 2.8.1 + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -15110,6 +16390,10 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-windows@1.0.2: {} + + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -15118,6 +16402,10 @@ snapshots: dependencies: ws: 8.21.0 + isows@1.0.7(ws@8.21.0): + dependencies: + ws: 8.21.0 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -15141,12 +16429,6 @@ snapshots: iterare@1.2.1: {} - jackspeak@2.3.6: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -15283,6 +16565,10 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -15417,6 +16703,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.5.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -15504,6 +16792,10 @@ snapshots: dependencies: brace-expansion: 1.1.15 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.1 + minimatch@8.0.7: dependencies: brace-expansion: 2.1.1 @@ -15639,7 +16931,15 @@ snapshots: neo4j-driver-core: 5.28.3 rxjs: 7.8.2 - next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-intl@3.26.5(next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + '@formatjs/intl-localematcher': 0.5.10 + negotiator: 1.0.0 + next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + use-intl: 3.26.5(react@18.3.1) + + next@14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.35 '@swc/helpers': 0.5.5 @@ -15691,6 +16991,12 @@ snapshots: optionalDependencies: encoding: 0.1.13 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-releases@2.0.50: {} node-schedule@2.1.1: @@ -15720,6 +17026,11 @@ snapshots: gauge: 3.0.2 set-blocking: 2.0.0 + nuqs@1.20.0(next@14.2.35(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + dependencies: + mitt: 3.0.1 + next: 14.2.35(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nwsapi@2.2.24: {} object-assign@4.1.1: {} @@ -15977,6 +17288,12 @@ snapshots: playwright-core@1.61.1: {} + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.1.0: {} @@ -16061,6 +17378,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + process-nextick-args@2.0.1: {} + process-warning@5.0.0: {} process@0.11.10: {} @@ -16078,6 +17397,16 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + properties-reader@2.3.0: + dependencies: + mkdirp: 1.0.4 + protobufjs@7.6.5: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -16266,6 +17595,12 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remedial@1.0.8: {} + + remove-trailing-separator@1.1.0: {} + + remove-trailing-spaces@1.0.9: {} + repeat-string@1.6.1: {} require-directory@2.1.1: {} @@ -16309,6 +17644,8 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + rettime@0.11.11: {} reusify@1.1.0: {} @@ -16382,6 +17719,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} safe-push-apply@1.0.0: @@ -16642,6 +17981,15 @@ snapshots: streamsearch@1.1.0: {} + streamx@2.28.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + strict-event-emitter@0.5.1: {} string-argv@0.3.2: {} @@ -16696,6 +18044,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.2 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -16759,6 +18111,18 @@ snapshots: symbol-tree@3.2.4: {} + sync-fetch@0.6.0: + dependencies: + node-fetch: 3.3.2 + timeout-signal: 2.0.0 + whatwg-mimetype: 4.0.0 + + sync-fetch@0.6.0-2: + dependencies: + node-fetch: 3.3.2 + timeout-signal: 2.0.0 + whatwg-mimetype: 4.0.0 + table-layout@4.1.1: dependencies: array-back: 6.2.3 @@ -16766,6 +18130,8 @@ snapshots: tagged-tag@1.0.0: {} + tailwind-merge@2.6.1: {} + tailwindcss@3.4.19(tsx@4.23.0)(yaml@2.9.0): dependencies: '@alloc/quick-lru': 5.2.0 @@ -16875,7 +18241,6 @@ snapshots: webpack: 5.97.1(postcss@8.5.16) optionalDependencies: postcss: 8.5.16 - optional: true terser-webpack-plugin@5.6.1(webpack@5.97.1): dependencies: @@ -16943,6 +18308,8 @@ snapshots: through@2.3.8: {} + timeout-signal@2.0.0: {} + tiny-invariant@1.3.3: {} tinybench@2.9.0: {} @@ -16962,6 +18329,10 @@ snapshots: tinyspy@3.0.2: {} + title-case@3.0.3: + dependencies: + tslib: 2.8.1 + tldts-core@6.1.86: {} tldts-core@7.4.8: {} @@ -17056,6 +18427,8 @@ snapshots: type-fest@0.21.3: {} + type-fest@0.7.1: {} + type-fest@5.8.0: dependencies: tagged-tag: 1.0.0 @@ -17149,6 +18522,10 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + unc-path-regex@0.1.2: {} + + undici-types@5.26.5: {} + undici-types@6.21.0: {} undici-types@7.18.2: {} @@ -17171,6 +18548,13 @@ snapshots: unpipe@1.0.0: {} + unplugin@1.0.1: + dependencies: + acorn: 8.17.0 + chokidar: 3.6.0 + webpack-sources: 3.5.1 + webpack-virtual-modules: 0.5.0 + until-async@3.0.2: {} upath@2.0.1: {} @@ -17193,12 +18577,31 @@ snapshots: dependencies: punycode: 2.3.1 + urlpattern-polyfill@10.1.0: {} + + urql@2.2.3(graphql@16.14.2)(react@18.3.1): + dependencies: + '@urql/core': 2.6.1(graphql@16.14.2) + graphql: 16.14.2 + react: 18.3.1 + wonka: 4.0.15 + urql@4.2.2(@urql/core@5.2.0(graphql@16.14.2))(react@18.3.1): dependencies: '@urql/core': 5.2.0(graphql@16.14.2) react: 18.3.1 wonka: 6.3.6 + use-intl@3.26.5(react@18.3.1): + dependencies: + '@formatjs/fast-memoize': 2.2.7 + intl-messageformat: 10.7.18 + react: 18.3.1 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} @@ -17302,6 +18705,8 @@ snapshots: dependencies: defaults: 1.0.4 + web-streams-polyfill@3.3.3: {} + web-vitals@4.2.4: {} webidl-conversions@3.0.1: {} @@ -17430,7 +18835,6 @@ snapshots: - lightningcss - postcss - uglify-js - optional: true whatwg-encoding@3.1.1: dependencies: @@ -17502,6 +18906,8 @@ snapshots: dependencies: string-width: 4.2.3 + wonka@4.0.15: {} + wonka@6.3.6: {} word-wrap@1.2.5: {} diff --git a/services/student-bff/src/main.ts b/services/student-bff/src/main.ts index 1daebf6..b00d863 100644 --- a/services/student-bff/src/main.ts +++ b/services/student-bff/src/main.ts @@ -52,8 +52,8 @@ async function bootstrap(): Promise { // B1 GraphQL Yoga endpoint 挂载 const yoga = (await app.resolve(GRAPHQL_YOGA)) as YogaServerInstance< - Record, - unknown + { req: Request; res: Response }, + Record >; const httpAdapter = app.getHttpAdapter().getInstance(); httpAdapter.use( @@ -96,7 +96,9 @@ async function bootstrap(): Promise { const downstream = app.get(DOWNSTREAM_CLIENT); await downstream.close(); - const circuitBreaker = app.get(CircuitBreakerService); + const circuitBreaker = app.get( + CircuitBreakerService, + ); await circuitBreaker.shutdown(); await shutdownTracer(); diff --git a/services/student-bff/src/shared/action-state.ts b/services/student-bff/src/shared/action-state.ts index aa2b13f..edf8814 100644 --- a/services/student-bff/src/shared/action-state.ts +++ b/services/student-bff/src/shared/action-state.ts @@ -59,7 +59,10 @@ export interface Degradable { /** * 构造成功响应. */ -export function ok(data: T, meta?: ActionStateSuccess["meta"]): ActionStateSuccess { +export function ok( + data: T, + meta?: ActionStateSuccess["meta"], +): ActionStateSuccess { return { success: true, data, meta }; } @@ -96,7 +99,7 @@ export function fail( * - data.degradedReason=原因 * - data.degradedFields=哪些字段降级了 */ -export function degraded( +export function degraded( data: T, reason: string, degradedFields: string[], diff --git a/services/student-bff/src/shared/cache/cache.module.ts b/services/student-bff/src/shared/cache/cache.module.ts index cee25d3..bcda586 100644 --- a/services/student-bff/src/shared/cache/cache.module.ts +++ b/services/student-bff/src/shared/cache/cache.module.ts @@ -20,7 +20,7 @@ * student:viewports:{userId} TTL 300s */ import { Global, Module, OnModuleDestroy } from "@nestjs/common"; -import Redis from "ioredis"; +import { Redis } from "ioredis"; import { env } from "../../config/env.js"; import { logger } from "../observability/logger.js"; import { recordCacheAccess } from "../observability/metrics.js"; @@ -46,7 +46,10 @@ export const CacheTTL = { /** * 缓存 Key 构建器 (统一前缀 + 规范化). */ -export function buildCacheKey(pattern: string, ...parts: (string | number)[]): string { +export function buildCacheKey( + pattern: string, + ...parts: (string | number)[] +): string { const suffix = parts.map(String).join(":"); return `${env.REDIS_KEY_PREFIX}${pattern}:${suffix}`; } @@ -100,7 +103,10 @@ export class CacheService { * 读取缓存, 自动 JSON 反序列化. * 失败时返回 null (不抛异常, 上层走降级模式). */ - async get(pattern: string, ...keyParts: (string | number)[]): Promise { + async get( + pattern: string, + ...keyParts: (string | number)[] + ): Promise { const key = buildCacheKey(pattern, ...keyParts); try { const raw = await this.redis.get(key); @@ -143,7 +149,10 @@ export class CacheService { * 失效缓存 (按 pattern 通配符删除). * 用于写操作后主动失效相关缓存. */ - async invalidate(pattern: string, ...keyParts: (string | number)[]): Promise { + async invalidate( + pattern: string, + ...keyParts: (string | number)[] + ): Promise { const key = buildCacheKey(pattern, ...keyParts); try { // 如果 keyParts 含通配符, 用 SCAN 删除 diff --git a/services/student-bff/src/shared/circuit-breaker/circuit-breaker.service.ts b/services/student-bff/src/shared/circuit-breaker/circuit-breaker.service.ts index db70c46..5ee7dba 100644 --- a/services/student-bff/src/shared/circuit-breaker/circuit-breaker.service.ts +++ b/services/student-bff/src/shared/circuit-breaker/circuit-breaker.service.ts @@ -21,12 +21,18 @@ */ import { Injectable } from "@nestjs/common"; import CircuitBreaker from "opossum"; +import promClient from "prom-client"; import type { DownstreamClient, CallOptions } from "@edu/shared-ts/bff"; import { ServiceUnavailableError } from "../errors/application-error.js"; import { metricsRegistry } from "../observability/metrics.js"; import { logger } from "../observability/logger.js"; import { env } from "../../config/env.js"; +/** + * 熔断器状态 (字符串字面量, 对齐 opossum 事件名). + */ +type BreakerState = "closed" | "open" | "half_open"; + /** * 熔断器配置. */ @@ -52,13 +58,13 @@ const DEFAULT_CONFIG: BreakerConfig = { /** * 熔断器状态映射到指标值. */ -function stateToMetricValue(state: CircuitBreaker.Status): number { +function stateToMetricValue(state: BreakerState): number { switch (state) { - case CircuitBreaker.CLOSED: + case "closed": return 0; - case CircuitBreaker.OPEN: + case "open": return 1; - case CircuitBreaker.HALF_OPEN: + case "half_open": return 2; default: return 0; @@ -68,13 +74,13 @@ function stateToMetricValue(state: CircuitBreaker.Status): number { /** * 熔断器状态名称. */ -function stateName(state: CircuitBreaker.Status): string { +function stateName(state: BreakerState): string { switch (state) { - case CircuitBreaker.CLOSED: + case "closed": return "closed"; - case CircuitBreaker.OPEN: + case "open": return "open"; - case CircuitBreaker.HALF_OPEN: + case "half_open": return "half_open"; default: return "unknown"; @@ -108,7 +114,13 @@ export class CircuitBreakerService { request: TRequest, options?: CallOptions, ): Promise { - const breaker = this.getOrCreateBreaker(service, downstream, method, request, options); + const breaker = this.getOrCreateBreaker( + service, + downstream, + method, + request, + options, + ); try { return (await breaker.fire()) as TResponse; @@ -124,9 +136,12 @@ export class CircuitBreakerService { /** * 获取熔断器当前状态. */ - getState(service: string): CircuitBreaker.Status | null { + getState(service: string): BreakerState | null { const breaker = this.breakers.get(service); - return breaker ? breaker.status : null; + if (!breaker) return null; + if (breaker.opened) return "open"; + if (breaker.halfOpen) return "half_open"; + return "closed"; } /** @@ -165,17 +180,17 @@ export class CircuitBreakerService { // 状态变更监听 breaker.on("open", () => { logger.warn({ service }, "Circuit breaker OPENED"); - this.updateMetric(service, CircuitBreaker.OPEN); + this.updateMetric(service, "open"); }); breaker.on("close", () => { logger.info({ service }, "Circuit breaker CLOSED (recovered)"); - this.updateMetric(service, CircuitBreaker.CLOSED); + this.updateMetric(service, "closed"); }); breaker.on("halfOpen", () => { logger.info({ service }, "Circuit breaker HALF-OPEN"); - this.updateMetric(service, CircuitBreaker.HALF_OPEN); + this.updateMetric(service, "half_open"); }); // fallback: 熔断开启时返回 ServiceUnavailableError @@ -187,15 +202,16 @@ export class CircuitBreakerService { }); this.breakers.set(service, breaker); - this.updateMetric(service, CircuitBreaker.CLOSED); + this.updateMetric(service, "closed"); } else { // 更新执行函数 (opossum 允许重新设置 action) // 由于 opossum 不支持直接替换 action, 我们使用 wrapper 方式 // 实际上 opossum 的 fire() 会调用构造时传入的函数, // 所以我们用一个可变的 wrapper - (breaker as unknown as { action: () => Promise }).action = async () => { - return downstream.call(service, method, request, options); - }; + (breaker as unknown as { action: () => Promise }).action = + async () => { + return downstream.call(service, method, request, options); + }; } return breaker; @@ -204,12 +220,13 @@ export class CircuitBreakerService { /** * 更新熔断器指标. */ - private updateMetric(service: string, state: CircuitBreaker.Status): void { + private updateMetric(service: string, state: BreakerState): void { const value = stateToMetricValue(state); const name = stateName(state); - metricsRegistry - .getSingleMetric("student_bff_circuit_state") - ?.set({ service, state: name }, value); + ( + metricsRegistry.getSingleMetric("student_bff_circuit_state") as + promClient.Gauge | undefined + )?.set({ service, state: name }, value); } /** diff --git a/services/student-bff/src/shared/errors/application-error.test.ts b/services/student-bff/src/shared/errors/application-error.test.ts index a2eafc6..d493ef7 100644 --- a/services/student-bff/src/shared/errors/application-error.test.ts +++ b/services/student-bff/src/shared/errors/application-error.test.ts @@ -133,33 +133,78 @@ describe("ApplicationError", () => { const err = new ValidationError("Invalid input", { field: "name" }); err.traceId = "trace-abc"; const json = err.toJSON(); + const error = ( + json as { + error: { + code: string; + message: string; + i18nKey?: string; + details?: unknown; + traceId?: string; + }; + } + ).error; expect(json.success).toBe(false); expect(json.error).toBeDefined(); - expect(json.error.code).toBe("BFF_STUDENT_VALIDATION_ERROR"); - expect(json.error.message).toBe("Invalid input"); - expect(json.error.i18nKey).toBe("error.bffStudent.validation_error"); - expect(json.error.details).toEqual({ field: "name" }); - expect(json.error.traceId).toBe("trace-abc"); + expect(error.code).toBe("BFF_STUDENT_VALIDATION_ERROR"); + expect(error.message).toBe("Invalid input"); + expect(error.i18nKey).toBe("error.bffStudent.validation_error"); + expect(error.details).toEqual({ field: "name" }); + expect(error.traceId).toBe("trace-abc"); }); it("should generate correct i18n key for each error code", () => { const cases = [ - { error: new ValidationError(), expectedKey: "error.bffStudent.validation_error" }, - { error: new UnauthorizedError(), expectedKey: "error.bffStudent.unauthorized" }, - { error: new ForbiddenResourceError("test"), expectedKey: "error.bffStudent.forbidden_resource" }, - { error: new IdentityMismatchError("test"), expectedKey: "error.bffStudent.identity_mismatch" }, - { error: new NotFoundError("X", "1"), expectedKey: "error.bffStudent.not_found" }, - { error: new ConflictError("test"), expectedKey: "error.bffStudent.conflict" }, - { error: new BusinessError("test"), expectedKey: "error.bffStudent.business_error" }, - { error: new BadGatewayError("test"), expectedKey: "error.bffStudent.bad_gateway" }, - { error: new GatewayTimeoutError("test"), expectedKey: "error.bffStudent.gateway_timeout" }, - { error: new ServiceUnavailableError("test"), expectedKey: "error.bffStudent.service_unavailable" }, - { error: new InternalError("test"), expectedKey: "error.bffStudent.internal_error" }, + { + error: new ValidationError("test"), + expectedKey: "error.bffStudent.validation_error", + }, + { + error: new UnauthorizedError(), + expectedKey: "error.bffStudent.unauthorized", + }, + { + error: new ForbiddenResourceError("test"), + expectedKey: "error.bffStudent.forbidden_resource", + }, + { + error: new IdentityMismatchError("test"), + expectedKey: "error.bffStudent.identity_mismatch", + }, + { + error: new NotFoundError("X", "1"), + expectedKey: "error.bffStudent.not_found", + }, + { + error: new ConflictError("test"), + expectedKey: "error.bffStudent.conflict", + }, + { + error: new BusinessError("test"), + expectedKey: "error.bffStudent.business_error", + }, + { + error: new BadGatewayError("test"), + expectedKey: "error.bffStudent.bad_gateway", + }, + { + error: new GatewayTimeoutError("test"), + expectedKey: "error.bffStudent.gateway_timeout", + }, + { + error: new ServiceUnavailableError("test"), + expectedKey: "error.bffStudent.service_unavailable", + }, + { + error: new InternalError("test"), + expectedKey: "error.bffStudent.internal_error", + }, ]; for (const { error, expectedKey } of cases) { const json = error.toJSON(); - expect(json.error.i18nKey).toBe(expectedKey); + const errObj = (json as { error: { i18nKey?: string } }).error; + expect(errObj.i18nKey).toBe(expectedKey); } }); }); @@ -168,7 +213,9 @@ describe("ApplicationError", () => { it("should set constructor name as error.name", () => { expect(new ValidationError("x").name).toBe("ValidationError"); expect(new UnauthorizedError().name).toBe("UnauthorizedError"); - expect(new ForbiddenResourceError("x").name).toBe("ForbiddenResourceError"); + expect(new ForbiddenResourceError("x").name).toBe( + "ForbiddenResourceError", + ); expect(new IdentityMismatchError("x").name).toBe("IdentityMismatchError"); }); }); diff --git a/services/student-bff/src/shared/graphql/yoga.ts b/services/student-bff/src/shared/graphql/yoga.ts index 1f908f8..f568707 100644 --- a/services/student-bff/src/shared/graphql/yoga.ts +++ b/services/student-bff/src/shared/graphql/yoga.ts @@ -18,15 +18,26 @@ import { makeExecutableSchema } from "@graphql-tools/schema"; import type { Request, Response } from "express"; import type { DownstreamClient } from "@edu/shared-ts/bff"; import { env } from "../../config/env.js"; -import { logger } from "./logger.js"; +import { logger } from "../observability/logger.js"; import type { Redis } from "ioredis"; -import { createDataLoaders, type StudentBffDataLoaders } from "../../student/dataloaders/data-loader.module.js"; +import { CacheService } from "../cache/cache.module.js"; +import { + createDataLoaders, + type StudentBffDataLoaders, +} from "../../student/dataloaders/data-loader.module.js"; import { extractUserIdFromRequest, extractTraceIdFromRequest, extractUserRolesFromRequest, } from "../../student/guards/authorization.guard.js"; +/** + * makeExecutableSchema 接受的 resolver 类型 (从函数签名推断, 避免额外依赖). + */ +type SchemaResolvers = NonNullable< + Parameters[0]["resolvers"] +>; + /** * GraphQL Context (每个请求一份). */ @@ -36,6 +47,7 @@ export interface StudentBffContext { userRoles: string[]; downstream: DownstreamClient; redis: Redis; + cache: CacheService; dataLoaders: StudentBffDataLoaders; requestId: string; } @@ -69,12 +81,16 @@ export async function loadSchemaSDL(): Promise { * @param resolvers GraphQL Resolver 映射表 (由 StudentModule 装配) * @param downstream DownstreamClient 实例 (由 NestJS DI 注入) * @param redis Redis 客户端 (由 NestJS DI 注入) + * @param cache CacheService 实例 (由 NestJS DI 注入) */ export async function createStudentBffYoga( - resolvers: Record, + resolvers: SchemaResolvers, downstream: DownstreamClient, redis: Redis, -): Promise, StudentBffContext>> { + cache: CacheService, +): Promise< + YogaServerInstance<{ req: Request; res: Response }, StudentBffContext> +> { const typeDefs = await loadSchemaSDL(); const schema = makeExecutableSchema({ @@ -82,10 +98,13 @@ export async function createStudentBffYoga( resolvers, }); - const yoga = createYoga<{ - req: Request; - res: Response; - }, StudentBffContext>({ + const yoga = createYoga< + { + req: Request; + res: Response; + }, + StudentBffContext + >({ schema, graphqlEndpoint: "/graphql", context: ({ req }): StudentBffContext => { @@ -98,6 +117,7 @@ export async function createStudentBffYoga( userRoles, downstream, redis, + cache, dataLoaders: createDataLoaders(downstream), requestId: traceId, }; @@ -111,26 +131,6 @@ export async function createStudentBffYoga( maskedErrors: env.NODE_ENV === "production", // 开发环境启用 Playground graphiql: env.GRAPHQL_PLAYGROUND && env.NODE_ENV === "development", - // 错误格式化 (G8): GraphQL errors 数组 + extensions.code + extensions.traceId - formatError: (err) => { - const originalError = err.originalError; - const code = - (originalError as { code?: string })?.code ?? - "BFF_STUDENT_INTERNAL_ERROR"; - const traceId = err.context?.requestId ?? "unknown"; - - return { - message: err.message, - extensions: { - code, - traceId, - i18nKey: `error.bffStudent.${code.replace(/^BFF_STUDENT_/, "").toLowerCase()}`, - severity: "error", - }, - path: err.path, - locations: err.locations, - }; - }, }); logger.info( diff --git a/services/student-bff/src/shared/observability/metrics.ts b/services/student-bff/src/shared/observability/metrics.ts index f81d627..c4a05b9 100644 --- a/services/student-bff/src/shared/observability/metrics.ts +++ b/services/student-bff/src/shared/observability/metrics.ts @@ -127,33 +127,35 @@ export function recordDownstreamCall( errorType?: string, ): void { const labels = { service, method, status }; - registry - .getSingleMetric("student_bff_downstream_calls_total") - ?.inc(labels); - registry - .getSingleMetric("student_bff_downstream_duration_seconds") - ?.observe({ service, method }, durationMs / 1000); + ( + registry.getSingleMetric("student_bff_downstream_calls_total") as + promClient.Counter | undefined + )?.inc(labels); + ( + registry.getSingleMetric("student_bff_downstream_duration_seconds") as + promClient.Histogram | undefined + )?.observe({ service, method }, durationMs / 1000); if (status === "error" && errorType) { - registry - .getSingleMetric("student_bff_downstream_errors_total") - ?.inc({ service, method, error_type: errorType }); + ( + registry.getSingleMetric("student_bff_downstream_errors_total") as + promClient.Counter | undefined + )?.inc({ service, method, error_type: errorType }); } } /** * 缓存命中/未命中指标辅助器. */ -export function recordCacheAccess( - keyPattern: string, - hit: boolean, -): void { +export function recordCacheAccess(keyPattern: string, hit: boolean): void { if (hit) { - registry - .getSingleMetric("student_bff_cache_hits_total") - ?.inc({ cache_key_pattern: keyPattern }); + ( + registry.getSingleMetric("student_bff_cache_hits_total") as + promClient.Counter | undefined + )?.inc({ cache_key_pattern: keyPattern }); } else { - registry - .getSingleMetric("student_bff_cache_misses_total") - ?.inc({ cache_key_pattern: keyPattern }); + ( + registry.getSingleMetric("student_bff_cache_misses_total") as + promClient.Counter | undefined + )?.inc({ cache_key_pattern: keyPattern }); } } diff --git a/services/student-bff/src/student/events/event-subscriber.ts b/services/student-bff/src/student/events/event-subscriber.ts index 73096be..dfba742 100644 --- a/services/student-bff/src/student/events/event-subscriber.ts +++ b/services/student-bff/src/student/events/event-subscriber.ts @@ -21,8 +21,14 @@ * * 幂等性: Redis SETNX event_id 去重 (workline §5.4) */ -import { Injectable, OnModuleDestroy, OnModuleInit, Inject } from "@nestjs/common"; +import { + Injectable, + OnModuleDestroy, + OnModuleInit, + Inject, +} from "@nestjs/common"; import { Kafka, type Consumer, type EachMessagePayload } from "kafkajs"; +import promClient from "prom-client"; import { REDIS_CLIENT, CacheService } from "../../shared/cache/cache.module.js"; import type { Redis } from "ioredis"; import { env } from "../../config/env.js"; @@ -126,7 +132,10 @@ export class EventSubscriberService implements OnModuleInit, OnModuleDestroy { const { topic, partition, message } = payload; const eventStr = message.value?.toString("utf-8"); if (!eventStr) { - logger.warn({ topic, partition, offset: message.offset }, "Empty Kafka message"); + logger.warn( + { topic, partition, offset: message.offset }, + "Empty Kafka message", + ); return; } @@ -149,15 +158,22 @@ export class EventSubscriberService implements OnModuleInit, OnModuleDestroy { } // 指标记录 - metricsRegistry - .getSingleMetric("student_bff_event_consumed_total") - ?.inc({ topic, event_type: event.event_type }); + ( + metricsRegistry.getSingleMetric("student_bff_event_consumed_total") as + promClient.Counter | undefined + )?.inc({ topic, event_type: event.event_type }); // 失效相关缓存 + 推送给学生 const studentId = event.student_id ?? event.user_id; if (studentId) { await this.invalidateCache(topic, studentId); - await this.pushGateway.pushToStudent(studentId, topic, event.event_type, event.payload, event.timestamp); + await this.pushGateway.pushToStudent( + studentId, + topic, + event.event_type, + event.payload, + event.timestamp, + ); } } catch (err) { logger.error( @@ -170,7 +186,10 @@ export class EventSubscriberService implements OnModuleInit, OnModuleDestroy { /** * 按 topic 失效相关缓存. */ - private async invalidateCache(topic: string, studentId: string): Promise { + private async invalidateCache( + topic: string, + studentId: string, + ): Promise { try { if (topic.startsWith("edu.teaching.homework")) { await this.cacheService.invalidate("homework", studentId); @@ -185,7 +204,9 @@ export class EventSubscriberService implements OnModuleInit, OnModuleDestroy { await this.cacheService.invalidate("viewports", studentId); await this.cacheService.invalidate("dashboard", studentId); } else if (topic === "edu.notification.sent") { - await this.cacheService.invalidateByPrefix(`notifications:${studentId}`); + await this.cacheService.invalidateByPrefix( + `notifications:${studentId}`, + ); } } catch (err) { logger.warn({ err, topic, studentId }, "Cache invalidation failed"); diff --git a/services/student-bff/src/student/push/push-gateway.service.test.ts b/services/student-bff/src/student/push/push-gateway.service.test.ts index 87daac3..6ba5a03 100644 --- a/services/student-bff/src/student/push/push-gateway.service.test.ts +++ b/services/student-bff/src/student/push/push-gateway.service.test.ts @@ -113,7 +113,9 @@ describe("PushGatewayService", () => { }); it("should return success=false when fetch throws AbortError (timeout)", async () => { - fetchMock.mockRejectedValue(new Error("The operation was aborted due to timeout")); + fetchMock.mockRejectedValue( + new Error("The operation was aborted due to timeout"), + ); const result = await service.pushToStudent( "u-stu-001", @@ -139,10 +141,13 @@ describe("PushGatewayService", () => { ); const callArgs = fetchMock.mock.calls[0]; - const body = JSON.parse(callArgs[1].body as string); + const body = JSON.parse(callArgs![1].body as string); expect(body.type).toBe("homework.assigned"); expect(body.topic).toBe("edu.teaching.homework.assigned"); - expect(body.payload).toEqual({ homeworkId: "hw-001", title: "Math Chapter 3" }); + expect(body.payload).toEqual({ + homeworkId: "hw-001", + title: "Math Chapter 3", + }); expect(body.timestamp).toBe("2026-07-10T10:00:00Z"); }); }); diff --git a/services/student-bff/src/student/push/push-gateway.service.ts b/services/student-bff/src/student/push/push-gateway.service.ts index c116a69..c5fe268 100644 --- a/services/student-bff/src/student/push/push-gateway.service.ts +++ b/services/student-bff/src/student/push/push-gateway.service.ts @@ -16,6 +16,7 @@ * - 未来可直接由 resolver 调用 (如主动推送通知) */ import { Injectable } from "@nestjs/common"; +import promClient from "prom-client"; import { env } from "../../config/env.js"; import { logger } from "../../shared/observability/logger.js"; import { metricsRegistry } from "../../shared/observability/metrics.js"; @@ -80,9 +81,10 @@ export class PushGatewayService { ); const pushStatus = response.ok ? "success" : `http_${response.status}`; - metricsRegistry - .getSingleMetric("student_bff_event_pushed_total") - ?.inc({ topic, push_status: pushStatus }); + ( + metricsRegistry.getSingleMetric("student_bff_event_pushed_total") as + promClient.Counter | undefined + )?.inc({ topic, push_status: pushStatus }); if (!response.ok) { logger.warn( @@ -97,9 +99,10 @@ export class PushGatewayService { { err, studentId, topic }, "Push-gateway push failed (soft failure)", ); - metricsRegistry - .getSingleMetric("student_bff_event_pushed_total") - ?.inc({ topic, push_status: "error" }); + ( + metricsRegistry.getSingleMetric("student_bff_event_pushed_total") as + promClient.Counter | undefined + )?.inc({ topic, push_status: "error" }); return { success: false, diff --git a/services/student-bff/src/student/resolvers/ai-stream.resolver.ts b/services/student-bff/src/student/resolvers/ai-stream.resolver.ts index 0a3ec48..02df4be 100644 --- a/services/student-bff/src/student/resolvers/ai-stream.resolver.ts +++ b/services/student-bff/src/student/resolvers/ai-stream.resolver.ts @@ -19,7 +19,10 @@ */ import { z } from "zod"; import type { StudentBffContext } from "../../shared/graphql/yoga.js"; -import { UnauthorizedError, ValidationError } from "../../shared/errors/application-error.js"; +import { + UnauthorizedError, + ValidationError, +} from "../../shared/errors/application-error.js"; const AIStreamChatInputSchema = z.object({ messages: z @@ -45,7 +48,7 @@ const AIStreamChatInputSchema = z.object({ /** * AI 流式响应 chunk 结构 (对齐 ai.StreamChat gRPC stream). */ -interface AIStreamChunk { +export interface AIStreamChunk { content: string; done: boolean; model?: string; @@ -85,28 +88,40 @@ export const aiStreamResolvers = { ); } const input = parseResult.data; + const userId = ctx.userId; - return (async function* (): AsyncGenerator<{ aiStreamChat: AIStreamChunk }> { + return (async function* (): AsyncGenerator<{ + aiStreamChat: AIStreamChunk; + }> { try { // 调用 ai.StreamChat (gRPC server-streaming) // DownstreamClient.callStream 返回 AsyncIterable - const stream = ctx.downstream.callStream("ai", "StreamChat", { - userId: ctx.userId, - messages: input.messages, - model: input.model, - context: input.context, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - timeoutMs: 60000, // 流式调用 60s 超时 - }); + const stream = ctx.downstream.callStream( + "ai", + "StreamChat", + { + userId, + messages: input.messages, + model: input.model, + context: input.context, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": userId }, + timeoutMs: 60000, // 流式调用 60s 超时 + }, + ); for await (const chunk of stream) { const typed = chunk as { content?: string; done?: boolean; model?: string; - usage?: { promptTokens: number; completionTokens: number; totalTokens: number }; + usage?: { + promptTokens: number; + completionTokens: number; + totalTokens: number; + }; }; yield { diff --git a/services/student-bff/src/student/resolvers/analytics.resolver.ts b/services/student-bff/src/student/resolvers/analytics.resolver.ts index 397543c..5863700 100644 --- a/services/student-bff/src/student/resolvers/analytics.resolver.ts +++ b/services/student-bff/src/student/resolvers/analytics.resolver.ts @@ -32,20 +32,31 @@ export const analyticsResolvers = { assertOwnData(ctx.userId, args.studentId); - const cached = await ctx.redis.get("analytics:weakness", ctx.userId); + const cached = await ctx.cache.get( + "analytics:weakness", + ctx.userId, + ); if (cached) { - return ok(cached, { traceId: ctx.traceId, cachedAt: new Date().toISOString() }); + return ok(cached, { + traceId: ctx.traceId, + cachedAt: new Date().toISOString(), + }); } try { - const result = await ctx.downstream.call("data-ana", "GetStudentWeakness", { - studentId: ctx.userId, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "data-ana", + "GetStudentWeakness", + { + studentId: ctx.userId, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); - await ctx.redis.set( + await ctx.cache.set( "analytics:weakness", result, CacheTTL.ANALYTICS_WEAKNESS, @@ -79,21 +90,34 @@ export const analyticsResolvers = { const range = args.range ?? "30d"; const cacheKey = `${ctx.userId}:${range}`; - const cached = await ctx.redis.get("analytics:trend", cacheKey); + const cached = await ctx.cache.get("analytics:trend", cacheKey); if (cached) { - return ok(cached, { traceId: ctx.traceId, cachedAt: new Date().toISOString() }); + return ok(cached, { + traceId: ctx.traceId, + cachedAt: new Date().toISOString(), + }); } try { - const result = await ctx.downstream.call("data-ana", "GetLearningTrend", { - studentId: ctx.userId, - range, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "data-ana", + "GetLearningTrend", + { + studentId: ctx.userId, + range, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); - await ctx.redis.set("analytics:trend", result, CacheTTL.ANALYTICS_TREND, cacheKey); + await ctx.cache.set( + "analytics:trend", + result, + CacheTTL.ANALYTICS_TREND, + cacheKey, + ); return ok(result, { traceId: ctx.traceId }); } catch (err) { return fail( diff --git a/services/student-bff/src/student/resolvers/content.resolver.ts b/services/student-bff/src/student/resolvers/content.resolver.ts index 28ad893..42649fd 100644 --- a/services/student-bff/src/student/resolvers/content.resolver.ts +++ b/services/student-bff/src/student/resolvers/content.resolver.ts @@ -23,7 +23,12 @@ export const contentResolvers = { */ async textbooks( _parent: unknown, - args: { gradeId?: string; subjectId?: string; page?: number; pageSize?: number }, + args: { + gradeId?: string; + subjectId?: string; + page?: number; + pageSize?: number; + }, ctx: StudentBffContext, ): Promise { if (!ctx.userId) { @@ -31,24 +36,32 @@ export const contentResolvers = { } const cacheKey = `${args.gradeId ?? "all"}:${args.subjectId ?? "all"}`; - const cached = await ctx.redis.get("textbooks", cacheKey); + const cached = await ctx.cache.get("textbooks", cacheKey); if (cached) { - return ok(cached, { traceId: ctx.traceId, cachedAt: new Date().toISOString() }); + return ok(cached, { + traceId: ctx.traceId, + cachedAt: new Date().toISOString(), + }); } try { - const result = await ctx.downstream.call("content", "ListTextbooks", { - gradeId: args.gradeId, - subjectId: args.subjectId, - page: args.page ?? 1, - pageSize: Math.min(args.pageSize ?? 20, 50), - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "content", + "ListTextbooks", + { + gradeId: args.gradeId, + subjectId: args.subjectId, + page: args.page ?? 1, + pageSize: Math.min(args.pageSize ?? 20, 50), + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); const data = result as { textbooks: unknown[] }; - await ctx.redis.set("textbooks", data, CacheTTL.TEXTBOOKS, cacheKey); + await ctx.cache.set("textbooks", data, CacheTTL.TEXTBOOKS, cacheKey); return ok(data, { traceId: ctx.traceId }); } catch (err) { return fail( @@ -72,21 +85,34 @@ export const contentResolvers = { throw new UnauthorizedError(); } - const cached = await ctx.redis.get("chapters", args.textbookId); + const cached = await ctx.cache.get("chapters", args.textbookId); if (cached) { - return ok(cached, { traceId: ctx.traceId, cachedAt: new Date().toISOString() }); + return ok(cached, { + traceId: ctx.traceId, + cachedAt: new Date().toISOString(), + }); } try { - const result = await ctx.downstream.call("content", "ListChapters", { - textbookId: args.textbookId, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "content", + "ListChapters", + { + textbookId: args.textbookId, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); const data = result as { chapters: unknown[] }; - await ctx.redis.set("chapters", data, CacheTTL.CHAPTERS, args.textbookId); + await ctx.cache.set( + "chapters", + data, + CacheTTL.CHAPTERS, + args.textbookId, + ); return ok(data, { traceId: ctx.traceId }); } catch (err) { return fail( @@ -112,13 +138,18 @@ export const contentResolvers = { } try { - const result = await ctx.downstream.call("content", "GetLearningPath", { - studentId: ctx.userId, - knowledgePointId: args.knowledgePointId, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "content", + "GetLearningPath", + { + studentId: ctx.userId, + knowledgePointId: args.knowledgePointId, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); return ok(result, { traceId: ctx.traceId }); } catch (err) { diff --git a/services/student-bff/src/student/resolvers/dashboard.resolver.ts b/services/student-bff/src/student/resolvers/dashboard.resolver.ts index d2f1689..3a922a4 100644 --- a/services/student-bff/src/student/resolvers/dashboard.resolver.ts +++ b/services/student-bff/src/student/resolvers/dashboard.resolver.ts @@ -19,7 +19,12 @@ */ import type { StudentBffContext } from "../../shared/graphql/yoga.js"; import type { DownstreamResponse } from "@edu/shared-ts/bff"; -import { ok, fail, degraded, DegradedReason } from "../../shared/action-state.js"; +import { + ok, + fail, + degraded, + DegradedReason, +} from "../../shared/action-state.js"; import { CacheTTL } from "../../shared/cache/cache.module.js"; import { UnauthorizedError } from "../../shared/errors/application-error.js"; @@ -40,10 +45,12 @@ export const dashboardResolvers = { } // 缓存命中检查 - const cacheKey = ["dashboard", ctx.userId] as const; - const cached = await ctx.redis.get("dashboard", ctx.userId); + const cached = await ctx.cache.get("dashboard", ctx.userId); if (cached) { - return ok(cached, { traceId: ctx.traceId, cachedAt: new Date().toISOString() }); + return ok(cached, { + traceId: ctx.traceId, + cachedAt: new Date().toISOString(), + }); } // 并行调用下游 (Promise.allSettled 容错) @@ -52,42 +59,62 @@ export const dashboardResolvers = { service: "iam", method: "GetUserInfo", request: { userId: ctx.userId }, - options: { traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } }, + options: { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, }, { service: "core-edu", method: "ListHomeworkByStudent", request: { studentId: ctx.userId, status: "pending" }, - options: { traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } }, + options: { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, }, { service: "core-edu", method: "ListExamsByClass", request: { studentId: ctx.userId }, - options: { traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } }, + options: { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, }, { service: "core-edu", method: "ListGradesByStudent", request: { studentId: ctx.userId, page: 1, pageSize: 1 }, - options: { traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } }, + options: { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, }, { service: "data-ana", method: "GetStudentDashboard", request: { studentId: ctx.userId }, - options: { traceId: ctx.traceId, metadata: { "x-user-id": ctx.userId } }, + options: { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, }, ] as const); - const [userInfoResp, homeworkResp, examsResp, gradesResp, dashboardAnaResp] = - results as [ - DownstreamResponse, - DownstreamResponse, - DownstreamResponse, - DownstreamResponse, - DownstreamResponse, - ]; + const [ + userInfoResp, + homeworkResp, + examsResp, + gradesResp, + dashboardAnaResp, + ] = results as [ + DownstreamResponse, + DownstreamResponse, + DownstreamResponse, + DownstreamResponse, + DownstreamResponse, + ]; // 必需字段失败检查 if (!userInfoResp.success) { @@ -113,13 +140,13 @@ export const dashboardResolvers = { // 容错聚合: 各字段独立降级 const degradedFields: string[] = []; const pendingHomework = homeworkResp.success - ? (homeworkResp.data as { homework: unknown[] }).homework ?? [] + ? ((homeworkResp.data as { homework: unknown[] }).homework ?? []) : (degradedFields.push("pendingHomework"), []); const upcomingExams = examsResp.success - ? (examsResp.data as { exams: unknown[] }).exams ?? [] + ? ((examsResp.data as { exams: unknown[] }).exams ?? []) : (degradedFields.push("upcomingExams"), []); const lastGrade = gradesResp.success - ? ((gradesResp.data as { grades: unknown[] }).grades ?? [])[0] ?? null + ? (((gradesResp.data as { grades: unknown[] }).grades ?? [])[0] ?? null) : (degradedFields.push("lastGrade"), null); const analyticsSummary = dashboardAnaResp.success ? dashboardAnaResp.data @@ -141,7 +168,7 @@ export const dashboardResolvers = { }; // 写缓存 - await ctx.redis.set("dashboard", data, CacheTTL.DASHBOARD, ctx.userId); + await ctx.cache.set("dashboard", data, CacheTTL.DASHBOARD, ctx.userId); if (degradedFields.length > 0) { return degraded( diff --git a/services/student-bff/src/student/resolvers/exams.resolver.ts b/services/student-bff/src/student/resolvers/exams.resolver.ts index 6f50942..a1d5893 100644 --- a/services/student-bff/src/student/resolvers/exams.resolver.ts +++ b/services/student-bff/src/student/resolvers/exams.resolver.ts @@ -28,23 +28,31 @@ export const examsResolvers = { } const cacheKey = `${ctx.userId}:${args.classId ?? "all"}:${args.status ?? "all"}`; - const cached = await ctx.redis.get("exams", cacheKey); + const cached = await ctx.cache.get("exams", cacheKey); if (cached) { - return ok(cached, { traceId: ctx.traceId, cachedAt: new Date().toISOString() }); + return ok(cached, { + traceId: ctx.traceId, + cachedAt: new Date().toISOString(), + }); } try { - const result = await ctx.downstream.call("core-edu", "ListExamsByClass", { - studentId: ctx.userId, - classId: args.classId, - status: args.status ?? "upcoming", - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "core-edu", + "ListExamsByClass", + { + studentId: ctx.userId, + classId: args.classId, + status: args.status ?? "upcoming", + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); const data = result as { exams: unknown[] }; - await ctx.redis.set("exams", data, CacheTTL.EXAMS, cacheKey); + await ctx.cache.set("exams", data, CacheTTL.EXAMS, cacheKey); return ok(data, { traceId: ctx.traceId }); } catch (err) { return fail( diff --git a/services/student-bff/src/student/resolvers/grades.resolver.ts b/services/student-bff/src/student/resolvers/grades.resolver.ts index 7ab279d..efea884 100644 --- a/services/student-bff/src/student/resolvers/grades.resolver.ts +++ b/services/student-bff/src/student/resolvers/grades.resolver.ts @@ -40,24 +40,32 @@ export const gradesResolvers = { const pageSize = Math.min(args.pageSize ?? 20, 50); const cacheKey = `${ctx.userId}:${page}:${args.subject ?? "all"}`; - const cached = await ctx.redis.get("grades", cacheKey); + const cached = await ctx.cache.get("grades", cacheKey); if (cached) { - return ok(cached, { traceId: ctx.traceId, cachedAt: new Date().toISOString() }); + return ok(cached, { + traceId: ctx.traceId, + cachedAt: new Date().toISOString(), + }); } try { - const result = await ctx.downstream.call("core-edu", "ListGradesByStudent", { - studentId: ctx.userId, - subject: args.subject, - page, - pageSize, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "core-edu", + "ListGradesByStudent", + { + studentId: ctx.userId, + subject: args.subject, + page, + pageSize, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); const data = result as { grades: unknown[]; totalCount: number }; - await ctx.redis.set("grades", data, CacheTTL.GRADES, cacheKey); + await ctx.cache.set("grades", data, CacheTTL.GRADES, cacheKey); return ok(data, { traceId: ctx.traceId }); } catch (err) { return fail( diff --git a/services/student-bff/src/student/resolvers/homework.resolver.test.ts b/services/student-bff/src/student/resolvers/homework.resolver.test.ts index da2abfb..a2e0301 100644 --- a/services/student-bff/src/student/resolvers/homework.resolver.test.ts +++ b/services/student-bff/src/student/resolvers/homework.resolver.test.ts @@ -8,20 +8,27 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { homeworkResolvers } from "./homework.resolver.js"; import type { StudentBffContext } from "../../shared/graphql/yoga.js"; -import { UnauthorizedError, ValidationError, ForbiddenResourceError } from "../../shared/errors/application-error.js"; +import { + UnauthorizedError, + ValidationError, + ForbiddenResourceError, +} from "../../shared/errors/application-error.js"; // Mock env to disable DEV_MODE vi.mock("../../config/env.js", () => ({ env: { DEV_MODE: false, NODE_ENV: "test" }, })); -function mockContext(overrides: Partial = {}): StudentBffContext { +function mockContext( + overrides: Partial = {}, +): StudentBffContext { const downstream = { call: vi.fn(), callStream: vi.fn(), callAll: vi.fn(), }; - const redis = { + const redis = {}; + const cache = { get: vi.fn().mockResolvedValue(null), set: vi.fn().mockResolvedValue(undefined), invalidate: vi.fn().mockResolvedValue(undefined), @@ -35,6 +42,7 @@ function mockContext(overrides: Partial = {}): StudentBffCont userRoles: ["student"], downstream: downstream as never, redis: redis as never, + cache: cache as never, dataLoaders, requestId: "trace-test-001", ...overrides, @@ -58,11 +66,15 @@ describe("homeworkResolvers", () => { it("should return cached data when cache hits", async () => { const cachedData = { homework: [{ id: "hw-001" }] }; - ctx.redis.get = vi.fn().mockResolvedValue(cachedData); + ctx.cache.get = vi.fn().mockResolvedValue(cachedData); - const result = await homeworkResolvers.Query.myHomework(null, {}, ctx); + const result = (await homeworkResolvers.Query.myHomework( + null, + {}, + ctx, + )) as { success: boolean; data: unknown }; expect(result.success).toBe(true); - expect((result as { data: unknown }).data).toEqual(cachedData); + expect(result.data).toEqual(cachedData); expect(ctx.downstream.call).not.toHaveBeenCalled(); }); @@ -70,12 +82,11 @@ describe("homeworkResolvers", () => { const downstreamData = { homework: [{ id: "hw-001", title: "Math HW" }] }; ctx.downstream.call = vi.fn().mockResolvedValue(downstreamData); - const result = await homeworkResolvers.Query.myHomework( + const result = (await homeworkResolvers.Query.myHomework( null, { status: "ASSIGNED", classId: "c-001" }, ctx, - ); - + )) as { success: boolean }; expect(result.success).toBe(true); expect(ctx.downstream.call).toHaveBeenCalledWith( "core-edu", @@ -86,15 +97,21 @@ describe("homeworkResolvers", () => { metadata: { "x-user-id": "u-stu-001" }, }), ); - expect(ctx.redis.set).toHaveBeenCalled(); + expect(ctx.cache.set).toHaveBeenCalled(); }); it("should return fail response when downstream fails", async () => { - ctx.downstream.call = vi.fn().mockRejectedValue(new Error("gRPC unavailable")); + ctx.downstream.call = vi + .fn() + .mockRejectedValue(new Error("gRPC unavailable")); - const result = await homeworkResolvers.Query.myHomework(null, {}, ctx); + const result = (await homeworkResolvers.Query.myHomework( + null, + {}, + ctx, + )) as { success: boolean; error: { code: string } }; expect(result.success).toBe(false); - expect((result as { error: { code: string } }).error.code).toBe("BFF_STUDENT_BAD_GATEWAY"); + expect(result.error.code).toBe("BFF_STUDENT_BAD_GATEWAY"); }); }); @@ -102,15 +119,17 @@ describe("homeworkResolvers", () => { const validInput = { homeworkId: "hw-001", studentId: "u-stu-001", - answers: [ - { questionId: "q-001", content: "My answer" }, - ], + answers: [{ questionId: "q-001", content: "My answer" }], }; it("should throw UnauthorizedError when userId is null", async () => { ctx.userId = null; await expect( - homeworkResolvers.Mutation.submitHomework(null, { input: validInput }, ctx), + homeworkResolvers.Mutation.submitHomework( + null, + { input: validInput }, + ctx, + ), ).rejects.toThrow(UnauthorizedError); }); @@ -130,7 +149,11 @@ describe("homeworkResolvers", () => { studentId: "u-stu-002", }; await expect( - homeworkResolvers.Mutation.submitHomework(null, { input: maliciousInput }, ctx), + homeworkResolvers.Mutation.submitHomework( + null, + { input: maliciousInput }, + ctx, + ), ).rejects.toThrow(ForbiddenResourceError); }); @@ -143,11 +166,11 @@ describe("homeworkResolvers", () => { }; ctx.downstream.call = vi.fn().mockResolvedValue(submitResult); - const result = await homeworkResolvers.Mutation.submitHomework( + const result = (await homeworkResolvers.Mutation.submitHomework( null, { input: validInput }, ctx, - ); + )) as { success: boolean }; expect(result.success).toBe(true); expect(ctx.downstream.call).toHaveBeenCalledWith( @@ -162,8 +185,14 @@ describe("homeworkResolvers", () => { metadata: { "x-user-id": "u-stu-001" }, }), ); - expect(ctx.redis.invalidate).toHaveBeenCalledWith("homework", "u-stu-001"); - expect(ctx.redis.invalidate).toHaveBeenCalledWith("dashboard", "u-stu-001"); + expect(ctx.cache.invalidate).toHaveBeenCalledWith( + "homework", + "u-stu-001", + ); + expect(ctx.cache.invalidate).toHaveBeenCalledWith( + "dashboard", + "u-stu-001", + ); }); it("should submit successfully when studentId is not provided in input", async () => { @@ -178,26 +207,28 @@ describe("homeworkResolvers", () => { status: "SUBMITTED", }); - const result = await homeworkResolvers.Mutation.submitHomework( + const result = (await homeworkResolvers.Mutation.submitHomework( null, { input: inputWithoutStudentId }, ctx, - ); + )) as { success: boolean }; expect(result.success).toBe(true); }); it("should return fail response when downstream fails", async () => { - ctx.downstream.call = vi.fn().mockRejectedValue(new Error("Submission failed")); + ctx.downstream.call = vi + .fn() + .mockRejectedValue(new Error("Submission failed")); - const result = await homeworkResolvers.Mutation.submitHomework( + const result = (await homeworkResolvers.Mutation.submitHomework( null, { input: validInput }, ctx, - ); + )) as { success: boolean; error: { code: string } }; expect(result.success).toBe(false); - expect((result as { error: { code: string } }).error.code).toBe("BFF_STUDENT_BAD_GATEWAY"); + expect(result.error.code).toBe("BFF_STUDENT_BAD_GATEWAY"); }); it("should validate answer content max length", async () => { @@ -207,7 +238,11 @@ describe("homeworkResolvers", () => { answers: [{ questionId: "q-001", content: "x".repeat(10001) }], }; await expect( - homeworkResolvers.Mutation.submitHomework(null, { input: longContentInput }, ctx), + homeworkResolvers.Mutation.submitHomework( + null, + { input: longContentInput }, + ctx, + ), ).rejects.toThrow(ValidationError); }); @@ -224,7 +259,11 @@ describe("homeworkResolvers", () => { ], }; await expect( - homeworkResolvers.Mutation.submitHomework(null, { input: invalidAttachmentInput }, ctx), + homeworkResolvers.Mutation.submitHomework( + null, + { input: invalidAttachmentInput }, + ctx, + ), ).rejects.toThrow(ValidationError); }); }); diff --git a/services/student-bff/src/student/resolvers/homework.resolver.ts b/services/student-bff/src/student/resolvers/homework.resolver.ts index 9bf4d6f..51c242e 100644 --- a/services/student-bff/src/student/resolvers/homework.resolver.ts +++ b/services/student-bff/src/student/resolvers/homework.resolver.ts @@ -53,23 +53,31 @@ export const homeworkResolvers = { // 缓存命中 const cacheKey = ctx.userId + (args.classId ? `:${args.classId}` : ""); - const cached = await ctx.redis.get("homework", cacheKey); + const cached = await ctx.cache.get("homework", cacheKey); if (cached) { - return ok(cached, { traceId: ctx.traceId, cachedAt: new Date().toISOString() }); + return ok(cached, { + traceId: ctx.traceId, + cachedAt: new Date().toISOString(), + }); } try { - const result = await ctx.downstream.call("core-edu", "ListHomeworkByStudent", { - studentId: ctx.userId, - status: args.status, - classId: args.classId, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "core-edu", + "ListHomeworkByStudent", + { + studentId: ctx.userId, + status: args.status, + classId: args.classId, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); const data = result as { homework: unknown[] }; - await ctx.redis.set("homework", data, CacheTTL.HOMEWORK, cacheKey); + await ctx.cache.set("homework", data, CacheTTL.HOMEWORK, cacheKey); return ok(data, { traceId: ctx.traceId }); } catch (err) { return fail( @@ -110,18 +118,23 @@ export const homeworkResolvers = { assertOwnData(ctx.userId, input.studentId); try { - const result = await ctx.downstream.call("core-edu", "SubmitHomework", { - homeworkId: input.homeworkId, - studentId: ctx.userId, - answers: input.answers, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "core-edu", + "SubmitHomework", + { + homeworkId: input.homeworkId, + studentId: ctx.userId, + answers: input.answers, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); // 失效相关缓存 - await ctx.redis.invalidate("homework", ctx.userId); - await ctx.redis.invalidate("dashboard", ctx.userId); + await ctx.cache.invalidate("homework", ctx.userId); + await ctx.cache.invalidate("dashboard", ctx.userId); const data = result as { submissionId: string; diff --git a/services/student-bff/src/student/resolvers/index.ts b/services/student-bff/src/student/resolvers/index.ts index e190bc3..961716d 100644 --- a/services/student-bff/src/student/resolvers/index.ts +++ b/services/student-bff/src/student/resolvers/index.ts @@ -22,7 +22,6 @@ * Subscription: * - aiStreamChat (ai-stream, P5 SSE) */ -import { mergeResolvers } from "@graphql-tools/merge"; import { authResolvers } from "./auth.resolver.js"; import { dashboardResolvers } from "./dashboard.resolver.js"; import { homeworkResolvers } from "./homework.resolver.js"; @@ -36,21 +35,29 @@ import { aiResolvers } from "./ai.resolver.js"; import { aiStreamResolvers } from "./ai-stream.resolver.js"; /** - * 全部 Resolver 合并. + * 全部 Resolver 手动合并 (spread 运算符). * * 注意: 各 resolver 文件导出的对象结构为 { Query: {...}, Mutation: {...}, Subscription: {...} }, - * mergeResolvers 自动合并同名 Query/Mutation/Subscription 字段. + * 这里按 Query/Mutation/Subscription 分别合并字段. */ -export const studentBffResolvers = mergeResolvers([ - authResolvers, - dashboardResolvers, - homeworkResolvers, - gradesResolvers, - examsResolvers, - classesResolvers, - contentResolvers, - analyticsResolvers, - notificationsResolvers, - aiResolvers, - aiStreamResolvers, -]); +export const studentBffResolvers = { + Query: { + ...(authResolvers.Query ?? {}), + ...(dashboardResolvers.Query ?? {}), + ...(homeworkResolvers.Query ?? {}), + ...(gradesResolvers.Query ?? {}), + ...(examsResolvers.Query ?? {}), + ...(classesResolvers.Query ?? {}), + ...(contentResolvers.Query ?? {}), + ...(analyticsResolvers.Query ?? {}), + ...(notificationsResolvers.Query ?? {}), + ...(aiResolvers.Query ?? {}), + }, + Mutation: { + ...(homeworkResolvers.Mutation ?? {}), + ...(notificationsResolvers.Mutation ?? {}), + }, + Subscription: { + ...(aiStreamResolvers.Subscription ?? {}), + }, +}; diff --git a/services/student-bff/src/student/resolvers/notifications.resolver.ts b/services/student-bff/src/student/resolvers/notifications.resolver.ts index 1a731af..d01061d 100644 --- a/services/student-bff/src/student/resolvers/notifications.resolver.ts +++ b/services/student-bff/src/student/resolvers/notifications.resolver.ts @@ -44,28 +44,41 @@ export const notificationsResolvers = { const pageSize = Math.min(args.pageSize ?? 20, 50); const cacheKey = `${ctx.userId}:${page}:${args.unreadOnly ?? false}`; - const cached = await ctx.redis.get("notifications", cacheKey); + const cached = await ctx.cache.get("notifications", cacheKey); if (cached) { - return ok(cached, { traceId: ctx.traceId, cachedAt: new Date().toISOString() }); + return ok(cached, { + traceId: ctx.traceId, + cachedAt: new Date().toISOString(), + }); } try { - const result = await ctx.downstream.call("msg", "ListNotifications", { - userId: ctx.userId, - page, - pageSize, - unreadOnly: args.unreadOnly ?? false, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "msg", + "ListNotifications", + { + userId: ctx.userId, + page, + pageSize, + unreadOnly: args.unreadOnly ?? false, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); const data = result as { notifications: unknown[]; totalCount: number; unreadCount: number; }; - await ctx.redis.set("notifications", data, CacheTTL.NOTIFICATIONS, cacheKey); + await ctx.cache.set( + "notifications", + data, + CacheTTL.NOTIFICATIONS, + cacheKey, + ); return ok(data, { traceId: ctx.traceId }); } catch (err) { return fail( @@ -90,12 +103,17 @@ export const notificationsResolvers = { } try { - const result = await ctx.downstream.call("msg", "GetUnreadCount", { - userId: ctx.userId, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "msg", + "GetUnreadCount", + { + userId: ctx.userId, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); const data = result as { unreadCount: number }; return ok(data, { traceId: ctx.traceId }); @@ -137,16 +155,21 @@ export const notificationsResolvers = { assertOwnData(ctx.userId, input.studentId); try { - const result = await ctx.downstream.call("msg", "MarkNotificationAsRead", { - notificationId: input.notificationId, - userId: ctx.userId, - }, { - traceId: ctx.traceId, - metadata: { "x-user-id": ctx.userId }, - }); + const result = await ctx.downstream.call( + "msg", + "MarkNotificationAsRead", + { + notificationId: input.notificationId, + userId: ctx.userId, + }, + { + traceId: ctx.traceId, + metadata: { "x-user-id": ctx.userId }, + }, + ); // 失效通知缓存 - await ctx.redis.invalidateByPrefix(`notifications:${ctx.userId}`); + await ctx.cache.invalidateByPrefix(`notifications:${ctx.userId}`); return ok(result, { traceId: ctx.traceId }); } catch (err) { diff --git a/services/student-bff/src/student/student.module.ts b/services/student-bff/src/student/student.module.ts index c5afae5..b0e98c5 100644 --- a/services/student-bff/src/student/student.module.ts +++ b/services/student-bff/src/student/student.module.ts @@ -13,12 +13,16 @@ import { Module, OnModuleInit } from "@nestjs/common"; import { Inject } from "@nestjs/common"; import { DownstreamClient } from "@edu/shared-ts/bff"; -import { REDIS_CLIENT } from "../shared/cache/cache.module.js"; +import { REDIS_CLIENT, CacheService } from "../shared/cache/cache.module.js"; import type { Redis } from "ioredis"; -import { createStudentBffYoga, type StudentBffContext } from "../shared/graphql/yoga.js"; +import { + createStudentBffYoga, + type StudentBffContext, +} from "../shared/graphql/yoga.js"; import { studentBffResolvers } from "./resolvers/index.js"; import { logger } from "../shared/observability/logger.js"; import type { YogaServerInstance } from "graphql-yoga"; +import type { Request, Response } from "express"; export const GRAPHQL_YOGA = Symbol("GRAPHQL_YOGA"); @@ -29,8 +33,16 @@ export const GRAPHQL_YOGA = Symbol("GRAPHQL_YOGA"); useFactory: async ( downstream: DownstreamClient, redis: Redis, - ): Promise, StudentBffContext>> => { - return createStudentBffYoga(studentBffResolvers, downstream, redis); + ): Promise< + YogaServerInstance<{ req: Request; res: Response }, StudentBffContext> + > => { + const cache = new CacheService(redis); + return createStudentBffYoga( + studentBffResolvers, + downstream, + redis, + cache, + ); }, inject: [DownstreamClient, REDIS_CLIENT], },