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

599 lines 28.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.MSSQLAdapter = exports.MSSQLNativeAdapter = void 0; const odbc = __importStar(require("odbc")); const logger_1 = require("../utils/logger"); class MSSQLNativeAdapter { pool = null; connectionString; poolOptions; modelMeta = new Map(); // Store model metadata for @map support capabilities = { transactions: true, bulkByIds: true, maxIn: 2100, // SQL Server parameter limit supportedOperators: ['eq', 'ne', 'in', 'notIn', 'gt', 'gte', 'lt', 'lte', 'contains', 'startsWith', 'endsWith'] }; /** * Set model metadata for @map attribute support * Called by the runtime after IR is loaded */ setModelMetadata(models) { for (const [name, model] of Object.entries(models)) { this.modelMeta.set(name, model); } } /** * Coerce ODBC result types to match schema field types * ODBC returns bit fields as "0"/"1" strings instead of booleans */ coerceResultTypes(model, result) { if (!result || typeof result !== 'object') return result; const modelDef = this.modelMeta.get(model); if (!modelDef) return result; const coerced = {}; for (const [key, value] of Object.entries(result)) { // Find the field definition (check both original field name and mapped column name) let fieldDef = modelDef.fields[key]; if (!fieldDef) { // Try to find by mapped column name const foundField = Object.values(modelDef.fields).find(f => f.map === key); if (foundField) { fieldDef = foundField; } } if (fieldDef && fieldDef.type === 'Boolean') { // Convert ODBC bit field strings to booleans if (value === '1' || value === 1 || value === true) { coerced[key] = true; } else if (value === '0' || value === 0 || value === false) { coerced[key] = false; } else if (value === null || value === undefined) { coerced[key] = null; } else { coerced[key] = value; // Keep original if unexpected value } } else { coerced[key] = value; } } return coerced; } constructor(config) { // Build ODBC connection string if (config.options?.trustedConnection) { // Windows Authentication with ODBC // Use Driver 17 (most commonly installed) this.connectionString = `Driver={ODBC Driver 17 for SQL Server};Server=${config.server};Database=${config.database};Trusted_Connection=Yes;TrustServerCertificate=Yes;`; } else { // SQL Server Authentication this.connectionString = `Driver={ODBC Driver 17 for SQL Server};Server=${config.server};Database=${config.database};UID=${config.user};PWD=${config.password};TrustServerCertificate=Yes;`; } // Configure connection pool this.poolOptions = { connectionString: this.connectionString, connectionTimeout: config.options?.pool?.connectionTimeout ?? 60, // ODBC pool uses initialSize, incrementSize, maxSize initialSize: config.options?.pool?.min ?? 0, incrementSize: 1, // Grow pool by 1 connection at a time maxSize: config.options?.pool?.max ?? 10, reuseConnections: true, shrink: true // Shrink pool when connections idle }; if (config.options?.pool) { logger_1.logger.info('mssql', `ODBC connection pool configured: max=${this.poolOptions.maxSize}, min=${this.poolOptions.initialSize}`); } } async ensureConnected() { if (!this.pool) { this.pool = await odbc.pool(this.poolOptions); } return this.pool; } async findMany(model, args) { try { const pool = await this.ensureConnected(); const { query: sqlQuery, params } = this.buildSelectQuery(model, args); // ODBC uses ? placeholders, so we need to convert our query const { query: odbcQuery, values } = this.convertToODBCQuery(sqlQuery, params); logger_1.logger.debug('mssql', '📋 SQL Query (named params):', sqlQuery); logger_1.logger.debug('mssql', '📋 SQL Query (ODBC):', odbcQuery); logger_1.logger.debug('mssql', '📋 Parameters:', JSON.stringify(values)); const result = await pool.query(odbcQuery, values); return result.map(row => this.coerceResultTypes(model, row)); } catch (error) { throw new Error(`MSSQL findMany failed for ${model}: ${error.message}`); } } async findUnique(model, args) { const pool = await this.ensureConnected(); const { query: sqlQuery, params } = this.buildSelectQuery(model, { where: args.where, select: args.select, take: 1 }); const { query: odbcQuery, values } = this.convertToODBCQuery(sqlQuery, params); logger_1.logger.debug('mssql', '📋 SQL Query (named params):', sqlQuery); logger_1.logger.debug('mssql', '📋 SQL Query (ODBC):', odbcQuery); logger_1.logger.debug('mssql', '📋 Parameters:', JSON.stringify(values)); const result = await pool.query(odbcQuery, values); const row = result?.[0] || null; return row ? this.coerceResultTypes(model, row) : null; } async create(model, args) { const pool = await this.ensureConnected(); const tableName = this.toTableName(model); const fields = Object.keys(args.data); const values = Object.values(args.data).map(v => this.sanitizeODBCValue(v)); const fieldList = fields.map(f => this.toColumnName(f, model)).join(', '); const paramList = fields.map(() => '?').join(', '); const sqlQuery = `INSERT INTO ${tableName} (${fieldList}) OUTPUT INSERTED.* VALUES (${paramList})`; logger_1.logger.debug('mssql', '📋 SQL Query:', sqlQuery); logger_1.logger.debug('mssql', '📋 Parameters:', JSON.stringify(values)); const result = await pool.query(sqlQuery, values); const row = result?.[0]; return row ? this.coerceResultTypes(model, row) : null; } async update(model, args) { const pool = await this.ensureConnected(); const tableName = this.toTableName(model); // Build SET clause with named parameters const params = {}; let paramIdx = 0; const setFields = Object.keys(args.data) .map(field => { const paramName = `set${paramIdx++}`; params[paramName] = args.data[field]; return `${this.toColumnName(field, model)} = @${paramName}`; }) .join(', '); // Build WHERE clause with named parameters (continue param numbering) const { clause: whereClause, params: whereParams } = this.buildWhereClause(args.where, paramIdx, model); Object.assign(params, whereParams); const sqlQuery = `UPDATE ${tableName} SET ${setFields} OUTPUT INSERTED.* WHERE ${whereClause}`; logger_1.logger.debug('mssql', '📋 SQL Query (named params):', sqlQuery); logger_1.logger.debug('mssql', '📋 Parameters:', JSON.stringify(params)); // Convert named parameters to positional ? for ODBC const { query: odbcQuery, values } = this.convertToODBCQuery(sqlQuery, params); const result = await pool.query(odbcQuery, values); const row = result?.[0]; return row ? this.coerceResultTypes(model, row) : null; } async delete(model, args) { const pool = await this.ensureConnected(); const tableName = this.toTableName(model); const { clause: whereClause, params: whereParams } = this.buildWhereClause(args.where, 0, model); const sqlQuery = `DELETE FROM ${tableName} OUTPUT DELETED.* WHERE ${whereClause}`; logger_1.logger.debug('mssql', '📋 SQL Query (named params):', sqlQuery); logger_1.logger.debug('mssql', '📋 Parameters:', JSON.stringify(whereParams)); // Convert named parameters to positional ? for ODBC const { query: odbcQuery, values } = this.convertToODBCQuery(sqlQuery, whereParams); const result = await pool.query(odbcQuery, values); const row = result?.[0]; return row ? this.coerceResultTypes(model, row) : null; } async count(model, args) { const pool = await this.ensureConnected(); const tableName = this.toTableName(model); let sqlQuery = `SELECT COUNT(*) AS count FROM ${tableName}`; const params = {}; if (args.where) { const { clause, params: whereParams } = this.buildWhereClause(args.where, 0, model); if (clause) { sqlQuery += ` WHERE ${clause}`; Object.assign(params, whereParams); } } logger_1.logger.debug('mssql', '📋 SQL Query (named params):', sqlQuery); logger_1.logger.debug('mssql', '📋 Parameters:', JSON.stringify(params)); const { query: odbcQuery, values } = this.convertToODBCQuery(sqlQuery, params); const result = await pool.query(odbcQuery, values); return result?.[0]?.count || 0; } buildSelectQuery(model, args) { const tableName = this.toTableName(model); const params = {}; let paramIndex = 0; // 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(f => this.toColumnName(f, model)); selectClause = fields.join(', '); } let query = `SELECT ${args.take ? `TOP(@top)` : ''} ${selectClause} FROM ${tableName}`; if (args.take !== undefined) { params['top'] = args.take; } // WHERE clause if (args.where) { const { clause, params: whereParams } = this.buildWhereClause(args.where, paramIndex, model); if (clause) { query += ` WHERE ${clause}`; Object.assign(params, whereParams); paramIndex += Object.keys(whereParams).length; } } // ORDER BY clause if (args.orderBy) { const orderFields = Object.entries(args.orderBy).map(([field, dir]) => `${this.toColumnName(field, model)} ${dir.toUpperCase()}`); query += ` ORDER BY ${orderFields.join(', ')}`; } else if (args.skip !== undefined) { // SQL Server requires ORDER BY for OFFSET query += ` ORDER BY (SELECT NULL)`; } // OFFSET (skip) if (args.skip !== undefined) { query += ` OFFSET @skip ROWS`; params['skip'] = args.skip; if (args.take !== undefined) { query += ` FETCH NEXT @fetch ROWS ONLY`; params['fetch'] = args.take; } } return { query, params }; } buildWhereClause(where, startIdx, modelName) { const conditions = []; const params = {}; let paramIdx = startIdx; for (const [field, value] of Object.entries(where)) { if (field === 'AND') { const subConditions = value.map(w => { const { clause, params: whereParams } = this.buildWhereClause(w, paramIdx, modelName); paramIdx += Object.keys(whereParams).length; Object.assign(params, whereParams); return `(${clause})`; }); conditions.push(`(${subConditions.join(' AND ')})`); } else if (field === 'OR') { const subConditions = value.map(w => { const { clause, params: whereParams } = this.buildWhereClause(w, paramIdx, modelName); paramIdx += Object.keys(whereParams).length; Object.assign(params, whereParams); return `(${clause})`; }); conditions.push(`(${subConditions.join(' OR ')})`); } else if (field === 'NOT') { const { clause, params: whereParams } = this.buildWhereClause(value, paramIdx, modelName); paramIdx += Object.keys(whereParams).length; Object.assign(params, whereParams); conditions.push(`NOT (${clause})`); } else if (typeof value === 'object' && value !== null) { // Field operators: eq, ne, in, notIn, gt, gte, lt, lte, contains, startsWith, endsWith const columnName = this.toColumnName(field, modelName); for (const [op, opValue] of Object.entries(value)) { const paramName = `p${paramIdx++}`; switch (op) { case 'eq': conditions.push(`${columnName} = @${paramName}`); params[paramName] = opValue; break; case 'ne': conditions.push(`${columnName} != @${paramName}`); params[paramName] = opValue; break; case 'in': if (Array.isArray(opValue) && opValue.length > 0) { const inParams = opValue.map((_, i) => { const pName = `${paramName}_${i}`; params[pName] = opValue[i]; return `@${pName}`; }); conditions.push(`${columnName} IN (${inParams.join(', ')})`); } break; case 'notIn': if (Array.isArray(opValue) && opValue.length > 0) { const notInParams = opValue.map((_, i) => { const pName = `${paramName}_${i}`; params[pName] = opValue[i]; return `@${pName}`; }); conditions.push(`${columnName} NOT IN (${notInParams.join(', ')})`); } break; case 'gt': conditions.push(`${columnName} > @${paramName}`); params[paramName] = opValue; break; case 'gte': conditions.push(`${columnName} >= @${paramName}`); params[paramName] = opValue; break; case 'lt': conditions.push(`${columnName} < @${paramName}`); params[paramName] = opValue; break; case 'lte': conditions.push(`${columnName} <= @${paramName}`); params[paramName] = opValue; break; case 'contains': conditions.push(`${columnName} LIKE @${paramName}`); params[paramName] = `%${opValue}%`; break; case 'startsWith': conditions.push(`${columnName} LIKE @${paramName}`); params[paramName] = `${opValue}%`; break; case 'endsWith': conditions.push(`${columnName} LIKE @${paramName}`); params[paramName] = `%${opValue}`; break; } } } else { // Direct equality const paramName = `p${paramIdx++}`; conditions.push(`${this.toColumnName(field, modelName)} = @${paramName}`); params[paramName] = value; } } return { clause: conditions.join(' AND '), params }; } toTableName(model) { // Use model name as-is for table name // User should define model names to match their table names return `[dbo].[${model}]`; } /** * Convert field name to column name, using @map if available * @param field - The field name from the schema * @param modelName - The model name (optional, for @map lookup) * @returns The database column name */ toColumnName(field, modelName) { // Check if @map is defined for this field if (modelName && this.modelMeta.has(modelName)) { const model = this.modelMeta.get(modelName); const fieldDef = model.fields[field]; if (fieldDef && fieldDef.map) { return fieldDef.map; // Use @map value } } // Fallback to PascalCase convention return this.toPascalCase(field); } toPascalCase(str) { return str.charAt(0).toUpperCase() + str.slice(1); } /** * Sanitize value for ODBC binding * ODBC has issues with undefined and certain null representations */ sanitizeODBCValue(value) { // Convert undefined to null if (value === undefined) { return null; } // Handle empty strings that should be null if (value === '') { return null; } // Dates need to be converted to ISO string or kept as Date objects if (value instanceof Date) { return value; } return value; } /** * Convert named parameters (@param) to ODBC ? placeholders * ODBC uses positional parameters (?) instead of named parameters */ convertToODBCQuery(query, params) { const values = []; const paramNames = Object.keys(params).sort((a, b) => b.length - a.length); // Sort by length descending to avoid partial matches let odbcQuery = query; // Replace @paramName with ? in order for (const paramName of paramNames) { const regex = new RegExp(`@${paramName}\\b`, 'g'); odbcQuery = odbcQuery.replace(regex, () => { values.push(this.sanitizeODBCValue(params[paramName])); return '?'; }); } return { query: odbcQuery, values }; } async beginTransaction(options) { const pool = await this.ensureConnected(); const connection = await pool.connect(); const isolationLevel = options?.isolationLevel || 'READ COMMITTED'; const txId = `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const self = this; // Capture this for use in transaction methods logger_1.logger.debug('runtime', `Beginning MSSQL transaction ${txId} with isolation level: ${isolationLevel}`); // Begin transaction with isolation level await connection.query(`SET TRANSACTION ISOLATION LEVEL ${isolationLevel}; BEGIN TRANSACTION`); // Create transaction object const transaction = { id: txId, async commit() { logger_1.logger.debug('runtime', `Committing MSSQL transaction ${txId}`); await connection.query('COMMIT TRANSACTION'); await connection.close(); }, async rollback() { logger_1.logger.debug('runtime', `Rolling back MSSQL transaction ${txId}`); try { await connection.query('ROLLBACK TRANSACTION'); } catch (error) { logger_1.logger.error('runtime', `Error rolling back MSSQL transaction ${txId}: ${error.message}`); } finally { await connection.close(); } }, // CRUD operations within transaction use the connection async findMany(model, args) { const { query, params } = self.buildSelectQuery(model, args); const { query: odbcQuery, values } = self.convertToODBCQuery(query, params); logger_1.logger.debug('mssql', '📋 TX SQL Query:', odbcQuery); const result = await connection.query(odbcQuery, values); return result; }, async findUnique(model, args) { const { query, params } = self.buildSelectQuery(model, { where: args.where, select: args.select, take: 1 }); const { query: odbcQuery, values } = self.convertToODBCQuery(query, params); logger_1.logger.debug('mssql', '📋 TX SQL Query:', odbcQuery); const result = await connection.query(odbcQuery, values); return result[0] || null; }, async create(model, args) { const tableName = self.toTableName(model); const fields = Object.keys(args.data); const values = Object.values(args.data); const fieldList = fields.map(f => self.toColumnName(f, model)).join(', '); const paramList = fields.map(() => '?').join(', '); const sqlQuery = `INSERT INTO ${tableName} (${fieldList}) OUTPUT INSERTED.* VALUES (${paramList})`; logger_1.logger.debug('mssql', '📋 TX SQL Query:', sqlQuery); const result = await connection.query(sqlQuery, values); return result?.[0]; }, async update(model, args) { const tableName = self.toTableName(model); const params = {}; let paramIdx = 0; const setFields = Object.keys(args.data) .map(field => { const paramName = `set${paramIdx++}`; params[paramName] = args.data[field]; return `${self.toColumnName(field, model)} = @${paramName}`; }) .join(', '); const { clause: whereClause, params: whereParams } = self.buildWhereClause(args.where, paramIdx, model); Object.assign(params, whereParams); const sqlQuery = `UPDATE ${tableName} SET ${setFields} OUTPUT INSERTED.* WHERE ${whereClause}`; const { query: odbcQuery, values } = self.convertToODBCQuery(sqlQuery, params); logger_1.logger.debug('mssql', '📋 TX SQL Query:', odbcQuery); const result = await connection.query(odbcQuery, values); return result?.[0]; }, async delete(model, args) { const tableName = self.toTableName(model); const { clause: whereClause, params: whereParams } = self.buildWhereClause(args.where, 0, model); const sqlQuery = `DELETE FROM ${tableName} OUTPUT DELETED.* WHERE ${whereClause}`; const { query: odbcQuery, values } = self.convertToODBCQuery(sqlQuery, whereParams); logger_1.logger.debug('mssql', '📋 TX SQL Query:', odbcQuery); const result = await connection.query(odbcQuery, values); return result?.[0]; }, async count(model, args) { const tableName = self.toTableName(model); let sqlQuery = `SELECT COUNT(*) AS count FROM ${tableName}`; const params = {}; if (args.where) { const { clause, params: whereParams } = self.buildWhereClause(args.where, 0, model); if (clause) { sqlQuery += ` WHERE ${clause}`; Object.assign(params, whereParams); } } const { query: odbcQuery, values } = self.convertToODBCQuery(sqlQuery, params); logger_1.logger.debug('mssql', '📋 TX SQL Query:', odbcQuery); const result = await connection.query(odbcQuery, values); return result?.[0]?.count || 0; } }; return transaction; } async close() { if (this.pool) { await this.pool.close(); this.pool = null; } } /** * Execute a raw SQL query * @param sql - The SQL query string (use ? for parameters, or @p1, @p2 which will be converted) * @param params - Optional query parameters * @returns Query result recordset */ async query(sql, params) { logger_1.logger.debug('mssql', '📊 Raw SQL Query:', sql); logger_1.logger.debug('mssql', '📊 Parameters:', JSON.stringify(params || [])); const pool = await this.ensureConnected(); // Convert @p1, @p2, etc. to ? for ODBC if needed let odbcQuery = sql; let odbcParams = (params || []).map(v => this.sanitizeODBCValue(v)); // Check if query uses @p1, @p2 style parameters if (sql.includes('@p')) { // Convert @p1, @p2, @p3 to ? const paramPattern = /@p(\d+)/g; const paramMap = new Map(); // Maps @p number to ? position let questionMarkIndex = 0; odbcQuery = sql.replace(paramPattern, (match, num) => { const paramNum = parseInt(num); if (!paramMap.has(paramNum)) { paramMap.set(paramNum, questionMarkIndex++); } return '?'; }); // Reorder params based on @p numbers (1-indexed) if (params && params.length > 0) { const reorderedParams = []; for (let i = 1; i <= params.length; i++) { reorderedParams.push(params[i - 1]); } odbcParams = reorderedParams; } logger_1.logger.debug('mssql', '📊 Converted to ODBC Query:', odbcQuery); } const result = await pool.query(odbcQuery, odbcParams); return result; } } exports.MSSQLNativeAdapter = MSSQLNativeAdapter; exports.MSSQLAdapter = MSSQLNativeAdapter; //# sourceMappingURL=mssql.js.map