@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
366 lines • 16.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PostgresAdapter = void 0;
const pg_1 = require("pg");
const logger_1 = require("../utils/logger");
class PostgresAdapter {
pool;
modelMeta = new Map(); // Store model metadata for @map support
capabilities = {
transactions: true,
bulkByIds: true,
maxIn: 65000,
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);
}
}
constructor(connectionString) {
if (typeof connectionString === 'string') {
// Simple connection string
this.pool = new pg_1.Pool({
connectionString,
max: 10, // Default pool size
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000
});
}
else {
// Full config with pool options
this.pool = new pg_1.Pool({
connectionString: connectionString.connectionString,
max: connectionString.max ?? 10,
min: connectionString.min ?? 0,
idleTimeoutMillis: connectionString.idleTimeoutMillis ?? 30000,
connectionTimeoutMillis: connectionString.connectionTimeoutMillis ?? 2000
});
logger_1.logger.info('postgres', `Connection pool configured: max=${connectionString.max ?? 10}, min=${connectionString.min ?? 0}`);
}
}
async findMany(model, args) {
const { sql, params } = this.buildSelectQuery(model, args);
logger_1.logger.debug('postgres', '📋 SQL Query:', sql);
logger_1.logger.debug('postgres', '📋 Parameters:', JSON.stringify(params));
const result = await this.pool.query(sql, params);
return result.rows;
}
async findUnique(model, args) {
const { sql, params } = this.buildSelectQuery(model, {
where: args.where,
select: args.select,
take: 1
});
logger_1.logger.debug('postgres', '📋 SQL Query:', sql);
logger_1.logger.debug('postgres', '📋 Parameters:', JSON.stringify(params));
const result = await this.pool.query(sql, params);
return result.rows[0] || null;
}
async create(model, args) {
const { sql, params } = this.buildInsertQuery(model, args.data);
logger_1.logger.debug('postgres', '📋 SQL Query:', sql);
logger_1.logger.debug('postgres', '📋 Parameters:', JSON.stringify(params));
const result = await this.pool.query(sql, params);
return result.rows[0];
}
async update(model, args) {
const { sql, params } = this.buildUpdateQuery(model, args.where, args.data);
logger_1.logger.debug('postgres', '📋 SQL Query:', sql);
logger_1.logger.debug('postgres', '📋 Parameters:', JSON.stringify(params));
const result = await this.pool.query(sql, params);
return result.rows[0];
}
async delete(model, args) {
const { sql, params } = this.buildDeleteQuery(model, args.where);
logger_1.logger.debug('postgres', '📋 SQL Query:', sql);
logger_1.logger.debug('postgres', '📋 Parameters:', JSON.stringify(params));
const result = await this.pool.query(sql, params);
return result.rows[0];
}
async count(model, args) {
const { sql, params } = this.buildCountQuery(model, args);
logger_1.logger.debug('postgres', '📋 SQL Query:', sql);
logger_1.logger.debug('postgres', '📋 Parameters:', JSON.stringify(params));
const result = await this.pool.query(sql, params);
return parseInt(result.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, params.length, 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.length + 1}`;
params.push(args.take);
}
if (args.skip !== undefined) {
sql += ` OFFSET $${params.length + 1}`;
params.push(args.skip);
}
return { sql, 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, whereParams } = this.buildWhereClause(w, paramIdx, modelName);
paramIdx += whereParams.length;
params.push(...whereParams);
return `(${clause})`;
});
conditions.push(`(${subConditions.join(' AND ')})`);
}
else if (field === 'OR') {
const subConditions = value.map(w => {
const { clause, whereParams } = this.buildWhereClause(w, paramIdx, modelName);
paramIdx += whereParams.length;
params.push(...whereParams);
return `(${clause})`;
});
conditions.push(`(${subConditions.join(' OR ')})`);
}
else if (field === 'NOT') {
const { clause, whereParams } = this.buildWhereClause(value, paramIdx, modelName);
paramIdx += whereParams.length;
params.push(...whereParams);
conditions.push(`NOT (${clause})`);
}
else {
const columnName = this.toColumnName(field, modelName);
if (typeof value === 'object' && value !== null) {
// Filter operators
for (const [op, filterValue] of Object.entries(value)) {
const condition = this.buildOperatorCondition(columnName, op, filterValue, paramIdx);
if (condition) {
conditions.push(condition.clause);
params.push(...condition.params);
paramIdx += condition.params.length;
}
}
}
else {
// Simple equality
conditions.push(`${columnName} = $${paramIdx + 1}`);
params.push(value);
paramIdx++;
}
}
}
return { clause: conditions.join(' AND '), whereParams: params };
}
buildOperatorCondition(column, op, value, paramIdx) {
const idx = paramIdx + 1;
switch (op) {
case 'eq':
return { clause: `${column} = $${idx}`, params: [value] };
case 'ne':
return { clause: `${column} != $${idx}`, params: [value] };
case 'in':
return { clause: `${column} = ANY($${idx})`, params: [value] };
case 'notIn':
return { clause: `${column} != ALL($${idx})`, params: [value] };
case 'gt':
return { clause: `${column} > $${idx}`, params: [value] };
case 'gte':
return { clause: `${column} >= $${idx}`, params: [value] };
case 'lt':
return { clause: `${column} < $${idx}`, params: [value] };
case 'lte':
return { clause: `${column} <= $${idx}`, params: [value] };
case 'contains':
return { clause: `${column} ILIKE $${idx}`, params: [`%${value}%`] };
case 'startsWith':
return { clause: `${column} ILIKE $${idx}`, params: [`${value}%`] };
case 'endsWith':
return { clause: `${column} ILIKE $${idx}`, params: [`%${value}`] };
default:
return null;
}
}
buildInsertQuery(model, data) {
const tableName = this.toSnakeCase(model);
const columns = [];
const placeholders = [];
const params = [];
let paramIdx = 1;
for (const [field, value] of Object.entries(data)) {
columns.push(this.toColumnName(field, model));
placeholders.push(`$${paramIdx++}`);
params.push(value);
}
const sql = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
return { sql, params };
}
buildUpdateQuery(model, where, data) {
const tableName = this.toSnakeCase(model);
const setClauses = [];
const params = [];
let paramIdx = 1;
for (const [field, value] of Object.entries(data)) {
setClauses.push(`${this.toColumnName(field, model)} = $${paramIdx++}`);
params.push(value);
}
const { clause: whereClause, whereParams } = this.buildWhereClause(where, params.length, model);
params.push(...whereParams);
const sql = `UPDATE ${tableName} SET ${setClauses.join(', ')} WHERE ${whereClause} RETURNING *`;
return { sql, params };
}
buildDeleteQuery(model, where) {
const tableName = this.toSnakeCase(model);
const { clause: whereClause, whereParams } = this.buildWhereClause(where, 0, model);
const sql = `DELETE FROM ${tableName} WHERE ${whereClause} RETURNING *`;
return { sql, params: whereParams };
}
buildCountQuery(model, args) {
const tableName = this.toSnakeCase(model);
let sql = `SELECT COUNT(*) as count FROM ${tableName}`;
const params = [];
if (args.where) {
const { clause, whereParams } = this.buildWhereClause(args.where, 0, model);
if (clause) {
sql += ` WHERE ${clause}`;
params.push(...whereParams);
}
}
return { sql, params };
}
/**
* 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 snake_case convention
return this.toSnakeCase(field);
}
toSnakeCase(str) {
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`).replace(/^_/, '');
}
async beginTransaction(options) {
const client = await this.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 PostgreSQL transaction ${txId} with isolation level: ${isolationLevel}`);
// Begin transaction with isolation level
await client.query(`BEGIN ISOLATION LEVEL ${isolationLevel}`);
// Create transaction object
const transaction = {
id: txId,
async commit() {
logger_1.logger.debug('runtime', `Committing PostgreSQL transaction ${txId}`);
await client.query('COMMIT');
client.release();
},
async rollback() {
logger_1.logger.debug('runtime', `Rolling back PostgreSQL transaction ${txId}`);
try {
await client.query('ROLLBACK');
}
catch (error) {
logger_1.logger.error('runtime', `Error rolling back PostgreSQL transaction ${txId}: ${error.message}`);
}
finally {
client.release();
}
},
// CRUD operations within transaction use the client connection
async findMany(model, args) {
const { sql, params } = self.buildSelectQuery(model, args);
logger_1.logger.debug('postgres', '📋 TX SQL Query:', sql);
const result = await client.query(sql, params);
return result.rows;
},
async findUnique(model, args) {
const { sql, params } = self.buildSelectQuery(model, {
where: args.where,
select: args.select,
take: 1
});
logger_1.logger.debug('postgres', '📋 TX SQL Query:', sql);
const result = await client.query(sql, params);
return result.rows[0] || null;
},
async create(model, args) {
const { sql, params } = self.buildInsertQuery(model, args.data);
logger_1.logger.debug('postgres', '📋 TX SQL Query:', sql);
const result = await client.query(sql, params);
return result.rows[0];
},
async update(model, args) {
const { sql, params } = self.buildUpdateQuery(model, args.where, args.data);
logger_1.logger.debug('postgres', '📋 TX SQL Query:', sql);
const result = await client.query(sql, params);
return result.rows[0];
},
async delete(model, args) {
const { sql, params } = self.buildDeleteQuery(model, args);
logger_1.logger.debug('postgres', '📋 TX SQL Query:', sql);
const result = await client.query(sql, params);
return result.rows[0];
},
async count(model, args) {
const { sql, params } = self.buildCountQuery(model, args);
logger_1.logger.debug('postgres', '📋 TX SQL Query:', sql);
const result = await client.query(sql, params);
return parseInt(result.rows[0].count, 10);
}
};
return transaction;
}
async close() {
await this.pool.end();
}
/**
* Execute a raw SQL query
* @param sql - The SQL query string
* @param params - Optional query parameters
* @returns Query result rows
*/
async query(sql, params) {
logger_1.logger.debug('postgres', '📋 Raw SQL Query:', sql);
logger_1.logger.debug('postgres', '📋 Parameters:', JSON.stringify(params || []));
const result = await this.pool.query(sql, params || []);
return result.rows;
}
}
exports.PostgresAdapter = PostgresAdapter;
//# sourceMappingURL=postgres.js.map