npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@db-node/mysql2

v1.0.0

Published

Lightweight, type-safe CRUD wrapper around mysql2/promise — Where DSL, transactions, pagination, multi-DB.

Readme

easy-mysql2

Lightweight, type-safe CRUD wrapper around mysql2/promise — Where DSL, transactions, pagination, multi-DB.

License: MIT

  • 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' }); // 切到 log

API

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: nullundefinedfield IS NULL
  • in: []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 | null

page<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