/** * Tryton RPC Client for Node.js * TypeScript implementation of sabatron-tryton-rpc-client */ import { ServerProxy, ServerPool } from "./jsonrpc"; import { ServerProxyOptions, ServerPoolOptions } from "./types"; // Constants 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, } 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; /** * Create a new Tryton client */ constructor(config: TrytonClientConfig) { const { hostname, database, username, password, port = 8000, 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; } this.database = database; this.username = username; this.password = password; this.port = port; this.language = language; this.options = options; this.connection = null; this.session = null; } /** * Alternative constructor for backward compatibility */ 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}`); } } /** * 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, }; } /** * 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), }; } } // Export for CommonJS compatibility export default TrytonClient;