├── .husky └── pre-commit ├── src ├── index.ts ├── agent.ts ├── app.ts ├── typings │ └── index.d.ts ├── types.ts ├── boot.ts └── config │ └── config.default.ts ├── .prettierignore ├── tsconfig.json ├── test ├── fixtures │ └── apps │ │ ├── mysqlapp │ │ ├── package.json │ │ ├── app │ │ │ ├── router.js │ │ │ ├── service │ │ │ │ └── user.js │ │ │ └── controller │ │ │ │ └── home.js │ │ ├── config │ │ │ └── config.js │ │ └── agent.js │ │ ├── mysqlapp-disable │ │ ├── package.json │ │ └── config │ │ │ └── config.default.js │ │ ├── mysqlapp-new │ │ ├── package.json │ │ ├── app │ │ │ ├── router.js │ │ │ ├── service │ │ │ │ └── user.js │ │ │ └── controller │ │ │ │ └── home.js │ │ ├── config │ │ │ └── config.default.js │ │ └── agent.js │ │ ├── mysqlapp-dynamic │ │ ├── package.json │ │ ├── app │ │ │ ├── service │ │ │ │ └── user.js │ │ │ ├── router.js │ │ │ └── controller │ │ │ │ └── home.js │ │ ├── app.js │ │ ├── config │ │ │ └── config.default.js │ │ └── agent.js │ │ ├── mysqlapp-ts-esm │ │ ├── tsconfig.json │ │ ├── typings │ │ │ └── index.d.ts │ │ ├── package.json │ │ ├── app │ │ │ ├── router.ts │ │ │ ├── service │ │ │ │ └── user.ts │ │ │ └── controller │ │ │ │ └── home.ts │ │ ├── config │ │ │ └── config.ts │ │ └── agent.ts │ │ └── mysqlapp-multi-client-wrong │ │ ├── package.json │ │ ├── app │ │ ├── router.js │ │ ├── controller │ │ │ └── home.js │ │ └── service │ │ │ └── user.js │ │ └── config │ │ └── config.default.js ├── .setup.ts ├── npm_auth.sql ├── esm.test.ts └── mysql.test.ts ├── .prettierrc ├── .gitignore ├── .github └── workflows │ ├── release.yml │ └── nodejs.yml ├── __snapshots__ └── mysql.test.ts.js ├── docker-compose.yml ├── LICENSE ├── package.json ├── .oxlintrc.json ├── CHANGELOG.md ├── README.zh-CN.md └── README.md /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './types.js'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | __snapshots__ 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysqlapp" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-disable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysqlapp" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-new/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysqlapp-new" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-dynamic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysqlapp-dynamic" 3 | } 4 | -------------------------------------------------------------------------------- /src/agent.ts: -------------------------------------------------------------------------------- 1 | import { MySQLBootHook } from './boot.js'; 2 | 3 | export default MySQLBootHook; 4 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { MySQLBootHook } from './boot.js'; 2 | 3 | export default MySQLBootHook; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-ts-esm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-ts-esm/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | import '../../../../../src/index.ts'; 2 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-multi-client-wrong/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysqlapp-multi-client-wrong" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-ts-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mysqlapp-ts-esm", 3 | "type": "module" 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "arrowParens": "avoid" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-multi-client-wrong/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = app => { 2 | app.get('/', app.controller.home); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-new/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function exports(app) { 2 | app.get('/', app.controller.home); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp/app/router.js: -------------------------------------------------------------------------------- 1 | module.exports = function exports(app) { 2 | app.get('/', app.controller.home); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-new/app/service/user.js: -------------------------------------------------------------------------------- 1 | exports.list = async ctx => { 2 | return await ctx.app.mysql.query('select * from npm_auth'); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp/app/service/user.js: -------------------------------------------------------------------------------- 1 | exports.list = async ctx => { 2 | return await ctx.app.mysql.query('select * from npm_auth'); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-dynamic/app/service/user.js: -------------------------------------------------------------------------------- 1 | exports.list = async ctx => { 2 | return await ctx.app.mysql1.query('select * from npm_auth'); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-dynamic/app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function exports(app) { 4 | app.get('/', app.controller.home); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-ts-esm/app/router.ts: -------------------------------------------------------------------------------- 1 | import type { Application } from 'egg'; 2 | 3 | export default (app: Application) => { 4 | app.get('/', app.controller.home); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp/app/controller/home.js: -------------------------------------------------------------------------------- 1 | module.exports = async ctx => { 2 | const users = await ctx.service.user.list(ctx); 3 | 4 | ctx.body = { 5 | status: 'success', 6 | users, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-new/app/controller/home.js: -------------------------------------------------------------------------------- 1 | module.exports = async ctx => { 2 | const users = await ctx.service.user.list(ctx); 3 | 4 | ctx.body = { 5 | status: 'success', 6 | users: users, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-dynamic/app/controller/home.js: -------------------------------------------------------------------------------- 1 | module.exports = async ctx => { 2 | const users = await ctx.service.user.list(ctx); 3 | 4 | ctx.body = { 5 | status: 'success', 6 | users: users, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | node_modules/ 4 | coverage/ 5 | .idea/ 6 | run/ 7 | .DS_Store 8 | *.swp 9 | lib/*.js 10 | lib/*.d.ts 11 | config/*.js 12 | config/*.d.ts 13 | agent.js 14 | agent.d.ts 15 | app.js 16 | app.d.ts 17 | -------------------------------------------------------------------------------- /src/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | // make sure to import egg typings and let typescript know about it 2 | // @see https://github.com/whxaxes/blog/issues/11 3 | // and https://www.typescriptlang.org/docs/handbook/declaration-merging.html 4 | import 'egg'; 5 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-ts-esm/app/service/user.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'egg'; 2 | 3 | export default class UserService { 4 | async list(ctx: Context) { 5 | return await ctx.app.mysql.query('select * from npm_auth'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-disable/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.mysql = { 2 | host: '127.0.0.1', 3 | port: 3306, 4 | user: 'root', 5 | password: '', 6 | database: 'test', 7 | app: false, 8 | }; 9 | 10 | exports.keys = 'foo'; 11 | -------------------------------------------------------------------------------- /test/.setup.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | 3 | execSync('mysql -uroot -h 127.0.0.1 -e "create database IF NOT EXISTS test;"'); 4 | execSync('mysql -uroot -h 127.0.0.1 test < test/npm_auth.sql'); 5 | console.log('create table success'); 6 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp/config/config.js: -------------------------------------------------------------------------------- 1 | exports.mysql = { 2 | client: { 3 | host: '127.0.0.1', 4 | port: 3306, 5 | user: 'root', 6 | password: '', 7 | database: 'test', 8 | }, 9 | agent: true, 10 | }; 11 | 12 | exports.keys = 'foo'; 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-dynamic/app.js: -------------------------------------------------------------------------------- 1 | module.exports = class Boot { 2 | constructor(app) { 3 | this.app = app; 4 | } 5 | 6 | async didReady() { 7 | this.app.mysql1 = await this.app.mysql.createInstanceAsync(this.app.config.mysql1); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-new/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.mysql = { 2 | client: { 3 | host: '127.0.0.1', 4 | port: 3306, 5 | user: 'root', 6 | password: '', 7 | database: 'test', 8 | }, 9 | agent: true, 10 | }; 11 | 12 | exports.keys = 'foo'; 13 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-dynamic/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.mysql = { 2 | agent: true, 3 | }; 4 | 5 | exports.mysql1 = { 6 | host: '127.0.0.1', 7 | port: 3306, 8 | user: 'root', 9 | password: '', 10 | database: 'test', 11 | }; 12 | 13 | exports.keys = 'foo'; 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-ts-esm/app/controller/home.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'egg'; 2 | 3 | export default async (ctx: Context) => { 4 | const users = await ctx.service.user.list(ctx); 5 | 6 | ctx.body = { 7 | status: 'success', 8 | users, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-multi-client-wrong/app/controller/home.js: -------------------------------------------------------------------------------- 1 | module.exports = async ctx => { 2 | const dataArr = await ctx.service.user.list(ctx); 3 | 4 | ctx.body = { 5 | hasRows: 6 | dataArr[0].length > 0 && dataArr[1].length > 0 && dataArr[2].length > 0, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /test/npm_auth.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `npm_auth`; 2 | CREATE TABLE `npm_auth` ( 3 | `id` int(11) NOT NULL AUTO_INCREMENT, 4 | `user_id` varchar(255) DEFAULT NULL, 5 | `desc` varchar(255) DEFAULT NULL, 6 | `password` varchar(250) DEFAULT NULL, 7 | PRIMARY KEY (`id`) 8 | ) ENGINE=InnoDB; 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: eggjs/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-multi-client-wrong/app/service/user.js: -------------------------------------------------------------------------------- 1 | exports.list = async ctx => { 2 | return await Promise.all([ 3 | ctx.app.mysqls.get('db1').query('select * from npm_auth'), 4 | ctx.app.mysqls.get('db2').query('select * from npm_auth'), 5 | ctx.app.mysqls.get('db3').query('select * from npm_auth'), 6 | ]); 7 | }; 8 | -------------------------------------------------------------------------------- /__snapshots__/mysql.test.ts.js: -------------------------------------------------------------------------------- 1 | exports['test/mysql.test.ts should make default config stable 1'] = { 2 | default: { 3 | connectionLimit: 5, 4 | }, 5 | app: true, 6 | agent: true, 7 | client: { 8 | host: '127.0.0.1', 9 | port: 3306, 10 | user: 'root', 11 | password: '', 12 | database: 'test', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-ts-esm/config/config.ts: -------------------------------------------------------------------------------- 1 | import type { EggAppConfig, PowerPartial } from 'egg'; 2 | 3 | export default { 4 | keys: 'foo', 5 | mysql: { 6 | client: { 7 | host: '127.0.0.1', 8 | port: 3306, 9 | user: 'root', 10 | password: '', 11 | database: 'test', 12 | }, 13 | agent: true, 14 | }, 15 | } satisfies PowerPartial; 16 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test-mysql.yml@master 13 | with: 14 | os: 'ubuntu-latest' 15 | version: '18, 20, 22' 16 | secrets: 17 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 18 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Singleton } from '@eggjs/core'; 2 | import type { RDSClient } from '@eggjs/rds'; 3 | 4 | import type { MySQLConfig } from './config/config.default.js'; 5 | 6 | declare module '@eggjs/core' { 7 | // add EggAppConfig overrides types 8 | interface EggAppConfig { 9 | mysql: MySQLConfig; 10 | } 11 | 12 | interface EggCore { 13 | mysql: RDSClient & Singleton; 14 | mysqls: Singleton; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: eggjs_mysql_dev_services_mysql 2 | 3 | services: 4 | mysql: 5 | image: mysql:9 6 | environment: 7 | MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-} 8 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 9 | MYSQL_DATABASE: ${MYSQL_DATABASE:-test} 10 | ports: 11 | - 3306:3306 12 | healthcheck: 13 | test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost'] 14 | interval: 10s 15 | timeout: 5s 16 | retries: 5 17 | restart: unless-stopped 18 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp/agent.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const path = require('node:path'); 3 | 4 | module.exports = class Boot { 5 | constructor(agent) { 6 | this.agent = agent; 7 | } 8 | 9 | async didReady() { 10 | const p = path.join(__dirname, 'run/agent_result.json'); 11 | fs.existsSync(p) && fs.unlinkSync(p); 12 | 13 | const result = await this.agent.mysql.query('select now() as currentTime;'); 14 | fs.writeFileSync(p, JSON.stringify(result)); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-new/agent.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const path = require('node:path'); 3 | 4 | module.exports = class Boot { 5 | constructor(agent) { 6 | this.agent = agent; 7 | } 8 | 9 | async didReady() { 10 | const p = path.join(__dirname, 'run/agent_result.json'); 11 | fs.existsSync(p) && fs.unlinkSync(p); 12 | 13 | const result = await this.agent.mysql.query('select now() as currentTime;'); 14 | fs.writeFileSync(p, JSON.stringify(result)); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-ts-esm/agent.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | 4 | import type { Agent } from 'egg'; 5 | 6 | export default class Boot { 7 | private readonly agent: Agent; 8 | constructor(agent: Agent) { 9 | this.agent = agent; 10 | } 11 | 12 | async didReady() { 13 | const p = path.join(this.agent.baseDir, 'run/agent_result.json'); 14 | await fs.rm(p, { force: true }); 15 | 16 | const result = await this.agent.mysql.query('select now() as currentTime;'); 17 | await fs.writeFile(p, JSON.stringify(result)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-dynamic/agent.js: -------------------------------------------------------------------------------- 1 | const fs = require('node:fs'); 2 | const path = require('node:path'); 3 | 4 | module.exports = class Boot { 5 | constructor(agent) { 6 | this.agent = agent; 7 | } 8 | 9 | async didReady() { 10 | this.agent.mysql1 = await this.agent.mysql.createInstanceAsync(this.agent.config.mysql1); 11 | const p = path.join(__dirname, 'run/agent_result.json'); 12 | fs.existsSync(p) && fs.unlinkSync(p); 13 | 14 | const result = await this.agent.mysql1.query('select now() as currentTime;'); 15 | fs.writeFileSync(p, JSON.stringify(result)); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /test/fixtures/apps/mysqlapp-multi-client-wrong/config/config.default.js: -------------------------------------------------------------------------------- 1 | exports.mysql = { 2 | clients: [ 3 | { 4 | clientId: 'db1', 5 | host: '127.0.0.1', 6 | port: 3306, 7 | user: 'root', 8 | password: '123', 9 | database: 'test', 10 | }, 11 | { 12 | clientId: 'db2', 13 | host: '127.0.0.1', 14 | port: 3306, 15 | user: 'root', 16 | password: 'wrong', 17 | database: 'test', 18 | }, 19 | { 20 | clientId: 'db3', 21 | host: '127.0.0.1', 22 | port: 3306, 23 | user: 'root', 24 | password: '', 25 | database: 'test', 26 | }, 27 | ], 28 | agent: true, 29 | }; 30 | 31 | exports.keys = 'foo'; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present Alibaba Group Holding Limited and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/esm.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { randomUUID } from 'node:crypto'; 3 | 4 | import { mm, type MockApplication } from '@eggjs/mock'; 5 | 6 | describe('test/esm.test.ts', () => { 7 | let app: MockApplication; 8 | const uid = randomUUID(); 9 | 10 | before(() => { 11 | app = mm.app({ 12 | baseDir: 'apps/mysqlapp-ts-esm', 13 | }); 14 | return app.ready(); 15 | }); 16 | 17 | beforeEach(async () => { 18 | // init test datas 19 | await app.mysql.query( 20 | `insert into npm_auth set user_id = 'egg-${uid}-1', password = '1'` 21 | ); 22 | await app.mysql.query( 23 | `insert into npm_auth set user_id = 'egg-${uid}-2', password = '2'` 24 | ); 25 | await app.mysql.query( 26 | `insert into npm_auth set user_id = 'egg-${uid}-3', password = '3'` 27 | ); 28 | await app.mysql.queryOne( 29 | `select * from npm_auth where user_id = 'egg-${uid}-3'` 30 | ); 31 | }); 32 | 33 | afterEach(async () => { 34 | await app.mysql.query( 35 | `delete from npm_auth where user_id like 'egg-${uid}%'` 36 | ); 37 | }); 38 | 39 | after(async () => { 40 | await app.close(); 41 | }); 42 | 43 | it('should query mysql user table success', async () => { 44 | const res = await app.httpRequest().get('/').expect(200); 45 | 46 | assert.equal(res.body.status, 'success'); 47 | assert.equal(res.body.users.length, 3); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/boot.ts: -------------------------------------------------------------------------------- 1 | import { RDSClient, type RDSClientOptions } from '@eggjs/rds'; 2 | import type { EggCore, ILifecycleBoot } from '@eggjs/core'; 3 | 4 | async function createMySQLClient( 5 | config: RDSClientOptions, 6 | app: EggCore, 7 | clientName = 'default' 8 | ) { 9 | app.coreLogger.info( 10 | '[@eggjs/mysql] clientName[%s] connecting %s@%s:%s/%s', 11 | clientName, 12 | config.user, 13 | config.host, 14 | config.port, 15 | config.database 16 | ); 17 | const client = new RDSClient(config); 18 | 19 | const rows = await client.query('select now() as currentTime;'); 20 | app.coreLogger.info( 21 | '[@eggjs/mysql] clientName[%s] status OK, MySQL Server currentTime: %j', 22 | clientName, 23 | rows[0].currentTime 24 | ); 25 | return client; 26 | } 27 | 28 | export class MySQLBootHook implements ILifecycleBoot { 29 | private readonly app: EggCore; 30 | constructor(app: EggCore) { 31 | this.app = app; 32 | } 33 | 34 | configDidLoad() { 35 | if (this.app.type === 'application' && !this.app.config.mysql.app) { 36 | return; 37 | } else if (this.app.type === 'agent' && !this.app.config.mysql.agent) { 38 | return; 39 | } 40 | 41 | this.app.addSingleton('mysql', createMySQLClient); 42 | // alias to app.mysqls 43 | // https://github.com/eggjs/core/blob/41fe40ff68432db1f0bd89a88bdc33dd321bffb6/src/singleton.ts#L50 44 | Reflect.defineProperty(this.app, 'mysqls', { 45 | get() { 46 | return this.mysql; 47 | }, 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/config/config.default.ts: -------------------------------------------------------------------------------- 1 | import type { RDSClientOptions } from '@eggjs/rds'; 2 | 3 | export type MySQLClientOptions = RDSClientOptions; 4 | 5 | export interface MySQLClientsOptions { 6 | [clientName: string]: MySQLClientOptions; 7 | } 8 | 9 | export interface MySQLConfig { 10 | default?: MySQLClientOptions; 11 | /** 12 | * load into app, default is `true` 13 | */ 14 | app?: boolean; 15 | /** 16 | * load into agent, default is `false` 17 | */ 18 | agent?: boolean; 19 | /** 20 | * single database 21 | */ 22 | client?: MySQLClientOptions; 23 | /** 24 | * multi databases 25 | */ 26 | clients?: MySQLClientsOptions; 27 | } 28 | 29 | export default { 30 | mysql: { 31 | default: { 32 | connectionLimit: 5, 33 | }, 34 | app: true, 35 | agent: false, 36 | 37 | // Single Database 38 | // client: { 39 | // host: 'host', 40 | // port: 'port', 41 | // user: 'user', 42 | // password: 'password', 43 | // database: 'database', 44 | // }, 45 | 46 | // Multi Databases 47 | // clients: { 48 | // db1: { 49 | // host: 'host', 50 | // port: 'port', 51 | // user: 'user', 52 | // password: 'password', 53 | // database: 'database', 54 | // }, 55 | // db2: { 56 | // host: 'host', 57 | // port: 'port', 58 | // user: 'user', 59 | // password: 'password', 60 | // database: 'database', 61 | // }, 62 | // }, 63 | } satisfies MySQLConfig, 64 | }; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@eggjs/mysql", 3 | "publishConfig": { 4 | "access": "public" 5 | }, 6 | "version": "6.0.0", 7 | "description": "MySQL plugin for Egg.js", 8 | "eggPlugin": { 9 | "name": "mysql", 10 | "exports": { 11 | "import": "./dist/esm", 12 | "require": "./dist/commonjs", 13 | "typescript": "./src" 14 | } 15 | }, 16 | "keywords": [ 17 | "egg", 18 | "eggPlugin", 19 | "egg-plugin", 20 | "mysql", 21 | "database" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/eggjs/mysql.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/eggjs/mysql/issues" 29 | }, 30 | "homepage": "https://github.com/eggjs/mysql#readme", 31 | "author": "jtyjty99999", 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">= 18.19.0" 35 | }, 36 | "dependencies": { 37 | "@eggjs/core": "^6.4.0", 38 | "@eggjs/rds": "^1.2.1" 39 | }, 40 | "devDependencies": { 41 | "@arethetypeswrong/cli": "^0.17.4", 42 | "@eggjs/bin": "7", 43 | "@eggjs/mock": "^6.0.7", 44 | "@eggjs/tsconfig": "2", 45 | "@types/mocha": "10", 46 | "@types/node": "22", 47 | "egg": "^4.0.10", 48 | "husky": "^9.1.7", 49 | "lint-staged": "^15.5.0", 50 | "oxlint": "^0.16.2", 51 | "prettier": "^3.5.3", 52 | "rimraf": "6", 53 | "snap-shot-it": "^7.9.10", 54 | "tshy": "3", 55 | "tshy-after": "1", 56 | "typescript": "5" 57 | }, 58 | "scripts": { 59 | "lint": "oxlint", 60 | "pretest": "npm run clean && npm run lint -- --fix", 61 | "test": "egg-bin test", 62 | "preci": "npm run clean && npm run lint", 63 | "ci": "egg-bin cov", 64 | "postci": "npm run prepublishOnly && npm run clean", 65 | "clean": "rimraf dist", 66 | "prepublishOnly": "tshy && tshy-after && attw --pack", 67 | "prepare": "husky" 68 | }, 69 | "lint-staged": { 70 | "*": "prettier --write --ignore-unknown --cache", 71 | "*.{ts,js,json,md,yml}": [ 72 | "prettier --ignore-unknown --write", 73 | "oxlint --fix" 74 | ] 75 | }, 76 | "type": "module", 77 | "tshy": { 78 | "exports": { 79 | ".": "./src/index.ts", 80 | "./package.json": "./package.json" 81 | } 82 | }, 83 | "exports": { 84 | ".": { 85 | "import": { 86 | "types": "./dist/esm/index.d.ts", 87 | "default": "./dist/esm/index.js" 88 | }, 89 | "require": { 90 | "types": "./dist/commonjs/index.d.ts", 91 | "default": "./dist/commonjs/index.js" 92 | } 93 | }, 94 | "./package.json": "./package.json" 95 | }, 96 | "files": [ 97 | "dist", 98 | "src" 99 | ], 100 | "types": "./dist/commonjs/index.d.ts", 101 | "main": "./dist/commonjs/index.js" 102 | } 103 | -------------------------------------------------------------------------------- /.oxlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/oxlint/configuration_schema.json", 3 | "env": { 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "categories": { 8 | "correctness": "error", 9 | "perf": "error", 10 | "nursery": "error", 11 | "restriction": "error", 12 | "style": "error", 13 | "pedantic": "error", 14 | "suspicious": "error" 15 | }, 16 | "plugins": [ 17 | "import", 18 | "typescript", 19 | "unicorn", 20 | "jsdoc", 21 | "node", 22 | "promise", 23 | "oxc" 24 | ], 25 | "rules": { 26 | // eslint 27 | "constructor-super": "error", 28 | "getter-return": "error", 29 | "no-undef": "error", 30 | "no-unreachable": "error", 31 | "no-var": "error", 32 | "no-eq-null": "error", 33 | "no-await-in-loop": "allow", 34 | "eqeqeq": ["error", "smart"], 35 | "init-declarations": "allow", 36 | "curly": "allow", 37 | "no-ternary": "allow", 38 | "max-params": ["error", 5], 39 | "no-await-expression-member": "error", 40 | "no-continue": "allow", 41 | "guard-for-in": "allow", 42 | "func-style": "allow", 43 | "sort-imports": "allow", 44 | "yoda": "allow", 45 | "sort-keys": "allow", 46 | "no-magic-numbers": "allow", 47 | "no-duplicate-imports": "error", 48 | "no-multi-assign": "error", 49 | "func-names": "error", 50 | "default-param-last": "error", 51 | "prefer-object-spread": "error", 52 | "no-undefined": "allow", 53 | "no-plusplus": "allow", 54 | // maybe warn 55 | "no-console": "warn", 56 | "no-extraneous-class": "allow", 57 | "no-empty-function": "error", 58 | "max-depth": ["error", 6], 59 | "max-lines-per-function": "allow", 60 | "no-lonely-if": "error", 61 | "max-lines": "allow", 62 | "require-await": "allow", 63 | "max-nested-callbacks": ["error", 5], 64 | "max-classes-per-file": "allow", 65 | "radix": "allow", 66 | "no-negated-condition": "error", 67 | "no-else-return": "error", 68 | "no-throw-literal": "error", 69 | 70 | // import 71 | "import/exports-last": "allow", 72 | "import/max-dependencies": "allow", 73 | "import/no-cycle": "error", 74 | "import/no-anonymous-default-export": "allow", 75 | "import/no-namespace": "error", 76 | "import/named": "error", 77 | "import/export": "error", 78 | "import/no-default-export": "allow", 79 | "import/unambiguous": "error", 80 | 81 | // promise 82 | "promise/no-return-wrap": "error", 83 | "promise/param-names": "error", 84 | "promise/prefer-await-to-callbacks": "error", 85 | "promise/prefer-await-to-then": "error", 86 | "promise/prefer-catch": "error", 87 | "promise/no-return-in-finally": "error", 88 | "promise/avoid-new": "error", 89 | 90 | // unicorn 91 | "unicorn/error-message": "error", 92 | "unicorn/no-null": "allow", 93 | "unicorn/filename-case": "allow", 94 | "unicorn/prefer-structured-clone": "error", 95 | "unicorn/prefer-logical-operator-over-ternary": "error", 96 | "unicorn/prefer-number-properties": "error", 97 | "unicorn/prefer-array-some": "error", 98 | "unicorn/prefer-string-slice": "error", 99 | // "unicorn/no-null": "error", 100 | "unicorn/throw-new-error": "error", 101 | "unicorn/catch-error-name": "allow", 102 | "unicorn/prefer-spread": "allow", 103 | "unicorn/numeric-separators-style": "error", 104 | "unicorn/prefer-string-raw": "error", 105 | "unicorn/text-encoding-identifier-case": "error", 106 | "unicorn/no-array-for-each": "error", 107 | "unicorn/explicit-length-check": "error", 108 | "unicorn/no-lonely-if": "error", 109 | "unicorn/no-useless-undefined": "allow", 110 | "unicorn/prefer-date-now": "error", 111 | "unicorn/no-static-only-class": "allow", 112 | "unicorn/no-typeof-undefined": "error", 113 | "unicorn/prefer-negative-index": "error", 114 | "unicorn/no-anonymous-default-export": "allow", 115 | 116 | // oxc 117 | "oxc/no-map-spread": "error", 118 | "oxc/no-rest-spread-properties": "allow", 119 | "oxc/no-optional-chaining": "allow", 120 | "oxc/no-async-await": "allow", 121 | 122 | // typescript 123 | "typescript/explicit-function-return-type": "allow", 124 | "typescript/consistent-type-imports": "error", 125 | "typescript/consistent-type-definitions": "error", 126 | "typescript/consistent-indexed-object-style": "allow", 127 | "typescript/no-inferrable-types": "error", 128 | "typescript/array-type": "error", 129 | "typescript/no-non-null-assertion": "error", 130 | "typescript/no-explicit-any": "error", 131 | "typescript/no-import-type-side-effects": "error", 132 | "typescript/no-dynamic-delete": "error", 133 | "typescript/prefer-ts-expect-error": "error", 134 | "typescript/ban-ts-comment": "error", 135 | "typescript/prefer-enum-initializers": "error", 136 | 137 | // jsdoc 138 | "jsdoc/require-returns": "allow", 139 | "jsdoc/require-param": "allow" 140 | }, 141 | "ignorePatterns": ["index.d.ts", "test/fixtures/**", "__snapshots__"] 142 | } 143 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [6.0.0](https://github.com/eggjs/mysql/compare/v5.0.0...v6.0.0) (2025-03-23) 4 | 5 | ### ⚠ BREAKING CHANGES 6 | 7 | - drop Node.js < 18.19.0 support and only support egg>=4 8 | 9 | part of https://github.com/eggjs/egg/issues/3644 10 | 11 | https://github.com/eggjs/egg/issues/5257 12 | 13 | 15 | 16 | ## Summary by CodeRabbit 17 | 18 | - **New Features** 19 | - Refined MySQL plugin integration with improved lifecycle management 20 | and simplified singleton access. 21 | - Added a ready-to-use Docker Compose setup for local MySQL development. 22 | 23 | - **Documentation** 24 | - Updated all documentation with the new package name (@eggjs/mysql) and 25 | revised usage examples for clarity. 26 | 27 | - **Chores** 28 | - Enhanced package metadata and dependency management. 29 | - Updated CI configuration to support Node.js versions 18, 20, and 22. 30 | 31 | 32 | 33 | ### Features 34 | 35 | - support cjs and esm both by tshy ([#33](https://github.com/eggjs/mysql/issues/33)) ([dfdaa74](https://github.com/eggjs/mysql/commit/dfdaa746641ce31a0fe1e4add167c7483e742bef)) 36 | 37 | ## [5.0.0](https://github.com/eggjs/egg-mysql/compare/v4.1.0...v5.0.0) (2025-03-08) 38 | 39 | ### ⚠ BREAKING CHANGES 40 | 41 | - drop Node.js < 18 support 42 | - use mysql2 instead of mysql 43 | 44 | closes https://github.com/eggjs/egg-mysql/issues/31 45 | 46 | 48 | 49 | ## Summary by CodeRabbit 50 | 51 | - **Documentation** 52 | - Updated usage instructions and contributor displays with corrected 53 | typographical errors. 54 | - **Chores** 55 | - Removed outdated contributor templates and automation workflows. 56 | - **Dependencies** 57 | - Migrated to a new database client package and raised the minimum 58 | Node.js version requirement. 59 | - **Tests** 60 | - Refined error validations in database operations for improved error 61 | reporting. 62 | 63 | 64 | ### Features 65 | 66 | - use @eggjs/rds instead of ali-rds ([#32](https://github.com/eggjs/egg-mysql/issues/32)) ([3093528](https://github.com/eggjs/egg-mysql/commit/30935285268b1c674f9a70b4cb77501f89276467)) 67 | 68 | ## [4.1.0](https://github.com/eggjs/egg-mysql/compare/v4.0.0...v4.1.0) (2025-03-08) 69 | 70 | ### Features 71 | 72 | - update ali-rds@6.4.0 ([#30](https://github.com/eggjs/egg-mysql/issues/30)) ([31ed369](https://github.com/eggjs/egg-mysql/commit/31ed369d954ce4bded89469907f81dcbde3060df)) 73 | 74 | ## [4.0.0](https://github.com/eggjs/egg-mysql/compare/v3.4.0...v4.0.0) (2023-03-06) 75 | 76 | ### ⚠ BREAKING CHANGES 77 | 78 | - drop Node.js < 16 support 79 | 80 | ### Features 81 | 82 | - refactor with TypeScript ([#27](https://github.com/eggjs/egg-mysql/issues/27)) ([4b7311d](https://github.com/eggjs/egg-mysql/commit/4b7311d2beb43a0338337c7128e016690ea04c9e)) 83 | 84 | ## [3.4.0](https://github.com/eggjs/egg-mysql/compare/v3.3.0...v3.4.0) (2023-02-15) 85 | 86 | ### Features 87 | 88 | - **type:** add type definition for mysql.count method ([#26](https://github.com/eggjs/egg-mysql/issues/26)) ([7aef13e](https://github.com/eggjs/egg-mysql/commit/7aef13eb861b41c538d3b3d561c92a666a61110b)) 89 | 90 | ## [3.3.0](https://github.com/eggjs/egg-mysql/compare/v3.2.0...v3.3.0) (2022-12-18) 91 | 92 | ### Features 93 | 94 | - upgrade ali-rds v4 ([#24](https://github.com/eggjs/egg-mysql/issues/24)) ([1b129e8](https://github.com/eggjs/egg-mysql/commit/1b129e8f94b0739a5515d5704be301df85f97b30)) 95 | 96 | --- 97 | 98 | # 3.2.0 / 2022-12-03 99 | 100 | **features** 101 | 102 | - [[`4cf93ce`](http://github.com/eggjs/egg-mysql/commit/4cf93ce5fbeeb3fc734a8e7ba708b27994adad88)] - feat: add more type definition for mysql.get method (#20) (Xin(Khalil) Zhang <>) 103 | 104 | **fixes** 105 | 106 | - [[`d3c8a31`](http://github.com/eggjs/egg-mysql/commit/d3c8a31e02beccc8823820340bda89fe307a34ea)] - fix: remove reckless assertion (#15) (WangJie <>) 107 | 108 | **others** 109 | 110 | - [[`ed419d6`](http://github.com/eggjs/egg-mysql/commit/ed419d6c51e25fa3ea2a4b91628375d4d4dcb77d)] - 🤖 TEST: Add tsd test (#22) (fengmk2 <>) 111 | - [[`4137cfc`](http://github.com/eggjs/egg-mysql/commit/4137cfc46e0db04f6122b065516055a99765eb19)] - 📖 DOC: Use async/await isntead of yield (fengmk2 <>) 112 | - [[`b103400`](http://github.com/eggjs/egg-mysql/commit/b103400c153176bd9c38e35d72aa3a791999ec27)] - 📖 DOC: Update contributors (fengmk2 <>) 113 | - [[`a67989c`](http://github.com/eggjs/egg-mysql/commit/a67989c4e6c55604d8d61d1af7af9bc5df35df2e)] - 🤖 TEST: Run test on GitHub Action (#19) (fengmk2 <>) 114 | - [[`3d04360`](http://github.com/eggjs/egg-mysql/commit/3d04360fd7745ef45d32e8e27c5691878d0cd3bf)] - Create codeql-analysis.yml (fengmk2 <>) 115 | 116 | # 3.1.1 / 2022-06-03 117 | 118 | **fixes** 119 | 120 | - [[`bb7856b`](http://github.com/eggjs/egg-mysql/commit/bb7856bbf8e363f2ee0ce9410204fd227c2ccd08)] - fix: mysql.update missing condition define (#18) (shangwenhe <>) 121 | 122 | # 3.1.0 / 2022-02-11 123 | 124 | **features** 125 | 126 | - [[`aade70b`](http://github.com/eggjs/egg-mysql/commit/aade70bce78afb39e8e9b3201261bbb8bcf26847)] - feat: add complete typescript typings (#17) (AntiMoron <>) 127 | 128 | **others** 129 | 130 | - [[`2e02e40`](http://github.com/eggjs/egg-mysql/commit/2e02e402d6d23740f68ae26c28633303d4d9e206)] - chore: update travis (#16) (TZ | 天猪 <>) 131 | - [[`89910f6`](http://github.com/eggjs/egg-mysql/commit/89910f6ef17be38b59bc066d754793cc65a84624)] - test: add null query test case (#13) (Century Guo <<648772021@qq.com>>) 132 | - [[`b0dd988`](http://github.com/eggjs/egg-mysql/commit/b0dd988d51b95d576c852d54d26014a845ac2f3d)] - deps: autod (#12) (TZ | 天猪 <>) 133 | - [[`18b67fd`](http://github.com/eggjs/egg-mysql/commit/18b67fd3e43627ad420ed3df8e8a6e305f5202f6)] - deps: upgrade dependencies (#10) (Haoliang Gao <>) 134 | 135 | # 3.0.0 / 2017-04-03 136 | 137 | - deps: ali-rds@3 (#9) 138 | 139 | # 2.0.0 / 2017-02-09 140 | 141 | - feat: remove app.instrument and fix test (#5) 142 | 143 | # 1.0.1 / 2016-12-30 144 | 145 | - docs: add app.js and agent.js extend intro (#3) 146 | - docs: add English translation doc (#2) 147 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # @eggjs/mysql 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![CI](https://github.com/eggjs/mysql/actions/workflows/nodejs.yml/badge.svg?branch=master)](https://github.com/eggjs/mysql/actions/workflows/nodejs.yml) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![npm download][download-image]][download-url] 7 | [![Node.js Version](https://img.shields.io/node/v/@eggjs/mysql.svg?style=flat)](https://nodejs.org/en/download/) 8 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) 9 | ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/eggjs/mysql) 10 | 11 | [npm-image]: https://img.shields.io/npm/v/@eggjs/mysql.svg?style=flat-square 12 | [npm-url]: https://npmjs.org/package/@eggjs/mysql 13 | [codecov-image]: https://img.shields.io/codecov/c/github/eggjs/mysql.svg?style=flat-square 14 | [codecov-url]: https://codecov.io/github/eggjs/mysql?branch=master 15 | [download-image]: https://img.shields.io/npm/dm/@eggjs/mysql.svg?style=flat-square 16 | [download-url]: https://npmjs.org/package/@eggjs/mysql 17 | 18 | MySQL 插件是为 egg 提供 MySQL 数据库访问的功能 19 | 20 | 此插件基于 [@eggjs/rds] 实现一个简单的配置封装,具体使用方法你还需要阅读 [@eggjs/rds] 的文档。 21 | 22 | ## 安装 23 | 24 | ```bash 25 | npm i @eggjs/mysql 26 | ``` 27 | 28 | ## 配置 29 | 30 | 通过 `config/plugin.ts` 配置启动 MySQL 插件: 31 | 32 | ```ts 33 | export default { 34 | mysql: { 35 | enable: true, 36 | package: '@eggjs/mysql', 37 | }, 38 | }; 39 | ``` 40 | 41 | 在 `config/config.${env}.ts` 配置各个环境的数据库连接信息: 42 | 43 | ### 单数据源 44 | 45 | ```ts 46 | export default { 47 | mysql: { 48 | // 单数据库信息配置 49 | client: { 50 | // host 51 | host: 'mysql.com', 52 | // 端口号 53 | port: '3306', 54 | // 用户名 55 | user: 'test_user', 56 | // 密码 57 | password: 'test_password', 58 | // 数据库名 59 | database: 'test', 60 | }, 61 | // 是否加载到 app 上,默认开启 62 | app: true, 63 | // 是否加载到 agent 上,默认关闭 64 | agent: false, 65 | }, 66 | }; 67 | ``` 68 | 69 | 使用方式: 70 | 71 | ```ts 72 | await app.mysql.query(sql, values); // 单实例可以直接通过 app.mysql 访问 73 | ``` 74 | 75 | ### 多数据源 76 | 77 | ```ts 78 | export default { 79 | mysql: { 80 | clients: { 81 | // clientId, 获取client实例,需要通过 app.mysql.get('clientId') 获取 82 | db1: { 83 | // host 84 | host: 'mysql.com', 85 | // 端口号 86 | port: '3306', 87 | // 用户名 88 | user: 'test_user', 89 | // 密码 90 | password: 'test_password', 91 | // 数据库名 92 | database: 'test', 93 | }, 94 | // ... 95 | }, 96 | // 所有数据库配置的默认值 97 | default: {}, 98 | 99 | // 是否加载到 app 上,默认开启 100 | app: true, 101 | // 是否加载到 agent 上,默认关闭 102 | agent: false, 103 | }, 104 | }; 105 | ``` 106 | 107 | 使用方式: 108 | 109 | ```ts 110 | const client1 = app.mysqls.get('db1'); 111 | await client1.query(sql, values); 112 | 113 | const client2 = app.mysqls.get('db2'); 114 | await client2.query(sql, values); 115 | ``` 116 | 117 | ## 扩展 118 | 119 | ### Application 120 | 121 | #### app.mysql 122 | 123 | 如果开启了 `config.mysql.app = true`,则会在 app 上注入 [@eggjs/rds] 客户端 的 [Singleton 单例](https://github.com/eggjs/core/blob/master/src/singleton.ts)。 124 | 125 | ```ts 126 | await app.mysql.query(sql); 127 | await app.mysqls.getSingletonInstance('db1').query(sql); 128 | ``` 129 | 130 | ### Agent 131 | 132 | #### agent.mysql 133 | 134 | 如果开启了 `config.mysql.agent = true`,则会在 agent 上注入 [@eggjs/rds] 客户端 的 [Singleton 单例](https://github.com/eggjs/core/blob/master/src/singleton.ts)。 135 | 136 | ```ts 137 | await agent.mysql.query(sql); 138 | await agent.mysqls.getSingletonInstance('db1').query(sql); 139 | ``` 140 | 141 | ## CRUD 使用指南 142 | 143 | ### Create 144 | 145 | ```ts 146 | // 插入 147 | const result = await app.mysql.insert('posts', { title: 'Hello World' }); 148 | const insertSuccess = result.affectedRows === 1; 149 | ``` 150 | 151 | ### Read 152 | 153 | ```ts 154 | // 获得一个 155 | const post = await app.mysql.get('posts', { id: 12 }); 156 | // 查询 157 | const results = await app.mysql.select('posts', { 158 | where: { status: 'draft' }, 159 | orders: [ 160 | ['created_at', 'desc'], 161 | ['id', 'desc'], 162 | ], 163 | limit: 10, 164 | offset: 0, 165 | }); 166 | ``` 167 | 168 | ### Update 169 | 170 | ```ts 171 | // 修改数据,将会根据主键 ID 查找,并更新 172 | const row = { 173 | id: 123, 174 | name: 'fengmk2', 175 | otherField: 'other field value', 176 | modifiedAt: app.mysql.literals.now, // `now()` on db server 177 | }; 178 | const result = await app.mysql.update('posts', row); 179 | const updateSuccess = result.affectedRows === 1; 180 | ``` 181 | 182 | ### Delete 183 | 184 | ```ts 185 | const result = await app.mysql.delete('table-name', { 186 | name: 'fengmk2', 187 | }); 188 | ``` 189 | 190 | ## 事务 191 | 192 | ### 手动控制 193 | 194 | - 优点:beginTransaction, commit 或 rollback 都由开发者来完全控制,可以做到非常细粒度的控制。 195 | - 缺点:手写代码比较多,不是每个人都能写好。忘记了捕获异常和 cleanup 都会导致严重 bug。 196 | 197 | ```ts 198 | const conn = await app.mysql.beginTransaction(); 199 | 200 | try { 201 | await conn.insert(table, row1); 202 | await conn.update(table, row2); 203 | await conn.commit(); 204 | } catch (err) { 205 | // error, rollback 206 | await conn.rollback(); // rollback call won't throw err 207 | throw err; 208 | } 209 | ``` 210 | 211 | ### 自动控制:Transaction with scope 212 | 213 | - API:`async beginTransactionScope(scope, ctx)` 214 | - `scope`: 一个 generatorFunction,在这个函数里面执行这次事务的所有 sql 语句。 215 | - `ctx`: 当前请求的上下文对象,传入 ctx 可以保证即便在出现事务嵌套的情况下,一次请求中同时只有一个激活状态的事务。 216 | - 优点:使用简单,不容易犯错,就感觉事务不存在的样子。 217 | - 缺点:整个事务要么成功,要么失败,无法做细粒度控制。 218 | 219 | ```ts 220 | const result = await app.mysql.beginTransactionScope(async conn => { 221 | // don't commit or rollback by yourself 222 | await conn.insert(table, row1); 223 | await conn.update(table, row2); 224 | return { success: true }; 225 | }, ctx); // ctx 是当前请求的上下文,如果是在 service 文件中,可以从 `this.ctx` 获取到 226 | // if error throw on scope, will auto rollback 227 | ``` 228 | 229 | ## 进阶 230 | 231 | ### 自定义SQL拼接 232 | 233 | ```ts 234 | const results = await app.mysql.query( 235 | 'update posts set hits = (hits + ?) where id = ?', 236 | [1, postId] 237 | ); 238 | ``` 239 | 240 | ### 表达式(Literal) 241 | 242 | 如果需要调用mysql内置的函数(或表达式),可以使用`Literal`。 243 | 244 | #### 内置表达式 245 | 246 | - `NOW()`: 数据库当前系统时间,通过 `app.mysql.literals.now` 获取。 247 | 248 | ```ts 249 | await app.mysql.insert(table, { 250 | create_time: app.mysql.literals.now, 251 | }); 252 | 253 | // INSERT INTO `$table`(`create_time`) VALUES(NOW()) 254 | ``` 255 | 256 | #### 自定义表达式 257 | 258 | 下例展示了如何调用mysql内置的 `CONCAT(s1, ...sn)` 函数,做字符串拼接。 259 | 260 | ```ts 261 | const Literal = app.mysql.literals.Literal; 262 | const first = 'James'; 263 | const last = 'Bond'; 264 | await app.mysql.insert(table, { 265 | id: 123, 266 | fullname: new Literal(`CONCAT("${first}", "${last}"`), 267 | }); 268 | 269 | // INSERT INTO `$table`(`id`, `fullname`) VALUES(123, CONCAT("James", "Bond")) 270 | ``` 271 | 272 | ## Questions & Suggestions 273 | 274 | Please open an issue [here](https://github.com/eggjs/mysql/issues). 275 | 276 | ## License 277 | 278 | [MIT](LICENSE) 279 | 280 | ## Contributors 281 | 282 | [![Contributors](https://contrib.rocks/image?repo=eggjs/mysql)](https://github.com/eggjs/mysql/graphs/contributors) 283 | 284 | Made with [contributors-img](https://contrib.rocks). 285 | 286 | [@eggjs/rds]: https://github.com/node-modules/rds 287 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @eggjs/mysql 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![CI](https://github.com/eggjs/mysql/actions/workflows/nodejs.yml/badge.svg?branch=master)](https://github.com/eggjs/mysql/actions/workflows/nodejs.yml) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![npm download][download-image]][download-url] 7 | [![Node.js Version](https://img.shields.io/node/v/@eggjs/mysql.svg?style=flat)](https://nodejs.org/en/download/) 8 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) 9 | ![CodeRabbit Pull Request Reviews](https://img.shields.io/coderabbit/prs/github/eggjs/mysql) 10 | 11 | [npm-image]: https://img.shields.io/npm/v/@eggjs/mysql.svg?style=flat-square 12 | [npm-url]: https://npmjs.org/package/@eggjs/mysql 13 | [codecov-image]: https://img.shields.io/codecov/c/github/eggjs/mysql.svg?style=flat-square 14 | [codecov-url]: https://codecov.io/github/eggjs/mysql?branch=master 15 | [download-image]: https://img.shields.io/npm/dm/@eggjs/mysql.svg?style=flat-square 16 | [download-url]: https://npmjs.org/package/@eggjs/mysql 17 | 18 | MySQL plugin for Egg.js 19 | 20 | ## Install 21 | 22 | ```bash 23 | npm i @eggjs/mysql 24 | ``` 25 | 26 | MySQL Plugin for `egg@4`, support egg application access to MySQL database. 27 | 28 | > If you're using `egg@3`, please use `egg-mysql@5` instead. 29 | 30 | This plugin based on [@eggjs/rds], if you want to know specific usage, you should refer to the document of [@eggjs/rds]. 31 | 32 | ## Configuration 33 | 34 | Change `${app_root}/config/plugin.ts` to enable MySQL plugin: 35 | 36 | ```ts 37 | export default { 38 | mysql: { 39 | enable: true, 40 | package: '@eggjs/mysql', 41 | }, 42 | }; 43 | ``` 44 | 45 | Configure database information in `${app_root}/config/config.default.ts`: 46 | 47 | ### Simple database instance 48 | 49 | ```ts 50 | export default { 51 | mysql: { 52 | // database configuration 53 | client: { 54 | // host 55 | host: 'mysql.com', 56 | // port 57 | port: '3306', 58 | // username 59 | user: 'test_user', 60 | // password 61 | password: 'test_password', 62 | // database 63 | database: 'test', 64 | }, 65 | // load into app, default is `true` 66 | app: true, 67 | // load into agent, default is `false` 68 | agent: false, 69 | }, 70 | }; 71 | ``` 72 | 73 | Usage: 74 | 75 | ```ts 76 | await app.mysql.query(sql, values); // you can access to simple database instance by using app.mysql. 77 | ``` 78 | 79 | ### Multiple database instance 80 | 81 | ```ts 82 | export default { 83 | mysql: { 84 | clients: { 85 | // clientId, access the client instance by app.mysql.get('clientId') 86 | db1: { 87 | // host 88 | host: 'mysql.com', 89 | // port 90 | port: '3306', 91 | // username 92 | user: 'test_user', 93 | // password 94 | password: 'test_password', 95 | // database 96 | database: 'test', 97 | }, 98 | // ... 99 | }, 100 | // default configuration for all databases 101 | default: {}, 102 | // load into app, default is open 103 | app: true, 104 | // load into agent, default is close 105 | agent: false, 106 | }, 107 | }; 108 | ``` 109 | 110 | Usage: 111 | 112 | ```ts 113 | const client1 = app.mysqls.getSingletonInstance('db1'); 114 | await client1.query(sql, values); 115 | 116 | const client2 = app.mysqls.getSingletonInstance('db2'); 117 | await client2.query(sql, values); 118 | ``` 119 | 120 | ## CRUD user guide 121 | 122 | ### Create 123 | 124 | ```ts 125 | // insert 126 | const result = await app.mysql.insert('posts', { title: 'Hello World' }); 127 | const insertSuccess = result.affectedRows === 1; 128 | ``` 129 | 130 | ### Read 131 | 132 | ```ts 133 | // get 134 | const post = await app.mysql.get('posts', { id: 12 }); 135 | // query 136 | const results = await app.mysql.select('posts', { 137 | where: { status: 'draft' }, 138 | orders: [ 139 | ['created_at', 'desc'], 140 | ['id', 'desc'], 141 | ], 142 | limit: 10, 143 | offset: 0, 144 | }); 145 | ``` 146 | 147 | ### Update 148 | 149 | ```ts 150 | // update by primary key ID, and refresh 151 | const row = { 152 | id: 123, 153 | name: 'fengmk2', 154 | otherField: 'other field value', 155 | modifiedAt: app.mysql.literals.now, // `now()` on db server 156 | }; 157 | const result = await app.mysql.update('posts', row); 158 | const updateSuccess = result.affectedRows === 1; 159 | ``` 160 | 161 | ### Delete 162 | 163 | ```ts 164 | const result = await app.mysql.delete('table-name', { 165 | name: 'fengmk2', 166 | }); 167 | ``` 168 | 169 | ## Transaction 170 | 171 | ### Manual control 172 | 173 | - adventage: `beginTransaction`, `commit` or `rollback` can be completely under control by developer 174 | - disadventage: more handwritten code, Forgot catching error or cleanup will lead to serious bug. 175 | 176 | ```ts 177 | const conn = await app.mysql.beginTransaction(); 178 | 179 | try { 180 | await conn.insert(table, row1); 181 | await conn.update(table, row2); 182 | await conn.commit(); 183 | } catch (err) { 184 | // error, rollback 185 | await conn.rollback(); // rollback call won't throw err 186 | throw err; 187 | } 188 | ``` 189 | 190 | ### Automatic control: Transaction with scope 191 | 192 | - API:`async beginTransactionScope(scope, ctx)` 193 | - `scope`: A generatorFunction which will execute all sqls of this transaction. 194 | - `ctx`: The context object of current request, it will ensures that even in the case of a nested transaction, there is only one active transaction in a request at the same time. 195 | - adventage: easy to use, as if there is no transaction in your code. 196 | - disadvantage: all transation will be successful or failed, cannot control precisely 197 | 198 | ```ts 199 | const result = await app.mysql.beginTransactionScope(async conn => { 200 | // don't commit or rollback by yourself 201 | await conn.insert(table, row1); 202 | await conn.update(table, row2); 203 | return { success: true }; 204 | }, ctx); // ctx is the context of current request, access by `this.ctx`. 205 | // if error throw on scope, will auto rollback 206 | ``` 207 | 208 | ## Advance 209 | 210 | ### Custom SQL splicing 211 | 212 | ```ts 213 | const results = await app.mysql.query( 214 | 'update posts set hits = (hits + ?) where id = ?', 215 | [1, postId] 216 | ); 217 | ``` 218 | 219 | ### Literal 220 | 221 | If you want to call literals or functions in mysql , you can use `Literal`. 222 | 223 | #### Inner Literal 224 | 225 | - NOW(): The database system time, you can obtain by `app.mysql.literals.now`. 226 | 227 | ```ts 228 | await app.mysql.insert(table, { 229 | create_time: app.mysql.literals.now, 230 | }); 231 | 232 | // INSERT INTO `$table`(`create_time`) VALUES(NOW()) 233 | ``` 234 | 235 | #### Custom literal 236 | 237 | The following demo showed how to call `CONCAT(s1, ...sn)` function in mysql to do string splicing. 238 | 239 | ```ts 240 | const Literal = app.mysql.literals.Literal; 241 | const first = 'James'; 242 | const last = 'Bond'; 243 | await app.mysql.insert(table, { 244 | id: 123, 245 | fullname: new Literal(`CONCAT("${first}", "${last}"`), 246 | }); 247 | 248 | // INSERT INTO `$table`(`id`, `fullname`) VALUES(123, CONCAT("James", "Bond")) 249 | ``` 250 | 251 | ## For the local dev 252 | 253 | Run docker compose to start test mysql service 254 | 255 | ```bash 256 | docker compose -f docker-compose.yml up -d 257 | # if you run the first time, should wait for ~20s to let mysql service init started 258 | ``` 259 | 260 | Run the unit tests 261 | 262 | ```bash 263 | npm test 264 | ``` 265 | 266 | Stop test mysql service 267 | 268 | ```bash 269 | docker compose -f docker-compose.yml down 270 | ``` 271 | 272 | ## Questions & Suggestions 273 | 274 | Please open an issue [here](https://github.com/eggjs/mysql/issues). 275 | 276 | ## License 277 | 278 | [MIT](LICENSE) 279 | 280 | ## Contributors 281 | 282 | [![Contributors](https://contrib.rocks/image?repo=eggjs/mysql)](https://github.com/eggjs/mysql/graphs/contributors) 283 | 284 | Made with [contributors-img](https://contrib.rocks). 285 | 286 | [@eggjs/rds]: https://github.com/node-modules/rds 287 | -------------------------------------------------------------------------------- /test/mysql.test.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict'; 2 | import { randomUUID } from 'node:crypto'; 3 | import path from 'node:path'; 4 | import fs from 'node:fs'; 5 | import { fileURLToPath } from 'node:url'; 6 | 7 | import snapshot from 'snap-shot-it'; 8 | import { mm, type MockApplication } from '@eggjs/mock'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | describe('test/mysql.test.ts', () => { 14 | let app: MockApplication; 15 | const uid = randomUUID(); 16 | 17 | before(() => { 18 | app = mm.app({ 19 | baseDir: 'apps/mysqlapp', 20 | }); 21 | return app.ready(); 22 | }); 23 | 24 | beforeEach(async () => { 25 | // init test datas 26 | try { 27 | await app.mysql.query( 28 | `insert into npm_auth set user_id = 'egg-${uid}-1', password = '1'` 29 | ); 30 | await app.mysql.query( 31 | `insert into npm_auth set user_id = 'egg-${uid}-2', password = '2'` 32 | ); 33 | await app.mysql.query( 34 | `insert into npm_auth set user_id = 'egg-${uid}-3', password = '3'` 35 | ); 36 | await app.mysql.queryOne( 37 | `select * from npm_auth where user_id = 'egg-${uid}-3'` 38 | ); 39 | } catch (err) { 40 | console.log('init test datas error: %s', err); 41 | } 42 | }); 43 | 44 | afterEach(async () => { 45 | // 清空测试数据 46 | await app.mysql.query( 47 | `delete from npm_auth where user_id like 'egg-${uid}%'` 48 | ); 49 | }); 50 | 51 | after(async () => { 52 | await app.mysql.end(); 53 | await app.close(); 54 | }); 55 | 56 | afterEach(mm.restore); 57 | 58 | it('should make default config stable', () => { 59 | snapshot(app.config.mysql); 60 | }); 61 | 62 | it('should query mysql user table success', () => { 63 | return app.httpRequest().get('/').expect(200); 64 | }); 65 | 66 | it('should query limit 2', async () => { 67 | const users = await app.mysql.query( 68 | 'select * from npm_auth order by id desc limit 2' 69 | ); 70 | assert(users.length === 2); 71 | 72 | const rows = await app.mysql.select('npm_auth', { 73 | orders: [['id', 'desc']], 74 | limit: 2, 75 | }); 76 | assert(rows.length === 2); 77 | assert.deepEqual(rows[0], users[0]); 78 | assert.deepEqual(rows[1], users[1]); 79 | }); 80 | 81 | it('should update successfully', async () => { 82 | const user = await app.mysql.queryOne( 83 | 'select * from npm_auth order by id desc limit 10' 84 | ); 85 | const result = await app.mysql.update('npm_auth', { 86 | id: user.id, 87 | user_id: `79744-${uid}-update`, 88 | }); 89 | assert(result.affectedRows === 1); 90 | }); 91 | 92 | it('should delete successfully', async () => { 93 | const user = await app.mysql.queryOne( 94 | 'select * from npm_auth order by id desc limit 10' 95 | ); 96 | const result = await app.mysql.delete('npm_auth', { id: user.id }); 97 | assert(result.affectedRows === 1); 98 | }); 99 | 100 | it('should query one success', async () => { 101 | const user = await app.mysql.queryOne( 102 | 'select * from npm_auth order by id desc limit 10' 103 | ); 104 | assert(user); 105 | assert(typeof user.user_id === 'string' && user.user_id); 106 | 107 | const row = await app.mysql.get('npm_auth', { user_id: user.user_id }); 108 | assert(row.id === user.id); 109 | }); 110 | 111 | it('should query one desc is NULL success', async () => { 112 | const user = await app.mysql.queryOne( 113 | 'select * from npm_auth where `desc` is NULL' 114 | ); 115 | assert(user); 116 | assert(typeof user.user_id === 'string' && user.user_id); 117 | 118 | const row = await app.mysql.get('npm_auth', { desc: null }); 119 | assert(row.id === user.id); 120 | }); 121 | 122 | it('should query with literal in where conditions', async () => { 123 | const user = await app.mysql.queryOne( 124 | 'select * from npm_auth where `password` is not NULL' 125 | ); 126 | assert(user); 127 | assert(typeof user.user_id === 'string' && user.user_id); 128 | 129 | // breaking change on mysql2 130 | // SELECT * FROM `npm_auth` WHERE `password` = is not NULL LIMIT 0, 1 131 | // const row = await app.mysql.get('npm_auth', { password: new app.mysql.literals.Literal('is not NULL') }); 132 | // assert(row.id === user.id); 133 | }); 134 | 135 | it('should query one not exists return null', async () => { 136 | let user = await app.mysql.queryOne('select * from npm_auth where id = -1'); 137 | assert(!user); 138 | 139 | user = await app.mysql.get('npm_auth', { id: -1 }); 140 | assert(!user); 141 | }); 142 | 143 | it('should escape value', () => { 144 | const val = app.mysql.escape('\'"?><=!@#'); 145 | assert(val === String.raw`'\'\"?><=!@#'`); 146 | }); 147 | 148 | it('should agent error when password wrong on multi clients', async () => { 149 | const app = mm.app({ 150 | baseDir: 'apps/mysqlapp-multi-client-wrong', 151 | }); 152 | await assert.rejects( 153 | async () => { 154 | await app.ready(); 155 | }, 156 | err => { 157 | assert(err instanceof Error); 158 | assert.match(err.message, /Access denied for user/); 159 | assert.equal(Reflect.get(err, 'code'), 'ER_ACCESS_DENIED_ERROR'); 160 | assert.equal(Reflect.get(err, 'errno'), 1045); 161 | assert.equal(Reflect.get(err, 'sqlState'), '28000'); 162 | assert.match( 163 | Reflect.get(err, 'sqlMessage'), 164 | /^Access denied for user 'root'@'[^']+' \(using password: YES\)$/ 165 | ); 166 | assert.equal(err.name, 'RDSClientGetConnectionError'); 167 | return true; 168 | } 169 | ); 170 | }); 171 | 172 | it('should queryOne work on transaction', async () => { 173 | const result = await app.mysql.beginTransactionScope(async conn => { 174 | const row = await conn.queryOne( 175 | 'select * from npm_auth order by id desc limit 10' 176 | ); 177 | return { row }; 178 | }); 179 | assert(result.row); 180 | assert(result.row.user_id && typeof result.row.user_id === 'string'); 181 | assert(result.row.password === '3'); 182 | }); 183 | 184 | describe('config.mysql.agent = true', () => { 185 | let app: MockApplication; 186 | before(() => { 187 | app = mm.cluster({ 188 | baseDir: 'apps/mysqlapp', 189 | }); 190 | return app.ready(); 191 | }); 192 | after(() => app.close()); 193 | 194 | it('should agent.mysql work', () => { 195 | const result = fs.readFileSync( 196 | path.join(__dirname, './fixtures/apps/mysqlapp/run/agent_result.json'), 197 | 'utf8' 198 | ); 199 | assert( 200 | /\[\{"currentTime":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"\}\]/.test( 201 | result 202 | ) 203 | ); 204 | }); 205 | }); 206 | 207 | describe('config.mysql.app = false', () => { 208 | let app: MockApplication; 209 | before(() => { 210 | app = mm.app({ 211 | baseDir: 'apps/mysqlapp-disable', 212 | }); 213 | return app.ready(); 214 | }); 215 | after(() => app.close()); 216 | 217 | it('should disable app work', () => { 218 | assert(!app.mysql); 219 | }); 220 | }); 221 | 222 | describe('newConfig', () => { 223 | let app: MockApplication; 224 | before(() => { 225 | app = mm.cluster({ 226 | baseDir: 'apps/mysqlapp-new', 227 | }); 228 | return app.ready(); 229 | }); 230 | 231 | after(() => app.close()); 232 | 233 | it('should new config agent.mysql work', () => { 234 | const result = fs.readFileSync( 235 | path.join( 236 | __dirname, 237 | './fixtures/apps/mysqlapp-new/run/agent_result.json' 238 | ), 239 | 'utf8' 240 | ); 241 | assert( 242 | /\[\{"currentTime":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"\}\]/.test( 243 | result 244 | ) 245 | ); 246 | }); 247 | 248 | it('should query mysql user table success', () => { 249 | return app.httpRequest().get('/').expect(200); 250 | }); 251 | }); 252 | 253 | describe('createInstanceAsync', () => { 254 | let app: MockApplication; 255 | before(() => { 256 | app = mm.cluster({ 257 | baseDir: 'apps/mysqlapp-dynamic', 258 | }); 259 | return app.ready(); 260 | }); 261 | 262 | after(() => app.close()); 263 | 264 | it('should new config agent.mysql work', () => { 265 | const result = fs.readFileSync( 266 | path.join( 267 | __dirname, 268 | './fixtures/apps/mysqlapp-dynamic/run/agent_result.json' 269 | ), 270 | 'utf8' 271 | ); 272 | assert( 273 | /\[\{"currentTime":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z"\}\]/.test( 274 | result 275 | ) 276 | ); 277 | }); 278 | 279 | it('should query mysql user table success', () => { 280 | return app.httpRequest().get('/').expect(200); 281 | }); 282 | }); 283 | }); 284 | --------------------------------------------------------------------------------