yn-be-developer-typescript
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBackend 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 when refactoring, testing, documenting, or creating new projects.
commands/本技能为采用以下架构的TypeScript后端项目提供指导:基于src/目录结构、ESM规范、Express框架、PostgreSQL/MongoDB数据库,以及使用Mocha + tsx的测试方案。在进行重构、测试、文档编写或创建新项目时,请对齐目录下的refactor.md、test.md、doc.md和create-project.md文件中的规范。
commands/When to Use
适用场景
- Writing new controllers, models, or utilities in TypeScript
- Creating or updating tests (, Mocha + tsx/cjs)
.test.ts - Implementing features in a codebase that uses ,
src/,env.pgConnectionenv.pgModels - Reviewing or refactoring TypeScript backend code
- Generating OpenAPI docs or new project scaffolds
- 在TypeScript中编写新的控制器、模型或工具类
- 创建或更新测试用例(文件,基于Mocha + tsx/cjs)
.test.ts - 在采用、
src/、env.pgConnection的代码库中实现新功能env.pgModels - 评审或重构TypeScript后端代码
- 生成OpenAPI文档或新项目脚手架
Core Technologies
核心技术栈
Runtime & Language
运行时与语言
- Node.js: ESM (), run with tsx (or compiled
"type": "module"with node)dist/ - TypeScript: Strict mode, only under src/ (and config/ if needed)
.ts - Imports: Use .js extension in import paths for ESM resolution (e.g. ); tsx/Node resolve to
from "./app.js"when needed. No.ts..mjs
- Node.js: 采用ESM规范(),使用tsx运行(或编译为
"type": "module"目录后用node运行)dist/ - TypeScript: 启用严格模式,仅在**src/目录下使用文件(必要时可在config/**目录下使用)
.ts - 导入规范: ESM解析时,导入路径需使用**.js**扩展名(例如);tsx/Node会在需要时自动解析为
from "./app.js"文件。禁止使用.ts格式。.mjs
Testing
测试方案
- Mocha: Test runner; scripts must use (not
--require tsx/cjs) sotsx/registerfiles load.test.ts - Chai: Assertions (,
expect)assert - Sinon: Stubs/spies; stub real method names (e.g. ), not
sinon.stub(controller as any, 'login')__methodName - Structure: Top-level , nested
describe('<ClassName>')with the real method name (nodescribe('<methodName>'))__
- 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 (e.g.
res.locals,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. In tests call viagetAssociations(controller as any).methodName(...) - Flow: try/catch → validate (early return with ) →
HttpResponseStatus/env.pgConnection→ shape responseenv.pgModels - Use ExpressMiddlewares: ,
validIntegerPathParam('<param>'),parsePaginationParams(required). Middleware setsvalidIntegerQueryParam('<param>', required?)(not alwaysres.locals[param]). Pagination offset = (page - 1) * limit.res.locals.id
- 继承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])。分页偏移量计算方式为:(page - 1) * limit**res.locals.id
Data Layer
数据层
- PostgreSQL: (
env.pgConnection,query,queryReturnFirst,queryPaged,insert,updateByKey,startTransaction,commit). Passrollback(nottransactionClient: t) when using a transaction.transaction - Models: Prefer ; add new models in src/model/postgres/pg-models.ts (extend
env.pgModels.<model>.<method>()). 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 likeAbstract_PgModel,getStatusIdByCode(status)and call them from the controller.getBySlug(slug) - Read-only: Use for COUNT and read-only SELECT; never for INSERT/UPDATE/DELETE.
isSlave: true - updateByKey: Keys are an array (e.g. ,
["id_us"]), not a string. Payload may include['id_up']fromid_last_updater_up.request.session.idUser - MongoDB: ,
env.mongoClient; same patterns as in JS skill for parameterized access and sanitization.env.mongoModels
- PostgreSQL: 使用提供的方法(
env.pgConnection、query、queryReturnFirst、queryPaged、insert、updateByKey、startTransaction、commit)。使用事务时,需传入**rollback**(而非transactionClient: t)transaction - 模型规范: 优先使用;新增模型需在src/model/postgres/pg-models.ts中注册(继承
env.pgModels.<model>.<method>())。通过编码/名称查询(例如通过状态码获取ID、通过slug获取记录)的逻辑应放在模型层,而非控制器层:需暴露Abstract_PgModel、getStatusIdByCode(status)等方法供控制器调用getBySlug(slug) - 只读查询: 对于COUNT和只读SELECT查询,需设置****;禁止在INSERT/UPDATE/DELETE操作中使用
isSlave: true - updateByKey方法: 主键参数为数组(例如、
["id_us"]),而非字符串。请求负载可包含来自['id_up']的request.session.idUser字段id_last_updater_up - MongoDB: 使用、
env.mongoClient;参数化访问与数据清理遵循JS技能中的相同模式env.mongoModels
TypeScript Conventions
TypeScript编码规范
Types and Interfaces
类型与接口
- One model file per table: Each table has exactly one model file (e.g. →
scheduled_phone_settings_sp). File name mirrors the table name (without suffix).scheduled-phone-settings.model.ts - One interface per table: Exactly one interface per table with all columns. No subset interfaces (no
I<TableName>Record,IRetryRow, etc.). Interface name reflects table:IListItem→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 interface from the model.
*Record - Record vs extended: Base interface = DB columns only (e.g. with
IUserRecordfields). Extended interface = computed/joined (e.g._uswithIUserExtended,fullname,departmentFullname,pbx). Model methods return the extended type when the query includes joins.plan - Object properties in *Record interfaces: Properties that are object types (e.g. JSONB columns) must be typed with in the Record interface, because on insert/update they are passed to the database as serialized strings (e.g.
| string). Example:JSON.stringify(...).automatic_data_pm?: IAutomaticDataPm | string - Split model interfaces: e.g. (table only) and
IWorkingPlanRecord(addsIWorkingPlanExtended extends IWorkingPlanRecord). In controllers use optional chaining:users?.workingPlan.users?.map(...) ?? [] - Callbacks: When mapping over arrays with mixed types, type the callback parameter to accept the source type; use when a value can be either.
Buffer | string
- 一张表对应一个模型文件: 每个数据库表对应唯一的模型文件(例如→
scheduled_phone_settings_sp)。文件名与表名一致(不含后缀)scheduled-phone-settings.model.ts - 一张表对应一个接口: 每个表对应唯一的接口,包含所有列字段。禁止定义子集接口(如
I<TableName>Record、IRetryRow等)。接口名称需与表名对应:IListItem→scheduled_phone_setting_fails_sfIScheduledPhoneSettingFailRecord - 单一数据源: 接口仅在一个模型文件中定义;其他地方需通过导入使用。禁止重复定义
- 使用模型接口: 必须使用模型层定义的接口。禁止在控制器或工具类中为相同数据结构定义自定义接口。需从模型文件导入接口
*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 for null/undefined;
_.isNil(variable)when a value must be an array (e.g._.isArray(x)).if (_.isNil(numbers) || !_.isArray(numbers) || numbers.length === 0) - For IDs (path/query/body): so strings like
Number.isInteger(id) && id > 0return 400, not 404.'invalid' - In model methods: same checks; return or throw when query result is null/undefined where appropriate.
result?.rows ?? []
- 使用**判断null/undefined;当值必须为数组时,使用
_.isNil(variable)**(例如_.isArray(x))if (_.isNil(numbers) || !_.isArray(numbers) || numbers.length === 0) - 对于ID参数(路径/查询/请求体):使用****进行验证,确保类似
Number.isInteger(id) && id > 0的字符串返回400错误,而非404'invalid' - 模型方法中:使用相同的验证逻辑;查询结果为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). Use TypeScriptgetNationswhen appropriate.private - Constants: UPPER_SNAKE_CASE; HttpResponseStatus constants, never hardcoded numeric codes.
- Indentation: 2 spaces; early returns; small, cohesive functions.
- 文件命名: 短横线分隔式(kebab-case),例如、
auth.controller.tsexpress-middlewares.ts - 类命名: 大驼峰式(PascalCase),例如、
AuthControllerExpressMiddlewares - 私有方法: 使用真实方法名,无需前缀(例如
__、login)。合适时使用TypeScript的**getNations**修饰符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 . Use UPPER_SNAKE_CASE codes that describe the specific failure (e.g.
.send("ERROR_MESSAGE"),STATUS_REQUIRED,CATEGORY_NAME_REQUIRED), not a genericCHOICE_VALUE_ALREADY_EXISTSor"MISSING_PARAMS"only. This lets clients show a clear message or map codes to i18n. Example:sendStatusreturn response.status(HttpResponseStatus.MISSING_PARAMS).send("QUESTION_LABEL_REQUIRED"); - Structured errors: , optional
error.statusarray; never expose stack or raw DB errors in responses.error.errors - Cookies: When setting session cookie, pass an options object (e.g. or
{ maxAge, ... }), never{}.null
- 所有响应均使用HttpResponseStatus常量;通过****传递异常
next(error) - 验证错误(例如MISSING_PARAMS): 返回400错误时,需发送描述性错误码作为纯文本,使用****。错误码需采用大写下划线分隔式(UPPER_SNAKE_CASE),明确描述具体失败原因(例如
.send("ERROR_MESSAGE")、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 . Tests must stub and assert on the method actually used.
{ rows } - 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 on the target table that references the same columns used by the write, to confirm table/column existence.
SELECT
- 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 . Return columns directly so each row has a flat structure. Example:
SELECT row_to_json(q) AS question FROM (...) q.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 JOINandGROUP BY(andarray_agg(...) FILTER (WHERE ...)for empty arrays) instead of two round-trips (one SELECT parents, one SELECT children by parent ids then merge in code). Example:COALESCE(..., '{}')::integer[].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 (e.g.
row_to_json(alias) AS column_name) so the row has one column with the full record. Type it with the model interface (e.g.row_to_json(tct) AS tree).tree: ITicketCustomizedTreesRecord - Array of related records: Use so the row has one column with an array of full records. Type it (e.g.
COALESCE((SELECT json_agg(row_to_json(alias)) FROM table alias WHERE ...), '[]'::json) AS column_name). Do not return only IDs when you need full records; usechoices: ITicketQuestionChoiceRecord[]for arrays.json_agg(row_to_json(...)) - Define and use interfaces for each table involved.
I*Record
- Single related record: Use
- No unnecessary variables: Do not introduce intermediate variables when the value is used only once (e.g. use directly in the SQL template, not
${filterTree.getWhere(false)}).const treeWhere = ... - PgFilter (common-mjs): ,
addEqual,addIn, and alwaysaddConditionfor custom conditions (never manualgetParameterPlaceHolder(value),$1in the SQL string). Ranges:$2=addGreaterThan(col, val, true),>==addLessThan(col, val, true)(third param = orEqual). Pagination: use<=andaddPagination(limit, offset)in the SQL (do not buildgetPagination()by hand). Ordering: useLIMIT $n OFFSET $mandaddOrderByCondition(field, direction)(do not buildgetOrderBy()by hand when the filter supports it). Replacements: preferORDER BY ...and have the filter own all placeholders (e.g. callnew PgFilter(0)for any value used in JOIN or SELECT); then usegetParameterPlaceHolder(id)only, without prepending values that the filter can manage.replacements = where.replacements
- 单行检查使用queryReturnFirst(例如文件夹计数);多行查询或预期返回时使用query。测试中需桩化并断言实际使用的方法
{ rows } - 交付前必须验证SQL存在性: 所有SQL语句需在目标数据库中验证,确保引用的表和列存在
- SELECT语句: 直接执行原SQL(使用有效参数),验证无表/列不存在的错误
- INSERT / UPDATE语句: 验证时禁止执行写入操作。需执行只读探测SELECT语句,查询目标表中与写入操作引用相同列的内容,以确认表/列存在
- 查询结果结构——扁平化行,无包装: 查询结果的类型需与SELECT返回的精确行结构一致。禁止将整行包裹在中。需直接返回列,使每行保持扁平化结构。示例:
SELECT row_to_json(q) AS question FROM (...) qquery<{ id_tq: number; mandatory: boolean; type: string; choices: ITicketQuestionChoiceRecord[]; tree: ITicketCustomizedTreesRecord }> - 使用array_agg实现父数据+聚合子数据的单查询: 当加载父行数据及对应的子数据数组时(例如包含用户/组可见性ID的分类),需使用单查询,通过+
LEFT JOIN和**GROUP BY**(空数组使用array_agg(...) FILTER (WHERE ...)处理),而非两次数据库往返(先查询父数据,再根据父ID查询子数据,最后在代码中合并)。示例:COALESCE(..., '{}')::integer[]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)。当需要完整记录时,禁止仅返回ID;需使用choices: ITicketQuestionChoiceRecord[]处理数组json_agg(row_to_json(...)) - 参与查询的每个表都需定义并使用接口
I*Record
- 单个关联记录: 使用
- 禁止冗余变量: 当值仅使用一次时,禁止引入中间变量(例如直接在SQL模板中使用,而非
${filterTree.getWhere(false)})const treeWhere = ... - PgFilter (common-mjs): 使用、
addEqual、addIn,自定义条件时必须使用**addCondition(禁止在SQL字符串中手动编写getParameterPlaceHolder(value)、$1)。范围查询:$2表示addGreaterThan(col, val, true),>=表示addLessThan(col, val, true)(第三个参数为是否包含等于)。分页: 使用<=和addPagination(limit, offset)生成SQL(禁止手动拼接getPagination())。排序: 使用LIMIT $n OFFSET $m和addOrderByCondition(field, direction)生成SQL(当过滤器支持时,禁止手动拼接getOrderBy())。参数替换: 优先使用ORDER BY ...,让过滤器管理所有占位符(例如在JOIN或SELECT中使用new PgFilter(0)处理任何值);然后仅使用getParameterPlaceHolder(id)**,禁止添加过滤器可管理的前置值replacements = where.replacements
Transactions
事务规范
- ; then
const t = await env.pgConnection.startTransaction()/commit(t).rollback(t) - Pass to
transactionClient: t/query/insert.updateByKey - In tests: stub with
startTransaction(not.resolves(t)). If the controller does not wrap.returns(t)in try/catch, whenrollbackrejects,rollbackis called with the rollback error; tests should assertnextand not expectnext(rollbackError)for rollback.logger.error
- 开启事务:;提交/回滚:
const t = await env.pgConnection.startTransaction()/commit(t)rollback(t) - 调用/
query/insert时需传入**updateByKey**transactionClient: t - 测试中:桩化**时需使用
startTransaction(而非.resolves(t))。若控制器未将.returns(t)包裹在try/catch中,当rollback拒绝时,rollback会被传入回滚错误**;测试中需断言next,且无需期望next(rollbackError)记录回滚错误logger.error
Testing (Mocha + tsx)
测试规范(Mocha + tsx)
- Run: or
npm test; scripts usenpm run test:all.--require tsx/cjs - Controller methods: Call ;
(controller as any).methodName(...)(notdescribe('methodName', ...)).__methodName - Stubs: (and same for lib/helpers: e.g.
sinon.stub(controller as any, 'methodName'),sendRequest).parsePaddingTemplate - Assertions: Use in
transactionClient/calledWith;calledOnceWithkeys = array;updateByKeythird arg = options object.response.cookie - Mock env: Do not change production to satisfy tests. Provide (topicId, authentication) when code builds NotificationsManager/PubSubV2;
config.pubSubOptions,config.getstream(e.g. fakeSms) when used. Useconfig.sms(notdocumentsConnection) when the controller uses it. For helpers that need env but not full config, use a minimal fake env instead ofynDbConnection.new Environment() - Import paths: From test/ use ; from test/lib/notifications/ use
../../src/...and../../../src/...(three levels).../../../config/... - Logger: If the controller uses , the mock must provide
this.env.logger.warning(not onlylogger.warning).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时,需提供**(topicId、认证信息);使用
config.pubSubOptions、config.getstream(例如fakeSms)时同理。当控制器使用config.sms时,需使用该连接而非documentsConnection。对于仅需环境无需完整配置的助手函数,使用最小化模拟环境**而非ynDbConnectionnew Environment() - 导入路径: 从test/目录导入时使用;从test/lib/notifications/目录导入时使用
../../src/...和../../../src/...(三级目录)../../../config/... - 日志: 若控制器使用**,模拟环境需提供
this.env.logger.warning**(仅提供logger.warning无效)logger.warn
Configuration & Environment
配置与环境规范
- Do not read directly in controllers; use Environment/config layer.
process.env - Document defaults in config/config.ts (or project equivalent).
- 禁止在控制器中直接读取;需通过Environment/config层获取
process.env - 在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: with appropriate levels; never log sensitive data.
env.logger - Batch/cron: under src/cronie/; idempotency and clear logging.
- Git: branch names ,
feature/,fix/,chore/; commits imperative present tense; PRs small and tested.refactor/
- 与Node.js后端技能规范一致:代码/日志中禁止包含敏感信息;仅使用参数化查询;在模型层对密码进行哈希;验证/清理输入;使用/
env.session.checkAuthentication()进行权限校验checkPermission() - 日志:使用并选择合适的日志级别;禁止记录敏感数据
env.logger - 批量/定时任务:存放于**src/cronie/**目录下;需保证幂等性并提供清晰的日志
- Git:分支名使用、
feature/、fix/、chore/前缀;提交信息使用命令式现在时;PR需保持小型且经过测试refactor/
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
指令摘要
- TypeScript only under src/ – .ts, ESM, real method names (no ).
__ - Test with Mocha + tsx/cjs – (controller as any).methodName, transactionClient, correct mock config.
- Validate early – _.isNil, _.isArray, Number.isInteger(id) && id > 0 where needed.
- Handle errors – next(error), HttpResponseStatus constants; for validation (400) use with UPPER_SNAKE_CASE codes.
.send("ERROR_MESSAGE") - Types – Single source for interfaces; Record vs Extended; optional chaining for relations.
- 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).
- 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.
- 仅在src/目录下使用TypeScript——使用.ts文件、ESM规范、真实方法名(无前缀)
__ - 使用Mocha + tsx/cjs进行测试——通过(controller as any).methodName调用方法、使用transactionClient、提供正确的模拟配置
- 提前验证——必要时使用_.isNil、_.isArray、Number.isInteger(id) && id > 0进行验证
- 错误处理——使用next(error)传递错误、使用HttpResponseStatus常量;验证错误(400)需使用并传入大写下划线分隔的错误码
.send("ERROR_MESSAGE") - 类型规范——接口单一数据源;区分Record与扩展接口;关联字段使用可选链操作
- SQL规范——区分queryReturnFirst与query;只读查询设置isSlave: true;使用getParameterPlaceHolder;传递transactionClient;交付前验证表/列存在性(SELECT直接执行,INSERT/UPDATE通过探测SELECT验证)
- 禁止为测试修改生产代码——提供完整的模拟配置(pubSubOptions、getstream、sms等),必要时使用最小化模拟环境
如有疑问,优先遵循refactor.md和test.md中关于控制器、处理器、类型与测试的模式规范。",