yn-be-developer-typescript

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Backend TypeScript – Best Practices & Skills

TypeScript后端开发——最佳实践与技能指南

This skill provides guidance for working on TypeScript backend projects that follow this pattern: structure under src/, ESM, Express, PostgreSQL/MongoDB, and tests with Mocha + tsx. Align with refactor.md, test.md, doc.md, and create-project.md in
commands/
when refactoring, testing, documenting, or creating new projects.
本技能为采用以下架构的TypeScript后端项目提供指导:基于src/目录结构、ESM规范、Express框架、PostgreSQL/MongoDB数据库,以及使用Mocha + tsx的测试方案。在进行重构、测试、文档编写或创建新项目时,请对齐
commands/
目录下的
refactor.md
test.mddoc.mdcreate-project.md文件中的规范。

When to Use

适用场景

  • Writing new controllers, models, or utilities in TypeScript
  • Creating or updating tests (
    .test.ts
    , Mocha + tsx/cjs)
  • Implementing features in a codebase that uses
    src/
    ,
    env.pgConnection
    ,
    env.pgModels
  • Reviewing or refactoring TypeScript backend code
  • Generating OpenAPI docs or new project scaffolds
  • 在TypeScript中编写新的控制器、模型或工具类
  • 创建或更新测试用例(
    .test.ts
    文件,基于Mocha + tsx/cjs)
  • 在采用
    src/
    env.pgConnection
    env.pgModels
    的代码库中实现新功能
  • 评审或重构TypeScript后端代码
  • 生成OpenAPI文档或新项目脚手架

Core Technologies

核心技术栈

Runtime & Language

