@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
JavaScript
;
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