@dlovely/mysql
Version:
对mysql的简易连接,能创建数据库、数据表管理,并使用sql编辑器
783 lines (772 loc) • 25 kB
JavaScript
/*!
* @dlovely/mysql v1.1.0
* (c) 2023 MZ-Dlovely
* @license MIT
*/
import { join, dirname } from 'node:path';
import { existsSync, rmSync, mkdirSync } from 'node:fs';
import { createRequire } from 'node:module';
import { createPool, createConnection } from 'mysql2/promise';
import { formatSql, formatCreateDatabase, formatJoinSelect, formatInsert, formatDelete, formatUpdate, formatSelect, formatCreateTable } from '@dlovely/sql-editor';
import { fill, isArray } from '@dlovely/utils';
import { mkdir, writeFile } from 'node:fs/promises';
/* istanbul ignore file -- @preserve */
const root = process.cwd();
const require = createRequire(root);
const runtime_dir_path = join(root, '.mysql');
if (existsSync(runtime_dir_path))
rmSync(runtime_dir_path, { recursive: true, force: true });
const types_dir_path = join(runtime_dir_path, 'types');
mkdirSync(types_dir_path, { recursive: true });
const global_type_path = join(runtime_dir_path, 'global.d.ts');
const defineMysqlConfig = (config) => config;
const ext_map = [
['.json', readJsonConfig],
['.js', readJsConfig],
['.ts', readJsConfig],
['.cjs', readJsConfig],
['.mjs', readJsConfig],
['.cts', readJsConfig],
['.mts', readJsConfig],
];
const default_config = {
host: process.env.MYSQL_HOST || 'localhost',
port: Number(process.env.MYSQL_PORT || '3306'),
user: process.env.MYSQL_USER || 'localhost',
password: process.env.MYSQL_AUTH,
};
const genConfig = () => {
let options = null;
for (const [ext, fn] of ext_map) {
const config_path = join(root, `mysql.config${ext}`);
if (existsSync(config_path)) {
options = fn(config_path);
break;
}
}
if (!options)
return { type: 'pool', config: default_config };
return {
...options,
type: options.type || 'pool',
config: { ...default_config, ...options.config },
};
};
function readJsonConfig(path) {
return require(path);
}
function readJsConfig(path) {
const module = require(path);
if (module.default) {
if ('config' in module.default)
return module.default;
module.config = module.default;
delete module.default;
}
return module;
}
/* istanbul ignore file -- @preserve */
/*#__PURE*/ class Saver {
changed = new Set();
change(control) {
if (this.changed.has(control))
this.changed.delete(control);
this.changed.add(control);
this._start();
}
_timer = null;
_start() {
if (this._timer)
return;
this._timer = setTimeout(() => {
this._timer = null;
this.save();
}, 0);
}
_stop() {
if (!this._timer)
return;
clearTimeout(this._timer);
this._timer = null;
}
async _save(control) {
const dir = dirname(control.path);
if (!existsSync(dir))
await mkdir(dir, { recursive: true });
await writeFile(control.path, control.content, {
flag: 'w',
encoding: 'utf-8',
});
}
save() {
const controls = [...this.changed];
this.changed.clear();
this._stop();
return Promise.all(controls.map(control => this._save(control)));
}
}
const saver = /*#__PURE*/ new Saver();
class GlobalControl {
_global = new Set();
get content() {
return [...this._global].join('\n');
}
push(path) {
if (this._global.has(path))
return;
this._global.add(path);
saver.change(this);
}
remove(path) {
if (!this._global.has(path))
return;
this._global.delete(path);
saver.change(this);
}
path = global_type_path;
}
const global_control = /*#__PURE*/ new GlobalControl();
class DatabaseControl {
constructor() {
const name = 'database.d.ts';
this.path = join(types_dir_path, name);
this.reference = `/// <reference path="./types/${name}" />`;
}
_database = new Map();
_content_cache = new Map();
get content() {
const content = [`declare namespace MySql {`, ` interface DataBase {`];
for (const [database_name, table_list] of this._database) {
if (this._content_cache.has(database_name)) {
content.push(this._content_cache.get(database_name));
continue;
}
const database = [` ${database_name}: {`];
for (const table_name of table_list) {
database.push(` ${table_name}: Table['${database_name}.${table_name}']`);
}
database.push(` }`);
const _content = database.join('\n');
content.push(_content);
this._content_cache.set(database_name, _content);
}
content.push(` }`, `}`);
return content.join('\n');
}
load(database_name, table_list = []) {
this._database.set(database_name, new Set(table_list));
saver.change(this);
global_control.push(this.reference);
}
drop(database_name) {
this._database.delete(database_name);
this._content_cache.delete(database_name);
saver.change(this);
}
create(database_name, table_name) {
if (!this._database.has(database_name))
this.load(database_name);
const table_list = this._database.get(database_name);
if (table_list.has(table_name))
return;
table_list.add(table_name);
this._content_cache.delete(database_name);
saver.change(this);
}
delete(database_name, table_name) {
if (!this._database.has(database_name))
return;
const table_list = this._database.get(database_name);
if (!table_list.has(table_name))
return;
table_list.delete(table_name);
if (table_list.size === 0)
this.drop(database_name);
else {
this._content_cache.delete(database_name);
saver.change(this);
}
}
path;
reference;
}
const database_control = /*#__PURE*/ new DatabaseControl();
class TablesControl {
_tables = new Map();
get(database_name, table_name) {
const key = `${database_name}.${table_name}`;
let control = this._tables.get(key);
if (control)
return control;
control = new TableControl(database_name, table_name);
this._tables.set(key, control);
database_control.create(database_name, table_name);
return control;
}
delete(database_name, table_name) {
const key = `${database_name}.${table_name}`;
if (!this._tables.has(key))
return;
this._tables.delete(key);
database_control.delete(database_name, table_name);
}
}
class TableControl {
database_name;
table_name;
constructor(database_name, table_name) {
this.database_name = database_name;
this.table_name = table_name;
this.path = join(types_dir_path, database_name, `${table_name}.d.ts`);
this.reference = `/// <reference path="./types/${database_name}/${table_name}.d.ts" />`;
global_control.push(this.reference);
}
_column = new Map();
_content_cache = new Map();
get content() {
const table = [
` interface Table {`,
` ['${this.database_name}.${this.table_name}']: {`,
];
const column = [` interface Column {`];
for (const [column_name, { type, readonly, not_null, has_defa }] of this
._column) {
table.push(` ${column_name}: Column['${this.database_name}.${this.table_name}.${column_name}']`);
if (this._content_cache.has(column_name)) {
column.push(this._content_cache.get(column_name));
continue;
}
const _content = [
` ['${this.database_name}.${this.table_name}.${column_name}']: {`,
` type: ${type}`,
` readonly: ${readonly}`,
` not_null: ${not_null}`,
` has_defa: ${has_defa}`,
` }`,
].join('\n');
column.push(_content);
this._content_cache.set(column_name, _content);
}
table.push(` }`, ` }`);
column.push(` }`);
return [`declare namespace MySql {`, ...table, ...column, `}`].join('\n');
}
load(column_list = []) {
this._column = new Map(column_list);
saver.change(this);
}
drop() {
this._column.clear();
this._content_cache.clear();
saver.change(this);
global_control.remove(this.reference);
tables_control.delete(this.database_name, this.table_name);
}
create(column_name, column) {
if (this._column.has(column_name))
return;
this._column.set(column_name, column);
saver.change(this);
}
delete(column_name) {
if (!this._column.has(column_name))
return;
this._column.delete(column_name);
this._content_cache.delete(column_name);
saver.change(this);
}
path;
reference;
}
const tables_control = /*#__PURE*/ new TablesControl();
/* istanbul ignore file -- @preserve */
const genGlobalType = async ({ json_key, }) => {
const result = await getColumnInfo();
for (const { TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, EXTRA, IS_NULLABLE, COLUMN_DEFAULT, } of result) {
const type = columnTypeToTypeScriptType(DATA_TYPE, COLUMN_TYPE, json_key?.[TABLE_SCHEMA]?.[TABLE_NAME]?.[COLUMN_NAME]);
// TODO 可配置
const readonly = EXTRA === 'auto_increment';
const not_null = IS_NULLABLE === 'NO' || DATA_TYPE === 'json';
const has_defa = COLUMN_DEFAULT !== null;
const column = {
type,
readonly,
not_null,
has_defa,
};
const table_control = tables_control.get(TABLE_SCHEMA, TABLE_NAME);
table_control.create(COLUMN_NAME, column);
}
};
function columnTypeToTypeScriptType(DATA_TYPE, COLUMN_TYPE, json_key) {
switch (DATA_TYPE) {
case 'tinyint':
case 'smallint':
case 'mediumint':
case 'int':
case 'bigint':
case 'float':
case 'double':
case 'decimal':
case 'bit':
case 'bool':
case 'boolean':
case 'serial':
return 'number';
case 'date':
case 'datetime':
case 'time':
case 'timestamp':
case 'year':
return 'Date';
case 'char':
case 'varchar':
case 'tinytext':
case 'text':
case 'mediumtext':
case 'longtext':
case 'binary':
case 'varbinary':
case 'tinyblob':
case 'mediumblob':
case 'blob':
case 'longblob':
return 'string';
case 'enum':
return (COLUMN_TYPE.match(/'.*'/)?.[0].replace(/\s?,\s?/g, ' | ') || 'string');
case 'json':
if (json_key)
return genTypeFromKeyType(json_key);
return 'object';
case 'set':
return ((COLUMN_TYPE.match(/'.*'/)?.[0].replace(/\s?,\s?/g, ' | ') ||
'string') + '[]');
default:
return 'any';
}
}
function genTypeFromKeyType(json_key) {
if (typeof json_key === 'string')
return json_key;
const { string, number, records, is_array } = json_key;
const type = [];
if (string)
type.push(`[key:string]: ${genTypeFromKeyType(string)}`);
if (number)
type.push(`[key:number]: ${genTypeFromKeyType(number)}`);
if (records) {
for (const [key, value] of Object.entries(records)) {
type.push(`${key}: ${genTypeFromKeyType(value)}`);
}
}
if (is_array)
type.push(`[]`);
return `{${type.join(';')}}`;
}
async function getColumnInfo() {
const server = useServer();
const table = `information_schema.COLUMNS`;
const exclude_tables = [
'mysql',
'sys',
'information_schema',
'performance_schema',
];
const order_by = ['TABLE_SCHEMA', 'TABLE_NAME', 'ORDINAL_POSITION'];
const result = await server.execute({
sql: `SELECT * FROM ${table} WHERE TABLE_SCHEMA NOT IN (${fill(exclude_tables.length)}) ORDER BY ${order_by.join()}`,
params: exclude_tables,
});
return result;
}
class Transaction {
type;
connection;
constructor(type, connection) {
this.type = type;
this.connection = connection;
}
_active = false;
get active() {
return this._active;
}
async begin() {
if (this._active)
return;
await this.connection.beginTransaction();
this._active = true;
}
/* istanbul ignore next -- @preserve */
async execute(options) {
if (!this._active)
throw new Error('Transaction not begin');
try {
const { sql, params } = formatSql(options);
// TODO 错误处理
const result = await this.connection.execute(sql, params);
return (isArray(result) ? result[0] : result);
}
catch (err) {
this._handleError(err);
this._rollback();
await this.release();
}
}
async commit() {
if (!this._active)
throw new Error('Transaction not begin');
try {
await this.connection.commit();
this._active = false;
}
catch (err) {
await this._handleError(err);
await this._rollback();
}
finally {
await this.release();
}
}
_handleError = async function (err) {
console.error(err);
};
setHandleError(handleError) {
this._handleError = handleError.bind(this);
}
async release() {
if (this._active)
throw new Error('Transaction is active');
if (this.type === 'pool') {
this.connection.release();
}
else {
await this.connection.end();
}
}
async _rollback() {
this._active = false;
await this.connection.rollback();
}
async rollback() {
if (!this._active)
throw new Error('Transaction not begin');
this._rollback();
await this.release();
}
}
class MysqlServer {
type;
config;
options;
json_key;
constructor() {
const { type, config, database, json_key } = genConfig();
this.type = type;
this.config = config;
this.options = database;
this._active_database = config.database;
this.json_key = json_key;
}
_active_database;
/** 当前选中的database */
get active_database() {
return this._active_database;
}
/** 切换选中的database */
use(database) {
if (this.type === 'connection') {
this._active_database = database;
this.config.database = database;
}
}
_pool;
/** 从配置处获取连接 */
/* istanbul ignore next -- @preserve */
async getConnection() {
const { active_database } = this;
if (this.type === 'pool') {
if (!this._pool)
this._pool = createPool(this.config);
const connection = await this._pool.getConnection();
const release = () => connection.release();
return { active_database, connection, release };
}
else {
const connection = await createConnection(this.config);
const release = () => connection.end();
return { active_database, connection, release };
}
}
/* istanbul ignore next -- @preserve */
async execute(options, database) {
const { sql, params } = formatSql(options);
const { active_database, connection, release } = await this.getConnection();
if (database && database !== active_database) {
await connection.changeUser({ database });
}
// TODO 错误处理
const [result] = await connection.execute(sql, params);
release();
return result;
}
/** 调用连接并获取事务实例 */
/* istanbul ignore next -- @preserve */
async transaction() {
const { connection } = await this.getConnection();
return new Transaction(this.type, connection);
}
}
let server = null;
const useServer = () => {
if (!server) {
server = new MysqlServer();
/* istanbul ignore next -- @preserve */
genGlobalType({ json_key: server.json_key });
}
return server;
};
class DataBase {
name;
constructor(name) {
this.name = name;
}
// TODO 智能类型
// ! 有点问题,不能用
/* istanbul ignore next -- @preserve */
async setConfig({ charset = 'utf8mb4', collate = 'utf8mb4_general_ci', }) {
const server = useServer();
const result = await server.execute({
sql: `ALTER DATABASE ? CHARACTER SET ? COLLATE ?`,
params: [this.name, charset, collate],
});
return result;
}
/**
* 创建数据库
*/
async create(options = {}) {
const server = useServer();
const sql = formatCreateDatabase({
name: this.name,
...options,
});
const result = await server.execute(sql);
/* istanbul ignore next -- @preserve */
database_control.load(this.name);
return result;
}
/** 抛弃数据库 */
async drop() {
const server = useServer();
const result = await server.execute({
sql: `DROP DATABASE IF EXISTS ?`,
params: [this.name],
});
/* istanbul ignore next -- @preserve */
database_control.drop(this.name);
return result;
}
}
class JoinTable {
left_table;
left_key;
right_table;
right_key;
join_type;
constructor(left_table, left_key, right_table, right_key, join_type) {
this.left_table = left_table;
this.left_key = left_key;
this.right_table = right_table;
this.right_key = right_key;
this.join_type = join_type;
if ((left_table instanceof JoinTable && left_table._used) ||
(right_table instanceof JoinTable && right_table._used)) {
throw new Error('JoinTable has been used');
}
if (left_table instanceof JoinTable)
left_table._used = true;
if (right_table instanceof JoinTable)
right_table._used = true;
}
_used = false;
get used() {
return this._used;
}
get name() {
let { name: left_name } = this.left_table;
let { name: right_name } = this.right_table;
let left_key = this.left_key;
let right_key = this.right_key;
if (this.left_table instanceof JoinTable) {
left_name = `(${left_name})`;
}
else {
left_key = `${left_name}.${left_key}`;
}
if (this.right_table instanceof JoinTable) {
right_name = `(${right_name})`;
}
else {
right_key = `${right_name}.${right_key}`;
}
return `${left_name} ${this.join_type} JOIN ${right_name} ON ${left_key}=${right_key}`;
}
join(table, key, self_key, type = JoinType.INNER) {
const join_table = new JoinTable(this, self_key, table, key, type);
return join_table;
}
leftJoin(table, key, self_key) {
return this.join(table, key, self_key, JoinType.LEFT);
}
rightJoin(table, key, self_key) {
return this.join(table, key, self_key, JoinType.RIGHT);
}
fullJoin(table, key, self_key) {
return this.join(table, key, self_key, JoinType.FULL);
}
async select(columns, where, options = {}) {
const server = useServer();
const sql = formatJoinSelect({
...options,
table: this.name,
// @ts-ignore
columns,
where,
});
const result = await server.execute(sql);
return result;
}
get __showTCR() {
return null;
}
}
var JoinType;
(function (JoinType) {
JoinType["INNER"] = "INNER";
JoinType["LEFT"] = "LEFT";
JoinType["RIGHT"] = "RIGHT";
JoinType["FULL"] = "FULL";
})(JoinType || (JoinType = {}));
class Table {
database;
name;
constructor(database, name) {
this.database = database;
this.name = name;
this._json_keys = new Map();
}
_json_keys;
async insert(...datas) {
const server = useServer();
const sql = formatInsert({
table: this.name,
datas,
json_key: this._json_keys,
});
const result = await server.execute(sql, this.database);
return result;
}
delete(where) {
const server = useServer();
const sql = formatDelete({
table: this.name,
where,
});
const result = server.execute(sql, this.database);
return result;
}
update(data, where) {
const server = useServer();
const sql = formatUpdate({
table: this.name,
data,
where,
json_key: this._json_keys,
});
const result = server.execute(sql, this.database);
return result;
}
select(columns, where, options = {}) {
const server = useServer();
// TODO 对columns进行校验
const sql = formatSelect({
...options,
table: this.name,
columns,
where,
});
return server.execute(sql, this.database);
}
join(table, key, self_key, type = JoinType.INNER) {
const join_table = new JoinTable(this, self_key, table, key, type);
return join_table;
}
leftJoin(table, key, self_key) {
return this.join(table, key, self_key, JoinType.LEFT);
}
rightJoin(table, key, self_key) {
return this.join(table, key, self_key, JoinType.RIGHT);
}
fullJoin(table, key, self_key) {
return this.join(table, key, self_key, JoinType.FULL);
}
async create(columns, options = {}) {
const server = useServer();
const sql = formatCreateTable({
...options,
database: this.database,
name: this.name,
columns,
});
const result = await server.execute(sql, this.database);
/* istanbul ignore next -- @preserve */
{
const table = tables_control.get(this.database, this.name);
const _columns = new Map();
const { json_key } = server;
for (const column of columns) {
const type = ((column, json_key) => {
switch (column.type) {
case 'enum':
return column.values.map(v => `'${v}'`).join(' | ');
case 'json':
if (json_key)
return genTypeFromKeyType(json_key);
return 'object';
case 'set':
return `(${column.values.map(v => `'${v}'`).join(' | ')})[]`;
default:
return columnTypeToTypeScriptType(column.type, '', json_key?.[this.database]?.[this.name]?.[column.name]);
}
})(column, json_key);
const readonly = ('auto_increment' in column && column.auto_increment) ?? false;
const not_null = column.not_null ?? false;
const has_defa = column.default !== null && column.default !== undefined;
const _column = {
type,
readonly,
not_null,
has_defa,
};
_columns.set(column.name, _column);
}
table.load(_columns);
}
return result;
}
async truncate() {
const server = useServer();
const sql = `TRUNCATE TABLE ${this.name}`;
const result = await server.execute(sql, this.database);
return result;
}
async drop() {
const server = useServer();
const sql = `DROP TABLE ${this.name}`;
const result = await server.execute(sql, this.database);
/* istanbul ignore next -- @preserve */
tables_control.delete(this.database, this.name);
return result;
}
}
export { DataBase, JoinTable, JoinType, Table, defineMysqlConfig, useServer };