From 9d25b3e6a9e3d98c426ac47d9fb2a579a5827537 Mon Sep 17 00:00:00 2001 From: aserrador Date: Fri, 3 Apr 2026 23:35:10 -0500 Subject: [PATCH] fix: port by default 443 --- examples/basic-connection.ts | 260 ++++---- src/client.ts | 1213 ++++++++++++++++------------------ 2 files changed, 721 insertions(+), 752 deletions(-) diff --git a/examples/basic-connection.ts b/examples/basic-connection.ts index b865acb..d12aea8 100644 --- a/examples/basic-connection.ts +++ b/examples/basic-connection.ts @@ -18,155 +18,161 @@ import { TrytonClient } from "../src/client"; // ==================================================== // CONFIGURACIÓN - EDITA ESTOS VALORES CON TUS DATOS // ==================================================== +// const config = { +// hostname: "https://demo7.6.tryton.org", // Servidor demo con HTTPS +// port: 8000, // Puerto del servidor Tryton (generalmente 8000) +// database: "demo7.6", // Base de datos demo +// username: "admin", // Usuario demo +// password: "admin", // Contraseña demo +// language: "es", // Idioma (es, en, etc.) +// options: { +// verbose: true, // Mostrar información detallada de conexión +// timeout: 30000, // Timeout en milisegundos +// }, +// }; + const config = { - hostname: "https://demo7.6.tryton.org", // Servidor demo con HTTPS - port: 8000, // Puerto del servidor Tryton (generalmente 8000) - database: "demo7.6", // Base de datos demo - username: "admin", // Usuario demo - password: "admin", // Contraseña demo - language: "es", // Idioma (es, en, etc.) - options: { - verbose: true, // Mostrar información detallada de conexión - timeout: 30000, // Timeout en milisegundos - }, + hostname: "demo.naliia.co", // Servidor demo con HTTPS + port: 80, // Puerto del servidor Tryton (generalmente 8000) + database: "naliia", // Base de datos demo + username: "admin", // Usuario demo + password: "S0p0rt3f1n4l*", // Contraseña demo + language: "es", // Idioma (es, en, etc.) + options: { + verbose: true, // Mostrar información detallada de conexión + timeout: 30000, // Timeout en milisegundos + }, }; // Verificar que se han completado los datos de configuración function validateConfig() { - const requiredFields: (keyof typeof config)[] = [ - "hostname", - "database", - "username", - "password", - ]; - const missingFields = requiredFields.filter((field) => !config[field]); + const requiredFields: (keyof typeof config)[] = [ + "hostname", + "database", + "username", + "password", + ]; + const missingFields = requiredFields.filter((field) => !config[field]); - if (missingFields.length > 0) { - console.error( - "❌ Error: Faltan los siguientes campos de configuración:" - ); - missingFields.forEach((field) => console.error(` - ${field}`)); - console.error( - "\n💡 Edita este archivo y completa la configuración antes de ejecutar." - ); - process.exit(1); - } + if (missingFields.length > 0) { + console.error("❌ Error: Faltan los siguientes campos de configuración:"); + missingFields.forEach((field) => console.error(` - ${field}`)); + console.error( + "\n💡 Edita este archivo y completa la configuración antes de ejecutar.", + ); + process.exit(1); + } } // Interfaz para los terceros (party.party) interface Party { - id: number; - name: string; - code?: string; - active: boolean; - lang?: string; + id: number; + name: string; + code?: string; + active: boolean; + lang?: string; } async function main() { - console.log("🚀 Iniciando ejemplo de conexión con Tryton...\n"); + console.log("🚀 Iniciando ejemplo de conexión con Tryton...\n"); - // Validar configuración - validateConfig(); + // Validar configuración + validateConfig(); - // Crear cliente - console.log("📡 Creando cliente Tryton..."); - const client = new TrytonClient(config); + // Crear cliente + console.log("📡 Creando cliente Tryton..."); + const client = new TrytonClient(config); - try { - // Conectar al servidor - console.log("🔗 Conectando al servidor..."); - const connected = await client.connect(); + try { + // Conectar al servidor + console.log("🔗 Conectando al servidor..."); + const connected = await client.connect(); - if (!connected) { - throw new Error("No se pudo establecer la conexión"); - } - - console.log("✅ Conexión exitosa!\n"); - - // ==================================================== - // INFORMACIÓN DEL USUARIO Y SESIÓN - // ==================================================== - console.log("👤 INFORMACIÓN DEL USUARIO"); - console.log("=".repeat(40)); - - // Mostrar información básica del usuario - console.log(`Usuario: ${config.username}`); - console.log(`Base de datos: ${config.database}`); - console.log(`Servidor: ${config.hostname}:${config.port}`); - - // Mostrar token de sesión - const sessionToken = client.getSession(); - console.log(`Token de sesión: ${sessionToken}`); - console.log(""); - - // ==================================================== - // BÚSQUEDA DE TERCEROS - // ==================================================== - console.log("🏢 LISTA DE TERCEROS"); - console.log("=".repeat(40)); - - // Buscar todos los terceros activos (limitado a 20 para el ejemplo) - console.log("Buscando terceros..."); - const partiesData = await client.searchRead( - "party.party", // Modelo de terceros - [], // Dominio: todos los terceros (sin filtros) - ["id", "name", "code"], // Campos a obtener - 0, // Offset - 20 // Límite de resultados - // Sin parámetro order - ); - - if (partiesData.length === 0) { - console.log("❌ No se encontraron terceros."); - } else { - console.log(`✅ Se encontraron ${partiesData.length} terceros:\n`); - - // Mostrar lista de terceros - partiesData.forEach((party, index) => { - const code = party.code ? ` (${party.code})` : ""; - console.log( - `${(index + 1).toString().padStart(2)}. ${ - party.name - }${code}` - ); - }); - } - - console.log("\n🎉 ¡Ejemplo completado exitosamente!"); - } catch (error) { - console.error("❌ Error durante la ejecución:"); - const errorMessage = - error instanceof Error ? error.message : String(error); - console.error(errorMessage); - - if (config.options.verbose && error instanceof Error && error.stack) { - console.error("\nStack trace:"); - console.error(error.stack); - } - - // Sugerencias de solución de problemas - console.error("\n💡 Posibles soluciones:"); - console.error(" - Verifica que el servidor Tryton esté ejecutándose"); - console.error(" - Confirma que los datos de conexión sean correctos"); - console.error( - " - Asegúrate de que el usuario tenga permisos adecuados" - ); - console.error(" - Verifica la conectividad de red al servidor"); - - process.exit(1); - } finally { - // Cerrar la conexión si está abierta - if (client && client.isConnected) { - console.log("\n🔌 Cerrando conexión..."); - client.close(); - console.log("\n✅ Conexión cerrada."); - } + if (!connected) { + throw new Error("No se pudo establecer la conexión"); } + + console.log("✅ Conexión exitosa!\n"); + + // ==================================================== + // INFORMACIÓN DEL USUARIO Y SESIÓN + // ==================================================== + console.log("👤 INFORMACIÓN DEL USUARIO"); + console.log("=".repeat(40)); + + // Mostrar información básica del usuario + console.log(`Usuario: ${config.username}`); + console.log(`Base de datos: ${config.database}`); + console.log(`Servidor: ${config.hostname}:${config.port}`); + + // Mostrar token de sesión + const sessionToken = client.getSession(); + console.log(`Token de sesión: ${sessionToken}`); + console.log(""); + + // ==================================================== + // BÚSQUEDA DE TERCEROS + // ==================================================== + console.log("🏢 LISTA DE TERCEROS"); + console.log("=".repeat(40)); + + // Buscar todos los terceros activos (limitado a 20 para el ejemplo) + console.log("Buscando terceros..."); + const partiesData = await client.searchRead( + "party.party", // Modelo de terceros + [], // Dominio: todos los terceros (sin filtros) + ["id", "name", "code"], // Campos a obtener + 0, // Offset + 20, // Límite de resultados + // Sin parámetro order + ); + + if (partiesData.length === 0) { + console.log("❌ No se encontraron terceros."); + } else { + console.log(`✅ Se encontraron ${partiesData.length} terceros:\n`); + + // Mostrar lista de terceros + partiesData.forEach((party, index) => { + const code = party.code ? ` (${party.code})` : ""; + console.log( + `${(index + 1).toString().padStart(2)}. ${party.name}${code}`, + ); + }); + } + + console.log("\n🎉 ¡Ejemplo completado exitosamente!"); + } catch (error) { + console.error("❌ Error durante la ejecución:"); + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(errorMessage); + + if (config.options.verbose && error instanceof Error && error.stack) { + console.error("\nStack trace:"); + console.error(error.stack); + } + + // Sugerencias de solución de problemas + console.error("\n💡 Posibles soluciones:"); + console.error(" - Verifica que el servidor Tryton esté ejecutándose"); + console.error(" - Confirma que los datos de conexión sean correctos"); + console.error(" - Asegúrate de que el usuario tenga permisos adecuados"); + console.error(" - Verifica la conectividad de red al servidor"); + + process.exit(1); + } finally { + // Cerrar la conexión si está abierta + if (client && client.isConnected) { + console.log("\n🔌 Cerrando conexión..."); + client.close(); + console.log("\n✅ Conexión cerrada."); + } + } } // Ejecutar el ejemplo si se llama directamente if (require.main === module) { - main().catch(console.error); + main().catch(console.error); } export { main }; diff --git a/src/client.ts b/src/client.ts index 9236d17..61f7014 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,654 +11,617 @@ import { ServerProxyOptions, ServerPoolOptions } from "../types"; const CONNECT_TIMEOUT = 5000; // 5 seconds const DEFAULT_TIMEOUT = 30000; // 30 seconds import { - TrytonClientConfig, - TrytonClientOptions, - SearchDomain, - TrytonContext, - TrytonRecord, - TrytonUser, - UserPreferences, - LoginResult, - RecordId, - RecordIds, - FieldName, - ModelName, - ClientConfig, - TrytonMethodCall, - TypedModelOperations, + TrytonClientConfig, + TrytonClientOptions, + SearchDomain, + TrytonContext, + TrytonRecord, + TrytonUser, + UserPreferences, + LoginResult, + RecordId, + RecordIds, + FieldName, + ModelName, + ClientConfig, + TrytonMethodCall, + TypedModelOperations, } from "../types"; /** * Main client class for connecting to Tryton server via RPC */ export class TrytonClient { - private hostname: string; - private database: string; - private username: string; - private password: string; - private port: number; - private language: string; - private options: TrytonClientOptions; - private useHttps: boolean; - private connection: ServerPool | null; - private session: string | null; + private hostname: string; + private database: string; + private username: string; + private password: string; + private port: number; + private language: string; + private options: TrytonClientOptions; + private useHttps: boolean; + private connection: ServerPool | null; + private session: string | null; - /** - * Create a new Tryton client - * @param config Configuration object - * @param config.hostname - Hostname or IP address. Can include port (e.g., "46.62.242.210:8090") - * @param config.port - Port number (ignored if hostname includes port) - */ - constructor(config: TrytonClientConfig) { - const { - hostname, - database, - username, - password, - port = 8000, - language = "en", - options = {}, - } = config; + /** + * Create a new Tryton client + * @param config Configuration object + * @param config.hostname - Hostname or IP address. Can include port (e.g., "46.62.242.210:8090") + * @param config.port - Port number (ignored if hostname includes port) + */ + constructor(config: TrytonClientConfig) { + const { + hostname, + database, + username, + password, + port = 443, + language = "en", + options = {}, + } = config; - // Extract protocol from hostname if present - if (hostname.startsWith("https://")) { - this.hostname = hostname.replace("https://", ""); - this.useHttps = true; - } else if (hostname.startsWith("http://")) { - this.hostname = hostname.replace("http://", ""); - this.useHttps = false; - } else { - this.hostname = hostname; - this.useHttps = port === 443 || port === 8443; - } + // Extract protocol from hostname if present + if (hostname.startsWith("https://")) { + this.hostname = hostname.replace("https://", ""); + this.useHttps = true; + } else if (hostname.startsWith("http://")) { + this.hostname = hostname.replace("http://", ""); + this.useHttps = false; + } else { + this.hostname = hostname; + this.useHttps = port === 443 || port === 8443; + } - // Handle IP:Port format (e.g., "46.62.242.210:8090") - if (this.hostname.includes(":") && !this.hostname.startsWith("http")) { - const [ip, hostPort] = this.hostname.split(":"); - this.hostname = ip; - this.port = parseInt(hostPort, 10); - // Determine https based on extracted port - this.useHttps = this.port === 443 || this.port === 8443; - } else { - this.port = port; - } + // Handle IP:Port format (e.g., "46.62.242.210:8090") + if (this.hostname.includes(":") && !this.hostname.startsWith("http")) { + const [ip, hostPort] = this.hostname.split(":"); + this.hostname = ip; + this.port = parseInt(hostPort, 10); + // Determine https based on extracted port + this.useHttps = this.port === 443 || this.port === 8443; + } else { + this.port = port; + } - this.database = database; - this.username = username; - this.password = password || ""; // Empty string if not provided - this.language = language; - this.options = options; - this.connection = null; + this.database = database; + this.username = username; + this.password = password || ""; // Empty string if not provided + this.language = language; + this.options = options; + this.connection = null; + this.session = null; + } + + /** + * Alternative constructor for backward compatibility + * Note: If hostname includes port (IP:Port), the port parameter will be ignored + */ + static create( + hostname: string, + database: string, + username: string, + password: string, + port: number = 8000, + language: string = "en", + ): TrytonClient { + return new TrytonClient({ + hostname, + database, + username, + password, + port, + language, + }); + } + + /** + * Connect to Tryton server and authenticate + * @returns {Promise} - True if connection successful + * @throws {Error} - If connection or authentication fails + */ + async connect(): Promise { + try { + const proxy = new ServerProxy(this.hostname, this.port, this.database, { + verbose: this.options.verbose || false, + connectTimeout: this.options.connectTimeout || CONNECT_TIMEOUT, + timeout: this.options.timeout || DEFAULT_TIMEOUT, + useHttps: this.useHttps, + }); + + // Perform login + const parameters = { + password: this.password, + }; + + const result = await proxy.request("common.db.login", [ + this.username, + parameters, + this.language, + ]); + + proxy.close(); + + this.session = [this.username, ...result].join(":"); + + // Create connection pool with session + this.connection = new ServerPool( + this.hostname, + this.port, + this.database, + { + session: this.session, + cache: this.options.cache !== false ? [] : null, // Enable cache by default + verbose: this.options.verbose || false, + connectTimeout: this.options.connectTimeout || CONNECT_TIMEOUT, + timeout: this.options.timeout || DEFAULT_TIMEOUT, + keepMax: this.options.keepMax || 4, + useHttps: this.useHttps, + }, + ); + + return true; + } catch (error) { + throw new Error(`Connection failed: ${(error as Error).message}`); + } + } + + /** + * Setup connection pool without authentication + * Used when restoring an existing session + * @returns {Promise} + */ + async setupConnection(): Promise { + this.connection = new ServerPool(this.hostname, this.port, this.database, { + session: undefined, // Session will be set by restoreSession() + cache: this.options.cache !== false ? [] : null, + verbose: this.options.verbose || false, + connectTimeout: this.options.connectTimeout || CONNECT_TIMEOUT, + timeout: this.options.timeout || DEFAULT_TIMEOUT, + keepMax: this.options.keepMax || 4, + useHttps: this.useHttps, + }); + } + + /** + * Call RPC method on server + * @param {string} methodName - RPC method name (e.g., 'model.party.party.read') + * @param {Array} args - Method arguments + * @returns {Promise<*>} - Method result + * @throws {Error} - If not connected or method call fails + */ + async call(methodName: string, args: any[] = []): Promise { + if (!this.connection) { + throw new Error("Not connected. Call connect() first."); + } + + return this.connection.withConnection(async (conn) => { + return conn.request(methodName, args); + }); + } + + /** + * Call multiple RPC methods in sequence + * @param {Array<{method: string, args: Array}>} calls - Array of method calls + * @returns {Promise} - Array of results + */ + async callMultiple(calls: TrytonMethodCall[]): Promise { + const results: any[] = []; + for (const call of calls) { + const result = await this.call(call.method, call.args); + results.push(result); + } + return results; + } + + /** + * Call multiple RPC methods in parallel + * @param {Array<{method: string, args: Array}>} calls - Array of method calls + * @returns {Promise} - Array of results + */ + async callParallel(calls: TrytonMethodCall[]): Promise { + const promises = calls.map((call) => this.call(call.method, call.args)); + return Promise.all(promises); + } + + /** + * Helper method to read records from a model + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} ids - Record IDs to read + * @param {Array} fields - Fields to read + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} - Array of records + */ + async read( + model: ModelName, + ids: RecordIds, + fields: FieldName[], + context: TrytonContext = {}, + ): Promise { + return this.call(`model.${model}.read`, [ids, fields, context]); + } + + /** + * Helper method to create records in a model + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} records - Records to create + * @param {Object} [context={}] - Context dictionary + * @returns {Promise>} - Array of created record IDs + */ + async create( + model: ModelName, + records: Record[], + context: TrytonContext = {}, + ): Promise { + return this.call(`model.${model}.create`, [records, context]); + } + + /** + * Helper method to write/update records in a model + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} ids - Record IDs to update + * @param {Object} values - Values to update + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} + */ + async write( + model: ModelName, + ids: RecordIds, + values: Record, + context: TrytonContext = {}, + ): Promise { + return this.call(`model.${model}.write`, [ids, values, context]); + } + + /** + * Helper method to delete records from a model + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} ids - Record IDs to delete + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} + */ + async delete( + model: ModelName, + ids: RecordIds, + context: TrytonContext = {}, + ): Promise { + return this.call(`model.${model}.delete`, [ids, context]); + } + + /** + * Helper method to search for records + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} domain - Search domain + * @param {number} [offset=0] - Offset for pagination + * @param {number} [limit=null] - Limit for pagination + * @param {Array} [order=null] - Order specification + * @param {Object} [context={}] - Context dictionary + * @returns {Promise>} - Array of record IDs + */ + async search( + model: ModelName, + domain: SearchDomain, + offset: number = 0, + limit: number | null = null, + order: string[] | null = null, + context: TrytonContext = {}, + ): Promise { + return this.call(`model.${model}.search`, [ + domain, + offset, + limit, + order, + context, + ]); + } + + /** + * Helper method to search and read records in one call + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} domain - Search domain + * @param {Array} fields - Fields to read + * @param {number} [offset=0] - Offset for pagination + * @param {number} [limit=null] - Limit for pagination + * @param {Array} [order=null] - Order specification + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} - Array of records + */ + async searchRead( + model: ModelName, + domain: SearchDomain, + fields: FieldName[], + offset: number = 0, + limit: number | null = null, + order: string[] | null = null, + context: TrytonContext = {}, + ): Promise { + return this.call(`model.${model}.search_read`, [ + domain, + offset, + limit, + order, + fields, + context, + ]); + } + + /** + * Helper method to count records + * @param {string} model - Model name (e.g., 'party.party') + * @param {Array} domain - Search domain + * @param {Object} [context={}] - Context dictionary + * @returns {Promise} - Number of records + */ + async searchCount( + model: ModelName, + domain: SearchDomain, + context: TrytonContext = {}, + ): Promise { + return this.call(`model.${model}.search_count`, [domain, context]); + } + + /** + * Get database information + * @returns {Promise} - Database info + */ + async getDatabaseInfo(): Promise { + return this.call("common.db.get_info", []); + } + + /** + * List available databases + * @returns {Promise>} - Database names + */ + async listDatabases(): Promise { + return this.call("common.db.list", []); + } + + /** + * Get server version + * @returns {Promise} - Server version + */ + async getVersion(): Promise { + return this.call("common.version", []); + } + + /** + * Get user preferences + */ + async getUserPreferences(): Promise { + return this.call("model.res.user.get_preferences", [ + true, + {}, + ]); + } + + /** + * Get current user information + */ + async getCurrentUser(): Promise { + const preferences = await this.getUserPreferences(); + const users = await this.read( + "res.user", + [preferences.id], + ["id", "name", "login", "language", "company", "email"], + ); + if (!users || users.length === 0) { + throw new Error("User not found"); + } + return users[0]!; + } + + /** + * Clear cache for specific prefix or all + * @param {string} [prefix] - Optional prefix to clear + */ + clearCache(prefix?: string): void { + if (this.connection) { + this.connection.clearCache(prefix); + } + } + + /** + * Get connection SSL status + * @returns {boolean|null} - SSL status or null if not connected + */ + get ssl(): boolean | null { + return this.connection ? this.connection.ssl : null; + } + + /** + * Get connection URL + * @returns {string|null} - Connection URL or null if not connected + */ + get url(): string | null { + return this.connection ? this.connection.url : null; + } + + /** + * Check if client is connected + * @returns {boolean} - True if connected + */ + get isConnected(): boolean { + return this.connection !== null; + } + + /** + * Get current session + * @returns {string|null} - Session string or null if not connected + */ + getSession(): string | null { + return this.session; + } + + /** + * Get current user ID from session + */ + getUserId(): number | null { + if (!this.session) return null; + const parts = this.session.split(":"); + return parts.length >= 2 ? parseInt(parts[1], 10) : null; + } + + /** + * Get session token from session + */ + getSessionToken(): string | null { + if (!this.session) return null; + const parts = this.session.split(":"); + return parts.length >= 3 ? parts[2] : null; + } + + /** + * Get username + */ + getUsername(): string { + return this.username; + } + + /** + * Close connection and cleanup resources + */ + close(): void { + if (this.connection) { + this.connection.close(); + this.connection = null; + this.session = null; + } + } + + /** + * Create a new client instance with the same configuration + * @returns {TrytonClient} - New client instance + */ + clone(): TrytonClient { + return new TrytonClient({ + hostname: this.hostname, + database: this.database, + username: this.username, + password: this.password, + port: this.port, + language: this.language, + options: { ...this.options }, + }); + } + + /** + * Get client configuration (without sensitive data) + * @returns {Object} - Client configuration + */ + getConfig(): ClientConfig { + return { + hostname: this.hostname, + database: this.database, + username: this.username, + port: this.port, + language: this.language, + isConnected: this.isConnected, + ssl: this.ssl, + url: this.url, + }; + } + + /** + * Restore session using saved session token + * @param sessionToken - Previously saved session string (format: "username:userId:sessionKey") + * @returns Promise - True if session is valid + */ + async restoreSession(sessionToken: string): Promise { + try { + // Parsear el token guardado + const parts = sessionToken.split(":"); + if (parts.length < 3) { + throw new Error("Invalid session token format"); + } + + const [username, userId, ...sessionKeyParts] = parts; + const sessionKey = sessionKeyParts.join(":"); // Por si el sessionKey tiene ":" + + // Establecer la sesión sin hacer login + this.session = sessionToken; + + // Actualizar la sesión en el connection pool ANTES de obtener proxy + if (this.connection) { + this.connection.setSession(sessionToken); + } else { + throw new Error("No connection pool available"); + } + + // Obtener proxy (ahora con la sesión ya actualizada) + const proxy = this.connection.getConnection(); + + // Test de validación de sesión + try { + // Usar un método que requiere autenticación para validar la sesión + await proxy.request("model.res.user.get_preferences", [true, {}]); + this.connection.putConnection(proxy); // Devolver conexión al pool + return true; + } catch (error) { + // Sesión inválida o expirada + console.error("❌ Error en validación:", error); this.session = null; + this.connection.putConnection(proxy); // Devolver conexión al pool + return false; + } + } catch (error) { + console.error("❌ Error restaurando sesión:", error); + this.session = null; + return false; } + } - /** - * Alternative constructor for backward compatibility - * Note: If hostname includes port (IP:Port), the port parameter will be ignored - */ - static create( - hostname: string, - database: string, - username: string, - password: string, - port: number = 8000, - language: string = "en" - ): TrytonClient { - return new TrytonClient({ - hostname, - database, - username, - password, - port, - language, - }); - } + /** + * Type-safe model operations factory + * Creates a typed interface for specific models + */ + model( + modelName: ModelName, + ): TypedModelOperations { + return { + read: (ids: RecordIds, fields: FieldName[], context?: TrytonContext) => + this.read(modelName, ids, fields, context), - /** - * Connect to Tryton server and authenticate - * @returns {Promise} - True if connection successful - * @throws {Error} - If connection or authentication fails - */ - async connect(): Promise { - try { - const proxy = new ServerProxy( - this.hostname, - this.port, - this.database, - { - verbose: this.options.verbose || false, - connectTimeout: - this.options.connectTimeout || CONNECT_TIMEOUT, - timeout: this.options.timeout || DEFAULT_TIMEOUT, - useHttps: this.useHttps, - } - ); + create: (records: Partial>[], context?: TrytonContext) => + this.create(modelName, records as Record[], context), - // Perform login - const parameters = { - password: this.password, - }; - - const result = await proxy.request("common.db.login", [ - this.username, - parameters, - this.language, - ]); - - proxy.close(); - - this.session = [this.username, ...result].join(":"); - - // Create connection pool with session - this.connection = new ServerPool( - this.hostname, - this.port, - this.database, - { - session: this.session, - cache: this.options.cache !== false ? [] : null, // Enable cache by default - verbose: this.options.verbose || false, - connectTimeout: - this.options.connectTimeout || CONNECT_TIMEOUT, - timeout: this.options.timeout || DEFAULT_TIMEOUT, - keepMax: this.options.keepMax || 4, - useHttps: this.useHttps, - } - ); - - return true; - } catch (error) { - throw new Error(`Connection failed: ${(error as Error).message}`); - } - } - - /** - * Setup connection pool without authentication - * Used when restoring an existing session - * @returns {Promise} - */ - async setupConnection(): Promise { - this.connection = new ServerPool( - this.hostname, - this.port, - this.database, - { - session: undefined, // Session will be set by restoreSession() - cache: this.options.cache !== false ? [] : null, - verbose: this.options.verbose || false, - connectTimeout: this.options.connectTimeout || CONNECT_TIMEOUT, - timeout: this.options.timeout || DEFAULT_TIMEOUT, - keepMax: this.options.keepMax || 4, - useHttps: this.useHttps, - } - ); - } - - /** - * Call RPC method on server - * @param {string} methodName - RPC method name (e.g., 'model.party.party.read') - * @param {Array} args - Method arguments - * @returns {Promise<*>} - Method result - * @throws {Error} - If not connected or method call fails - */ - async call(methodName: string, args: any[] = []): Promise { - if (!this.connection) { - throw new Error("Not connected. Call connect() first."); - } - - return this.connection.withConnection(async (conn) => { - return conn.request(methodName, args); - }); - } - - /** - * Call multiple RPC methods in sequence - * @param {Array<{method: string, args: Array}>} calls - Array of method calls - * @returns {Promise} - Array of results - */ - async callMultiple(calls: TrytonMethodCall[]): Promise { - const results: any[] = []; - for (const call of calls) { - const result = await this.call(call.method, call.args); - results.push(result); - } - return results; - } - - /** - * Call multiple RPC methods in parallel - * @param {Array<{method: string, args: Array}>} calls - Array of method calls - * @returns {Promise} - Array of results - */ - async callParallel(calls: TrytonMethodCall[]): Promise { - const promises = calls.map((call) => this.call(call.method, call.args)); - return Promise.all(promises); - } - - /** - * Helper method to read records from a model - * @param {string} model - Model name (e.g., 'party.party') - * @param {Array} ids - Record IDs to read - * @param {Array} fields - Fields to read - * @param {Object} [context={}] - Context dictionary - * @returns {Promise} - Array of records - */ - async read( - model: ModelName, + write: ( ids: RecordIds, - fields: FieldName[], - context: TrytonContext = {} - ): Promise { - return this.call(`model.${model}.read`, [ids, fields, context]); - } + values: Partial>, + context?: TrytonContext, + ) => this.write(modelName, ids, values as Record, context), - /** - * Helper method to create records in a model - * @param {string} model - Model name (e.g., 'party.party') - * @param {Array} records - Records to create - * @param {Object} [context={}] - Context dictionary - * @returns {Promise>} - Array of created record IDs - */ - async create( - model: ModelName, - records: Record[], - context: TrytonContext = {} - ): Promise { - return this.call(`model.${model}.create`, [ - records, - context, - ]); - } + delete: (ids: RecordIds, context?: TrytonContext) => + this.delete(modelName, ids, context), - /** - * Helper method to write/update records in a model - * @param {string} model - Model name (e.g., 'party.party') - * @param {Array} ids - Record IDs to update - * @param {Object} values - Values to update - * @param {Object} [context={}] - Context dictionary - * @returns {Promise} - */ - async write( - model: ModelName, - ids: RecordIds, - values: Record, - context: TrytonContext = {} - ): Promise { - return this.call(`model.${model}.write`, [ids, values, context]); - } - - /** - * Helper method to delete records from a model - * @param {string} model - Model name (e.g., 'party.party') - * @param {Array} ids - Record IDs to delete - * @param {Object} [context={}] - Context dictionary - * @returns {Promise} - */ - async delete( - model: ModelName, - ids: RecordIds, - context: TrytonContext = {} - ): Promise { - return this.call(`model.${model}.delete`, [ids, context]); - } - - /** - * Helper method to search for records - * @param {string} model - Model name (e.g., 'party.party') - * @param {Array} domain - Search domain - * @param {number} [offset=0] - Offset for pagination - * @param {number} [limit=null] - Limit for pagination - * @param {Array} [order=null] - Order specification - * @param {Object} [context={}] - Context dictionary - * @returns {Promise>} - Array of record IDs - */ - async search( - model: ModelName, + search: ( domain: SearchDomain, - offset: number = 0, - limit: number | null = null, - order: string[] | null = null, - context: TrytonContext = {} - ): Promise { - return this.call(`model.${model}.search`, [ - domain, - offset, - limit, - order, - context, - ]); - } + offset?: number, + limit?: number, + order?: string[], + context?: TrytonContext, + ) => this.search(modelName, domain, offset, limit, order, context), - /** - * Helper method to search and read records in one call - * @param {string} model - Model name (e.g., 'party.party') - * @param {Array} domain - Search domain - * @param {Array} fields - Fields to read - * @param {number} [offset=0] - Offset for pagination - * @param {number} [limit=null] - Limit for pagination - * @param {Array} [order=null] - Order specification - * @param {Object} [context={}] - Context dictionary - * @returns {Promise} - Array of records - */ - async searchRead( - model: ModelName, + searchRead: ( domain: SearchDomain, fields: FieldName[], - offset: number = 0, - limit: number | null = null, - order: string[] | null = null, - context: TrytonContext = {} - ): Promise { - return this.call(`model.${model}.search_read`, [ - domain, - offset, - limit, - order, - fields, - context, - ]); - } + offset?: number, + limit?: number, + order?: string[], + context?: TrytonContext, + ) => + this.searchRead( + modelName, + domain, + fields, + offset, + limit, + order, + context, + ), - /** - * Helper method to count records - * @param {string} model - Model name (e.g., 'party.party') - * @param {Array} domain - Search domain - * @param {Object} [context={}] - Context dictionary - * @returns {Promise} - Number of records - */ - async searchCount( - model: ModelName, - domain: SearchDomain, - context: TrytonContext = {} - ): Promise { - return this.call(`model.${model}.search_count`, [ - domain, - context, - ]); - } - - /** - * Get database information - * @returns {Promise} - Database info - */ - async getDatabaseInfo(): Promise { - return this.call("common.db.get_info", []); - } - - /** - * List available databases - * @returns {Promise>} - Database names - */ - async listDatabases(): Promise { - return this.call("common.db.list", []); - } - - /** - * Get server version - * @returns {Promise} - Server version - */ - async getVersion(): Promise { - return this.call("common.version", []); - } - - /** - * Get user preferences - */ - async getUserPreferences(): Promise { - return this.call("model.res.user.get_preferences", [ - true, - {}, - ]); - } - - /** - * Get current user information - */ - async getCurrentUser(): Promise { - const preferences = await this.getUserPreferences(); - const users = await this.read( - "res.user", - [preferences.id], - ["id", "name", "login", "language", "company", "email"] - ); - if (!users || users.length === 0) { - throw new Error("User not found"); - } - return users[0]!; - } - - /** - * Clear cache for specific prefix or all - * @param {string} [prefix] - Optional prefix to clear - */ - clearCache(prefix?: string): void { - if (this.connection) { - this.connection.clearCache(prefix); - } - } - - /** - * Get connection SSL status - * @returns {boolean|null} - SSL status or null if not connected - */ - get ssl(): boolean | null { - return this.connection ? this.connection.ssl : null; - } - - /** - * Get connection URL - * @returns {string|null} - Connection URL or null if not connected - */ - get url(): string | null { - return this.connection ? this.connection.url : null; - } - - /** - * Check if client is connected - * @returns {boolean} - True if connected - */ - get isConnected(): boolean { - return this.connection !== null; - } - - /** - * Get current session - * @returns {string|null} - Session string or null if not connected - */ - getSession(): string | null { - return this.session; - } - - /** - * Get current user ID from session - */ - getUserId(): number | null { - if (!this.session) return null; - const parts = this.session.split(":"); - return parts.length >= 2 ? parseInt(parts[1], 10) : null; - } - - /** - * Get session token from session - */ - getSessionToken(): string | null { - if (!this.session) return null; - const parts = this.session.split(":"); - return parts.length >= 3 ? parts[2] : null; - } - - /** - * Get username - */ - getUsername(): string { - return this.username; - } - - /** - * Close connection and cleanup resources - */ - close(): void { - if (this.connection) { - this.connection.close(); - this.connection = null; - this.session = null; - } - } - - /** - * Create a new client instance with the same configuration - * @returns {TrytonClient} - New client instance - */ - clone(): TrytonClient { - return new TrytonClient({ - hostname: this.hostname, - database: this.database, - username: this.username, - password: this.password, - port: this.port, - language: this.language, - options: { ...this.options }, - }); - } - - /** - * Get client configuration (without sensitive data) - * @returns {Object} - Client configuration - */ - getConfig(): ClientConfig { - return { - hostname: this.hostname, - database: this.database, - username: this.username, - port: this.port, - language: this.language, - isConnected: this.isConnected, - ssl: this.ssl, - url: this.url, - }; - } - - /** - * Restore session using saved session token - * @param sessionToken - Previously saved session string (format: "username:userId:sessionKey") - * @returns Promise - True if session is valid - */ - async restoreSession(sessionToken: string): Promise { - try { - // Parsear el token guardado - const parts = sessionToken.split(":"); - if (parts.length < 3) { - throw new Error("Invalid session token format"); - } - - const [username, userId, ...sessionKeyParts] = parts; - const sessionKey = sessionKeyParts.join(":"); // Por si el sessionKey tiene ":" - - // Establecer la sesión sin hacer login - this.session = sessionToken; - - // Actualizar la sesión en el connection pool ANTES de obtener proxy - if (this.connection) { - this.connection.setSession(sessionToken); - } else { - throw new Error("No connection pool available"); - } - - // Obtener proxy (ahora con la sesión ya actualizada) - const proxy = this.connection.getConnection(); - - // Test de validación de sesión - try { - // Usar un método que requiere autenticación para validar la sesión - await proxy.request("model.res.user.get_preferences", [ - true, - {}, - ]); - this.connection.putConnection(proxy); // Devolver conexión al pool - return true; - } catch (error) { - // Sesión inválida o expirada - console.error("❌ Error en validación:", error); - this.session = null; - this.connection.putConnection(proxy); // Devolver conexión al pool - return false; - } - } catch (error) { - console.error("❌ Error restaurando sesión:", error); - this.session = null; - return false; - } - } - - /** - * Type-safe model operations factory - * Creates a typed interface for specific models - */ - model( - modelName: ModelName - ): TypedModelOperations { - return { - read: ( - ids: RecordIds, - fields: FieldName[], - context?: TrytonContext - ) => this.read(modelName, ids, fields, context), - - create: ( - records: Partial>[], - context?: TrytonContext - ) => - this.create( - modelName, - records as Record[], - context - ), - - write: ( - ids: RecordIds, - values: Partial>, - context?: TrytonContext - ) => - this.write( - modelName, - ids, - values as Record, - context - ), - - delete: (ids: RecordIds, context?: TrytonContext) => - this.delete(modelName, ids, context), - - search: ( - domain: SearchDomain, - offset?: number, - limit?: number, - order?: string[], - context?: TrytonContext - ) => this.search(modelName, domain, offset, limit, order, context), - - searchRead: ( - domain: SearchDomain, - fields: FieldName[], - offset?: number, - limit?: number, - order?: string[], - context?: TrytonContext - ) => - this.searchRead( - modelName, - domain, - fields, - offset, - limit, - order, - context - ), - - searchCount: (domain: SearchDomain, context?: TrytonContext) => - this.searchCount(modelName, domain, context), - }; - } + searchCount: (domain: SearchDomain, context?: TrytonContext) => + this.searchCount(modelName, domain, context), + }; + } } // Export for CommonJS compatibility