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