UNPKG

@aradox/multi-orm

Version:

Type-safe ORM with multi-datasource support, row-level security, and Prisma-like API for PostgreSQL, SQL Server, and HTTP APIs

375 lines 16.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MySQLAdapter = void 0; const promise_1 = __importDefault(require("mysql2/promise")); const logger_1 = require("../utils/logger"); class MySQLAdapter { pool; modelMeta = new Map(); capabilities = { transactions: true, bulkByIds: true, maxIn: 65000, // MySQL supports large IN clauses supportedOperators: ['eq', 'ne', 'in', 'notIn', 'gt', 'gte', 'lt', 'lte', 'contains', 'startsWith', 'endsWith'] }; /** * Set model metadata for table name mapping */ setModelMetadata(models) { for (const [name, model] of Object.entries(models)) { this.modelMeta.set(name, model); } } constructor(connectionString) { if (typeof connectionString === 'string') { // Parse connection string: mysql://user:password@host:port/database const url = new URL(connectionString); this.pool = promise_1.default.createPool({ host: url.hostname, port: url.port ? parseInt(url.port) : 3306, user: url.username, password: url.password, database: url.pathname.substring(1), connectionLimit: 10, waitForConnections: true, queueLimit: 0, connectTimeout: 10000 }); } else { // Full config with pool options this.pool = promise_1.default.createPool({ host: connectionString.host, port: connectionString.port ?? 3306, user: connectionString.user, password: connectionString.password, database: connectionString.database, connectionLimit: connectionString.connectionLimit ?? 10, waitForConnections: connectionString.waitForConnections ?? true, queueLimit: connectionString.queueLimit ?? 0, connectTimeout: connectionString.connectTimeout ?? 10000 }); logger_1.logger.info('mysql', `Connection pool configured: limit=${connectionString.connectionLimit ?? 10}`); } } toSnakeCase(str) { return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`).replace(/^_/, ''); } toColumnName(fieldName, modelName) { const model = this.modelMeta.get(modelName); if (!model) return this.toSnakeCase(fieldName); const field = model.fields?.[fieldName]; if (field?.map) { return field.map; } return this.toSnakeCase(fieldName); } async findMany(model, args) { const { sql, params } = this.buildSelectQuery(model, args); logger_1.logger.debug('mysql', '📋 SQL Query:', sql); logger_1.logger.debug('mysql', '📋 Parameters:', JSON.stringify(params)); const [rows] = await this.pool.query(sql, params); return rows; } async findUnique(model, args) { const { sql, params } = this.buildSelectQuery(model, { where: args.where, select: args.select, take: 1 }); logger_1.logger.debug('mysql', '📋 SQL Query:', sql); logger_1.logger.debug('mysql', '📋 Parameters:', JSON.stringify(params)); const [rows] = await this.pool.query(sql, params); return rows[0] || null; } async create(model, args) { const { sql, params } = this.buildInsertQuery(model, args.data); logger_1.logger.debug('mysql', '📋 SQL Query:', sql); logger_1.logger.debug('mysql', '📋 Parameters:', JSON.stringify(params)); const [result] = await this.pool.query(sql, params); // Fetch the inserted row const tableName = this.toSnakeCase(model); const [rows] = await this.pool.query(`SELECT * FROM ${tableName} WHERE id = ?`, [result.insertId]); return rows[0] || null; } async update(model, args) { const { sql, params } = this.buildUpdateQuery(model, args.where, args.data); logger_1.logger.debug('mysql', '📋 SQL Query:', sql); logger_1.logger.debug('mysql', '📋 Parameters:', JSON.stringify(params)); await this.pool.query(sql, params); // Fetch the updated row const { sql: selectSql, params: selectParams } = this.buildSelectQuery(model, { where: args.where, take: 1 }); const [rows] = await this.pool.query(selectSql, selectParams); return rows[0] || null; } async delete(model, args) { // Fetch the row before deleting const { sql: selectSql, params: selectParams } = this.buildSelectQuery(model, { where: args.where, take: 1 }); const [rows] = await this.pool.query(selectSql, selectParams); const deletedRow = rows[0]; if (deletedRow) { const { sql, params } = this.buildDeleteQuery(model, args.where); logger_1.logger.debug('mysql', '📋 SQL Query:', sql); logger_1.logger.debug('mysql', '📋 Parameters:', JSON.stringify(params)); await this.pool.query(sql, params); } return deletedRow || null; } async count(model, args) { const { sql, params } = this.buildCountQuery(model, args); logger_1.logger.debug('mysql', '📋 SQL Query:', sql); logger_1.logger.debug('mysql', '📋 Parameters:', JSON.stringify(params)); const [rows] = await this.pool.query(sql, params); return rows[0].count; } buildSelectQuery(model, args) { const tableName = this.toSnakeCase(model); const params = []; // SELECT clause let selectClause = '*'; if (args.select && Object.keys(args.select).length > 0) { const fields = Object.keys(args.select) .filter(k => args.select[k]) .map(field => this.toColumnName(field, model)); selectClause = fields.join(', '); } let sql = `SELECT ${selectClause} FROM ${tableName}`; // WHERE clause if (args.where) { const { clause, whereParams } = this.buildWhereClause(args.where, model); if (clause) { sql += ` WHERE ${clause}`; params.push(...whereParams); } } // ORDER BY clause if (args.orderBy) { const orderFields = Object.entries(args.orderBy).map(([field, dir]) => `${this.toColumnName(field, model)} ${dir.toUpperCase()}`); sql += ` ORDER BY ${orderFields.join(', ')}`; } // LIMIT and OFFSET if (args.take !== undefined) { sql += ` LIMIT ?`; params.push(args.take); } if (args.skip !== undefined) { sql += ` OFFSET ?`; params.push(args.skip); } return { sql, params }; } buildWhereClause(where, model) { const conditions = []; const params = []; for (const [field, value] of Object.entries(where)) { const columnName = this.toColumnName(field, model); if (value === null || value === undefined) { conditions.push(`${columnName} IS NULL`); } else if (typeof value === 'object' && !Array.isArray(value)) { // Operator object for (const [op, val] of Object.entries(value)) { const condition = this.buildOperatorCondition(columnName, op, val); if (condition) { conditions.push(condition.clause); params.push(...condition.params); } } } else { // Simple equality conditions.push(`${columnName} = ?`); params.push(value); } } return { clause: conditions.join(' AND '), whereParams: params }; } buildOperatorCondition(column, op, value) { switch (op) { case 'eq': return { clause: `${column} = ?`, params: [value] }; case 'ne': return { clause: `${column} != ?`, params: [value] }; case 'in': return { clause: `${column} IN (?)`, params: [value] }; case 'notIn': return { clause: `${column} NOT IN (?)`, params: [value] }; case 'gt': return { clause: `${column} > ?`, params: [value] }; case 'gte': return { clause: `${column} >= ?`, params: [value] }; case 'lt': return { clause: `${column} < ?`, params: [value] }; case 'lte': return { clause: `${column} <= ?`, params: [value] }; case 'contains': return { clause: `${column} LIKE ?`, params: [`%${value}%`] }; case 'startsWith': return { clause: `${column} LIKE ?`, params: [`${value}%`] }; case 'endsWith': return { clause: `${column} LIKE ?`, params: [`%${value}`] }; default: return null; } } buildInsertQuery(model, data) { const tableName = this.toSnakeCase(model); const columns = []; const placeholders = []; const params = []; for (const [field, value] of Object.entries(data)) { const columnName = this.toColumnName(field, model); columns.push(columnName); placeholders.push('?'); params.push(value); } const sql = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`; return { sql, params }; } buildUpdateQuery(model, where, data) { const tableName = this.toSnakeCase(model); const setClauses = []; const params = []; for (const [field, value] of Object.entries(data)) { const columnName = this.toColumnName(field, model); setClauses.push(`${columnName} = ?`); params.push(value); } let sql = `UPDATE ${tableName} SET ${setClauses.join(', ')}`; const { clause, whereParams } = this.buildWhereClause(where, model); if (clause) { sql += ` WHERE ${clause}`; params.push(...whereParams); } return { sql, params }; } buildDeleteQuery(model, where) { const tableName = this.toSnakeCase(model); const params = []; let sql = `DELETE FROM ${tableName}`; const { clause, whereParams } = this.buildWhereClause(where, model); if (clause) { sql += ` WHERE ${clause}`; params.push(...whereParams); } return { sql, params }; } buildCountQuery(model, args) { const tableName = this.toSnakeCase(model); const params = []; let sql = `SELECT COUNT(*) as count FROM ${tableName}`; if (args.where) { const { clause, whereParams } = this.buildWhereClause(args.where, model); if (clause) { sql += ` WHERE ${clause}`; params.push(...whereParams); } } return { sql, params }; } async beginTransaction(options) { const connection = await this.pool.getConnection(); const isolationLevel = options?.isolationLevel || 'READ COMMITTED'; const txId = `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const self = this; logger_1.logger.debug('runtime', `Beginning MySQL transaction ${txId} with isolation level: ${isolationLevel}`); // Map isolation levels to MySQL syntax const mysqlIsolationLevel = isolationLevel.replace(/ /g, '-'); // Convert "READ COMMITTED" to "READ-COMMITTED" // Set isolation level and begin transaction await connection.query(`SET TRANSACTION ISOLATION LEVEL ${mysqlIsolationLevel}`); await connection.beginTransaction(); // Create transaction object const transaction = { id: txId, async commit() { logger_1.logger.debug('runtime', `Committing MySQL transaction ${txId}`); await connection.commit(); connection.release(); }, async rollback() { logger_1.logger.debug('runtime', `Rolling back MySQL transaction ${txId}`); try { await connection.rollback(); } catch (error) { logger_1.logger.error('runtime', `Error rolling back MySQL transaction ${txId}: ${error.message}`); } finally { connection.release(); } }, // CRUD operations within transaction use the connection async findMany(model, args) { const { sql, params } = self.buildSelectQuery(model, args); logger_1.logger.debug('mysql', '📋 TX SQL Query:', sql); const [rows] = await connection.query(sql, params); return rows; }, async findUnique(model, args) { const { sql, params } = self.buildSelectQuery(model, { where: args.where, select: args.select, take: 1 }); logger_1.logger.debug('mysql', '📋 TX SQL Query:', sql); const [rows] = await connection.query(sql, params); return rows[0] || null; }, async create(model, args) { const { sql, params } = self.buildInsertQuery(model, args.data); logger_1.logger.debug('mysql', '📋 TX SQL Query:', sql); const [result] = await connection.query(sql, params); const tableName = self.toSnakeCase(model); const [rows] = await connection.query(`SELECT * FROM ${tableName} WHERE id = ?`, [result.insertId]); return rows[0] || null; }, async update(model, args) { const { sql, params } = self.buildUpdateQuery(model, args.where, args.data); logger_1.logger.debug('mysql', '📋 TX SQL Query:', sql); await connection.query(sql, params); const { sql: selectSql, params: selectParams } = self.buildSelectQuery(model, { where: args.where, take: 1 }); const [rows] = await connection.query(selectSql, selectParams); return rows[0] || null; }, async delete(model, args) { const { sql: selectSql, params: selectParams } = self.buildSelectQuery(model, { where: args.where, take: 1 }); const [rows] = await connection.query(selectSql, selectParams); const deletedRow = rows[0]; if (deletedRow) { const { sql, params } = self.buildDeleteQuery(model, args.where); logger_1.logger.debug('mysql', '📋 TX SQL Query:', sql); await connection.query(sql, params); } return deletedRow || null; }, async count(model, args) { const { sql, params } = self.buildCountQuery(model, args); logger_1.logger.debug('mysql', '📋 TX SQL Query:', sql); const [rows] = await connection.query(sql, params); return rows[0].count; } }; return transaction; } async close() { await this.pool.end(); } } exports.MySQLAdapter = MySQLAdapter; //# sourceMappingURL=mysql.js.map