运行时与语言

  • Node.js: ESM (
    "type": "module"
    ), run with tsx (or compiled
    dist/
    with node)
  • TypeScript: Strict mode,
    .ts
    only under src/ (and config/ if needed)
  • Imports: Use .js extension in import paths for ESM resolution (e.g.
    from "./app.js"
    ); tsx/Node resolve to
    .ts
    when needed. No
    .mjs
    .
  • Node.js: 采用ESM规范(
    "type": "module"
    ),使用tsx运行(或编译为
    dist/
    目录后用node运行)
  • TypeScript: 启用严格模式,仅在**src/目录下使用
    .ts
    文件(必要时可在
    config/**目录下使用)
  • 导入规范: ESM解析时,导入路径需使用**.js**扩展名(例如
    from "./app.js"
    );tsx/Node会在需要时自动解析为
    .ts
    文件。禁止使用
    .mjs
    格式。

Testing

测试方案

  • Mocha: Test runner; scripts must use
    --require tsx/cjs
    (not
    tsx/register
    ) so
    .test.ts
    files load
  • Chai: Assertions (
    expect
    ,
    assert
    )
  • Sinon: Stubs/spies; stub real method names (e.g.
    sinon.stub(controller as any, 'login')
    ), not
    __methodName
  • Structure: Top-level
    describe('<ClassName>')
    , nested
    describe('<methodName>')
    with the real method name (no
    __
    )
  • Mocha: 测试运行器;脚本必须使用**
    --require tsx/cjs
    **(而非
    tsx/register
    )以加载
    .test.ts
    文件
  • Chai: 断言库(使用
    expect
    assert
    语法)
  • Sinon: 桩函数/间谍函数;需桩化真实方法名称(例如
    sinon.stub(controller as any, 'login')
    ),而非
    __methodName
    格式
  • 测试结构: 顶层使用
    describe('<ClassName>')
    ,嵌套使用
    describe('<methodName>')
    且使用真实方法名称(禁止使用
    __
    前缀)

Web Framework

Web框架

  • Express: Routing and middleware; read parsed values from
    res.locals
    (e.g.
    res.locals.id
    ,
    res.locals.limit
    ,
    res.locals.offset
    )
  • Express: 路由与中间件;从**
    res.locals
    **中读取解析后的值(例如
    res.locals.id
    res.locals.limit
    res.locals.offset

Architecture Patterns

架构模式

Project Structure

项目结构

src/
  controllers/   # HTTP layer (extend Abstract_Controller, bind this.methodName)
  lib/           # Utilities, express-middlewares (validIntegerPathParam, parsePaginationParams)
  model/
    postgres/    # PgModels, Abstract_PgModel; register in pg-models.ts
    mongo/       # MongoModels, Abstract_BaseCollection
  cronie/        # Batch entry points (e.g. main-cronie.ts)
config/          # config.ts (or config.json)
docs/            # OpenAPI YAML fragments
test/            # .test.ts mirroring src (test/controllers/, test/lib/, test/lib/notifications/, test/model/...)
Source lives under src/; no app/.
src/
  controllers/   # HTTP层(继承Abstract_Controller,绑定this.methodName)
  lib/           # 工具类、Express中间件(如validIntegerPathParam、parsePaginationParams)
  model/
    postgres/    # PgModels、Abstract_PgModel;在pg-models.ts中注册
    mongo/       # MongoModels、Abstract_BaseCollection
  cronie/        # 批量任务入口(例如main-cronie.ts)
config/          # config.ts(或config.json)
docs/            # OpenAPI YAML片段
test/            # 与src目录镜像的测试文件(test/controllers/、test/lib/、test/lib/notifications/、test/model/...)
源码仅存放于**src/目录下;禁止使用app/**目录。

Controller Pattern

控制器模式

  • Extend Abstract_Controller; call
    super(env, "<scope>")
  • Register routes:
    this.router.get("/", ..., this.methodName.bind(this))
  • Private methods without
    __
    : e.g.
    login
    ,
    getNations
    ,
    getAssociations
    . In tests call via
    (controller as any).methodName(...)
  • Flow: try/catch → validate (early return with
    HttpResponseStatus
    ) →
    env.pgConnection
    /
    env.pgModels
    → shape response
  • Use ExpressMiddlewares:
    validIntegerPathParam('<param>')
    ,
    parsePaginationParams(required)
    ,
    validIntegerQueryParam('<param>', required?)
    . Middleware sets
    res.locals[param]
    (not always
    res.locals.id
    ). Pagination offset = (page - 1) * limit.
  • 继承Abstract_Controller;调用
    super(env, "<scope>")
    初始化
  • 注册路由:
    this.router.get("/", ..., this.methodName.bind(this))
  • 私有方法无需
    __
    前缀
    :例如
    login
    getNations
    getAssociations
    。在测试中通过**
    (controller as any).methodName(...)
    **调用
  • 流程:try/catch捕获异常 → 提前验证(验证失败时返回
    HttpResponseStatus
    ) → 调用
    env.pgConnection
    /
    env.pgModels
    处理数据 → 格式化响应
  • 使用Express中间件
    validIntegerPathParam('<param>')
    parsePaginationParams(required)
    validIntegerQueryParam('<param>', required?)
    。中间件会将值存入**
    res.locals[param]
    (并非总是
    res.locals.id
    )。分页偏移量计算方式为:
    (page - 1) * limit**

Data Layer

数据层

  • PostgreSQL:
    env.pgConnection
    (
    query
    ,
    queryReturnFirst
    ,
    queryPaged
    ,
    insert
    ,
    updateByKey
    ,
    startTransaction
    ,
    commit
    ,
    rollback
    ). Pass
    transactionClient: t
    (not
    transaction
    ) when using a transaction.
  • Models: Prefer
    env.pgModels.<model>.<method>()
    ; add new models in src/model/postgres/pg-models.ts (extend
    Abstract_PgModel
    ). Lookups by code/name (e.g. get id by status code, get record by slug) belong in the model, not in the controller: expose methods like
    getStatusIdByCode(status)
    ,
    getBySlug(slug)
    and call them from the controller.
  • Read-only: Use
    isSlave: true
    for COUNT and read-only SELECT; never for INSERT/UPDATE/DELETE.
  • updateByKey: Keys are an array (e.g.
    ["id_us"]
    ,
    ['id_up']
    ), not a string. Payload may include
    id_last_updater_up
    from
    request.session.idUser
    .
  • MongoDB:
    env.mongoClient
    ,
    env.mongoModels
    ; same patterns as in JS skill for parameterized access and sanitization.
  • PostgreSQL: 使用
    env.pgConnection
    提供的方法(
    query
    queryReturnFirst
    queryPaged
    insert
    updateByKey
    startTransaction
    commit
    rollback
    )。使用事务时,需传入**
    transactionClient: t
    **(而非
    transaction
  • 模型规范: 优先使用
    env.pgModels.<model>.<method>()
    ;新增模型需在src/model/postgres/pg-models.ts中注册(继承
    Abstract_PgModel
    )。通过编码/名称查询(例如通过状态码获取ID、通过slug获取记录)的逻辑应放在模型层,而非控制器层:需暴露
    getStatusIdByCode(status)
    getBySlug(slug)
    等方法供控制器调用
  • 只读查询: 对于COUNT和只读SELECT查询,需设置**
    isSlave: true
    **;禁止在INSERT/UPDATE/DELETE操作中使用
  • updateByKey方法: 主键参数为数组(例如
    ["id_us"]
    ['id_up']
    ),而非字符串。请求负载可包含来自
    request.session.idUser
    id_last_updater_up
    字段
  • MongoDB: 使用
    env.mongoClient
    env.mongoModels
    ;参数化访问与数据清理遵循JS技能中的相同模式

TypeScript Conventions

TypeScript编码规范

Types and Interfaces

类型与接口

  • One model file per table: Each table has exactly one model file (e.g.
    scheduled_phone_settings_sp
    scheduled-phone-settings.model.ts
    ). File name mirrors the table name (without suffix).
  • One interface per table: Exactly one
    I<TableName>Record
    interface per table with all columns. No subset interfaces (no
    IRetryRow
    ,
    IListItem
    , etc.). Interface name reflects table:
    scheduled_phone_setting_fails_sf
    IScheduledPhoneSettingFailRecord
    .
  • Single source of truth: Define interfaces in one model file; import elsewhere. Do not duplicate.
  • Use model interfaces: Always use the model interface. Do not define custom interfaces in controllers or lib for the same shape. Import the
    *Record
    interface from the model.
  • Record vs extended: Base interface = DB columns only (e.g.
    IUserRecord
    with
    _us
    fields). Extended interface = computed/joined (e.g.
    IUserExtended
    with
    fullname
    ,
    departmentFullname
    ,
    pbx
    ,
    plan
    ). Model methods return the extended type when the query includes joins.
  • Object properties in *Record interfaces: Properties that are object types (e.g. JSONB columns) must be typed with
    | string
    in the Record interface, because on insert/update they are passed to the database as serialized strings (e.g.
    JSON.stringify(...)
    ). Example:
    automatic_data_pm?: IAutomaticDataPm | string
    .
  • Split model interfaces: e.g.
    IWorkingPlanRecord
    (table only) and
    IWorkingPlanExtended extends IWorkingPlanRecord
    (adds
    users?
    ). In controllers use optional chaining:
    workingPlan.users?.map(...) ?? []
    .
  • Callbacks: When mapping over arrays with mixed types, type the callback parameter to accept the source type; use
    Buffer | string
    when a value can be either.
  • 一张表对应一个模型文件: 每个数据库表对应唯一的模型文件(例如
    scheduled_phone_settings_sp
    scheduled-phone-settings.model.ts
    )。文件名与表名一致(不含后缀)
  • 一张表对应一个接口: 每个表对应唯一的
    I<TableName>Record
    接口,包含所有列字段。禁止定义子集接口(如
    IRetryRow
    IListItem
    等)。接口名称需与表名对应:
    scheduled_phone_setting_fails_sf
    IScheduledPhoneSettingFailRecord
  • 单一数据源: 接口仅在一个模型文件中定义;其他地方需通过导入使用。禁止重复定义
  • 使用模型接口: 必须使用模型层定义的接口。禁止在控制器或工具类中为相同数据结构定义自定义接口。需从模型文件导入
    *Record
    接口
  • Record与扩展接口的区别: 基础接口仅包含数据库列字段(例如
    IUserRecord
    包含
    _us
    前缀的字段)。扩展接口包含计算/关联字段(例如
    IUserExtended
    包含
    fullname
    departmentFullname
    pbx
    plan
    字段)。当查询包含关联表时,模型方法需返回扩展类型
  • Record接口中的对象类型属性: 若属性为对象类型(例如JSONB列),在Record接口中必须同时标注**
    | string
    **类型,因为插入/更新时会被序列化为字符串传入数据库(例如
    JSON.stringify(...)
    )。示例:
    automatic_data_pm?: IAutomaticDataPm | string
  • 拆分模型接口: 例如
    IWorkingPlanRecord
    (仅包含表字段)和
    IWorkingPlanExtended extends IWorkingPlanRecord
    (新增
    users?
    字段)。在控制器中使用可选链操作:
    workingPlan.users?.map(...) ?? []
  • 回调函数类型: 当遍历混合类型数组时,需为回调参数指定源类型;若值可能为Buffer或string,需标注为
    Buffer | string

Validation (TypeScript)

TypeScript验证

  • Use
    _.isNil(variable)
    for null/undefined;
    _.isArray(x)
    when a value must be an array (e.g.
    if (_.isNil(numbers) || !_.isArray(numbers) || numbers.length === 0)
    ).
  • For IDs (path/query/body):
    Number.isInteger(id) && id > 0
    so strings like
    'invalid'
    return 400, not 404.
  • In model methods: same checks; return
    result?.rows ?? []
    or throw when query result is null/undefined where appropriate.
  • 使用**
    _.isNil(variable)
    判断null/undefined;当值必须为数组时,使用
    _.isArray(x)
    **(例如
    if (_.isNil(numbers) || !_.isArray(numbers) || numbers.length === 0)
  • 对于ID参数(路径/查询/请求体):使用**
    Number.isInteger(id) && id > 0
    **进行验证,确保类似
    'invalid'
    的字符串返回400错误,而非404
  • 模型方法中:使用相同的验证逻辑;查询结果为null/undefined时,需返回**
    result?.rows ?? []
    **或抛出异常

Code Style & Naming

代码风格与命名规范

  • Files: kebab-case (e.g.
    auth.controller.ts
    ,
    express-middlewares.ts
    )
  • Classes: PascalCase (
    AuthController
    ,
    ExpressMiddlewares
    )
  • Private methods: Real names, no
    __
    (e.g.
    login
    ,
    getNations
    ). Use TypeScript
    private
    when appropriate.
  • Constants: UPPER_SNAKE_CASE; HttpResponseStatus constants, never hardcoded numeric codes.
  • Indentation: 2 spaces; early returns; small, cohesive functions.
  • 文件命名: 短横线分隔式(kebab-case),例如
    auth.controller.ts
    express-middlewares.ts
  • 类命名: 大驼峰式(PascalCase),例如
    AuthController
    ExpressMiddlewares
  • 私有方法: 使用真实方法名,无需
    __
    前缀(例如
    login
    getNations
    )。合适时使用TypeScript的**
    private
    **修饰符
  • 常量命名: 下划线分隔大写式(UPPER_SNAKE_CASE);使用HttpResponseStatus常量,禁止硬编码数字状态码
  • 缩进: 2个空格;优先提前返回;保持函数短小、职责单一

Error Handling & HTTP

错误处理与HTTP规范

  • Use HttpResponseStatus for all responses; propagate errors via
    next(error)
    .
  • Validation errors (e.g. MISSING_PARAMS): When returning 400 for missing or invalid parameters, send a descriptive error code as plain text with
    .send("ERROR_MESSAGE")
    . Use UPPER_SNAKE_CASE codes that describe the specific failure (e.g.
    STATUS_REQUIRED
    ,
    CATEGORY_NAME_REQUIRED
    ,
    CHOICE_VALUE_ALREADY_EXISTS
    ), not a generic
    "MISSING_PARAMS"
    or
    sendStatus
    only. This lets clients show a clear message or map codes to i18n. Example:
    return response.status(HttpResponseStatus.MISSING_PARAMS).send("QUESTION_LABEL_REQUIRED");
  • Structured errors:
    error.status
    , optional
    error.errors
    array; never expose stack or raw DB errors in responses.
  • Cookies: When setting session cookie, pass an options object (e.g.
    { maxAge, ... }
    or
    {}
    ), never
    null
    .
  • 所有响应均使用HttpResponseStatus常量;通过**
    next(error)
    **传递异常
  • 验证错误(例如MISSING_PARAMS): 返回400错误时,需发送描述性错误码作为纯文本,使用**
    .send("ERROR_MESSAGE")
    **。错误码需采用大写下划线分隔式(UPPER_SNAKE_CASE),明确描述具体失败原因(例如
    STATUS_REQUIRED
    CATEGORY_NAME_REQUIRED
    CHOICE_VALUE_ALREADY_EXISTS
    ),禁止使用通用的
    "MISSING_PARAMS"
    或仅返回状态码。这样客户端可根据错误码展示清晰提示或映射多语言文案。示例:
    return response.status(HttpResponseStatus.MISSING_PARAMS).send("QUESTION_LABEL_REQUIRED");
  • 结构化错误:错误需包含
    error.status
    ,可选包含
    error.errors
    数组;禁止在响应中暴露堆栈信息或原始数据库错误
  • Cookie设置:设置会话Cookie时,需传入选项对象(例如
    { maxAge, ... }
    {}
    ),禁止传入
    null

SQL & PgFilter

SQL与PgFilter规范

  • Use queryReturnFirst for single-row checks (e.g. folder count); query for multi-row or when expecting
    { rows }
    . Tests must stub and assert on the method actually used.
  • Mandatory SQL existence check before delivery: Validate every SQL statement against the target DB to ensure referenced tables and columns exist.
    • SELECT: Execute the query as-is (same SQL text, with valid parameters) and verify it runs without relation/column errors.
    • INSERT / UPDATE: Do not execute the write during validation. Execute a read-only probe
      SELECT
      on the target table that references the same columns used by the write, to confirm table/column existence.
  • Query result shape — flat row, no wrapper: Type the query result as the exact row shape returned by the SELECT. Do not wrap the whole row in an outer
    SELECT row_to_json(q) AS question FROM (...) q
    . Return columns directly so each row has a flat structure. Example:
    query<{ id_tq: number; mandatory: boolean; type: string; choices: ITicketQuestionChoiceRecord[]; tree: ITicketCustomizedTreesRecord }>
    .
  • Single query with array_agg for parent + aggregated child data: When loading parent rows with per-parent arrays of child values (e.g. categories with user/group visibility ids), use one query with
    LEFT JOIN
    +
    GROUP BY
    and
    array_agg(...) FILTER (WHERE ...)
    (and
    COALESCE(..., '{}')::integer[]
    for empty arrays) instead of two round-trips (one SELECT parents, one SELECT children by parent ids then merge in code). Example:
    SELECT tc.id_tc, tc.name_tc, COALESCE(array_agg(tcv.id_user_tcv) FILTER (WHERE tcv.id_user_tcv IS NOT NULL), '{}')::integer[] AS user_ids, ... FROM ticket_categories_tc tc LEFT JOIN ticket_category_visibilities_tcv tcv ON ... WHERE tc.id_customer_tc = $1 GROUP BY tc.id_tc, tc.name_tc ORDER BY tc.name_tc
    .
  • row_to_json for joined/related data:
    • Single related record: Use
      row_to_json(alias) AS column_name
      (e.g.
      row_to_json(tct) AS tree
      ) so the row has one column with the full record. Type it with the model interface (e.g.
      tree: ITicketCustomizedTreesRecord
      ).
    • Array of related records: Use
      COALESCE((SELECT json_agg(row_to_json(alias)) FROM table alias WHERE ...), '[]'::json) AS column_name
      so the row has one column with an array of full records. Type it (e.g.
      choices: ITicketQuestionChoiceRecord[]
      ). Do not return only IDs when you need full records; use
      json_agg(row_to_json(...))
      for arrays.
    • Define and use
      I*Record
      interfaces for each table involved.
  • No unnecessary variables: Do not introduce intermediate variables when the value is used only once (e.g. use
    ${filterTree.getWhere(false)}
    directly in the SQL template, not
    const treeWhere = ...
    ).
  • PgFilter (common-mjs):
    addEqual
    ,
    addIn
    ,
    addCondition
    , and always
    getParameterPlaceHolder(value)
    for custom conditions (never manual
    $1
    ,
    $2
    in the SQL string). Ranges:
    addGreaterThan(col, val, true)
    =
    >=
    ,
    addLessThan(col, val, true)
    =
    <=
    (third param = orEqual). Pagination: use
    addPagination(limit, offset)
    and
    getPagination()
    in the SQL (do not build
    LIMIT $n OFFSET $m
    by hand). Ordering: use
    addOrderByCondition(field, direction)
    and
    getOrderBy()
    (do not build
    ORDER BY ...
    by hand when the filter supports it). Replacements: prefer
    new PgFilter(0)
    and have the filter own all placeholders (e.g. call
    getParameterPlaceHolder(id)
    for any value used in JOIN or SELECT); then use
    replacements = where.replacements
    only, without prepending values that the filter can manage.
  • 单行检查使用queryReturnFirst(例如文件夹计数);多行查询或预期返回
    { rows }
    时使用query。测试中需桩化并断言实际使用的方法
  • 交付前必须验证SQL存在性: 所有SQL语句需在目标数据库中验证,确保引用的表和列存在
    • SELECT语句: 直接执行原SQL(使用有效参数),验证无表/列不存在的错误
    • INSERT / UPDATE语句: 验证时禁止执行写入操作。需执行只读探测SELECT语句,查询目标表中与写入操作引用相同列的内容,以确认表/列存在
  • 查询结果结构——扁平化行,无包装: 查询结果的类型需与SELECT返回的精确行结构一致。禁止将整行包裹在
    SELECT row_to_json(q) AS question FROM (...) q
    中。需直接返回列,使每行保持扁平化结构。示例:
    query<{ id_tq: number; mandatory: boolean; type: string; choices: ITicketQuestionChoiceRecord[]; tree: ITicketCustomizedTreesRecord }>
  • 使用array_agg实现父数据+聚合子数据的单查询: 当加载父行数据及对应的子数据数组时(例如包含用户/组可见性ID的分类),需使用单查询,通过
    LEFT JOIN
    +
    GROUP BY
    和**
    array_agg(...) FILTER (WHERE ...)
    **(空数组使用
    COALESCE(..., '{}')::integer[]
    处理),而非两次数据库往返(先查询父数据,再根据父ID查询子数据,最后在代码中合并)。示例:
    SELECT tc.id_tc, tc.name_tc, COALESCE(array_agg(tcv.id_user_tcv) FILTER (WHERE tcv.id_user_tcv IS NOT NULL), '{}')::integer[] AS user_ids, ... FROM ticket_categories_tc tc LEFT JOIN ticket_category_visibilities_tcv tcv ON ... WHERE tc.id_customer_tc = $1 GROUP BY tc.id_tc, tc.name_tc ORDER BY tc.name_tc
  • 使用row_to_json处理关联数据:
    • 单个关联记录: 使用
      row_to_json(alias) AS column_name
      (例如
      row_to_json(tct) AS tree
      ),使行中包含一个完整记录的列。类型需使用模型接口(例如
      tree: ITicketCustomizedTreesRecord
    • 关联记录数组: 使用
      COALESCE((SELECT json_agg(row_to_json(alias)) FROM table alias WHERE ...), '[]'::json) AS column_name
      ,使行中包含一个完整记录数组的列。需标注类型(例如
      choices: ITicketQuestionChoiceRecord[]
      )。当需要完整记录时,禁止仅返回ID;需使用
      json_agg(row_to_json(...))
      处理数组
    • 参与查询的每个表都需定义并使用
      I*Record
      接口
  • 禁止冗余变量: 当值仅使用一次时,禁止引入中间变量(例如直接在SQL模板中使用
    ${filterTree.getWhere(false)}
    ,而非
    const treeWhere = ...
  • PgFilter (common-mjs): 使用
    addEqual
    addIn
    addCondition
    ,自定义条件时必须使用**
    getParameterPlaceHolder(value)
    (禁止在SQL字符串中手动编写
    $1
    $2
    )。范围查询:
    addGreaterThan(col, val, true)
    表示
    >=
    addLessThan(col, val, true)
    表示
    <=
    (第三个参数为是否包含等于)。分页: 使用
    addPagination(limit, offset)
    getPagination()
    生成SQL(禁止手动拼接
    LIMIT $n OFFSET $m
    )。排序: 使用
    addOrderByCondition(field, direction)
    getOrderBy()
    生成SQL(当过滤器支持时,禁止手动拼接
    ORDER BY ...
    )。参数替换: 优先使用
    new PgFilter(0)
    ,让过滤器管理所有占位符(例如在JOIN或SELECT中使用
    getParameterPlaceHolder(id)
    处理任何值);然后仅使用
    replacements = where.replacements
    **,禁止添加过滤器可管理的前置值

Transactions

事务规范

  • const t = await env.pgConnection.startTransaction()
    ; then
    commit(t)
    /
    rollback(t)
    .
  • Pass
    transactionClient: t
    to
    query
    /
    insert
    /
    updateByKey
    .
  • In tests: stub
    startTransaction
    with
    .resolves(t)
    (not
    .returns(t)
    ). If the controller does not wrap
    rollback
    in try/catch, when
    rollback
    rejects,
    next
    is called with the rollback error; tests should assert
    next(rollbackError)
    and not expect
    logger.error
    for rollback.
  • 开启事务:
    const t = await env.pgConnection.startTransaction()
    ;提交/回滚:
    commit(t)
    /
    rollback(t)
  • 调用
    query
    /
    insert
    /
    updateByKey
    时需传入**
    transactionClient: t
    **
  • 测试中:桩化**
    startTransaction
    时需使用
    .resolves(t)
    (而非
    .returns(t)
    )。若控制器未将
    rollback
    包裹在try/catch中,当
    rollback
    拒绝时,
    next
    会被传入回滚错误**;测试中需断言
    next(rollbackError)
    ,且无需期望
    logger.error
    记录回滚错误

Testing (Mocha + tsx)

测试规范(Mocha + tsx)

  • Run:
    npm test
    or
    npm run test:all
    ; scripts use
    --require tsx/cjs
    .
  • Controller methods: Call
    (controller as any).methodName(...)
    ;
    describe('methodName', ...)
    (not
    __methodName
    ).
  • Stubs:
    sinon.stub(controller as any, 'methodName')
    (and same for lib/helpers: e.g.
    sendRequest
    ,
    parsePaddingTemplate
    ).
  • Assertions: Use
    transactionClient
    in
    calledWith
    /
    calledOnceWith
    ;
    updateByKey
    keys = array;
    response.cookie
    third arg = options object.
  • Mock env: Do not change production to satisfy tests. Provide
    config.pubSubOptions
    (topicId, authentication) when code builds NotificationsManager/PubSubV2;
    config.getstream
    ,
    config.sms
    (e.g. fakeSms) when used. Use
    documentsConnection
    (not
    ynDbConnection
    ) when the controller uses it. For helpers that need env but not full config, use a minimal fake env instead of
    new Environment()
    .
  • Import paths: From test/ use
    ../../src/...
    ; from test/lib/notifications/ use
    ../../../src/...
    and
    ../../../config/...
    (three levels).
  • Logger: If the controller uses
    this.env.logger.warning
    , the mock must provide
    logger.warning
    (not only
    logger.warn
    ).
  • 运行测试: 使用
    npm test
    npm run test:all
    ;脚本需使用**
    --require tsx/cjs
    **
  • 控制器方法调用: 使用**
    (controller as any).methodName(...)
    ;测试块使用
    describe('methodName', ...)
    **(禁止使用
    __methodName
  • 桩函数: 使用
    sinon.stub(controller as any, 'methodName')
    (工具类/助手函数同理:例如
    sendRequest
    parsePaddingTemplate
  • 断言: 在
    calledWith
    /
    calledOnceWith
    中使用**
    transactionClient
    updateByKey
    的主键参数为数组;
    response.cookie
    **的第三个参数为选项对象
  • 模拟环境: 禁止修改生产代码以适配测试。当代码构建NotificationsManager/PubSubV2时,需提供**
    config.pubSubOptions
    (topicId、认证信息);使用
    config.getstream
    config.sms
    (例如fakeSms)时同理。当控制器使用
    documentsConnection
    时,需使用该连接而非
    ynDbConnection
    。对于仅需环境无需完整配置的助手函数,使用
    最小化模拟环境**而非
    new Environment()
  • 导入路径: 从test/目录导入时使用
    ../../src/...
    ;从test/lib/notifications/目录导入时使用
    ../../../src/...
    ../../../config/...
    (三级目录)
  • 日志: 若控制器使用**
    this.env.logger.warning
    ,模拟环境需提供
    logger.warning
    **(仅提供
    logger.warn
    无效)

Configuration & Environment

配置与环境规范

  • Do not read
    process.env
    directly in controllers; use Environment/config layer.
  • Document defaults in config/config.ts (or project equivalent).
  • 禁止在控制器中直接读取
    process.env
    ;需通过Environment/config层获取
  • config/config.ts(或项目等效文件)中记录默认配置

Security, Logging, Batch, Git

安全、日志、批量任务与Git规范

  • Same as in the Node.js backend skill: no secrets in code/logs; parameterized queries only; hash passwords in model layer; validate/sanitize input; use
    env.session.checkAuthentication()
    /
    checkPermission()
    .
  • Logging:
    env.logger
    with appropriate levels; never log sensitive data.
  • Batch/cron: under src/cronie/; idempotency and clear logging.
  • Git: branch names
    feature/
    ,
    fix/
    ,
    chore/
    ,
    refactor/
    ; commits imperative present tense; PRs small and tested.
  • 与Node.js后端技能规范一致:代码/日志中禁止包含敏感信息;仅使用参数化查询;在模型层对密码进行哈希;验证/清理输入;使用
    env.session.checkAuthentication()
    /
    checkPermission()
    进行权限校验
  • 日志:使用
    env.logger
    并选择合适的日志级别;禁止记录敏感数据
  • 批量/定时任务:存放于**src/cronie/**目录下;需保证幂等性并提供清晰的日志
  • Git:分支名使用
    feature/
    fix/
    chore/
    refactor/
    前缀;提交信息使用命令式现在时;PR需保持小型且经过测试

Commands Reference

命令参考

  • refactor.md: Port legacy controller to TypeScript (src/, no __, transactionClient, types, tests).
  • test.md: Write/update tests (.test.ts, tsx/cjs, (controller as any).methodName, mock env, no production changes).
  • doc.md: OpenAPI YAML from controller method name without __ and route registration.
  • create-project.md: New TypeScript project (src/, .ts, tsconfig, tsx, private methods without __).
  • refactor.md: 将遗留控制器迁移至TypeScript(src/目录、无__前缀、transactionClient、类型定义、测试)
  • test.md: 编写/更新测试用例(.test.ts、tsx/cjs、(controller as any).methodName、模拟环境、禁止修改生产代码)
  • doc.md: 根据控制器无__前缀的方法名和路由注册生成OpenAPI YAML文档
  • create-project.md: 创建新的TypeScript项目(src/目录、.ts文件、tsconfig、tsx、无__前缀的私有方法)

Instructions Summary

指令摘要

  1. TypeScript only under src/ – .ts, ESM, real method names (no
    __
    ).
  2. Test with Mocha + tsx/cjs – (controller as any).methodName, transactionClient, correct mock config.
  3. Validate early – _.isNil, _.isArray, Number.isInteger(id) && id > 0 where needed.
  4. Handle errors – next(error), HttpResponseStatus constants; for validation (400) use
    .send("ERROR_MESSAGE")
    with UPPER_SNAKE_CASE codes.
  5. Types – Single source for interfaces; Record vs Extended; optional chaining for relations.
  6. SQL – queryReturnFirst vs query; isSlave: true for read-only; getParameterPlaceHolder; transactionClient; validate table/column existence before delivery (SELECT as-is, INSERT/UPDATE via probe SELECT).
  7. No production changes for tests – complete mock config (pubSubOptions, getstream, sms, etc.) and minimal fake env when appropriate.
When in doubt, prefer the patterns described in refactor.md and test.md for controllers, handlers, types, and tests.
  1. 仅在src/目录下使用TypeScript——使用.ts文件、ESM规范、真实方法名(无
    __
    前缀)
  2. 使用Mocha + tsx/cjs进行测试——通过(controller as any).methodName调用方法、使用transactionClient、提供正确的模拟配置
  3. 提前验证——必要时使用_.isNil、_.isArray、Number.isInteger(id) && id > 0进行验证
  4. 错误处理——使用next(error)传递错误、使用HttpResponseStatus常量;验证错误(400)需使用
    .send("ERROR_MESSAGE")
    并传入大写下划线分隔的错误码
  5. 类型规范——接口单一数据源;区分Record与扩展接口;关联字段使用可选链操作
  6. SQL规范——区分queryReturnFirst与query;只读查询设置isSlave: true;使用getParameterPlaceHolder;传递transactionClient;交付前验证表/列存在性(SELECT直接执行,INSERT/UPDATE通过探测SELECT验证)
  7. 禁止为测试修改生产代码——提供完整的模拟配置(pubSubOptions、getstream、sms等),必要时使用最小化模拟环境
如有疑问,优先遵循refactor.mdtest.md中关于控制器、处理器、类型与测试的模式规范。",