ts-mysql-ddl
v1.2.0
Published
TypeScript DSL for MySQL schema definition + enum generation — define tables/enums in TS, generate DDL SQL and TypeScript enums
Readme
ts-mysql-ddl — MySQL Schema DSL
概述
ts-mysql-ddl 是一个 TypeScript DSL,用于以纯 TS 代码定义 MySQL 数据表结构,直接生成 DDL SQL。它是数据库结构的唯一事实来源(Single Source of Truth),同时为 mock 数据生成提供类型信息。
架构设计
| 层 | 文件 | 职责 |
|---|---|---|
| 类型系统 | mysql-type.ts | MysqlType 枚举 + 类型守卫 |
| 列定义 | column-def.ts | ColumnDef 联合类型(按 type 分支锁参数) |
| 列引用 | column-ref.ts | ColumnRef 类 + col() 工厂 |
| 表定义 | table-def.ts | TableSchema / ForeignKey / IndexDef 接口 |
| 工厂 | define-table.ts | defineTable(name, input) → TableSchema |
| DDL 生成 | generate-ddl.ts | generateDdl(tables) → SQL string |
| 自动扫描 | scan-tables.ts | scanTables(dir) 自动发现 *.table.ts |
| 枚举生成 | generate-enums.ts | generateEnums({ schemaDir, outDir }) 从 defineEnum 生成 TS enum 文件 |
核心类型
MysqlType
枚举定义 MySQL 数据类型家族,每个家族对应一组编译期校验的参数:
INT / TINYINT / SMALLINT / BIGINT → autoIncrement, default(number), enumRef
DECIMAL → precision, scale, default(string)
VARCHAR / CHAR → length, default(string), enumRef
TEXT → (无额外参数)
DATE → (无额外参数)
DATETIME / TIMESTAMP → default('CURRENT_TIMESTAMP' only)所有类型通用参数:primaryKey, index, unique, comment, deleted
ColumnDef 联合类型
type ColumnDef = IntColumn | DecimalColumn | StringColumn | TextColumn | DateColumn | DateTimeColumn每个接口按 type 分支限制参数,编译期拦截非法组合:
- IntColumn: 不能有
length,precision,scale - StringColumn: 不能有
autoIncrement,precision - DateTimeColumn:
default只能是'CURRENT_TIMESTAMP'
EnumShape
type EnumShape = Record<string, string | number>表示代码层面的枚举(非 MySQL ENUM),DDL 中存 int/string。由 enumRef 字段引用真实 TS 枚举,供 mock 生成使用。
ColumnRef
class ColumnRef {
readonly def: ColumnDef; // 列定义
_table: string = ''; // 所属表名(由 defineTable/registerColumns 设置)
_column: string = ''; // 列名(由 defineTable/registerColumns 设置)
toString(): string // 返回 _column,未初始化时返回 '?'
}_table / _column 是 identity 字段,由以下时机回填:
defineTable()调用时scanTables()的 Phase 1 阶段
关键约束:ColumnRef 创建时 identity 为空。两阶段初始化确保在所有 FK 引用解析前,所有列的 identity 已设置完成。
使用方式
方式一:手动编排(适用于少量表)
import { MysqlType, col, defineTable, generateDdl } from '@fastify-api/ts-mysql-ddl';
const merchantCols = {
id: col({ type: MysqlType.VARCHAR, length: 12, primaryKey: true }),
name: col({ type: MysqlType.VARCHAR, length: 100 }),
};
const merchant = defineTable('merchant', { columns: merchantCols, comment: 'Merchant' });
const orderCols = {
id: col({ type: MysqlType.INT, autoIncrement: true }),
mer_id: col({ type: MysqlType.VARCHAR, length: 20 }),
status: col({ type: MysqlType.VARCHAR, length: 20, enumRef: OrderStatus }),
};
const order = defineTable('order', {
columns: orderCols,
indexes: [{ columns: [orderCols.status, orderCols.deleted] }],
foreignKeys: {
fk_order_merchant: { columns: [orderCols.mer_id], ref: merchant.cols.id, onDelete: 'CASCADE' },
},
paginated: true, // 数据量大,需要分页查询
generator: 'snowflake', // 非自增 PK 的 ID 生成器
});
const sql = generateDdl([merchant, order]);方式二:自动扫描(推荐)
按文件命名约定组织 *.table.ts 文件:
schema/
├── merchant.table.ts
├── order.table.ts
├── coupon.table.ts
└── ...每个文件只需导出数据,不调 defineTable:
// schema/order.table.ts
import { col, MysqlType } from '@fastify-api/ts-mysql-ddl';
import { columns as merchant } from './merchant.table';
export const columns = { ... }; // Record<string, ColumnRef> 必需
export const indexes = [...]; // IndexDef[] 可选
export const foreignKeys = { ... }; // Record<string, ForeignKey> 可选
export const primaryKey = ...; // ColumnRef | ColumnRef[] 可选
export const comment = '...'; // string 可选
export const paginated = true; // boolean 可选,true=数据量大→分页/瀑布流/可搜索选择器
export const generator = 'snowflake'; // string 可选,非自增主键的 ID 生成器标识扫描:
const tables = await scanTables('./schema');
const sql = generateDdl(tables);表名从文件名推导:order.table.ts → order。
scanTables() 使用两阶段初始化:
- Phase 1:遍历所有模块,逐列回填
_table/_column(此时所有 ColumnRef 获得 identity) - Phase 2:为每个模块调
defineTable()构建完整 TableSchema
因此 FK 引用(ref: merchant.id)无论文件加载顺序如何,都能正确解析。
外键引用模式
直接引用(不同表)
import { columns as merchant } from './merchant.table';
export const foreignKeys = {
fk_order_merchant: {
columns: [columns.mer_id],
ref: merchant.id, // ColumnRef 直接引用
onDelete: 'CASCADE',
},
};懒引用(自引用/循环引用)
export const foreignKeys = {
fk_category_parent: {
columns: [columns.parent_id],
ref: () => columns.id, // 延迟求值,避免循环依赖
onDelete: 'CASCADE',
},
};当 ref 是 () => ColumnRef 时,DDL 生成阶段再求值。
复合外键
export const foreignKeys = {
fk_order_detail_product: {
columns: [columns.sku_id, columns.tenant_id],
ref: product.columns, // 注意:此处需要确认是否支持复合 FK → 见下方约束
},
};当前约束:复合 FK 的 ref 语法暂未定型,当前仅支持单列 FK。
分页标记(paginated)
paginated 是表级元数据,标记该表的数据量级,不参与 DDL 生成,供下游组件决定交互形式:
| 值 | 数据量 | 前端行为 | 选择器 |
|---|---|---|---|
| true | 大 | admin:分页列表;小程序:瀑布流 | auto-complete(可搜索) |
| false / 未设置 | 小 | 全量列表 | 简单下拉 |
手动编排:
defineTable('order', {
columns: orderCols,
paginated: true,
});自动扫描(导出即可):
// schema/order.table.ts
export const paginated = true;该标志可通过 table._schema.paginated 读取。
ID 生成器(generator)
为非自增主键表声明 ID 生成器标识,不参与 DDL 生成,供下游工具自动使用正确的生成逻辑。
手动编排:
defineTable('pay', {
columns: payCols,
primaryKey: payCols.id,
generator: 'snowflake',
});自动扫描(导出即可):
// schema/merchant.table.ts
export const primaryKey = columns.id;
export const generator = 'merchantId';自增主键表无需声明 generator。
索引与约束
单列索引(在列定义中)
col({ type: MysqlType.VARCHAR, length: 20, index: true })
// → KEY `idx_status` (`status`)单列唯一(在列定义中)
col({ type: MysqlType.VARCHAR, length: 50, unique: true })
// → UNIQUE KEY `uk_name` (`name`)复合索引
indexes: [
{ columns: [columns.status, columns.deleted] },
// → KEY `idx_status_deleted` (`status`, `deleted`)
]复合唯一
indexes: [
{ columns: [columns.user_id, columns.order_no], unique: true },
// → UNIQUE KEY `uk_user_id_order_no` (`user_id`, `order_no`)
]命名规则
| 来源 | 自动命名规则 |
|---|---|
| 列级 index: true | idx_{column} |
| 列级 unique: true | uk_{column} |
| 复合索引(未提供 name) | idx_{col1}_{col2}_... |
| 复合唯一(未提供 name) | uk_{col1}_{col2}_... |
如需自定义索引名,设置 IndexDef.name:
{ columns: [columns.status, columns.deleted], name: 'idx_order_status' }列定义完整参数
整数类型(INT / TINYINT / SMALLINT / BIGINT)
| 参数 | 类型 | 说明 | |---|---|---| | type | IntMysqlType | 必需 | | autoIncrement | boolean | 可选 | | primaryKey | boolean | 可选(简写,也可在表级声明) | | index | boolean | 可选,生成单列索引 | | unique | boolean | 可选,生成唯一索引 | | default | number | 可选 | | comment | string | 可选 | | enumRef | EnumShape | 可选,代码级枚举引用 | | deleted | boolean | 可选,标识为软删除字段 |
小数类型(DECIMAL)
| 参数 | 类型 | 说明 |
|---|---|---|
| type | 'DECIMAL' | 必需 |
| precision | number | 必需 |
| scale | number | 必需 |
| default | string | 可选(如 '0.00') |
| comment | string | 可选 |
| enumRef | EnumShape | 可选 |
字符串类型(VARCHAR / CHAR)
| 参数 | 类型 | 说明 | |---|---|---| | type | StringMysqlType | 必需 | | length | number | VARCHAR 必需,CHAR 必需 | | primaryKey | boolean | 可选 | | index | boolean | 可选 | | unique | boolean | 可选 | | default | string | 可选 | | comment | string | 可选 | | enumRef | EnumShape | 可选 | | deleted | boolean | 可选 |
文本类型(TEXT)
| 参数 | 类型 | 说明 | |---|---|---| | type | 'TEXT' | 必需 | | comment | string | 可选 |
日期时间类型(DATETIME / TIMESTAMP)
| 参数 | 类型 | 说明 | |---|---|---| | type | DateTimeMysqlType | 必需 | | default | 'CURRENT_TIMESTAMP' | 可选 | | comment | string | 可选 |
日期类型(DATE)
| 参数 | 类型 | 说明 | |---|---|---| | type | 'DATE' | 必需 | | comment | string | 可选 |
DDL 生成
generateDdl(tables: TableSchema[]) 输出 SQL:
- 每个表生成
CREATE TABLE IF NOT EXISTS - 默认 ENGINE=InnoDB, CHARSET=utf8mb4
- 按表定义顺序输出,以
-- 表注释的 SQL 注释分隔 - 外键自动推导引用表/列的标识
编译期校验
- 列定义:按 type 分支锁定合法参数(如 VARCHAR 不能设 autoIncrement)
- 列引用:
primaryKey/indexes.columns/foreignKeys.columns只能用 ColumnRef 对象,不能字符串 - 外键引用:
ref必须是 ColumnRef 或() => ColumnRef - 可选导出:
scanTables对缺少columns导出的文件输出警告并跳过
枚举生成(generateEnums)
从 schema defineEnum() 定义自动生成 TypeScript enum + label Record 文件。
使用方式
CLI(与 DDL 生成同一条命令):
# 生成 DDL SQL
ts-mysql-ddl --src ./schema --dest ./001_init.sql
# 生成 TS 枚举文件
ts-mysql-ddl generate-enums --src ./schema --dest ./enumsAPI:
import { generateEnums } from 'ts-mysql-ddl';
const files = await generateEnums({
schemaDir: './schema',
outDir: './enums',
});
// → ['_common', 'coupon', 'merchant', ...]输出结构
每个 *.table.ts 文件对应一个输出文件,文件名等于表名:
enums/
├── _common.ts # _common.ts 中顶层 export 的 defineEnum
├── coupon.ts # coupon.table.ts 的 enums 对象中定义的枚举
├── merchant.ts
├── pay.ts
└── ..._common.ts 处理
如果 schema 目录中存在 _common.ts,其顶层 export const Xxx = defineEnum(...) 定义会被:
- 生成到
_common.ts输出文件 - 自动去重:引用
_common枚举的 table 文件在生成的输出中用export { Xxx } from './_common'替代重复生成
示例
输入 _common.ts:
export const AcquiringTypeEnum = defineEnum('AcquiringType', {
WECHAT: { value: 'wechat', label: '微信' },
UNIONPAY: { value: 'unionpay', label: '银联商务' },
});输入 coupon.table.ts:
import { AcquiringTypeEnum } from './_common';
export const enums = {
CouponStatus: defineEnum('CouponStatus', {
PENDING: { value: 'pending', label: '待审核' },
LISTED: { value: 'listed', label: '已上架' },
}),
AcquiringType: AcquiringTypeEnum, // 来自 _common
};生成 enums/_common.ts:
export enum AcquiringType {
WECHAT = 'wechat',
UNIONPAY = 'unionpay',
}
export const ACQUIRING_TYPE_LABEL: Record<AcquiringType, string> = {
[AcquiringType.WECHAT]: '微信',
[AcquiringType.UNIONPAY]: '银联商务',
};生成 enums/coupon.ts:
export enum CouponStatus {
PENDING = 'pending',
LISTED = 'listed',
}
export const COUPON_STATUS_LABEL: Record<CouponStatus, string> = {
[CouponStatus.PENDING]: '待审核',
[CouponStatus.LISTED]: '已上架',
};
export { AcquiringType, ACQUIRING_TYPE_LABEL } from './_common';开发指南
# 类型检查
cd ts-mysql-ddl
npm run typecheck
# tsx 运行测试
npx tsx test-scan/verify.ts提交方式
git add -A && git commit -m "feat: add generateEnums API and CLI subcommand"
git push