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

366 lines 16.1 kB
"use strict"; 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