@db-node/mysql2
v1.0.0
Published
Lightweight, type-safe CRUD wrapper around mysql2/promise — Where DSL, transactions, pagination, multi-DB.
Maintainers
Readme
easy-mysql2
Lightweight, type-safe CRUD wrapper around
mysql2/promise— Where DSL, transactions, pagination, multi-DB.
- Zero magic:所有 SQL 都通过 mysql2 的
??/?占位符参数化 - 安全:标识符白名单校验、空 WHERE 默认拒绝、聚合函数白名单
- 完整 TypeScript 类型 (strict 模式)
- 支持单 Pool / 多 DB 注入
- 内置事务包装(自动 commit / rollback / release)
- 内置分页 + 聚合分页
安装
npm i easy-mysql2 mysql2
mysql2是 peer dependency,由你自己安装并管理版本。
快速开始
单 Pool 模式(最常见)
import mysql from 'mysql2/promise';
import { createDbApi } from 'easy-mysql2';
const pool = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: '',
database: 'demo',
connectionLimit: 10,
});
const db = createDbApi(pool); // 也可以写 createDbApi({ pool })
// 查询
const users = await db.select({
table: 'user',
where: { status: 1, age: { gte: 18 } },
orderBy: [['id', 'DESC']],
limit: 20,
});
// 插入
const ret = await db.insert({
table: 'user',
data: { name: 'alice', age: 20, ctime: new Date() },
});
console.log(ret.insertId);多 DB 模式
import { createDbApi, type GetPool } from 'easy-mysql2';
const pools = {
main: mysql.createPool({ /* ... */ }),
log: mysql.createPool({ /* ... */ }),
};
const getPool: GetPool = (name = 'main') => pools[name];
const db = createDbApi({ getPool, defaultDb: 'main' });
await db.select({ table: 'user' }); // 默认 main
await db.select({ table: 'event', dbName: 'log' }); // 切到 logAPI
createDbApi(arg)
| 参数 | 类型 | 说明 |
|------|------|------|
| arg | Pool | DbApiOptions | 直接传 Pool,或 { pool, getPool, defaultDb } |
返回 DbApi 实例。
DbApi 接口
| 方法 | 说明 |
|------|------|
| select<T>(opts) | 查询多行,返回 T[] |
| findOne<T>(opts) | 查询单行,返回 T \| null |
| page<T>(opts) | 分页查询,返回 { list, total, page, size, pages } |
| insert(opts) | 单条插入,返回 ResultSetHeader |
| insertMany(opts) | 批量插入,返回 ResultSetHeader |
| update(opts) | 更新(默认禁止空 WHERE) |
| del(opts) | 删除(默认禁止空 WHERE) |
| query<T>({sql, params}) | 原生 SQL 透传 |
| aggregatePage<T>(opts) | 聚合分页(GROUP BY + HAVING + 聚合函数) |
| transaction(cb, dbName?) | 自动管理的事务 |
| beginTransaction(dbName?) | 手动事务连接(需自己 commit/rollback/release) |
| getPool(dbName?) | 拿到底层 Pool |
所有 CRUD opts 共享 BaseOpts:
interface BaseOpts {
/** 显式传一个连接(事务里复用),优先级最高 */
connection?: Pool | PoolConnection;
/** 多 DB 模式下临时切库 */
dbName?: string;
}Where DSL
type WhereValue = SimpleValue | WhereOps;
// SimpleValue = string | number | boolean | null | undefined | Date | Buffer操作符
| 操作符 | 示例 | 生成 SQL |
|--------|------|----------|
| eq | { id: 1 } 或 { id: { eq: 1 } } | \id` = ?|
|ne|{ id: { ne: 1 } }|`id` <> ?|
|gt / gte / lt / lte|{ age: { gte: 18, lt: 60 } }|(`age` >= ? AND `age` < ?)|
|like|{ name: { like: 'a%' } }|`name` LIKE ?|
|notLike|{ name: { notLike: 'a%' } }|`name` NOT LIKE ?|
|in|{ status: { in: [1, 2, 3] } }|`status` IN (?,?,?)|
|notIn|{ status: { notIn: [9] } }|`status` NOT IN (?)|
|between|{ ctime: { between: [a, b] } }|`ctime` BETWEEN ? AND ?|
|isNull|{ deletedAt: { isNull: true } }|`deletedAt` IS NULL|
|isNotNull|{ phone: { isNotNull: true } }|`phone` IS NOT NULL` |
组合:$and / $or / $raw
db.select({
table: 'user',
where: {
status: 1,
$or: [{ name: { like: 'a%' } }, { email: { like: 'a%' } }],
$raw: { sql: 'JSON_EXTRACT(meta, "$.role") = ?', params: ['admin'] },
},
});边界行为
where: {}→ 无 WHERE 子句(update / del默认拒绝;显式allowEmptyWhere: true才执行)field: null或undefined→field IS NULLin: []→1 = 0(恒假,避免误命中所有行)notIn: []→1 = 1(恒真)
OrderBy DSL
三种写法等价:
orderBy: 'id' // ORDER BY `id` ASC
orderBy: [['id', 'DESC'], ['ctime']] // ORDER BY `id` DESC, `ctime` ASC
orderBy: { id: 'DESC' } // ORDER BY `id` DESC方向只允许 ASC / DESC(大小写不敏感),其他值抛错。
查询
select<T>:多行
const list = await db.select<User>({
table: 'user',
fields: ['id', 'name', 'age'], // 默认 '*'
where: { status: 1, age: { gte: 18 } },
orderBy: [['id', 'DESC']],
limit: 50,
offset: 0,
});findOne<T>:单行
const u = await db.findOne<User>({
table: 'user',
where: { id: 1 },
});
// u: User | nullpage<T>:分页
const r = await db.page<User>({
table: 'user',
where: { status: 1 },
orderBy: { id: 'DESC' },
page: 1,
size: 20,
// withCount: false, // 不查 total,性能更好
});
// r = { list: User[], total: number, page: number, size: number, pages: number }写操作
insert:单条插入
const ret = await db.insert({
table: 'user',
data: {
name: 'alice',
age: 20,
ctime: new Date(),
},
});
console.log(ret.insertId); // 自增主键
console.log(ret.affectedRows); // 1生成 SQL:
INSERT INTO `user` (`name`, `age`, `ctime`) VALUES (?, ?, ?)insertMany:批量插入
const ret = await db.insertMany({
table: 'user',
rows: [
{ name: 'a', age: 1 },
{ name: 'b', age: 2 },
{ name: 'c', age: 3 },
],
});
console.log(ret.affectedRows); // 3⚠️ 所有行的字段集合以第一行为准;其余行缺失的字段会以
undefined(即 NULL)插入。 单次 batch 行数过多时(> 1000),建议自行分批。
update:更新
const ret = await db.update({
table: 'user',
data: { name: 'alice2', age: 21 },
where: { id: 1 },
});
console.log(ret.affectedRows); // 实际更新行数生成 SQL:
UPDATE `user` SET `name` = ?, `age` = ? WHERE `id` = ?默认禁止空 WHERE(防误更新全表):
await db.update({ table: 'user', data: { x: 1 }, where: {} });
// ❌ throws: refusing empty WHERE; pass allowEmptyWhere: true to override
await db.update({
table: 'user',
data: { x: 1 },
where: {},
allowEmptyWhere: true, // 显式确认,全表更新
});复杂 where 同样支持:
await db.update({
table: 'order',
data: { status: 9 },
where: {
status: 0,
ctime: { lt: oneHourAgo },
$or: [{ payChannel: 'A' }, { payChannel: 'B' }],
},
});del:删除
因为
delete是 JS 关键字,方法名用了del。
const ret = await db.del({
table: 'user',
where: { id: 1 },
});
console.log(ret.affectedRows);生成 SQL:
DELETE FROM `user` WHERE `id` = ?同 update,默认禁止空 WHERE:
await db.del({ table: 'log', where: {} });
// ❌ throws
await db.del({
table: 'log',
where: {},
allowEmptyWhere: true, // 全表删除
});query:原生 SQL 透传
任何 CRUD 都可以降级到原生 SQL(参数化):
// SELECT
const rows = await db.query<{ id: number }[]>({
sql: 'SELECT id FROM user WHERE FIND_IN_SET(?, tag_ids)',
params: ['vip'],
});
// INSERT ... ON DUPLICATE KEY UPDATE
await db.query({
sql: 'INSERT INTO counter (k, v) VALUES (?, ?) ON DUPLICATE KEY UPDATE v = v + ?',
params: ['pv', 1, 1],
});
// REPLACE / TRUNCATE / DDL 等
await db.query({ sql: 'TRUNCATE TABLE tmp_log' });聚合分页
await db.aggregatePage({
table: 'order',
where: { ctime: { gte: yesterday } },
groupBy: ['userId', 'status'],
aggregates: {
cnt: { fn: 'COUNT', column: '*' },
sum: { fn: 'SUM', column: 'amount' },
},
having: { cnt: { gte: 2 } },
orderBy: { sum: 'DESC' },
page: 1,
size: 50,
});聚合函数白名单:COUNT / SUM / AVG / MIN / MAX,其他全部抛错。
事务
推荐:transaction(cb) 自动管理
await db.transaction(async (tx) => {
// 注意:事务里所有方法都要传 connection: tx
await db.update({
table: 'account',
data: { balance: { /* ... */ } },
where: { id: from },
connection: tx,
});
await db.insert({
table: 'transfer',
data: { from, to, amount: 100 },
connection: tx,
});
});
// 抛错自动 rollback;正常返回自动 commit;最终自动 release手动:beginTransaction()
const tx = await db.beginTransaction();
try {
await db.update({ /* ... */, connection: tx });
await tx.commit();
} catch (e) {
await tx.rollback();
throw e;
} finally {
tx.release();
}❗事务里如果不传
connection: tx,操作会落到主连接上,造成数据不一致。
TypeScript 用法
interface User {
id: number;
name: string;
age: number;
}
const u = await db.findOne<User>({ table: 'user', where: { id: 1 } });
// u: User | null
const list = await db.select<User>({ table: 'user' });
// list: User[]page<T> 也支持泛型,返回 PageResult<T>。
安全特性
| 防御项 | 实现 |
|--------|------|
| SQL 注入(值) | 全部走 mysql2 ? 占位符 |
| SQL 注入(标识符) | 表名/列名/聚合别名一律 assertIdent 白名单(/^[a-zA-Z_][a-zA-Z0-9_]*$/) |
| 误删全表 | update / del 默认拒绝空 WHERE,需显式 allowEmptyWhere: true |
| 聚合函数注入 | 白名单 COUNT / SUM / AVG / MIN / MAX |
| IN ([]) 误命中 | 转写为 1 = 0 |
| 事务连接错乱 | transaction() 内部独占连接,不复用 Pool |
完整类型导出
import {
createDbApi,
buildWhere, // 工具:构造 where 片段
buildOrderBy, // 工具:构造 order by 片段
transaction, // 独立事务函数(不依赖 DbApi 实例)
beginTransaction,
assertIdent,
type DbApi,
type DbApiOptions,
type Where,
type OrderBy,
type PageResult,
type Pool, // 重导出 mysql2 类型
type PoolConnection,
} from 'easy-mysql2';常见问题
想直接拼 SQL 怎么办?
用 db.query({ sql, params }),参数化一切:
const rows = await db.query<{ id: number }[]>({
sql: 'SELECT id FROM user WHERE FIND_IN_SET(?, tag_ids)',
params: ['vip'],
});字段名有保留字怎么办?
mysql2 的 ?? 占位符会自动加反引号,所以 select * from where order 这样的字段也安全。但变态字段名(含点、空格)暂不支持,想要请用 query()。
想跨库 join 怎么办?
db.query() 自己写 SQL,自由发挥。
License
MIT
