UNPKG

mysql-plain-dao

Version:

A plain dao for mysql, no ORM, just plain sql

385 lines (302 loc) 12 kB
# mysql-plain-dao A TypeScript-first tool for generating data model objects from existing MySQL databases and executing native SQL queries with type-safe DAO operations. It also provides a library of utilities to simplify database access operations. ## Warning > ⚠️ **Warning**: This package is still under active development and the API is not yet stable. Please do not use it in production environments. ## Why Choose Native SQL? In an era of AI-generated code, writing SQL queries has become easier than ever - AI can help generate queries while you maintain control over performance and debugging. This tool helps you maintain type safety while leveraging the full power of SQL. ## Quick Start ### 1. Installation ```bash # Using npm npm install mysql-plain-dao # Or using pnpm (recommended) pnpm add mysql-plain-dao ``` ### 2. Prepare MySQL Database First, create a MySQL database and tables. Here's an example of creating a `user` table: ```sql CREATE DATABASE IF NOT EXISTS mydb; USE mydb; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, first_name VARCHAR(50), last_name VARCHAR(50), is_active BOOLEAN DEFAULT true, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); ``` ### 3. Generate Model and DAO using Command Line Tool Generate TypeScript interfaces and DAOs from your existing MySQL database tables: ```bash npx mysql-plain-dao -c mysql://user:pass@localhost:3306/mydb -t user -o src/dao/ ``` Generated files example: ```typescript // src/dao/User.ts export interface User { id: number; username: string; email: string; password_hash: string; first_name?: string | null; last_name?: string | null; is_active?: boolean | null; created_at?: Date | null; updated_at?: Date | null; } export type InsertUser = Omit<User, 'id'>; ``` ```typescript // src/dao/UserDao.ts import type { User, InsertUser } from './User'; import { BaseDao } from 'mysql-plain-dao'; export class UserDao extends BaseDao<User, InsertUser> { constructor() { super({ table_name: 'user' }); } // Place your custom database access methods here } ``` ### 4. Here's how to use the generated DAO class for CRUD operations: ```typescript // src/user-crud-example.ts import { DbUtil } from 'mysql-plain-dao'; import { UserDao } from './dao/UserDao'; import type { InsertUser } from './dao/User'; // Database configuration const DB_CONFIG = { host: 'localhost', port: 3306, user: 'root', password: 'password', database: 'mydb' }; async function main() { try { // Initialize database connection await DbUtil.initialize(DB_CONFIG); console.log('Database connection initialized successfully'); const userDao = new UserDao(); // Create: Insert a new user const newUser: InsertUser = { username: 'john_doe', email: 'john@example.com', password_hash: 'hashed_password', first_name: 'John', last_name: 'Doe', is_active: true }; const userId = await userDao.insertAsync(newUser); console.log('Created user with ID:', userId); // Read: Get user by ID const user = await userDao.getByIdAsync(userId); console.log('Retrieved user:', user); // Update: Modify user data if (user) { const updatedUser = { ...user, first_name: 'Johnny', last_name: 'Doe Jr' }; await userDao.updateAsync(updatedUser); console.log('User updated successfully'); } // List all data const activeUsers = await userDao.getListAsync(); console.log('Active users:', activeUsers); // Delete await userDao.deleteByIdAsync(userId); console.log('User deleted successfully'); } catch (error) { console.error('Error occurred:', error); process.exit(1); } finally { // Always close the connection when done await DbUtil.endPoolAsync(); console.log('Database connection closed'); } } // Execute the example main().catch(console.error); ``` ## CLI Options | Option | Description | |--------|-------------| | `-c, --conn` | Database connection string (MySQL) | | `-t, --table` | Table name(s) to generate interfaces for | | `-o, --output` | Output directory for generated files | | `-g, --generate` | Generation type (model, dao, or all) | | `--model-dir` | Specific output directory for model files | | `--dao-dir` | Specific output directory for DAO files | | `--no-header` | Skip writing file header comment | ## Base DAO Classes The library provides three base DAO classes for different primary key scenarios: | Base Class | Description | Built-in CRUD Methods | |------------|-------------|----------------------| | `BaseDao<T>` | For tables with auto-increment ID | `insertAsync()` - Create new records<br>• `getByIdAsync()` - Retrieve by ID<br> `updateAsync()` - Update records<br>• `deleteByIdAsync()` - Delete by ID<br> `getListAsync()` - List all records | | `BaseDaoUUID<T>` | For tables with UUID primary key | `insertAsync()` - Create new records<br>• `getByUuidAsync()` - Retrieve by UUID<br> `updateAsync()` - Update records<br>• `deleteByUuidAsync()` - Delete by UUID<br> `getListAsync()` - List all records | | `BaseDaoDoubleID<T>` | For tables with both ID and UUID | `insertAsync()` - Create new records<br>• `getByIdAsync()` - Retrieve by ID<br> `getByUuidAsync()` - Retrieve by UUID<br> `updateAsync()` - Update records<br>• `deleteByIdAsync()` - Delete by ID<br> `deleteByUuidAsync()` - Delete by UUID<br> `getListAsync()` - List all records | The generator automatically selects the appropriate base class based on your table structure. ## Writing Custom DAO Methods Each generated DAO class comes with built-in CRUD operations: Need more specific database operations? You can add custom methods to your DAO class. Here's how to extend the `UserDao` class with custom SQL queries: ```typescript // src/dao/UserDao.ts import { BaseDaoDoubleID } from '../../dao/BaseDaoDoubleID'; import { DbUtil } from '../../dao/DbUtil'; import type { InsertUser, User } from './User'; export class UserDao extends BaseDaoDoubleID<User, InsertUser> { constructor() { super({ table_name: 'user', }); } // You can add your own methods below // Custom methods below /** Find active users who logged in within the last n days */ async findActiveUsersAsync(): Promise<User[]> { const sql = ` SELECT * FROM user WHERE is_active = true ORDER BY last_login DESC LIMIT 100 `; return await DbUtil.executeGetListAsync<User>(sql); } /** Update user status and record the change time */ async updateUserStatusAsync(userId: number, isActive: boolean): Promise<number> { const sql = ` UPDATE user SET is_active = ?, updated_at = NOW() WHERE id = ? `; return await DbUtil.executeUpdateAsync(sql, [isActive, userId]); } /** Get user statistics by registration date */ async getUserStatsByDateAsync(startDate: Date, endDate: Date) : Promise<Array<{ date: string; count: number }>> { const sql = ` SELECT DATE(created_at) as date, COUNT(*) as count FROM user WHERE created_at BETWEEN ? AND ? GROUP BY DATE(created_at) ORDER BY date `; const result = await DbUtil.executeGetListAsync<{ date: string; count: number }>( sql, [startDate, endDate] ); return result; } /** Search users with complex conditions */ async searchUsersAsync(params: { keyword?: string; isActive?: boolean; startDate?: Date; limit?: number; }): Promise<User[]> { const conditions: string[] = ['1=1']; const values: any[] = []; if (params.keyword) { const keyword = `%${params.keyword}%`; conditions.push('(username LIKE ? OR email LIKE ?)'); values.push(keyword, keyword); } if (params.isActive !== undefined) { conditions.push('is_active = ?'); values.push(params.isActive); } if (params.startDate) { conditions.push('created_at >= ?'); values.push(params.startDate); } const sql = ` SELECT * FROM user WHERE ${conditions.join(' AND ')} ORDER BY created_at DESC LIMIT ? `; values.push(params.limit || 100); return await DbUtil.executeGetListAsync<User>(sql, values); } } ``` Usage example: ```typescript const userDao = new UserDao(); // Find active users const recentUsers = await userDao.findActiveUsersAsync(); // Update user status await userDao.updateUserStatusAsync(123, false); // Get user registration statistics for the last month const startDate = new Date(); startDate.setMonth(startDate.getMonth() - 1); const stats = await userDao.getUserStatsByDateAsync(startDate, new Date()); // Search users with complex conditions const searchResults = await userDao.searchUsersAsync({ keyword: 'user', isActive: true, startDate: new Date('2024-01-01T00:00:00Z'), limit: 10 }); ``` ## SQL Injection Prevention SQL injection is one of the most common web application vulnerabilities. Here's how to write secure SQL queries using this library: ### Unsafe Example (DO NOT USE) ```typescript // DON'T DO THIS - Vulnerable to SQL injection class UnsafeUserDao { async searchUsers(keyword: string, isActive: boolean) { // DANGEROUS: Direct string concatenation const sql = ` SELECT * FROM user WHERE username LIKE '%${keyword}%' AND is_active = ${isActive} `; return await DbUtil.executeGetListAsync(sql); } } // This could be exploited: await userDao.searchUsers("' OR '1'='1'; DROP TABLE user; --", true); ``` ### Safe Example (Recommended) ```typescript class UserDao extends BaseDaoDoubleID<User, InsertUser> { // Method 1: Using parameterized queries (Recommended) async searchUsers(keyword: string, isActive: boolean) { const sql = ` SELECT * FROM user WHERE username LIKE ? AND is_active = ? `; return await DbUtil.executeGetListAsync(sql, [`%${keyword}%`, isActive]); } // Method 2: Using mysql2's format function async searchUsersWithFormat(keyword: string, isActive: boolean) { const sql = format( 'SELECT * FROM user WHERE username LIKE ? AND is_active = ?', [`%${keyword}%`, isActive] ); return await DbUtil.executeGetListAsync(sql); } } ``` ### Key Security Points 1. **Never** concatenate user input directly into SQL strings 2. **Always** use parameterized queries with `?` placeholders 3. **Consider** using `mysql2`'s `format` or `escape` functions for complex queries 4. **Validate** and sanitize input before using it in queries 5. **Follow** the built-in DAO methods pattern of using parameterized queries instead of string concatenation ## Inspired by This project was inspired by [schemats](https://github.com/SweetIQ/schemats), which is no longer actively maintained. Some code has been adapted from their implementation while the codebase has been rewritten in TypeScript and enhanced with modern features and additional functionality. ## License MIT