Lightdash 后端 REST API 工程实践:TSOA 控制器、认证中间件与端点弃用机制
发布时间:2026/9/17 22:29:24来源:尧图网络
Lightdash 后端 REST API 工程实践TSOA 控制器、认证中间件与端点弃用机制【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash本篇基于 Lightdash 仓库中packages/backend/src/controllers/CLAUDE.md这份开发指南展开系统讲解 Lightdash 后端如何基于 TSOA 构建 REST API控制器如何通过BaseController实现依赖注入、TSOA 装饰器如何同时完成路由注册与 OpenAPI 生成、认证中间件的组合与降级链路以及getDeprecatedRouteMiddleware提供的端点弃用Sunset标准流程。读完本文你可以在 Lightdash 后端中新增一个符合规范的 API 端点并正确执行一次带响应头、日志告警与文档联动可见性的端点弃用。控制器总览TSOA 驱动的路由与 OpenAPI 一体生成Lightdash 后端的 HTTP 控制器统一构建在 TSOA包含约 65 个控制器文件分为两层组织V1 控制器挂载在/api/v1/路由下承载既有 API 形态例如 projectController.ts、userController.tsV2 控制器位于 v2 目录聚焦异步操作与结果流式返回采用更干净的 RESTful 设计例如 QueryController.ts、ContentController.ts。路由和 API 文档都不是手写的而是由 TSOA 从控制器源码生成。配置见 tsoa.ymlentryFile: src/index.ts controllerPathGlobs: # Literal content-as-code routes must be registered before routes such as # /spaces/{spaceUuid}, otherwise Express treats code as a UUID. - src/controllers/ProjectCoderController.ts - src/**/*Controller.ts spec: name: Lightdash API outputDirectory: ./src/generated/ specVersion: 3 yaml: false securityDefinitions: session_cookie: type: apiKey in: cookie name: connect.sid api_key: type: apiKey in: header name: Authorization description: Value should be ApiKey your key routes: routesDir: ./src/generated/ iocModule: ./src/services/tsoaServiceContainer这里有两个值得注意的工程细节控制器注册顺序即路由匹配顺序。controllerPathGlobs中ProjectCoderController.ts被显式列在最前面——因为 content-as-code 的字面路由如果晚于/spaces/{spaceUuid}这类带路径参数的路由注册Express 会把字面量code当作 UUID 变量来匹配导致路由冲突。OpenAPI 规范与运行时路由同源生成。tsoa spec-and-routes同时产出src/generated/下的swagger.jsonspec 输出目录和路由代码二者永远与控制器源码一致不会漂移。生成动作对应 package.json 中的脚本generate-api: tsoa spec-and-routes --configuration tsoa.yml, generate-api:build: pnpm run generate-api pnpm run formatter --write ./src/generated, generate-api-dev: pnpm run generate-api:build chokidar ./src/**/controllers/**/*.ts -c pnpm run generate-api:build日常开发时运行pnpm generate-api-dev即可在控制器文件变动时自动重新生成OpenAPI 文档站点docs.lightdash.com 的页面与 llms.txt正是从src/generated/swagger.json生成的这也是后文弃用端点“文档可见性”规则的由来。BaseController依赖注入的基类所有控制器都继承 baseController.ts其完整实现如下import { Controller, Middlewares } from tsoa/runtime; import { sentrySetProjectUuidTagMiddleware } from ../middlewares/sentry; import type { ServiceRepository } from ../services/ServiceRepository; /** * Extends tsoas Controller with additional Lightdash-specific logic. */ Middlewares(sentrySetProjectUuidTagMiddleware) export class BaseController extends Controller { // TODO: This is currently just a placeholder layer over Controller. constructor(protected readonly services: ServiceRepository) { super(); } }它做了两件事注入ServiceRepositoryTSOA 通过tsoa.yml中的iocModule./src/services/tsoaServiceContainer在实例化控制器时把服务仓库传给构造函数控制器内以this.services.get{Service}Service()的方式按需取用具体服务控制器本身不 new 任何业务对象全局挂载 Sentry 标签中间件Middlewares(sentrySetProjectUuidTagMiddleware)让每个请求在进入业务逻辑前就带上projectUuid标签便于按项目维度排查问题。TSOA 的 IoC 机制意味着新控制器只要继承BaseController就会自动获得与现有控制器完全一致的服务注入能力无需手工接线。标准控制器写法完整 CRUD 示例与装饰器约定CLAUDE.md 给出的参考范式是一个项目图表Saved Chart控制器完整继承如下// Basic CRUD controller import { assertRegisteredAccount } from lightdash/common; Route(/api/v1/projects) ResponseApiErrorPayload(default, Error) Tags(Projects) export class ProjectController extends BaseController { /** * Retrieves all charts within a projects spaces * summary List charts */ Middlewares([allowApiKeyAuthentication, isAuthenticated]) SuccessResponse(200, Success) Get(/{projectUuid}/charts) OperationId(listCharts) async getCharts( Request() req: express.Request, Path() projectUuid: string, Query() includePrivate?: boolean, ): PromiseApiGetCharts { assertRegisteredAccount(req.account); const charts await this.services .getSavedChartService() .getAllSpaces(req.account, projectUuid, includePrivate); return { status: ok, results: charts, }; } /** * Creates a new chart in the specified project * summary Create chart */ Middlewares([allowApiKeyAuthentication, isAuthenticated]) Post(/{projectUuid}/charts) SuccessResponse(201, Created) async createChart( Path() projectUuid: string, Body() body: CreateSavedChart, Request() req: express.Request, ): PromiseApiCreateSavedChart { assertRegisteredAccount(req.account); this.setStatus(201); const chart await this.services .getSavedChartService() .createSavedChart(req.account, projectUuid, body); return { status: ok, results: chart, }; } }对照源码这套写法的硬性约定可以归纳为约定说明响应格式成功响应统一为{ status: ok, results: T }失败由类级ResponseApiErrorPayload(default, Error)描述统一错误结构状态码非 200 的成功响应如创建资源显式调用this.setStatus(201)并与SuccessResponse(201, Created)对应服务访问只通过this.services.get{Service}Service()获取服务业务逻辑全部下沉到服务层认证调用者处理器内通过req.account拿到认证主体并传给服务对只面向注册账号的端点第一行加assertRegisteredAccount(req.account)见下文有意服务 embed/JWT 流量的端点除外JSDoc每个端点必须有 JSDoc 注释且描述文字在前、summary标签在后summary限 23 个词——它会被生成到 OpenAPI并决定文档页 URL路径参数类型只接受 UUID 的参数声明为UUID类型TSOA 会做格式校验非法值直接 422既接受 UUID 又接受 slug经getByIdOrSlug解析的参数必须声明为UuidOrSlug并命名为*UuidOrSlug绝不能把 uuid-or-slug 参数标注成*Uuid关于assertRegisteredAccount其定义在 auth.ts 中RegisteredAccount是ExcludeAccount, AnonymousAccount即剥离匿名账号后的账户类型断言函数asserts account is RegisteredAccount在运行时把“未注册/匿名调用”挡在业务逻辑之前。req.user!是旧式类型新的代码应遵循 docs/account-patterns.md 中req.account/RegisteredAccount的模式。认证中间件从会话、API Key 到 OAuth 的完整链路认证逻辑集中在 authentication 目录策略与中间件实现见 middlewares.ts 与 strategies。控制器里最常用的三个中间件及其实际行为1.allowApiKeyAuthentication——多认证方式的降级链。源码显示它按以下优先级尝试认证任一成功即放行已认证会话req.isAuthenticated()直接通过OAuth bearer token通过node-oauth/oauth2-server校验 Authorization 头中的 bearer token成功则用findSessionUser加载用户并构造req.account fromOauth(user, token)服务账号service accountauthenticateServiceAccount处理企业版的服务账号凭证Personal Access TokenPAT走 passport 的headerapikey策略构造req.account fromApiKey(...)若lightdashConfig.auth.pat.enabled为 false则抛出AuthorizationError(Personal access tokens are disabled)。同时它会在请求上附加requestContextFromExpress(req)让下游日志、审计能拿到调用上下文。另有allowOauthAuthentication作为变体仅接受 OAuth bearer无 PAT、无服务账号用于“用 OAuth token 创建 PAT”这类刻意排除 PAT 认证的端点。还有一个allowApiKeyAuthenticationIfPresent匿名也可访问的端点如登录/邀请页读取 feature flag若携带 Authorization 头则尝试认证token 无效时以 401 显式失败而不是静默降级为匿名。2.isAuthenticated——要求已认证的活跃用户。它接受req.account?.isAuthenticated()或遗留的req.user?.userUuid通过后会校验账号活跃状态服务账号主体authentication.type service-account豁免isActive检查——源码注释说明服务账号运行在刻意is_active false的专属用户行上纵深防御杜绝任何登录路径isActive闸门只针对人类账号停用已停用的用户会触发req.session.destroy(...)并抛出DeactivatedAccountError强制下线其会话两者都不满足则抛AuthorizationError(Failed to authorize user)。3.unauthorisedInDemo——演示模式拦截。当lightdashConfig.mode LightdashMode.DEMO时抛出AuthorizationError(Action not available in demo)把危险操作挡在演示环境之外。认证方式的“入口”在 authentication/index.ts 的注释中有完整流程浏览器 cookie 中的加密 cookie id 由 express-session 在 Postgres 的 sessions 表查询数据挂到req.sessionpassport 再从req.session.passport.user调用deserializeUser还原完整用户对象到req.user。SSO 侧则注册了 google、azure、okta、oneLogin、oidc、snowflake、password、apiKey 等多个策略由index.ts的export * from ./strategies/...汇总导出。典型的受保护端点中间件组合即为 CLAUDE.md 推荐的形式Middlewares([allowApiKeyAuthentication, isAuthenticated])前者解决“请求可能来自会话、API Key、OAuth 或服务账号中的哪一种”后者保证“最终确实是一个认证过的活跃账号”。V1 与 V2 控制器的分工从 CLAUDE.md 的模块划分与 v2 目录的实际文件QueryController、ContentController、DashboardController、ProjectSavedChartController、SavedChartController等可以确认两条线的分工V1/api/v1/存量 API 形态保持既有模式与兼容性V2/api/v2/面向异步操作与结果流式返回提交查询 → 轮询/流式取结果采用更干净的 RESTful 设计与增强的错误处理模式。V1 中被 V2 取代的路径并不立即删除而是进入下一节的弃用流程。仓库里已有真实案例deprecation.ts 末尾的deprecatedResultsRoute把旧的查询结果路由指向 V2 替代方案——POST /api/v2/projects/{projectUuid}/query配合GET /api/v2/projects/{projectUuid}/query/{queryUuid}其弃用日期为 2025-03-20、移除日期为 2025-04-30。端点弃用getDeprecatedRouteMiddleware的标准流程CLAUDE.md 用独立章节规定了“Deprecating Endpoints”这是本文档最具操作价值的部分。适用范围与触发条件如下只针对 HTTP 端点。内部 service/model 方法、类型字段、数据库列、配置字段不是端点——它们只加deprecatedJSDoc 注释不挂弃用中间件时机只有当所有 first-party 调用方前端、CLI、其他内部消费者都已迁离该端点后才标记弃用默认窗口弃用后 3 个月移除除非另有商定的下线日期。操作方式在端点的Middlewares([...])中加入getDeprecatedRouteMiddleware(deprecatedOn, { suffixMessage })deprecatedOn是弃用日期suffixMessage指明替代端点同时保留 TSOA 的Deprecated()装饰器与deprecatedJSDoc二者驱动 OpenAPI 生成Middlewares([ allowApiKeyAuthentication, isAuthenticated, getDeprecatedRouteMiddleware(new Date(2025-08-26), { suffixMessage: Use ProjectRoleAssignments instead., }), ]) Deprecated() Patch({projectUuid}/access/{userUuid})实现细节deprecation.ts源码与文档描述一一对应关键常量与逻辑const SUNSET_MONTHS_FROM_DEPRECATION 3; const ERROR_WINDOW_MS 14 * 24 * 60 * 60 * 1000; export const getDefaultSunsetDate (deprecatedOn: Date): Date { const sunset new Date(deprecatedOn); sunset.setUTCMonth(sunset.getUTCMonth() SUNSET_MONTHS_FROM_DEPRECATION); return sunset; }; export const shouldEscalateToError (removeOn: Date, now: Date): boolean removeOn.getTime() - now.getTime() ERROR_WINDOW_MS;中间件在每个被调用的请求上执行设置三个响应头Deprecation弃用日期、Sunset移除日期默认为deprecatedOn 3 个月可用options.removeOn覆盖、以及 legacy 的Warning头299 - This API endpoint is deprecated and will be removed after ... suffixMessage记录日志shouldEscalateToError判断移除日期是否在 2 周内或已过期——否则Logger.warn到期窗口内升级为Logger.error上下文带route、deprecatedOn、removeOn。设计意图是被弃用的端点本不该再被任何 first-party 客户端调用任何一条日志都是“仍有依赖待移除路由”的信号不上报 Sentry升级后的 error 只是 GCP/日志侧的信号。文档明确DeprecatedRouteError不应上报 Sentry——过期路由每次请求都会触发会挤占 Sentry 配额该错误类型被列在IGNORE_ERRORS中。文档可见性的配套要求仅设置deprecated: true在 docs.lightdash.com 上几乎不可见页面与 llms.txt 都从src/generated/swagger.json生成因此 CLAUDE.md 要求弃用时同时做三件事JSDoc 描述首行加上纯文本Deprecated — use the v2 name endpoint instead.——它成为 llms.txt 条目与页面副标题该位置不支持 markdown/MDX追加Extension(x-mint, { content: Warning... })迁移横幅——渲染在生成的引用文档上方此处允许 MDX保持summary不变因为文档页 URL 由它推导。改完后执行pnpm generate-api重新生成swagger.json与路由。行为细节可由 deprecation.test.ts 的测试用例对照验证。关键约定速查汇总 CLAUDE.md 的 “Important to Know” 与源码印证Lightdash 后端控制层的完整约定如下所有控制器继承 BaseController通过 TSOA IoC 获得ServiceRepository注入路由注册、参数校验Path()的UUID/UuidOrSlug类型、OpenAPI 生成全部由 TSOA 装饰器驱动修改控制器后运行pnpm generate-api认证中间件按需组合allowApiKeyAuthentication会话 OAuth 服务账号 PAT 全链路、isAuthenticated活跃用户门禁、unauthorisedInDemo演示模式拦截、allowOauthAuthentication仅 OAuth响应统一{ status: ok, results: T }非 200 用this.setStatus(...)认证主体统一走req.account注册账号门禁用assertRegisteredAccount(req.account)req.user!视为遗留形态详见 docs/account-patterns.md项目级权限在服务层强制校验大多数操作要求组织成员身份演示模式限制由中间件承担端点弃用遵循“first-party 全部迁离 → 挂getDeprecatedRouteMiddleware→ 3 个月 Sunset 窗口 → 日志告警随期升级 → 移除”的流程且必须同步处理文档侧的 JSDoc 首行、x-mint横幅与summary稳定性。延伸阅读文件baseController.ts基类与 IoC 注入实现authentication/index.ts会话与 SSO 策略汇总、认证流程注释authentication/middlewares.tsisAuthenticated、allowApiKeyAuthentication等中间件实现authentication/deprecation.ts弃用中间件实现与deprecatedResultsRoute实例authentication/deprecation.test.ts弃用行为的测试验证tsoa.ymlTSOA 入口、控制器 glob、spec/routes 输出配置docs/account-patterns.mdreq.account、RegisteredAccount与SessionUser的使用模式【免费下载链接】lightdashAgentic BI. Analytics at the speed of code ⚡️项目地址: https://gitcode.com/GitHub_Trending/li/lightdash创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网