UNPKG

@dlovely/mysql

Version:

对mysql的简易连接,能创建数据库、数据表管理,并使用sql编辑器

789 lines (777 loc) 25.2 kB
/*! * @dlovely/mysql v1.1.0 * (c) 2023 MZ-Dlovely * @license MIT */ 'use strict'; var node_path = require('node:path'); var node_fs = require('node:fs'); var node_module = require('node:module'); var promise = require('mysql2/promise'); var sqlEditor = require('@dlovely/sql-editor'); var utils = require('@dlovely/utils'); var promises = require('node:fs/promises'); /* istanbul ignore file -- @preserve */ const root = process.cwd(); const require$1 = node_module.createRequire(root); const runtime_dir_path = node_path.join(root, '.mysql'); if (node_fs.existsSync(runtime_dir_path)) node_fs.rmSync(runtime_dir_path, { recursive: true, force: true }); const types_dir_path = node_path.join(runtime_dir_path, 'types'); node_fs.mkdirSync(types_dir_path, { recursive: true }); const global_type_path = node_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 = node_path.join(root, `mysql.config${ext}`); if (node_fs.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$1(path); } function readJsConfig(path) { const module = require$1(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 = node_path.dirname(control.path); if (!node_fs.existsSync(dir)) await promises.mkdir(dir, { recursive: true }); await promises.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 = node_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 = node_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 (${utils.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 } = sqlEditor.formatSql(options); // TODO 错误处理 const result = await this.connection.execute(sql, params); return (utils.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 = promise.createPool(this.config); const connection = await this._pool.getConnection(); const release = () => connection.release(); return { active_database, connection, release }; } else { const connection = await promise.createConnection(this.config); const release = () => connection.end(); return { active_database, connection, release }; } } /* istanbul ignore next -- @preserve */ async execute(options, database) { const { sql, params } = sqlEditor.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 = sqlEditor.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 = exports.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, exports.JoinType.LEFT); } rightJoin(table, key, self_key) { return this.join(table, key, self_key, exports.JoinType.RIGHT); } fullJoin(table, key, self_key) { return this.join(table, key, self_key, exports.JoinType.FULL); } async select(columns, where, options = {}) { const server = useServer(); const sql = sqlEditor.formatJoinSelect({ ...options, table: this.name, // @ts-ignore columns, where, }); const result = await server.execute(sql); return result; } get __showTCR() { return null; } } exports.JoinType = void 0; (function (JoinType) { JoinType["INNER"] = "INNER"; JoinType["LEFT"] = "LEFT"; JoinType["RIGHT"] = "RIGHT"; JoinType["FULL"] = "FULL"; })(exports.JoinType || (exports.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 = sqlEditor.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 = sqlEditor.formatDelete({ table: this.name, where, }); const result = server.execute(sql, this.database); return result; } update(data, where) { const server = useServer(); const sql = sqlEditor.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 = sqlEditor.formatSelect({ ...options, table: this.name, columns, where, }); return server.execute(sql, this.database); } join(table, key, self_key, type = exports.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, exports.JoinType.LEFT); } rightJoin(table, key, self_key) { return this.join(table, key, self_key, exports.JoinType.RIGHT); } fullJoin(table, key, self_key) { return this.join(table, key, self_key, exports.JoinType.FULL); } async create(columns, options = {}) { const server = useServer(); const sql = sqlEditor.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; } } exports.DataBase = DataBase; exports.JoinTable = JoinTable; exports.Table = Table; exports.defineMysqlConfig = defineMysqlConfig; exports.useServer = useServer;